@logto/client 2.1.0 → 2.2.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/adapter/index.cjs +63 -0
- package/lib/adapter/index.d.ts +29 -0
- package/lib/adapter/index.js +53 -0
- package/lib/adapter/index.test.d.ts +1 -0
- package/lib/adapter/types.cjs +24 -0
- package/lib/adapter/types.d.ts +45 -0
- package/lib/adapter/types.js +24 -0
- package/lib/index.cache.test.d.ts +1 -0
- package/lib/index.cjs +62 -50
- package/lib/index.constructor.test.d.ts +1 -0
- package/lib/index.d.ts +18 -10
- package/lib/index.js +53 -49
- package/lib/index.sign-in.test.d.ts +1 -0
- package/lib/index.sign-out.test.d.ts +1 -0
- package/lib/mock.d.ts +27 -7
- package/lib/remote-jwk-set.cjs +62 -0
- package/lib/remote-jwk-set.d.ts +10 -0
- package/lib/remote-jwk-set.js +60 -0
- package/lib/remote-jwk-set.test.d.ts +1 -0
- package/lib/utils/memoize.cjs +25 -0
- package/lib/utils/memoize.d.ts +1 -0
- package/lib/utils/memoize.js +23 -0
- package/package.json +5 -5
- package/lib/adapter.d.ts +0 -17
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var essentials = require('@silverhand/essentials');
|
|
4
|
+
var types = require('./types.cjs');
|
|
5
|
+
|
|
6
|
+
class ClientAdapterInstance {
|
|
7
|
+
/* END OF IMPLEMENTATION */
|
|
8
|
+
constructor(adapter) {
|
|
9
|
+
// eslint-disable-next-line @silverhand/fp/no-mutating-assign
|
|
10
|
+
Object.assign(this, adapter);
|
|
11
|
+
}
|
|
12
|
+
async setStorageItem(key, value) {
|
|
13
|
+
if (!value) {
|
|
14
|
+
await this.storage.removeItem(key);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
await this.storage.setItem(key, value);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Try to get the string value from the cache and parse as JSON.
|
|
21
|
+
* Return the parsed value if it is an object, return `undefined` otherwise.
|
|
22
|
+
*
|
|
23
|
+
* @param key The cache key to get value from.
|
|
24
|
+
*/
|
|
25
|
+
async getCachedObject(key) {
|
|
26
|
+
const cached = await essentials.trySafe(async () => {
|
|
27
|
+
const data = await this.unstable_cache?.getItem(key);
|
|
28
|
+
// It's actually `unknown`
|
|
29
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
30
|
+
return essentials.conditional(data && JSON.parse(data));
|
|
31
|
+
});
|
|
32
|
+
if (cached && typeof cached === 'object') {
|
|
33
|
+
// Trust cache for now
|
|
34
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
35
|
+
return cached;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Try to get the value from the cache first, if it doesn't exist in cache,
|
|
40
|
+
* run the getter function and store the result into cache.
|
|
41
|
+
*
|
|
42
|
+
* @param key The cache key to get value from.
|
|
43
|
+
*/
|
|
44
|
+
async getWithCache(key, getter) {
|
|
45
|
+
const cached = await this.getCachedObject(key);
|
|
46
|
+
if (cached) {
|
|
47
|
+
return cached;
|
|
48
|
+
}
|
|
49
|
+
const result = await getter();
|
|
50
|
+
await this.unstable_cache?.setItem(key, JSON.stringify(result));
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
Object.defineProperty(exports, 'CacheKey', {
|
|
56
|
+
enumerable: true,
|
|
57
|
+
get: function () { return types.CacheKey; }
|
|
58
|
+
});
|
|
59
|
+
Object.defineProperty(exports, 'PersistKey', {
|
|
60
|
+
enumerable: true,
|
|
61
|
+
get: function () { return types.PersistKey; }
|
|
62
|
+
});
|
|
63
|
+
exports.ClientAdapterInstance = ClientAdapterInstance;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Requester } from '@logto/js';
|
|
2
|
+
import { type Nullable } from '@silverhand/essentials';
|
|
3
|
+
import { type CacheKey, type Navigate, type PersistKey, type Storage, type StorageKey, type ClientAdapter, type InferStorageKey } from './types.js';
|
|
4
|
+
export declare class ClientAdapterInstance implements ClientAdapter {
|
|
5
|
+
requester: Requester;
|
|
6
|
+
storage: Storage<StorageKey | PersistKey>;
|
|
7
|
+
unstable_cache?: Storage<CacheKey> | undefined;
|
|
8
|
+
navigate: Navigate;
|
|
9
|
+
generateState: () => string;
|
|
10
|
+
generateCodeVerifier: () => string;
|
|
11
|
+
generateCodeChallenge: (codeVerifier: string) => Promise<string>;
|
|
12
|
+
constructor(adapter: ClientAdapter);
|
|
13
|
+
setStorageItem(key: InferStorageKey<typeof this.storage>, value: Nullable<string>): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Try to get the string value from the cache and parse as JSON.
|
|
16
|
+
* Return the parsed value if it is an object, return `undefined` otherwise.
|
|
17
|
+
*
|
|
18
|
+
* @param key The cache key to get value from.
|
|
19
|
+
*/
|
|
20
|
+
getCachedObject<T>(key: CacheKey): Promise<T | undefined>;
|
|
21
|
+
/**
|
|
22
|
+
* Try to get the value from the cache first, if it doesn't exist in cache,
|
|
23
|
+
* run the getter function and store the result into cache.
|
|
24
|
+
*
|
|
25
|
+
* @param key The cache key to get value from.
|
|
26
|
+
*/
|
|
27
|
+
getWithCache<T>(key: CacheKey, getter: () => Promise<T>): Promise<T>;
|
|
28
|
+
}
|
|
29
|
+
export * from './types.js';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { trySafe, conditional } from '@silverhand/essentials';
|
|
2
|
+
export { CacheKey, PersistKey } from './types.js';
|
|
3
|
+
|
|
4
|
+
class ClientAdapterInstance {
|
|
5
|
+
/* END OF IMPLEMENTATION */
|
|
6
|
+
constructor(adapter) {
|
|
7
|
+
// eslint-disable-next-line @silverhand/fp/no-mutating-assign
|
|
8
|
+
Object.assign(this, adapter);
|
|
9
|
+
}
|
|
10
|
+
async setStorageItem(key, value) {
|
|
11
|
+
if (!value) {
|
|
12
|
+
await this.storage.removeItem(key);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
await this.storage.setItem(key, value);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Try to get the string value from the cache and parse as JSON.
|
|
19
|
+
* Return the parsed value if it is an object, return `undefined` otherwise.
|
|
20
|
+
*
|
|
21
|
+
* @param key The cache key to get value from.
|
|
22
|
+
*/
|
|
23
|
+
async getCachedObject(key) {
|
|
24
|
+
const cached = await trySafe(async () => {
|
|
25
|
+
const data = await this.unstable_cache?.getItem(key);
|
|
26
|
+
// It's actually `unknown`
|
|
27
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
28
|
+
return conditional(data && JSON.parse(data));
|
|
29
|
+
});
|
|
30
|
+
if (cached && typeof cached === 'object') {
|
|
31
|
+
// Trust cache for now
|
|
32
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
33
|
+
return cached;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Try to get the value from the cache first, if it doesn't exist in cache,
|
|
38
|
+
* run the getter function and store the result into cache.
|
|
39
|
+
*
|
|
40
|
+
* @param key The cache key to get value from.
|
|
41
|
+
*/
|
|
42
|
+
async getWithCache(key, getter) {
|
|
43
|
+
const cached = await this.getCachedObject(key);
|
|
44
|
+
if (cached) {
|
|
45
|
+
return cached;
|
|
46
|
+
}
|
|
47
|
+
const result = await getter();
|
|
48
|
+
await this.unstable_cache?.setItem(key, JSON.stringify(result));
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { ClientAdapterInstance };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
exports.PersistKey = void 0;
|
|
4
|
+
(function (PersistKey) {
|
|
5
|
+
PersistKey["IdToken"] = "idToken";
|
|
6
|
+
PersistKey["RefreshToken"] = "refreshToken";
|
|
7
|
+
PersistKey["AccessToken"] = "accessToken";
|
|
8
|
+
PersistKey["SignInSession"] = "signInSession";
|
|
9
|
+
})(exports.PersistKey || (exports.PersistKey = {}));
|
|
10
|
+
exports.CacheKey = void 0;
|
|
11
|
+
(function (CacheKey) {
|
|
12
|
+
/**
|
|
13
|
+
* OpenID Configuration endpoint response.
|
|
14
|
+
*
|
|
15
|
+
* @see {@link https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse | OpenID Connect Discovery 1.0}
|
|
16
|
+
*/
|
|
17
|
+
CacheKey["OpenidConfig"] = "openidConfiguration";
|
|
18
|
+
/**
|
|
19
|
+
* The content of OpenID Provider's `jwks_uri` (JSON Web Key Set).
|
|
20
|
+
*
|
|
21
|
+
* @see {@link https://openid.net/specs/openid-connect-discovery-1_0-21.html#ProviderMetadata | OpenID Connect Discovery 1.0}
|
|
22
|
+
*/
|
|
23
|
+
CacheKey["Jwks"] = "jwks";
|
|
24
|
+
})(exports.CacheKey || (exports.CacheKey = {}));
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Requester } from '@logto/js';
|
|
2
|
+
import type { Nullable } from '@silverhand/essentials';
|
|
3
|
+
/** @deprecated Use {@link PersistKey} instead. */
|
|
4
|
+
export type StorageKey = 'idToken' | 'refreshToken' | 'accessToken' | 'signInSession';
|
|
5
|
+
export declare enum PersistKey {
|
|
6
|
+
IdToken = "idToken",
|
|
7
|
+
RefreshToken = "refreshToken",
|
|
8
|
+
AccessToken = "accessToken",
|
|
9
|
+
SignInSession = "signInSession"
|
|
10
|
+
}
|
|
11
|
+
export declare enum CacheKey {
|
|
12
|
+
/**
|
|
13
|
+
* OpenID Configuration endpoint response.
|
|
14
|
+
*
|
|
15
|
+
* @see {@link https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse | OpenID Connect Discovery 1.0}
|
|
16
|
+
*/
|
|
17
|
+
OpenidConfig = "openidConfiguration",
|
|
18
|
+
/**
|
|
19
|
+
* The content of OpenID Provider's `jwks_uri` (JSON Web Key Set).
|
|
20
|
+
*
|
|
21
|
+
* @see {@link https://openid.net/specs/openid-connect-discovery-1_0-21.html#ProviderMetadata | OpenID Connect Discovery 1.0}
|
|
22
|
+
*/
|
|
23
|
+
Jwks = "jwks"
|
|
24
|
+
}
|
|
25
|
+
export type Storage<Keys extends string> = {
|
|
26
|
+
getItem(key: Keys): Promise<Nullable<string>>;
|
|
27
|
+
setItem(key: Keys, value: string): Promise<void>;
|
|
28
|
+
removeItem(key: Keys): Promise<void>;
|
|
29
|
+
};
|
|
30
|
+
export type InferStorageKey<S> = S extends Storage<infer Key> ? Key : never;
|
|
31
|
+
export type Navigate = (url: string) => void;
|
|
32
|
+
export type ClientAdapter = {
|
|
33
|
+
requester: Requester;
|
|
34
|
+
storage: Storage<StorageKey | PersistKey>;
|
|
35
|
+
/**
|
|
36
|
+
* An optional storage for caching well-known data.
|
|
37
|
+
*
|
|
38
|
+
* @see {@link CacheKey}
|
|
39
|
+
*/
|
|
40
|
+
unstable_cache?: Storage<CacheKey>;
|
|
41
|
+
navigate: Navigate;
|
|
42
|
+
generateState: () => string;
|
|
43
|
+
generateCodeVerifier: () => string;
|
|
44
|
+
generateCodeChallenge: (codeVerifier: string) => Promise<string>;
|
|
45
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
var PersistKey;
|
|
2
|
+
(function (PersistKey) {
|
|
3
|
+
PersistKey["IdToken"] = "idToken";
|
|
4
|
+
PersistKey["RefreshToken"] = "refreshToken";
|
|
5
|
+
PersistKey["AccessToken"] = "accessToken";
|
|
6
|
+
PersistKey["SignInSession"] = "signInSession";
|
|
7
|
+
})(PersistKey || (PersistKey = {}));
|
|
8
|
+
var CacheKey;
|
|
9
|
+
(function (CacheKey) {
|
|
10
|
+
/**
|
|
11
|
+
* OpenID Configuration endpoint response.
|
|
12
|
+
*
|
|
13
|
+
* @see {@link https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse | OpenID Connect Discovery 1.0}
|
|
14
|
+
*/
|
|
15
|
+
CacheKey["OpenidConfig"] = "openidConfiguration";
|
|
16
|
+
/**
|
|
17
|
+
* The content of OpenID Provider's `jwks_uri` (JSON Web Key Set).
|
|
18
|
+
*
|
|
19
|
+
* @see {@link https://openid.net/specs/openid-connect-discovery-1_0-21.html#ProviderMetadata | OpenID Connect Discovery 1.0}
|
|
20
|
+
*/
|
|
21
|
+
CacheKey["Jwks"] = "jwks";
|
|
22
|
+
})(CacheKey || (CacheKey = {}));
|
|
23
|
+
|
|
24
|
+
export { CacheKey, PersistKey };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/index.cjs
CHANGED
|
@@ -4,23 +4,27 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
4
4
|
|
|
5
5
|
var js = require('@logto/js');
|
|
6
6
|
var jose = require('jose');
|
|
7
|
+
var index$1 = require('./adapter/index.cjs');
|
|
7
8
|
var errors = require('./errors.cjs');
|
|
9
|
+
var remoteJwkSet = require('./remote-jwk-set.cjs');
|
|
8
10
|
var index = require('./types/index.cjs');
|
|
9
|
-
var index$
|
|
11
|
+
var index$2 = require('./utils/index.cjs');
|
|
12
|
+
var memoize = require('./utils/memoize.cjs');
|
|
10
13
|
var once = require('./utils/once.cjs');
|
|
14
|
+
var types = require('./adapter/types.cjs');
|
|
11
15
|
var requester = require('./utils/requester.cjs');
|
|
12
16
|
|
|
13
17
|
class LogtoClient {
|
|
14
18
|
constructor(logtoConfig, adapter) {
|
|
15
|
-
this.getOidcConfig =
|
|
16
|
-
this.getJwtVerifyGetKey = once.once(this
|
|
19
|
+
this.getOidcConfig = memoize.memoize(this.#getOidcConfig);
|
|
20
|
+
this.getJwtVerifyGetKey = once.once(this.#getJwtVerifyGetKey);
|
|
17
21
|
this.accessTokenMap = new Map();
|
|
18
22
|
this.logtoConfig = {
|
|
19
23
|
...logtoConfig,
|
|
20
24
|
prompt: logtoConfig.prompt ?? js.Prompt.Consent,
|
|
21
25
|
scopes: js.withDefaultScopes(logtoConfig.scopes).split(' '),
|
|
22
26
|
};
|
|
23
|
-
this.adapter = adapter;
|
|
27
|
+
this.adapter = new index$1.ClientAdapterInstance(adapter);
|
|
24
28
|
void this.loadAccessTokenMap();
|
|
25
29
|
}
|
|
26
30
|
async isAuthenticated() {
|
|
@@ -36,7 +40,7 @@ class LogtoClient {
|
|
|
36
40
|
if (!(await this.getIdToken())) {
|
|
37
41
|
throw new errors.LogtoClientError('not_authenticated');
|
|
38
42
|
}
|
|
39
|
-
const accessTokenKey = index$
|
|
43
|
+
const accessTokenKey = index$2.buildAccessTokenKey(resource);
|
|
40
44
|
const accessToken = this.accessTokenMap.get(accessTokenKey);
|
|
41
45
|
if (accessToken && accessToken.expiresAt > Date.now() / 1000) {
|
|
42
46
|
return accessToken.token;
|
|
@@ -101,25 +105,38 @@ class LogtoClient {
|
|
|
101
105
|
return `${origin}${pathname}` === redirectUri;
|
|
102
106
|
}
|
|
103
107
|
async handleSignInCallback(callbackUri) {
|
|
104
|
-
const {
|
|
105
|
-
const { requester } = adapter;
|
|
108
|
+
const { requester } = this.adapter;
|
|
106
109
|
const signInSession = await this.getSignInSession();
|
|
107
110
|
if (!signInSession) {
|
|
108
111
|
throw new errors.LogtoClientError('sign_in_session.not_found');
|
|
109
112
|
}
|
|
110
113
|
const { redirectUri, state, codeVerifier } = signInSession;
|
|
111
114
|
const code = js.verifyAndParseCodeFromCallbackUri(callbackUri, redirectUri, state);
|
|
112
|
-
|
|
115
|
+
// NOTE: Will add scope to accessTokenKey when needed. (Linear issue LOG-1589)
|
|
116
|
+
const accessTokenKey = index$2.buildAccessTokenKey();
|
|
117
|
+
const { appId: clientId } = this.logtoConfig;
|
|
113
118
|
const { tokenEndpoint } = await this.getOidcConfig();
|
|
114
|
-
const
|
|
119
|
+
const requestedAt = Math.round(Date.now() / 1000);
|
|
120
|
+
const { idToken, refreshToken, accessToken, scope, expiresIn } = await js.fetchTokenByAuthorizationCode({
|
|
115
121
|
clientId,
|
|
116
122
|
tokenEndpoint,
|
|
117
123
|
redirectUri,
|
|
118
124
|
codeVerifier,
|
|
119
125
|
code,
|
|
120
126
|
}, requester);
|
|
121
|
-
await this.verifyIdToken(
|
|
122
|
-
await this.
|
|
127
|
+
await this.verifyIdToken(idToken);
|
|
128
|
+
await this.setRefreshToken(refreshToken ?? null);
|
|
129
|
+
await this.setIdToken(idToken);
|
|
130
|
+
this.accessTokenMap.set(accessTokenKey, {
|
|
131
|
+
token: accessToken,
|
|
132
|
+
scope,
|
|
133
|
+
/** The `expiresAt` variable provides an approximate estimation of the actual `exp` property
|
|
134
|
+
* in the token claims. It is utilized by the client to determine if the cached access token
|
|
135
|
+
* has expired and when a new access token should be requested.
|
|
136
|
+
*/
|
|
137
|
+
expiresAt: requestedAt + expiresIn,
|
|
138
|
+
});
|
|
139
|
+
await this.saveAccessTokenMap();
|
|
123
140
|
await this.setSignInSession(null);
|
|
124
141
|
}
|
|
125
142
|
async signOut(postLogoutRedirectUri) {
|
|
@@ -156,36 +173,24 @@ class LogtoClient {
|
|
|
156
173
|
}
|
|
157
174
|
return item;
|
|
158
175
|
}
|
|
159
|
-
async setSignInSession(
|
|
160
|
-
|
|
161
|
-
await this.adapter.storage.removeItem('signInSession');
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
const jsonItem = JSON.stringify(logtoSignInSessionItem);
|
|
165
|
-
await this.adapter.storage.setItem('signInSession', jsonItem);
|
|
176
|
+
async setSignInSession(value) {
|
|
177
|
+
return this.adapter.setStorageItem(types.PersistKey.SignInSession, value && JSON.stringify(value));
|
|
166
178
|
}
|
|
167
|
-
async setIdToken(
|
|
168
|
-
|
|
169
|
-
await this.adapter.storage.removeItem('idToken');
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
await this.adapter.storage.setItem('idToken', idToken);
|
|
179
|
+
async setIdToken(value) {
|
|
180
|
+
return this.adapter.setStorageItem(types.PersistKey.IdToken, value);
|
|
173
181
|
}
|
|
174
|
-
async setRefreshToken(
|
|
175
|
-
|
|
176
|
-
await this.adapter.storage.removeItem('refreshToken');
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
await this.adapter.storage.setItem('refreshToken', refreshToken);
|
|
182
|
+
async setRefreshToken(value) {
|
|
183
|
+
return this.adapter.setStorageItem(types.PersistKey.RefreshToken, value);
|
|
180
184
|
}
|
|
181
185
|
async getAccessTokenByRefreshToken(resource) {
|
|
182
186
|
const currentRefreshToken = await this.getRefreshToken();
|
|
183
187
|
if (!currentRefreshToken) {
|
|
184
188
|
throw new errors.LogtoClientError('not_authenticated');
|
|
185
189
|
}
|
|
186
|
-
const accessTokenKey = index$
|
|
190
|
+
const accessTokenKey = index$2.buildAccessTokenKey(resource);
|
|
187
191
|
const { appId: clientId } = this.logtoConfig;
|
|
188
192
|
const { tokenEndpoint } = await this.getOidcConfig();
|
|
193
|
+
const requestedAt = Math.round(Date.now() / 1000);
|
|
189
194
|
const { accessToken, refreshToken, idToken, scope, expiresIn } = await js.fetchTokenByRefreshToken({
|
|
190
195
|
clientId,
|
|
191
196
|
tokenEndpoint,
|
|
@@ -195,7 +200,11 @@ class LogtoClient {
|
|
|
195
200
|
this.accessTokenMap.set(accessTokenKey, {
|
|
196
201
|
token: accessToken,
|
|
197
202
|
scope,
|
|
198
|
-
expiresAt
|
|
203
|
+
/** The `expiresAt` variable provides an approximate estimation of the actual `exp` property
|
|
204
|
+
* in the token claims. It is utilized by the client to determine if the cached access token
|
|
205
|
+
* has expired and when a new access token should be requested.
|
|
206
|
+
*/
|
|
207
|
+
expiresAt: requestedAt + expiresIn,
|
|
199
208
|
});
|
|
200
209
|
await this.saveAccessTokenMap();
|
|
201
210
|
await this.setRefreshToken(refreshToken);
|
|
@@ -205,30 +214,12 @@ class LogtoClient {
|
|
|
205
214
|
}
|
|
206
215
|
return accessToken;
|
|
207
216
|
}
|
|
208
|
-
async _getOidcConfig() {
|
|
209
|
-
const { endpoint } = this.logtoConfig;
|
|
210
|
-
const discoveryEndpoint = index$1.getDiscoveryEndpoint(endpoint);
|
|
211
|
-
return js.fetchOidcConfig(discoveryEndpoint, this.adapter.requester);
|
|
212
|
-
}
|
|
213
|
-
async _getJwtVerifyGetKey() {
|
|
214
|
-
const { jwksUri } = await this.getOidcConfig();
|
|
215
|
-
return jose.createRemoteJWKSet(new URL(jwksUri));
|
|
216
|
-
}
|
|
217
217
|
async verifyIdToken(idToken) {
|
|
218
218
|
const { appId } = this.logtoConfig;
|
|
219
219
|
const { issuer } = await this.getOidcConfig();
|
|
220
220
|
const jwtVerifyGetKey = await this.getJwtVerifyGetKey();
|
|
221
221
|
await js.verifyIdToken(idToken, appId, issuer, jwtVerifyGetKey);
|
|
222
222
|
}
|
|
223
|
-
async saveCodeToken({ refreshToken, idToken, scope, accessToken, expiresIn, }) {
|
|
224
|
-
await this.setRefreshToken(refreshToken ?? null);
|
|
225
|
-
await this.setIdToken(idToken);
|
|
226
|
-
// NOTE: Will add scope to accessTokenKey when needed. (Linear issue LOG-1589)
|
|
227
|
-
const accessTokenKey = index$1.buildAccessTokenKey();
|
|
228
|
-
const expiresAt = Date.now() / 1000 + expiresIn;
|
|
229
|
-
this.accessTokenMap.set(accessTokenKey, { token: accessToken, scope, expiresAt });
|
|
230
|
-
await this.saveAccessTokenMap();
|
|
231
|
-
}
|
|
232
223
|
async saveAccessTokenMap() {
|
|
233
224
|
const data = {};
|
|
234
225
|
for (const [key, accessToken] of this.accessTokenMap.entries()) {
|
|
@@ -256,6 +247,19 @@ class LogtoClient {
|
|
|
256
247
|
console.warn(error);
|
|
257
248
|
}
|
|
258
249
|
}
|
|
250
|
+
async #getOidcConfig() {
|
|
251
|
+
return this.adapter.getWithCache(types.CacheKey.OpenidConfig, async () => {
|
|
252
|
+
return js.fetchOidcConfig(index$2.getDiscoveryEndpoint(this.logtoConfig.endpoint), this.adapter.requester);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
async #getJwtVerifyGetKey() {
|
|
256
|
+
const { jwksUri } = await this.getOidcConfig();
|
|
257
|
+
if (!this.adapter.unstable_cache) {
|
|
258
|
+
return jose.createRemoteJWKSet(new URL(jwksUri));
|
|
259
|
+
}
|
|
260
|
+
const cachedJwkSet = new remoteJwkSet.CachedRemoteJwkSet(new URL(jwksUri), this.adapter);
|
|
261
|
+
return async (...args) => cachedJwkSet.getKey(...args);
|
|
262
|
+
}
|
|
259
263
|
}
|
|
260
264
|
|
|
261
265
|
Object.defineProperty(exports, 'LogtoError', {
|
|
@@ -285,5 +289,13 @@ Object.defineProperty(exports, 'UserScope', {
|
|
|
285
289
|
exports.LogtoClientError = errors.LogtoClientError;
|
|
286
290
|
exports.isLogtoAccessTokenMap = index.isLogtoAccessTokenMap;
|
|
287
291
|
exports.isLogtoSignInSessionItem = index.isLogtoSignInSessionItem;
|
|
292
|
+
Object.defineProperty(exports, 'CacheKey', {
|
|
293
|
+
enumerable: true,
|
|
294
|
+
get: function () { return types.CacheKey; }
|
|
295
|
+
});
|
|
296
|
+
Object.defineProperty(exports, 'PersistKey', {
|
|
297
|
+
enumerable: true,
|
|
298
|
+
get: function () { return types.PersistKey; }
|
|
299
|
+
});
|
|
288
300
|
exports.createRequester = requester.createRequester;
|
|
289
301
|
exports.default = LogtoClient;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/index.d.ts
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
import { type IdTokenClaims, type UserInfoResponse, type InteractionMode, type AccessTokenClaims } from '@logto/js';
|
|
2
|
-
import type
|
|
3
|
-
import type
|
|
2
|
+
import { type Nullable } from '@silverhand/essentials';
|
|
3
|
+
import { type JWTVerifyGetKey } from 'jose';
|
|
4
|
+
import { ClientAdapterInstance, type ClientAdapter } from './adapter/index.js';
|
|
4
5
|
import type { AccessToken, LogtoConfig, LogtoSignInSessionItem } from './types/index.js';
|
|
5
6
|
export type { IdTokenClaims, LogtoErrorCode, UserInfoResponse, InteractionMode } from '@logto/js';
|
|
6
7
|
export { LogtoError, OidcError, Prompt, LogtoRequestError, ReservedScope, UserScope, } from '@logto/js';
|
|
7
8
|
export * from './errors.js';
|
|
8
|
-
export type { Storage, StorageKey, ClientAdapter } from './adapter.js';
|
|
9
|
+
export type { Storage, StorageKey, ClientAdapter } from './adapter/index.js';
|
|
10
|
+
export { PersistKey, CacheKey } from './adapter/index.js';
|
|
9
11
|
export { createRequester } from './utils/index.js';
|
|
10
12
|
export * from './types/index.js';
|
|
11
13
|
export default class LogtoClient {
|
|
14
|
+
#private;
|
|
12
15
|
protected readonly logtoConfig: LogtoConfig;
|
|
13
|
-
protected readonly getOidcConfig:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
protected readonly getOidcConfig: (this: unknown) => Promise<import("@silverhand/essentials").KeysToCamelCase<{
|
|
17
|
+
authorization_endpoint: string;
|
|
18
|
+
token_endpoint: string;
|
|
19
|
+
userinfo_endpoint: string;
|
|
20
|
+
end_session_endpoint: string;
|
|
21
|
+
revocation_endpoint: string;
|
|
22
|
+
jwks_uri: string;
|
|
23
|
+
issuer: string;
|
|
24
|
+
}>>;
|
|
25
|
+
protected readonly getJwtVerifyGetKey: (...args: unknown[]) => Promise<JWTVerifyGetKey>;
|
|
26
|
+
protected readonly adapter: ClientAdapterInstance;
|
|
16
27
|
protected readonly accessTokenMap: Map<string, AccessToken>;
|
|
17
28
|
constructor(logtoConfig: LogtoConfig, adapter: ClientAdapter);
|
|
18
29
|
isAuthenticated(): Promise<boolean>;
|
|
@@ -27,14 +38,11 @@ export default class LogtoClient {
|
|
|
27
38
|
handleSignInCallback(callbackUri: string): Promise<void>;
|
|
28
39
|
signOut(postLogoutRedirectUri?: string): Promise<void>;
|
|
29
40
|
protected getSignInSession(): Promise<Nullable<LogtoSignInSessionItem>>;
|
|
30
|
-
protected setSignInSession(
|
|
41
|
+
protected setSignInSession(value: Nullable<LogtoSignInSessionItem>): Promise<void>;
|
|
31
42
|
private setIdToken;
|
|
32
43
|
private setRefreshToken;
|
|
33
44
|
private getAccessTokenByRefreshToken;
|
|
34
|
-
private _getOidcConfig;
|
|
35
|
-
private _getJwtVerifyGetKey;
|
|
36
45
|
private verifyIdToken;
|
|
37
|
-
private saveCodeToken;
|
|
38
46
|
private saveAccessTokenMap;
|
|
39
47
|
private loadAccessTokenMap;
|
|
40
48
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
|
-
import { Prompt, withDefaultScopes, decodeIdToken, decodeAccessToken, fetchUserInfo, generateSignInUri, verifyAndParseCodeFromCallbackUri, fetchTokenByAuthorizationCode, revoke, generateSignOutUri, fetchTokenByRefreshToken,
|
|
1
|
+
import { Prompt, withDefaultScopes, decodeIdToken, decodeAccessToken, fetchUserInfo, generateSignInUri, verifyAndParseCodeFromCallbackUri, fetchTokenByAuthorizationCode, revoke, generateSignOutUri, fetchTokenByRefreshToken, verifyIdToken, fetchOidcConfig } from '@logto/js';
|
|
2
2
|
export { LogtoError, LogtoRequestError, OidcError, Prompt, ReservedScope, UserScope } from '@logto/js';
|
|
3
3
|
import { createRemoteJWKSet } from 'jose';
|
|
4
|
+
import { ClientAdapterInstance } from './adapter/index.js';
|
|
4
5
|
import { LogtoClientError } from './errors.js';
|
|
6
|
+
import { CachedRemoteJwkSet } from './remote-jwk-set.js';
|
|
5
7
|
import { isLogtoSignInSessionItem, isLogtoAccessTokenMap } from './types/index.js';
|
|
6
8
|
import { buildAccessTokenKey, getDiscoveryEndpoint } from './utils/index.js';
|
|
9
|
+
import { memoize } from './utils/memoize.js';
|
|
7
10
|
import { once } from './utils/once.js';
|
|
11
|
+
import { PersistKey, CacheKey } from './adapter/types.js';
|
|
8
12
|
export { createRequester } from './utils/requester.js';
|
|
9
13
|
|
|
10
14
|
class LogtoClient {
|
|
11
15
|
constructor(logtoConfig, adapter) {
|
|
12
|
-
this.getOidcConfig =
|
|
13
|
-
this.getJwtVerifyGetKey = once(this
|
|
16
|
+
this.getOidcConfig = memoize(this.#getOidcConfig);
|
|
17
|
+
this.getJwtVerifyGetKey = once(this.#getJwtVerifyGetKey);
|
|
14
18
|
this.accessTokenMap = new Map();
|
|
15
19
|
this.logtoConfig = {
|
|
16
20
|
...logtoConfig,
|
|
17
21
|
prompt: logtoConfig.prompt ?? Prompt.Consent,
|
|
18
22
|
scopes: withDefaultScopes(logtoConfig.scopes).split(' '),
|
|
19
23
|
};
|
|
20
|
-
this.adapter = adapter;
|
|
24
|
+
this.adapter = new ClientAdapterInstance(adapter);
|
|
21
25
|
void this.loadAccessTokenMap();
|
|
22
26
|
}
|
|
23
27
|
async isAuthenticated() {
|
|
@@ -98,25 +102,38 @@ class LogtoClient {
|
|
|
98
102
|
return `${origin}${pathname}` === redirectUri;
|
|
99
103
|
}
|
|
100
104
|
async handleSignInCallback(callbackUri) {
|
|
101
|
-
const {
|
|
102
|
-
const { requester } = adapter;
|
|
105
|
+
const { requester } = this.adapter;
|
|
103
106
|
const signInSession = await this.getSignInSession();
|
|
104
107
|
if (!signInSession) {
|
|
105
108
|
throw new LogtoClientError('sign_in_session.not_found');
|
|
106
109
|
}
|
|
107
110
|
const { redirectUri, state, codeVerifier } = signInSession;
|
|
108
111
|
const code = verifyAndParseCodeFromCallbackUri(callbackUri, redirectUri, state);
|
|
109
|
-
|
|
112
|
+
// NOTE: Will add scope to accessTokenKey when needed. (Linear issue LOG-1589)
|
|
113
|
+
const accessTokenKey = buildAccessTokenKey();
|
|
114
|
+
const { appId: clientId } = this.logtoConfig;
|
|
110
115
|
const { tokenEndpoint } = await this.getOidcConfig();
|
|
111
|
-
const
|
|
116
|
+
const requestedAt = Math.round(Date.now() / 1000);
|
|
117
|
+
const { idToken, refreshToken, accessToken, scope, expiresIn } = await fetchTokenByAuthorizationCode({
|
|
112
118
|
clientId,
|
|
113
119
|
tokenEndpoint,
|
|
114
120
|
redirectUri,
|
|
115
121
|
codeVerifier,
|
|
116
122
|
code,
|
|
117
123
|
}, requester);
|
|
118
|
-
await this.verifyIdToken(
|
|
119
|
-
await this.
|
|
124
|
+
await this.verifyIdToken(idToken);
|
|
125
|
+
await this.setRefreshToken(refreshToken ?? null);
|
|
126
|
+
await this.setIdToken(idToken);
|
|
127
|
+
this.accessTokenMap.set(accessTokenKey, {
|
|
128
|
+
token: accessToken,
|
|
129
|
+
scope,
|
|
130
|
+
/** The `expiresAt` variable provides an approximate estimation of the actual `exp` property
|
|
131
|
+
* in the token claims. It is utilized by the client to determine if the cached access token
|
|
132
|
+
* has expired and when a new access token should be requested.
|
|
133
|
+
*/
|
|
134
|
+
expiresAt: requestedAt + expiresIn,
|
|
135
|
+
});
|
|
136
|
+
await this.saveAccessTokenMap();
|
|
120
137
|
await this.setSignInSession(null);
|
|
121
138
|
}
|
|
122
139
|
async signOut(postLogoutRedirectUri) {
|
|
@@ -153,27 +170,14 @@ class LogtoClient {
|
|
|
153
170
|
}
|
|
154
171
|
return item;
|
|
155
172
|
}
|
|
156
|
-
async setSignInSession(
|
|
157
|
-
|
|
158
|
-
await this.adapter.storage.removeItem('signInSession');
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
const jsonItem = JSON.stringify(logtoSignInSessionItem);
|
|
162
|
-
await this.adapter.storage.setItem('signInSession', jsonItem);
|
|
173
|
+
async setSignInSession(value) {
|
|
174
|
+
return this.adapter.setStorageItem(PersistKey.SignInSession, value && JSON.stringify(value));
|
|
163
175
|
}
|
|
164
|
-
async setIdToken(
|
|
165
|
-
|
|
166
|
-
await this.adapter.storage.removeItem('idToken');
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
await this.adapter.storage.setItem('idToken', idToken);
|
|
176
|
+
async setIdToken(value) {
|
|
177
|
+
return this.adapter.setStorageItem(PersistKey.IdToken, value);
|
|
170
178
|
}
|
|
171
|
-
async setRefreshToken(
|
|
172
|
-
|
|
173
|
-
await this.adapter.storage.removeItem('refreshToken');
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
await this.adapter.storage.setItem('refreshToken', refreshToken);
|
|
179
|
+
async setRefreshToken(value) {
|
|
180
|
+
return this.adapter.setStorageItem(PersistKey.RefreshToken, value);
|
|
177
181
|
}
|
|
178
182
|
async getAccessTokenByRefreshToken(resource) {
|
|
179
183
|
const currentRefreshToken = await this.getRefreshToken();
|
|
@@ -183,6 +187,7 @@ class LogtoClient {
|
|
|
183
187
|
const accessTokenKey = buildAccessTokenKey(resource);
|
|
184
188
|
const { appId: clientId } = this.logtoConfig;
|
|
185
189
|
const { tokenEndpoint } = await this.getOidcConfig();
|
|
190
|
+
const requestedAt = Math.round(Date.now() / 1000);
|
|
186
191
|
const { accessToken, refreshToken, idToken, scope, expiresIn } = await fetchTokenByRefreshToken({
|
|
187
192
|
clientId,
|
|
188
193
|
tokenEndpoint,
|
|
@@ -192,7 +197,11 @@ class LogtoClient {
|
|
|
192
197
|
this.accessTokenMap.set(accessTokenKey, {
|
|
193
198
|
token: accessToken,
|
|
194
199
|
scope,
|
|
195
|
-
expiresAt
|
|
200
|
+
/** The `expiresAt` variable provides an approximate estimation of the actual `exp` property
|
|
201
|
+
* in the token claims. It is utilized by the client to determine if the cached access token
|
|
202
|
+
* has expired and when a new access token should be requested.
|
|
203
|
+
*/
|
|
204
|
+
expiresAt: requestedAt + expiresIn,
|
|
196
205
|
});
|
|
197
206
|
await this.saveAccessTokenMap();
|
|
198
207
|
await this.setRefreshToken(refreshToken);
|
|
@@ -202,30 +211,12 @@ class LogtoClient {
|
|
|
202
211
|
}
|
|
203
212
|
return accessToken;
|
|
204
213
|
}
|
|
205
|
-
async _getOidcConfig() {
|
|
206
|
-
const { endpoint } = this.logtoConfig;
|
|
207
|
-
const discoveryEndpoint = getDiscoveryEndpoint(endpoint);
|
|
208
|
-
return fetchOidcConfig(discoveryEndpoint, this.adapter.requester);
|
|
209
|
-
}
|
|
210
|
-
async _getJwtVerifyGetKey() {
|
|
211
|
-
const { jwksUri } = await this.getOidcConfig();
|
|
212
|
-
return createRemoteJWKSet(new URL(jwksUri));
|
|
213
|
-
}
|
|
214
214
|
async verifyIdToken(idToken) {
|
|
215
215
|
const { appId } = this.logtoConfig;
|
|
216
216
|
const { issuer } = await this.getOidcConfig();
|
|
217
217
|
const jwtVerifyGetKey = await this.getJwtVerifyGetKey();
|
|
218
218
|
await verifyIdToken(idToken, appId, issuer, jwtVerifyGetKey);
|
|
219
219
|
}
|
|
220
|
-
async saveCodeToken({ refreshToken, idToken, scope, accessToken, expiresIn, }) {
|
|
221
|
-
await this.setRefreshToken(refreshToken ?? null);
|
|
222
|
-
await this.setIdToken(idToken);
|
|
223
|
-
// NOTE: Will add scope to accessTokenKey when needed. (Linear issue LOG-1589)
|
|
224
|
-
const accessTokenKey = buildAccessTokenKey();
|
|
225
|
-
const expiresAt = Date.now() / 1000 + expiresIn;
|
|
226
|
-
this.accessTokenMap.set(accessTokenKey, { token: accessToken, scope, expiresAt });
|
|
227
|
-
await this.saveAccessTokenMap();
|
|
228
|
-
}
|
|
229
220
|
async saveAccessTokenMap() {
|
|
230
221
|
const data = {};
|
|
231
222
|
for (const [key, accessToken] of this.accessTokenMap.entries()) {
|
|
@@ -253,6 +244,19 @@ class LogtoClient {
|
|
|
253
244
|
console.warn(error);
|
|
254
245
|
}
|
|
255
246
|
}
|
|
247
|
+
async #getOidcConfig() {
|
|
248
|
+
return this.adapter.getWithCache(CacheKey.OpenidConfig, async () => {
|
|
249
|
+
return fetchOidcConfig(getDiscoveryEndpoint(this.logtoConfig.endpoint), this.adapter.requester);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
async #getJwtVerifyGetKey() {
|
|
253
|
+
const { jwksUri } = await this.getOidcConfig();
|
|
254
|
+
if (!this.adapter.unstable_cache) {
|
|
255
|
+
return createRemoteJWKSet(new URL(jwksUri));
|
|
256
|
+
}
|
|
257
|
+
const cachedJwkSet = new CachedRemoteJwkSet(new URL(jwksUri), this.adapter);
|
|
258
|
+
return async (...args) => cachedJwkSet.getKey(...args);
|
|
259
|
+
}
|
|
256
260
|
}
|
|
257
261
|
|
|
258
|
-
export { LogtoClientError, LogtoClient as default, isLogtoAccessTokenMap, isLogtoSignInSessionItem };
|
|
262
|
+
export { CacheKey, LogtoClientError, PersistKey, LogtoClient as default, isLogtoAccessTokenMap, isLogtoSignInSessionItem };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/mock.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/// <reference types="jest" />
|
|
2
2
|
import { Prompt } from '@logto/js';
|
|
3
|
-
import type
|
|
4
|
-
import type { Storage } from './adapter.js';
|
|
3
|
+
import { type Nullable } from '@silverhand/essentials';
|
|
4
|
+
import type { Storage } from './adapter/index.js';
|
|
5
5
|
import type { AccessToken, LogtoConfig, LogtoSignInSessionItem } from './index.js';
|
|
6
6
|
import LogtoClient from './index.js';
|
|
7
7
|
export declare const appId = "app_id_value";
|
|
8
8
|
export declare const endpoint = "https://logto.dev";
|
|
9
|
-
export declare class MockedStorage implements Storage {
|
|
9
|
+
export declare class MockedStorage implements Storage<string> {
|
|
10
10
|
private storage;
|
|
11
11
|
constructor(values?: Record<string, string>);
|
|
12
12
|
getItem(key: string): Promise<string | null>;
|
|
@@ -33,6 +33,15 @@ export declare const accessToken = "access_token_value";
|
|
|
33
33
|
export declare const refreshToken = "new_refresh_token_value";
|
|
34
34
|
export declare const idToken = "id_token_value";
|
|
35
35
|
export declare const currentUnixTimeStamp: number;
|
|
36
|
+
export declare const mockFetchOidcConfig: (delay?: number) => jest.Mock<Promise<{
|
|
37
|
+
authorizationEndpoint: string;
|
|
38
|
+
tokenEndpoint: string;
|
|
39
|
+
userinfoEndpoint: string;
|
|
40
|
+
endSessionEndpoint: string;
|
|
41
|
+
revocationEndpoint: string;
|
|
42
|
+
jwksUri: string;
|
|
43
|
+
issuer: string;
|
|
44
|
+
}>, [], any>;
|
|
36
45
|
export declare const fetchOidcConfig: jest.Mock<Promise<{
|
|
37
46
|
authorizationEndpoint: string;
|
|
38
47
|
tokenEndpoint: string;
|
|
@@ -48,19 +57,30 @@ export declare const navigate: jest.Mock<any, any, any>;
|
|
|
48
57
|
export declare const generateCodeChallenge: jest.Mock<Promise<string>, [], any>;
|
|
49
58
|
export declare const generateCodeVerifier: jest.Mock<string, [], any>;
|
|
50
59
|
export declare const generateState: jest.Mock<string, [], any>;
|
|
51
|
-
export declare const createAdapters: () => {
|
|
60
|
+
export declare const createAdapters: (withCache?: boolean) => {
|
|
52
61
|
requester: jest.Mock<any, any, any>;
|
|
53
62
|
storage: MockedStorage;
|
|
63
|
+
unstable_cache: import("@silverhand/essentials").Optional<MockedStorage>;
|
|
54
64
|
navigate: jest.Mock<any, any, any>;
|
|
55
65
|
generateCodeChallenge: jest.Mock<Promise<string>, [], any>;
|
|
56
66
|
generateCodeVerifier: jest.Mock<string, [], any>;
|
|
57
67
|
generateState: jest.Mock<string, [], any>;
|
|
58
68
|
};
|
|
59
|
-
export declare const createClient: (prompt?: Prompt, storage?: MockedStorage) =>
|
|
69
|
+
export declare const createClient: (prompt?: Prompt, storage?: MockedStorage, withCache?: boolean) => LogtoClientWithAccessors;
|
|
60
70
|
/**
|
|
61
|
-
* Make
|
|
71
|
+
* Make protected fields accessible for test
|
|
62
72
|
*/
|
|
63
|
-
export declare class
|
|
73
|
+
export declare class LogtoClientWithAccessors extends LogtoClient {
|
|
74
|
+
runGetOidcConfig(): Promise<import("@silverhand/essentials").KeysToCamelCase<{
|
|
75
|
+
authorization_endpoint: string;
|
|
76
|
+
token_endpoint: string;
|
|
77
|
+
userinfo_endpoint: string;
|
|
78
|
+
end_session_endpoint: string;
|
|
79
|
+
revocation_endpoint: string;
|
|
80
|
+
jwks_uri: string;
|
|
81
|
+
issuer: string;
|
|
82
|
+
}>>;
|
|
83
|
+
runGetJwtVerifyGetKey(): Promise<import("jose").JWTVerifyGetKey>;
|
|
64
84
|
getLogtoConfig(): Nullable<LogtoConfig>;
|
|
65
85
|
getSignInSessionItem(): Promise<Nullable<LogtoSignInSessionItem>>;
|
|
66
86
|
setSignInSessionItem(item: Nullable<LogtoSignInSessionItem>): Promise<void>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var essentials = require('@silverhand/essentials');
|
|
4
|
+
var jose = require('jose');
|
|
5
|
+
var types = require('./adapter/types.cjs');
|
|
6
|
+
|
|
7
|
+
// Edited from jose's internal util `isJWKSLike`
|
|
8
|
+
function isJwkSetLike(jwkSet) {
|
|
9
|
+
return Boolean(jwkSet &&
|
|
10
|
+
typeof jwkSet === 'object' &&
|
|
11
|
+
'keys' in jwkSet &&
|
|
12
|
+
Array.isArray(jwkSet.keys) &&
|
|
13
|
+
jwkSet.keys.every((element) => essentials.isObject(element)));
|
|
14
|
+
}
|
|
15
|
+
class CachedRemoteJwkSet {
|
|
16
|
+
constructor(url, adapter) {
|
|
17
|
+
this.url = url;
|
|
18
|
+
this.adapter = adapter;
|
|
19
|
+
if (!adapter.unstable_cache) {
|
|
20
|
+
throw new Error("No cache found in the client adapter. Use `createRemoteJWKSet()` from 'jose' instead.");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async getKey(...args) {
|
|
24
|
+
if (!this.jwkSet) {
|
|
25
|
+
this.jwkSet = await this.#load();
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return await this.#getLocalKey(...args);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
// Jose does not export the error definition
|
|
32
|
+
// Found in https://github.com/panva/jose/blob/d5b3cb672736112b1e1e31ac4d5e9cd641675206/src/util/errors.ts#L347
|
|
33
|
+
if (error instanceof Error && 'code' in error && error.code === 'ERR_JWKS_NO_MATCHING_KEY') {
|
|
34
|
+
this.jwkSet = await this.#load();
|
|
35
|
+
return this.#getLocalKey(...args);
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async #load() {
|
|
41
|
+
return this.adapter.getWithCache(types.CacheKey.Jwks, async () => {
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
const response = await fetch(this.url, { signal: controller.signal, redirect: 'manual' });
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
throw new Error('Expected OK from the JSON Web Key Set HTTP response');
|
|
46
|
+
}
|
|
47
|
+
const json = await response.json();
|
|
48
|
+
if (!isJwkSetLike(json)) {
|
|
49
|
+
throw new Error('JSON Web Key Set malformed');
|
|
50
|
+
}
|
|
51
|
+
return json;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async #getLocalKey(...args) {
|
|
55
|
+
if (!this.jwkSet) {
|
|
56
|
+
throw new Error('No local JWK Set found.');
|
|
57
|
+
}
|
|
58
|
+
return jose.createLocalJWKSet(this.jwkSet)(...args);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
exports.CachedRemoteJwkSet = CachedRemoteJwkSet;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type JSONWebKeySet, type JWTVerifyGetKey } from 'jose';
|
|
2
|
+
import { type ClientAdapterInstance } from './adapter/index.js';
|
|
3
|
+
export declare class CachedRemoteJwkSet {
|
|
4
|
+
#private;
|
|
5
|
+
readonly url: URL;
|
|
6
|
+
private readonly adapter;
|
|
7
|
+
protected jwkSet?: JSONWebKeySet;
|
|
8
|
+
constructor(url: URL, adapter: ClientAdapterInstance);
|
|
9
|
+
getKey(...args: Parameters<JWTVerifyGetKey>): Promise<import("jose").KeyLike>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { isObject } from '@silverhand/essentials';
|
|
2
|
+
import { createLocalJWKSet } from 'jose';
|
|
3
|
+
import { CacheKey } from './adapter/types.js';
|
|
4
|
+
|
|
5
|
+
// Edited from jose's internal util `isJWKSLike`
|
|
6
|
+
function isJwkSetLike(jwkSet) {
|
|
7
|
+
return Boolean(jwkSet &&
|
|
8
|
+
typeof jwkSet === 'object' &&
|
|
9
|
+
'keys' in jwkSet &&
|
|
10
|
+
Array.isArray(jwkSet.keys) &&
|
|
11
|
+
jwkSet.keys.every((element) => isObject(element)));
|
|
12
|
+
}
|
|
13
|
+
class CachedRemoteJwkSet {
|
|
14
|
+
constructor(url, adapter) {
|
|
15
|
+
this.url = url;
|
|
16
|
+
this.adapter = adapter;
|
|
17
|
+
if (!adapter.unstable_cache) {
|
|
18
|
+
throw new Error("No cache found in the client adapter. Use `createRemoteJWKSet()` from 'jose' instead.");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async getKey(...args) {
|
|
22
|
+
if (!this.jwkSet) {
|
|
23
|
+
this.jwkSet = await this.#load();
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
return await this.#getLocalKey(...args);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
// Jose does not export the error definition
|
|
30
|
+
// Found in https://github.com/panva/jose/blob/d5b3cb672736112b1e1e31ac4d5e9cd641675206/src/util/errors.ts#L347
|
|
31
|
+
if (error instanceof Error && 'code' in error && error.code === 'ERR_JWKS_NO_MATCHING_KEY') {
|
|
32
|
+
this.jwkSet = await this.#load();
|
|
33
|
+
return this.#getLocalKey(...args);
|
|
34
|
+
}
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async #load() {
|
|
39
|
+
return this.adapter.getWithCache(CacheKey.Jwks, async () => {
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const response = await fetch(this.url, { signal: controller.signal, redirect: 'manual' });
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
throw new Error('Expected OK from the JSON Web Key Set HTTP response');
|
|
44
|
+
}
|
|
45
|
+
const json = await response.json();
|
|
46
|
+
if (!isJwkSetLike(json)) {
|
|
47
|
+
throw new Error('JSON Web Key Set malformed');
|
|
48
|
+
}
|
|
49
|
+
return json;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async #getLocalKey(...args) {
|
|
53
|
+
if (!this.jwkSet) {
|
|
54
|
+
throw new Error('No local JWK Set found.');
|
|
55
|
+
}
|
|
56
|
+
return createLocalJWKSet(this.jwkSet)(...args);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { CachedRemoteJwkSet };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function memoize(run) {
|
|
4
|
+
const promiseCache = new Map();
|
|
5
|
+
const memoized = async function (...args) {
|
|
6
|
+
const promiseKey = args[0];
|
|
7
|
+
const cachedPromise = promiseCache.get(promiseKey);
|
|
8
|
+
if (cachedPromise) {
|
|
9
|
+
return cachedPromise;
|
|
10
|
+
}
|
|
11
|
+
const promise = (async () => {
|
|
12
|
+
try {
|
|
13
|
+
return await run.apply(this, args);
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
promiseCache.delete(promiseKey);
|
|
17
|
+
}
|
|
18
|
+
})();
|
|
19
|
+
promiseCache.set(promiseKey, promise);
|
|
20
|
+
return promise;
|
|
21
|
+
};
|
|
22
|
+
return memoized;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
exports.memoize = memoize;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function memoize<Args extends unknown[], Return>(run: (...args: Args) => Promise<Return>): (this: unknown, ...args: Args) => Promise<Return>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function memoize(run) {
|
|
2
|
+
const promiseCache = new Map();
|
|
3
|
+
const memoized = async function (...args) {
|
|
4
|
+
const promiseKey = args[0];
|
|
5
|
+
const cachedPromise = promiseCache.get(promiseKey);
|
|
6
|
+
if (cachedPromise) {
|
|
7
|
+
return cachedPromise;
|
|
8
|
+
}
|
|
9
|
+
const promise = (async () => {
|
|
10
|
+
try {
|
|
11
|
+
return await run.apply(this, args);
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
promiseCache.delete(promiseKey);
|
|
15
|
+
}
|
|
16
|
+
})();
|
|
17
|
+
promiseCache.set(promiseKey, promise);
|
|
18
|
+
return promise;
|
|
19
|
+
};
|
|
20
|
+
return memoized;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { memoize };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logto/client",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./lib/index.cjs",
|
|
6
6
|
"module": "./lib/index.js",
|
|
@@ -27,18 +27,18 @@
|
|
|
27
27
|
"jose": "^4.13.2"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@silverhand/eslint-config": "^
|
|
31
|
-
"@silverhand/ts-config": "^
|
|
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.
|
|
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": "^
|
|
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"
|
package/lib/adapter.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
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
|
-
};
|