@logto/client 2.2.4 → 2.3.1
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 +3 -0
- package/lib/errors.d.ts +1 -0
- package/lib/errors.js +3 -0
- package/lib/index.cjs +106 -57
- package/lib/index.d.ts +38 -23
- package/lib/index.js +93 -61
- package/lib/mock.d.ts +4 -12
- package/lib/types/index.cjs +22 -0
- package/lib/types/index.d.ts +11 -1
- package/lib/types/index.js +23 -2
- package/lib/types/index.test.d.ts +1 -0
- package/lib/utils/index.cjs +2 -1
- package/lib/utils/index.d.ts +1 -1
- package/lib/utils/index.js +2 -1
- package/package.json +3 -3
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.
|
|
@@ -23,9 +24,13 @@ var requester = require('./utils/requester.cjs');
|
|
|
23
24
|
*/
|
|
24
25
|
class LogtoClient {
|
|
25
26
|
constructor(logtoConfig, adapter) {
|
|
26
|
-
this.getOidcConfig = memoize.memoize(this.#getOidcConfig);
|
|
27
27
|
/**
|
|
28
|
-
* Get the
|
|
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);
|
|
32
|
+
/**
|
|
33
|
+
* Get the access token from the storage with refresh strategy.
|
|
29
34
|
*
|
|
30
35
|
* - If the access token has expired, it will try to fetch a new one using the Refresh Token.
|
|
31
36
|
* - If there's an ongoing Promise to fetch the access token, it will return the Promise.
|
|
@@ -39,13 +44,32 @@ class LogtoClient {
|
|
|
39
44
|
* @throws LogtoClientError if the user is not authenticated.
|
|
40
45
|
*/
|
|
41
46
|
this.getAccessToken = memoize.memoize(this.#getAccessToken);
|
|
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
|
+
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);
|
|
42
70
|
this.getJwtVerifyGetKey = once.once(this.#getJwtVerifyGetKey);
|
|
43
71
|
this.accessTokenMap = new Map();
|
|
44
|
-
this.logtoConfig =
|
|
45
|
-
...logtoConfig,
|
|
46
|
-
prompt: logtoConfig.prompt ?? js.Prompt.Consent,
|
|
47
|
-
scopes: js.withDefaultScopes(logtoConfig.scopes).split(' '),
|
|
48
|
-
};
|
|
72
|
+
this.logtoConfig = index.normalizeLogtoConfig(logtoConfig);
|
|
49
73
|
this.adapter = new index$1.ClientAdapterInstance(adapter);
|
|
50
74
|
void this.loadAccessTokenMap();
|
|
51
75
|
}
|
|
@@ -89,6 +113,15 @@ class LogtoClient {
|
|
|
89
113
|
const accessToken = await this.getAccessToken(resource);
|
|
90
114
|
return js.decodeAccessToken(accessToken);
|
|
91
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Get the organization token claims for the specified organization.
|
|
118
|
+
*
|
|
119
|
+
* @param organizationId The ID of the organization that the access token is granted for.
|
|
120
|
+
*/
|
|
121
|
+
async getOrganizationTokenClaims(organizationId) {
|
|
122
|
+
const accessToken = await this.getOrganizationToken(organizationId);
|
|
123
|
+
return js.decodeAccessToken(accessToken);
|
|
124
|
+
}
|
|
92
125
|
/**
|
|
93
126
|
* Get the user information from the Userinfo Endpoint.
|
|
94
127
|
*
|
|
@@ -164,50 +197,6 @@ class LogtoClient {
|
|
|
164
197
|
const { origin, pathname } = new URL(url);
|
|
165
198
|
return `${origin}${pathname}` === redirectUri;
|
|
166
199
|
}
|
|
167
|
-
/**
|
|
168
|
-
* Handle the sign-in callback by parsing the authorization code from the
|
|
169
|
-
* callback URI and exchanging it for the tokens.
|
|
170
|
-
*
|
|
171
|
-
* @param callbackUri The callback URI, including the search params, that the user is redirected to after the sign-in flow is completed.
|
|
172
|
-
* The origin and pathname of this URI must match the origin and pathname of the redirect URI specified in {@link signIn}.
|
|
173
|
-
* In many cases you'll probably end up passing `window.location.href` as the argument to this function.
|
|
174
|
-
* @throws LogtoClientError if the sign-in session is not found.
|
|
175
|
-
*/
|
|
176
|
-
async handleSignInCallback(callbackUri) {
|
|
177
|
-
const { requester } = this.adapter;
|
|
178
|
-
const signInSession = await this.getSignInSession();
|
|
179
|
-
if (!signInSession) {
|
|
180
|
-
throw new errors.LogtoClientError('sign_in_session.not_found');
|
|
181
|
-
}
|
|
182
|
-
const { redirectUri, state, codeVerifier } = signInSession;
|
|
183
|
-
const code = js.verifyAndParseCodeFromCallbackUri(callbackUri, redirectUri, state);
|
|
184
|
-
// NOTE: Will add scope to accessTokenKey when needed. (Linear issue LOG-1589)
|
|
185
|
-
const accessTokenKey = index$2.buildAccessTokenKey();
|
|
186
|
-
const { appId: clientId } = this.logtoConfig;
|
|
187
|
-
const { tokenEndpoint } = await this.getOidcConfig();
|
|
188
|
-
const requestedAt = Math.round(Date.now() / 1000);
|
|
189
|
-
const { idToken, refreshToken, accessToken, scope, expiresIn } = await js.fetchTokenByAuthorizationCode({
|
|
190
|
-
clientId,
|
|
191
|
-
tokenEndpoint,
|
|
192
|
-
redirectUri,
|
|
193
|
-
codeVerifier,
|
|
194
|
-
code,
|
|
195
|
-
}, requester);
|
|
196
|
-
await this.verifyIdToken(idToken);
|
|
197
|
-
await this.setRefreshToken(refreshToken ?? null);
|
|
198
|
-
await this.setIdToken(idToken);
|
|
199
|
-
this.accessTokenMap.set(accessTokenKey, {
|
|
200
|
-
token: accessToken,
|
|
201
|
-
scope,
|
|
202
|
-
/** The `expiresAt` variable provides an approximate estimation of the actual `exp` property
|
|
203
|
-
* in the token claims. It is utilized by the client to determine if the cached access token
|
|
204
|
-
* has expired and when a new access token should be requested.
|
|
205
|
-
*/
|
|
206
|
-
expiresAt: requestedAt + expiresIn,
|
|
207
|
-
});
|
|
208
|
-
await this.saveAccessTokenMap();
|
|
209
|
-
await this.setSignInSession(null);
|
|
210
|
-
}
|
|
211
200
|
/**
|
|
212
201
|
* Start the sign-out flow with the specified redirect URI. The URI must be
|
|
213
202
|
* registered in the Logto Console.
|
|
@@ -263,12 +252,12 @@ class LogtoClient {
|
|
|
263
252
|
async setRefreshToken(value) {
|
|
264
253
|
return this.adapter.setStorageItem(types.PersistKey.RefreshToken, value);
|
|
265
254
|
}
|
|
266
|
-
async getAccessTokenByRefreshToken(resource) {
|
|
255
|
+
async getAccessTokenByRefreshToken(resource, organizationId) {
|
|
267
256
|
const currentRefreshToken = await this.getRefreshToken();
|
|
268
257
|
if (!currentRefreshToken) {
|
|
269
258
|
throw new errors.LogtoClientError('not_authenticated');
|
|
270
259
|
}
|
|
271
|
-
const accessTokenKey = index$2.buildAccessTokenKey(resource);
|
|
260
|
+
const accessTokenKey = index$2.buildAccessTokenKey(resource, organizationId);
|
|
272
261
|
const { appId: clientId } = this.logtoConfig;
|
|
273
262
|
const { tokenEndpoint } = await this.getOidcConfig();
|
|
274
263
|
const requestedAt = Math.round(Date.now() / 1000);
|
|
@@ -277,6 +266,7 @@ class LogtoClient {
|
|
|
277
266
|
tokenEndpoint,
|
|
278
267
|
refreshToken: currentRefreshToken,
|
|
279
268
|
resource,
|
|
269
|
+
organizationId,
|
|
280
270
|
}, this.adapter.requester);
|
|
281
271
|
this.accessTokenMap.set(accessTokenKey, {
|
|
282
272
|
token: accessToken,
|
|
@@ -343,11 +333,11 @@ class LogtoClient {
|
|
|
343
333
|
const cachedJwkSet = new remoteJwkSet.CachedRemoteJwkSet(new URL(jwksUri), this.adapter);
|
|
344
334
|
return async (...args) => cachedJwkSet.getKey(...args);
|
|
345
335
|
}
|
|
346
|
-
async #getAccessToken(resource) {
|
|
347
|
-
if (!(await this.
|
|
336
|
+
async #getAccessToken(resource, organizationId) {
|
|
337
|
+
if (!(await this.isAuthenticated())) {
|
|
348
338
|
throw new errors.LogtoClientError('not_authenticated');
|
|
349
339
|
}
|
|
350
|
-
const accessTokenKey = index$2.buildAccessTokenKey(resource);
|
|
340
|
+
const accessTokenKey = index$2.buildAccessTokenKey(resource, organizationId);
|
|
351
341
|
const accessToken = this.accessTokenMap.get(accessTokenKey);
|
|
352
342
|
if (accessToken && accessToken.expiresAt > Date.now() / 1000) {
|
|
353
343
|
return accessToken.token;
|
|
@@ -359,9 +349,51 @@ class LogtoClient {
|
|
|
359
349
|
/**
|
|
360
350
|
* Need to fetch a new access token using refresh token.
|
|
361
351
|
*/
|
|
362
|
-
return this.getAccessTokenByRefreshToken(resource);
|
|
352
|
+
return this.getAccessTokenByRefreshToken(resource, organizationId);
|
|
353
|
+
}
|
|
354
|
+
async #getOrganizationToken(organizationId) {
|
|
355
|
+
if (!this.logtoConfig.scopes?.includes(js.UserScope.Organizations)) {
|
|
356
|
+
throw new errors.LogtoClientError('missing_scope_organizations');
|
|
357
|
+
}
|
|
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);
|
|
363
394
|
}
|
|
364
395
|
}
|
|
396
|
+
/* eslint-enable max-lines */
|
|
365
397
|
|
|
366
398
|
Object.defineProperty(exports, 'LogtoError', {
|
|
367
399
|
enumerable: true,
|
|
@@ -379,6 +411,10 @@ Object.defineProperty(exports, 'Prompt', {
|
|
|
379
411
|
enumerable: true,
|
|
380
412
|
get: function () { return js.Prompt; }
|
|
381
413
|
});
|
|
414
|
+
Object.defineProperty(exports, 'ReservedResource', {
|
|
415
|
+
enumerable: true,
|
|
416
|
+
get: function () { return js.ReservedResource; }
|
|
417
|
+
});
|
|
382
418
|
Object.defineProperty(exports, 'ReservedScope', {
|
|
383
419
|
enumerable: true,
|
|
384
420
|
get: function () { return js.ReservedScope; }
|
|
@@ -387,9 +423,22 @@ Object.defineProperty(exports, 'UserScope', {
|
|
|
387
423
|
enumerable: true,
|
|
388
424
|
get: function () { return js.UserScope; }
|
|
389
425
|
});
|
|
426
|
+
Object.defineProperty(exports, 'buildOrganizationUrn', {
|
|
427
|
+
enumerable: true,
|
|
428
|
+
get: function () { return js.buildOrganizationUrn; }
|
|
429
|
+
});
|
|
430
|
+
Object.defineProperty(exports, 'getOrganizationIdFromUrn', {
|
|
431
|
+
enumerable: true,
|
|
432
|
+
get: function () { return js.getOrganizationIdFromUrn; }
|
|
433
|
+
});
|
|
434
|
+
Object.defineProperty(exports, 'organizationUrnPrefix', {
|
|
435
|
+
enumerable: true,
|
|
436
|
+
get: function () { return js.organizationUrnPrefix; }
|
|
437
|
+
});
|
|
390
438
|
exports.LogtoClientError = errors.LogtoClientError;
|
|
391
439
|
exports.isLogtoAccessTokenMap = index.isLogtoAccessTokenMap;
|
|
392
440
|
exports.isLogtoSignInSessionItem = index.isLogtoSignInSessionItem;
|
|
441
|
+
exports.normalizeLogtoConfig = index.normalizeLogtoConfig;
|
|
393
442
|
Object.defineProperty(exports, 'CacheKey', {
|
|
394
443
|
enumerable: true,
|
|
395
444
|
get: function () { return types.CacheKey; }
|
package/lib/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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';
|
|
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';
|
|
@@ -20,17 +20,13 @@ 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
|
-
}>>;
|
|
32
23
|
/**
|
|
33
|
-
* Get the
|
|
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>;
|
|
28
|
+
/**
|
|
29
|
+
* Get the access token from the storage with refresh strategy.
|
|
34
30
|
*
|
|
35
31
|
* - If the access token has expired, it will try to fetch a new one using the Refresh Token.
|
|
36
32
|
* - If there's an ongoing Promise to fetch the access token, it will return the Promise.
|
|
@@ -43,7 +39,30 @@ export default class LogtoClient {
|
|
|
43
39
|
* @returns The access token string.
|
|
44
40
|
* @throws LogtoClientError if the user is not authenticated.
|
|
45
41
|
*/
|
|
46
|
-
readonly getAccessToken: (this: unknown, resource?: string | undefined) => Promise<string>;
|
|
42
|
+
readonly getAccessToken: (this: unknown, resource?: string | undefined, organizationId?: string | undefined) => Promise<string>;
|
|
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
|
+
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>;
|
|
47
66
|
protected readonly getJwtVerifyGetKey: (...args: unknown[]) => Promise<JWTVerifyGetKey>;
|
|
48
67
|
protected readonly adapter: ClientAdapterInstance;
|
|
49
68
|
protected readonly accessTokenMap: Map<string, AccessToken>;
|
|
@@ -73,6 +92,12 @@ export default class LogtoClient {
|
|
|
73
92
|
* resource, as specified in the Logto Console.
|
|
74
93
|
*/
|
|
75
94
|
getAccessTokenClaims(resource?: string): Promise<AccessTokenClaims>;
|
|
95
|
+
/**
|
|
96
|
+
* Get the organization token claims for the specified organization.
|
|
97
|
+
*
|
|
98
|
+
* @param organizationId The ID of the organization that the access token is granted for.
|
|
99
|
+
*/
|
|
100
|
+
getOrganizationTokenClaims(organizationId: string): Promise<AccessTokenClaims>;
|
|
76
101
|
/**
|
|
77
102
|
* Get the user information from the Userinfo Endpoint.
|
|
78
103
|
*
|
|
@@ -110,16 +135,6 @@ export default class LogtoClient {
|
|
|
110
135
|
* @param url The current URL.
|
|
111
136
|
*/
|
|
112
137
|
isSignInRedirected(url: string): Promise<boolean>;
|
|
113
|
-
/**
|
|
114
|
-
* Handle the sign-in callback by parsing the authorization code from the
|
|
115
|
-
* callback URI and exchanging it for the tokens.
|
|
116
|
-
*
|
|
117
|
-
* @param callbackUri The callback URI, including the search params, that the user is redirected to after the sign-in flow is completed.
|
|
118
|
-
* The origin and pathname of this URI must match the origin and pathname of the redirect URI specified in {@link signIn}.
|
|
119
|
-
* In many cases you'll probably end up passing `window.location.href` as the argument to this function.
|
|
120
|
-
* @throws LogtoClientError if the sign-in session is not found.
|
|
121
|
-
*/
|
|
122
|
-
handleSignInCallback(callbackUri: string): Promise<void>;
|
|
123
138
|
/**
|
|
124
139
|
* Start the sign-out flow with the specified redirect URI. The URI must be
|
|
125
140
|
* registered in the Logto Console.
|
package/lib/index.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { LogtoError, LogtoRequestError, OidcError, Prompt, ReservedScope, UserScope } from '@logto/js';
|
|
1
|
+
import { decodeIdToken, decodeAccessToken, fetchUserInfo, generateSignInUri, revoke, generateSignOutUri, fetchTokenByRefreshToken, verifyIdToken, fetchOidcConfig, UserScope, verifyAndParseCodeFromCallbackUri, fetchTokenByAuthorizationCode } 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.
|
|
@@ -20,9 +21,13 @@ export { createRequester } from './utils/requester.js';
|
|
|
20
21
|
*/
|
|
21
22
|
class LogtoClient {
|
|
22
23
|
constructor(logtoConfig, adapter) {
|
|
23
|
-
this.getOidcConfig = memoize(this.#getOidcConfig);
|
|
24
24
|
/**
|
|
25
|
-
* Get the
|
|
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);
|
|
29
|
+
/**
|
|
30
|
+
* Get the access token from the storage with refresh strategy.
|
|
26
31
|
*
|
|
27
32
|
* - If the access token has expired, it will try to fetch a new one using the Refresh Token.
|
|
28
33
|
* - If there's an ongoing Promise to fetch the access token, it will return the Promise.
|
|
@@ -36,13 +41,32 @@ class LogtoClient {
|
|
|
36
41
|
* @throws LogtoClientError if the user is not authenticated.
|
|
37
42
|
*/
|
|
38
43
|
this.getAccessToken = memoize(this.#getAccessToken);
|
|
44
|
+
/**
|
|
45
|
+
* Get the access token for the specified organization from the storage with refresh strategy.
|
|
46
|
+
*
|
|
47
|
+
* Scope {@link UserScope.Organizations} is required in the config to use organization-related
|
|
48
|
+
* methods.
|
|
49
|
+
*
|
|
50
|
+
* @param organizationId The ID of the organization that the access token is granted for.
|
|
51
|
+
* @returns The access token string.
|
|
52
|
+
* @throws LogtoClientError if the user is not authenticated.
|
|
53
|
+
* @remarks
|
|
54
|
+
* It uses the same refresh strategy as {@link getAccessToken}.
|
|
55
|
+
*/
|
|
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);
|
|
39
67
|
this.getJwtVerifyGetKey = once(this.#getJwtVerifyGetKey);
|
|
40
68
|
this.accessTokenMap = new Map();
|
|
41
|
-
this.logtoConfig =
|
|
42
|
-
...logtoConfig,
|
|
43
|
-
prompt: logtoConfig.prompt ?? Prompt.Consent,
|
|
44
|
-
scopes: withDefaultScopes(logtoConfig.scopes).split(' '),
|
|
45
|
-
};
|
|
69
|
+
this.logtoConfig = normalizeLogtoConfig(logtoConfig);
|
|
46
70
|
this.adapter = new ClientAdapterInstance(adapter);
|
|
47
71
|
void this.loadAccessTokenMap();
|
|
48
72
|
}
|
|
@@ -86,6 +110,15 @@ class LogtoClient {
|
|
|
86
110
|
const accessToken = await this.getAccessToken(resource);
|
|
87
111
|
return decodeAccessToken(accessToken);
|
|
88
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Get the organization token claims for the specified organization.
|
|
115
|
+
*
|
|
116
|
+
* @param organizationId The ID of the organization that the access token is granted for.
|
|
117
|
+
*/
|
|
118
|
+
async getOrganizationTokenClaims(organizationId) {
|
|
119
|
+
const accessToken = await this.getOrganizationToken(organizationId);
|
|
120
|
+
return decodeAccessToken(accessToken);
|
|
121
|
+
}
|
|
89
122
|
/**
|
|
90
123
|
* Get the user information from the Userinfo Endpoint.
|
|
91
124
|
*
|
|
@@ -161,50 +194,6 @@ class LogtoClient {
|
|
|
161
194
|
const { origin, pathname } = new URL(url);
|
|
162
195
|
return `${origin}${pathname}` === redirectUri;
|
|
163
196
|
}
|
|
164
|
-
/**
|
|
165
|
-
* Handle the sign-in callback by parsing the authorization code from the
|
|
166
|
-
* callback URI and exchanging it for the tokens.
|
|
167
|
-
*
|
|
168
|
-
* @param callbackUri The callback URI, including the search params, that the user is redirected to after the sign-in flow is completed.
|
|
169
|
-
* The origin and pathname of this URI must match the origin and pathname of the redirect URI specified in {@link signIn}.
|
|
170
|
-
* In many cases you'll probably end up passing `window.location.href` as the argument to this function.
|
|
171
|
-
* @throws LogtoClientError if the sign-in session is not found.
|
|
172
|
-
*/
|
|
173
|
-
async handleSignInCallback(callbackUri) {
|
|
174
|
-
const { requester } = this.adapter;
|
|
175
|
-
const signInSession = await this.getSignInSession();
|
|
176
|
-
if (!signInSession) {
|
|
177
|
-
throw new LogtoClientError('sign_in_session.not_found');
|
|
178
|
-
}
|
|
179
|
-
const { redirectUri, state, codeVerifier } = signInSession;
|
|
180
|
-
const code = verifyAndParseCodeFromCallbackUri(callbackUri, redirectUri, state);
|
|
181
|
-
// NOTE: Will add scope to accessTokenKey when needed. (Linear issue LOG-1589)
|
|
182
|
-
const accessTokenKey = buildAccessTokenKey();
|
|
183
|
-
const { appId: clientId } = this.logtoConfig;
|
|
184
|
-
const { tokenEndpoint } = await this.getOidcConfig();
|
|
185
|
-
const requestedAt = Math.round(Date.now() / 1000);
|
|
186
|
-
const { idToken, refreshToken, accessToken, scope, expiresIn } = await fetchTokenByAuthorizationCode({
|
|
187
|
-
clientId,
|
|
188
|
-
tokenEndpoint,
|
|
189
|
-
redirectUri,
|
|
190
|
-
codeVerifier,
|
|
191
|
-
code,
|
|
192
|
-
}, requester);
|
|
193
|
-
await this.verifyIdToken(idToken);
|
|
194
|
-
await this.setRefreshToken(refreshToken ?? null);
|
|
195
|
-
await this.setIdToken(idToken);
|
|
196
|
-
this.accessTokenMap.set(accessTokenKey, {
|
|
197
|
-
token: accessToken,
|
|
198
|
-
scope,
|
|
199
|
-
/** The `expiresAt` variable provides an approximate estimation of the actual `exp` property
|
|
200
|
-
* in the token claims. It is utilized by the client to determine if the cached access token
|
|
201
|
-
* has expired and when a new access token should be requested.
|
|
202
|
-
*/
|
|
203
|
-
expiresAt: requestedAt + expiresIn,
|
|
204
|
-
});
|
|
205
|
-
await this.saveAccessTokenMap();
|
|
206
|
-
await this.setSignInSession(null);
|
|
207
|
-
}
|
|
208
197
|
/**
|
|
209
198
|
* Start the sign-out flow with the specified redirect URI. The URI must be
|
|
210
199
|
* registered in the Logto Console.
|
|
@@ -260,12 +249,12 @@ class LogtoClient {
|
|
|
260
249
|
async setRefreshToken(value) {
|
|
261
250
|
return this.adapter.setStorageItem(PersistKey.RefreshToken, value);
|
|
262
251
|
}
|
|
263
|
-
async getAccessTokenByRefreshToken(resource) {
|
|
252
|
+
async getAccessTokenByRefreshToken(resource, organizationId) {
|
|
264
253
|
const currentRefreshToken = await this.getRefreshToken();
|
|
265
254
|
if (!currentRefreshToken) {
|
|
266
255
|
throw new LogtoClientError('not_authenticated');
|
|
267
256
|
}
|
|
268
|
-
const accessTokenKey = buildAccessTokenKey(resource);
|
|
257
|
+
const accessTokenKey = buildAccessTokenKey(resource, organizationId);
|
|
269
258
|
const { appId: clientId } = this.logtoConfig;
|
|
270
259
|
const { tokenEndpoint } = await this.getOidcConfig();
|
|
271
260
|
const requestedAt = Math.round(Date.now() / 1000);
|
|
@@ -274,6 +263,7 @@ class LogtoClient {
|
|
|
274
263
|
tokenEndpoint,
|
|
275
264
|
refreshToken: currentRefreshToken,
|
|
276
265
|
resource,
|
|
266
|
+
organizationId,
|
|
277
267
|
}, this.adapter.requester);
|
|
278
268
|
this.accessTokenMap.set(accessTokenKey, {
|
|
279
269
|
token: accessToken,
|
|
@@ -340,11 +330,11 @@ class LogtoClient {
|
|
|
340
330
|
const cachedJwkSet = new CachedRemoteJwkSet(new URL(jwksUri), this.adapter);
|
|
341
331
|
return async (...args) => cachedJwkSet.getKey(...args);
|
|
342
332
|
}
|
|
343
|
-
async #getAccessToken(resource) {
|
|
344
|
-
if (!(await this.
|
|
333
|
+
async #getAccessToken(resource, organizationId) {
|
|
334
|
+
if (!(await this.isAuthenticated())) {
|
|
345
335
|
throw new LogtoClientError('not_authenticated');
|
|
346
336
|
}
|
|
347
|
-
const accessTokenKey = buildAccessTokenKey(resource);
|
|
337
|
+
const accessTokenKey = buildAccessTokenKey(resource, organizationId);
|
|
348
338
|
const accessToken = this.accessTokenMap.get(accessTokenKey);
|
|
349
339
|
if (accessToken && accessToken.expiresAt > Date.now() / 1000) {
|
|
350
340
|
return accessToken.token;
|
|
@@ -356,8 +346,50 @@ class LogtoClient {
|
|
|
356
346
|
/**
|
|
357
347
|
* Need to fetch a new access token using refresh token.
|
|
358
348
|
*/
|
|
359
|
-
return this.getAccessTokenByRefreshToken(resource);
|
|
349
|
+
return this.getAccessTokenByRefreshToken(resource, organizationId);
|
|
350
|
+
}
|
|
351
|
+
async #getOrganizationToken(organizationId) {
|
|
352
|
+
if (!this.logtoConfig.scopes?.includes(UserScope.Organizations)) {
|
|
353
|
+
throw new LogtoClientError('missing_scope_organizations');
|
|
354
|
+
}
|
|
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);
|
|
360
391
|
}
|
|
361
392
|
}
|
|
393
|
+
/* eslint-enable max-lines */
|
|
362
394
|
|
|
363
|
-
export { CacheKey, LogtoClientError, PersistKey, LogtoClient as default, isLogtoAccessTokenMap, isLogtoSignInSessionItem };
|
|
395
|
+
export { CacheKey, LogtoClientError, PersistKey, LogtoClient as default, isLogtoAccessTokenMap, isLogtoSignInSessionItem, normalizeLogtoConfig };
|
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';
|
|
@@ -66,23 +66,15 @@ 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
|
*/
|
|
73
73
|
export declare class LogtoClientWithAccessors extends LogtoClient {
|
|
74
|
-
runGetOidcConfig(): Promise<
|
|
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
|
-
|
|
77
|
+
getSignInSession(): Promise<Nullable<LogtoSignInSessionItem>>;
|
|
86
78
|
setSignInSessionItem(item: Nullable<LogtoSignInSessionItem>): Promise<void>;
|
|
87
79
|
getAccessTokenMap(): Map<string, AccessToken>;
|
|
88
80
|
}
|
package/lib/types/index.cjs
CHANGED
|
@@ -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;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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;
|
package/lib/types/index.js
CHANGED
|
@@ -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 {};
|
package/lib/utils/index.cjs
CHANGED
|
@@ -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;
|
package/lib/utils/index.d.ts
CHANGED
|
@@ -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;
|
package/lib/utils/index.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "2.3.1",
|
|
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": "^
|
|
24
|
+
"@logto/js": "^3.0.0",
|
|
25
25
|
"@silverhand/essentials": "^2.6.2",
|
|
26
26
|
"camelcase-keys": "^7.0.1",
|
|
27
|
-
"jose": "^
|
|
27
|
+
"jose": "^5.0.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@silverhand/eslint-config": "^4.0.1",
|