@logto/api 1.42.0 → 1.44.0-beta.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.
- package/README.md +22 -3
- package/lib/client-credentials.d.ts +29 -2
- package/lib/client-credentials.js +118 -31
- package/lib/generated-types/management.d.ts +721 -47
- package/lib/management.d.ts +44 -4
- package/lib/management.js +60 -9
- package/lib/timeout.d.ts +7 -0
- package/lib/timeout.js +29 -0
- package/package.json +1 -1
package/lib/management.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export type CreateManagementApiOptions = {
|
|
|
19
19
|
/**
|
|
20
20
|
* Override the base URL generated from the tenant ID.
|
|
21
21
|
* Useful for testing or custom deployments.
|
|
22
|
+
* Trailing slashes are ignored.
|
|
22
23
|
*/
|
|
23
24
|
baseUrl?: string;
|
|
24
25
|
/**
|
|
@@ -26,6 +27,25 @@ export type CreateManagementApiOptions = {
|
|
|
26
27
|
* Useful for testing or custom deployments.
|
|
27
28
|
*/
|
|
28
29
|
apiIndicator?: string;
|
|
30
|
+
/**
|
|
31
|
+
* The maximum time in seconds allowed for fetching an access token.
|
|
32
|
+
* Non-positive and infinite values disable the timeout. `NaN` uses the default value.
|
|
33
|
+
* @default 10
|
|
34
|
+
*/
|
|
35
|
+
tokenRequestTimeout?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Returns an optional signal for each token request. The factory is called once per actual token
|
|
38
|
+
* fetch, so concurrent callers sharing a fetch also share its signal. The signal is composed with
|
|
39
|
+
* `tokenRequestTimeout`, and the first signal to abort cancels the request.
|
|
40
|
+
*/
|
|
41
|
+
getTokenRequestSignal?: () => AbortSignal | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* The maximum time in seconds allowed for the Management API network request after token
|
|
44
|
+
* retrieval. It is composed with per-request signals, and a timeout rejects the API call.
|
|
45
|
+
* Non-positive and infinite values disable the timeout. `NaN` uses the default value.
|
|
46
|
+
* @default 10
|
|
47
|
+
*/
|
|
48
|
+
requestTimeout?: number;
|
|
29
49
|
};
|
|
30
50
|
/**
|
|
31
51
|
* Options for creating an API client with custom token authentication.
|
|
@@ -33,6 +53,7 @@ export type CreateManagementApiOptions = {
|
|
|
33
53
|
export type CreateApiClientOptions = {
|
|
34
54
|
/**
|
|
35
55
|
* The base URL for the Management API.
|
|
56
|
+
* Trailing slashes are ignored.
|
|
36
57
|
*/
|
|
37
58
|
baseUrl: string;
|
|
38
59
|
/**
|
|
@@ -40,6 +61,13 @@ export type CreateApiClientOptions = {
|
|
|
40
61
|
* This function will be called for each request that requires authentication.
|
|
41
62
|
*/
|
|
42
63
|
getToken: () => Promise<string>;
|
|
64
|
+
/**
|
|
65
|
+
* The maximum time in seconds allowed for the API network request after `getToken()` resolves.
|
|
66
|
+
* It is composed with per-request signals, and a timeout rejects the API call.
|
|
67
|
+
* Non-positive and infinite values disable the timeout. `NaN` uses the default value.
|
|
68
|
+
* @default 10
|
|
69
|
+
*/
|
|
70
|
+
requestTimeout?: number;
|
|
43
71
|
};
|
|
44
72
|
/**
|
|
45
73
|
* Returns the base URL for the Management API based on the tenant ID.
|
|
@@ -59,6 +87,18 @@ export declare const getManagementApiIndicator: (tenantId: string) => string;
|
|
|
59
87
|
* This is used when requesting an access token for the Management API.
|
|
60
88
|
*/
|
|
61
89
|
export declare const allScope = "all";
|
|
90
|
+
type LowercaseHttpMethods = {
|
|
91
|
+
get: Client<paths>['GET'];
|
|
92
|
+
put: Client<paths>['PUT'];
|
|
93
|
+
post: Client<paths>['POST'];
|
|
94
|
+
delete: Client<paths>['DELETE'];
|
|
95
|
+
options: Client<paths>['OPTIONS'];
|
|
96
|
+
head: Client<paths>['HEAD'];
|
|
97
|
+
patch: Client<paths>['PATCH'];
|
|
98
|
+
trace: Client<paths>['TRACE'];
|
|
99
|
+
};
|
|
100
|
+
/** A typed Management API client with lowercase HTTP methods. */
|
|
101
|
+
export type ManagementApiClient = Client<paths> & LowercaseHttpMethods;
|
|
62
102
|
/**
|
|
63
103
|
* Creates an API client with custom token authentication.
|
|
64
104
|
*
|
|
@@ -78,12 +118,12 @@ export declare const allScope = "all";
|
|
|
78
118
|
* getToken: async () => getYourToken(),
|
|
79
119
|
* });
|
|
80
120
|
*
|
|
81
|
-
* const response = await client.
|
|
121
|
+
* const response = await client.get('/api/applications/{id}', {
|
|
82
122
|
* params: { path: { id: 'app-id' } },
|
|
83
123
|
* });
|
|
84
124
|
* ```
|
|
85
125
|
*/
|
|
86
|
-
export declare function createApiClient(options: CreateApiClientOptions):
|
|
126
|
+
export declare function createApiClient(options: CreateApiClientOptions): ManagementApiClient;
|
|
87
127
|
type ManagementApiReturnType = {
|
|
88
128
|
/**
|
|
89
129
|
* The API client for the Management API.
|
|
@@ -91,7 +131,7 @@ type ManagementApiReturnType = {
|
|
|
91
131
|
* This client is configured to use the provided client credentials
|
|
92
132
|
* and will automatically include the access token in requests.
|
|
93
133
|
*/
|
|
94
|
-
apiClient:
|
|
134
|
+
apiClient: ManagementApiClient;
|
|
95
135
|
/**
|
|
96
136
|
* The client credentials instance used for authentication.
|
|
97
137
|
*/
|
|
@@ -123,7 +163,7 @@ type ManagementApiReturnType = {
|
|
|
123
163
|
* });
|
|
124
164
|
*
|
|
125
165
|
* // Use apiClient to make requests to the Management API
|
|
126
|
-
* const response = await apiClient.
|
|
166
|
+
* const response = await apiClient.get('/api/users');
|
|
127
167
|
* console.log(response.data);
|
|
128
168
|
* ```
|
|
129
169
|
*
|
package/lib/management.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import createClient from 'openapi-fetch';
|
|
2
2
|
import { ClientCredentials } from './client-credentials.js';
|
|
3
|
+
import { normalizeTimeout, toTimeoutMilliseconds } from './timeout.js';
|
|
3
4
|
/**
|
|
4
5
|
* Returns the base URL for the Management API based on the tenant ID.
|
|
5
6
|
* @param tenantId The tenant ID to construct the base URL.
|
|
@@ -18,6 +19,28 @@ export const getManagementApiIndicator = (tenantId) => `${getBaseUrl(tenantId)}/
|
|
|
18
19
|
* This is used when requesting an access token for the Management API.
|
|
19
20
|
*/
|
|
20
21
|
export const allScope = 'all';
|
|
22
|
+
const bearerTokenPrefix = 'Bearer ';
|
|
23
|
+
const normalizeBaseUrl = (baseUrl) => baseUrl.replace(/\/+$/u, '');
|
|
24
|
+
const isWellKnownPath = (schemaPath) => schemaPath.includes('/.well-known/');
|
|
25
|
+
const createRequestTimeoutFetch = (requestTimeout) => async (request, requestInit) => {
|
|
26
|
+
const signal = AbortSignal.any([
|
|
27
|
+
request.signal,
|
|
28
|
+
AbortSignal.timeout(toTimeoutMilliseconds(requestTimeout)),
|
|
29
|
+
]);
|
|
30
|
+
return fetch(request, { ...requestInit, signal });
|
|
31
|
+
};
|
|
32
|
+
const addLowercaseHttpMethods = (client) =>
|
|
33
|
+
// eslint-disable-next-line @silverhand/fp/no-mutating-assign -- Keep the original client identity while adding lowercase aliases.
|
|
34
|
+
Object.assign(client, {
|
|
35
|
+
get: client.GET,
|
|
36
|
+
put: client.PUT,
|
|
37
|
+
post: client.POST,
|
|
38
|
+
delete: client.DELETE,
|
|
39
|
+
options: client.OPTIONS,
|
|
40
|
+
head: client.HEAD,
|
|
41
|
+
patch: client.PATCH,
|
|
42
|
+
trace: client.TRACE,
|
|
43
|
+
});
|
|
21
44
|
/**
|
|
22
45
|
* Creates an API client with custom token authentication.
|
|
23
46
|
*
|
|
@@ -37,25 +60,29 @@ export const allScope = 'all';
|
|
|
37
60
|
* getToken: async () => getYourToken(),
|
|
38
61
|
* });
|
|
39
62
|
*
|
|
40
|
-
* const response = await client.
|
|
63
|
+
* const response = await client.get('/api/applications/{id}', {
|
|
41
64
|
* params: { path: { id: 'app-id' } },
|
|
42
65
|
* });
|
|
43
66
|
* ```
|
|
44
67
|
*/
|
|
45
68
|
export function createApiClient(options) {
|
|
46
69
|
const { baseUrl, getToken } = options;
|
|
47
|
-
const
|
|
70
|
+
const requestTimeout = normalizeTimeout(options.requestTimeout);
|
|
71
|
+
const client = createClient({
|
|
72
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
73
|
+
...(requestTimeout > 0 && { fetch: createRequestTimeoutFetch(requestTimeout) }),
|
|
74
|
+
});
|
|
48
75
|
client.use({
|
|
49
76
|
async onRequest({ schemaPath, request }) {
|
|
50
|
-
if (schemaPath
|
|
77
|
+
if (isWellKnownPath(schemaPath)) {
|
|
51
78
|
return;
|
|
52
79
|
}
|
|
53
80
|
const token = await getToken();
|
|
54
|
-
request.headers.set('Authorization',
|
|
81
|
+
request.headers.set('Authorization', `${bearerTokenPrefix}${token}`);
|
|
55
82
|
return request;
|
|
56
83
|
},
|
|
57
84
|
});
|
|
58
|
-
return client;
|
|
85
|
+
return addLowercaseHttpMethods(client);
|
|
59
86
|
}
|
|
60
87
|
/**
|
|
61
88
|
* Creates a Management API client with the specified tenant ID and options.
|
|
@@ -83,7 +110,7 @@ export function createApiClient(options) {
|
|
|
83
110
|
* });
|
|
84
111
|
*
|
|
85
112
|
* // Use apiClient to make requests to the Management API
|
|
86
|
-
* const response = await apiClient.
|
|
113
|
+
* const response = await apiClient.get('/api/users');
|
|
87
114
|
* console.log(response.data);
|
|
88
115
|
* ```
|
|
89
116
|
*
|
|
@@ -99,8 +126,8 @@ export function createApiClient(options) {
|
|
|
99
126
|
* ```
|
|
100
127
|
*/
|
|
101
128
|
export function createManagementApi(tenantId, options) {
|
|
102
|
-
const { clientId, clientSecret } = options;
|
|
103
|
-
const baseUrl = options.baseUrl ?? getBaseUrl(tenantId);
|
|
129
|
+
const { clientId, clientSecret, tokenRequestTimeout, getTokenRequestSignal, requestTimeout } = options;
|
|
130
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl ?? getBaseUrl(tenantId));
|
|
104
131
|
const apiIndicator = options.apiIndicator ?? getManagementApiIndicator(tenantId);
|
|
105
132
|
const clientCredentials = new ClientCredentials({
|
|
106
133
|
clientId,
|
|
@@ -110,17 +137,41 @@ export function createManagementApi(tenantId, options) {
|
|
|
110
137
|
resource: apiIndicator,
|
|
111
138
|
scope: allScope,
|
|
112
139
|
},
|
|
140
|
+
tokenRequestTimeout,
|
|
141
|
+
getTokenRequestSignal,
|
|
113
142
|
});
|
|
143
|
+
const warnedScopes = new Set();
|
|
114
144
|
const apiClient = createApiClient({
|
|
115
145
|
baseUrl,
|
|
146
|
+
requestTimeout,
|
|
116
147
|
getToken: async () => {
|
|
117
148
|
const { value, scope } = await clientCredentials.getAccessToken();
|
|
118
|
-
if (scope !== allScope) {
|
|
149
|
+
if (scope !== allScope && !warnedScopes.has(scope)) {
|
|
150
|
+
warnedScopes.add(scope);
|
|
119
151
|
console.warn(`The scope "${scope}" is not equal to the expected value "${allScope}". This may cause issues with API access. See https://a.logto.io/m2m-mapi to learn more about configuring machine-to-machine access to the Management API.`);
|
|
120
152
|
}
|
|
121
153
|
return value;
|
|
122
154
|
},
|
|
123
155
|
});
|
|
156
|
+
apiClient.use({
|
|
157
|
+
onResponse({ schemaPath, request, response }) {
|
|
158
|
+
if (isWellKnownPath(schemaPath)) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const authorization = request.headers.get('Authorization');
|
|
162
|
+
if (!authorization?.startsWith(bearerTokenPrefix)) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const accessToken = authorization.slice(bearerTokenPrefix.length);
|
|
166
|
+
if (response.status === 401) {
|
|
167
|
+
// Do not retry here because the request body may have been consumed.
|
|
168
|
+
clientCredentials.invalidateAccessToken(accessToken);
|
|
169
|
+
}
|
|
170
|
+
else if (response.ok) {
|
|
171
|
+
clientCredentials.markAccessTokenAsValid(accessToken);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
});
|
|
124
175
|
return {
|
|
125
176
|
apiClient,
|
|
126
177
|
clientCredentials,
|
package/lib/timeout.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const normalizeTimeout: (timeout?: number) => number;
|
|
2
|
+
export declare const getAbortReason: (signal: AbortSignal, fallbackMessage: string) => Error;
|
|
3
|
+
export declare const toTimeoutMilliseconds: (timeout: number) => number;
|
|
4
|
+
export declare const createTimeoutSignal: (timeout: number, message: string) => {
|
|
5
|
+
signal: AbortSignal;
|
|
6
|
+
clear: () => void;
|
|
7
|
+
};
|
package/lib/timeout.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const defaultTimeout = 10;
|
|
2
|
+
// Browsers and Node.js reliably support timer delays up to a signed 32-bit integer.
|
|
3
|
+
const maximumTimeoutMilliseconds = 2_147_483_647;
|
|
4
|
+
export const normalizeTimeout = (timeout) => {
|
|
5
|
+
if (timeout === undefined || Number.isNaN(timeout)) {
|
|
6
|
+
return defaultTimeout;
|
|
7
|
+
}
|
|
8
|
+
return Number.isFinite(timeout) && timeout > 0 ? timeout : 0;
|
|
9
|
+
};
|
|
10
|
+
export const getAbortReason = (signal, fallbackMessage) => {
|
|
11
|
+
// eslint-disable-next-line prefer-destructuring -- Keep the DOM's any-typed reason narrowed to unknown.
|
|
12
|
+
const reason = signal.reason;
|
|
13
|
+
return reason instanceof Error ? reason : new Error(fallbackMessage, { cause: reason });
|
|
14
|
+
};
|
|
15
|
+
export const toTimeoutMilliseconds = (timeout) => Math.min(Math.max(Math.ceil(timeout * 1000), 1), maximumTimeoutMilliseconds);
|
|
16
|
+
export const createTimeoutSignal = (timeout, message) => {
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
const timer = timeout > 0
|
|
19
|
+
? setTimeout(() => {
|
|
20
|
+
controller.abort(new DOMException(message, 'TimeoutError'));
|
|
21
|
+
}, toTimeoutMilliseconds(timeout))
|
|
22
|
+
: undefined;
|
|
23
|
+
return {
|
|
24
|
+
signal: controller.signal,
|
|
25
|
+
clear: () => {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
};
|
package/package.json
CHANGED