@logto/api 1.43.0 → 1.44.0-beta.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/README.md +51 -7
- package/lib/client-credentials.d.ts +29 -2
- package/lib/client-credentials.js +118 -31
- package/lib/generated-types/management.js +0 -4
- package/lib/management.d.ts +71 -16
- package/lib/management.js +85 -51
- package/lib/pagination.d.ts +37 -0
- package/lib/pagination.js +66 -0
- package/lib/timeout.d.ts +7 -0
- package/lib/timeout.js +29 -0
- package/package.json +15 -13
package/README.md
CHANGED
|
@@ -27,30 +27,74 @@ For detailed setup instructions, visit: https://a.logto.io/m2m-mapi
|
|
|
27
27
|
```ts
|
|
28
28
|
import { createManagementApi } from '@logto/api/management';
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const { apiClient } = createManagementApi({
|
|
31
|
+
tenantId: 'your-tenant-id',
|
|
32
32
|
clientId: 'your-client-id',
|
|
33
33
|
clientSecret: 'your-client-secret',
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
// Make API calls
|
|
37
|
-
const response = await apiClient.
|
|
36
|
+
// Make API calls with lowercase or uppercase methods
|
|
37
|
+
const response = await apiClient.get('/api/users');
|
|
38
|
+
// const response = await apiClient.GET('/api/users');
|
|
38
39
|
console.log(response.data);
|
|
39
40
|
```
|
|
40
41
|
|
|
42
|
+
The positional `createManagementApi(tenantId, options)` form remains supported.
|
|
43
|
+
|
|
44
|
+
#### Pagination
|
|
45
|
+
|
|
46
|
+
Use `paginate()` to iterate over items from a paginated `GET` endpoint. The path, query parameters,
|
|
47
|
+
path parameters, headers, and signal use the same generated types as `get()`.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
for await (const user of apiClient.paginate('/api/users')) {
|
|
51
|
+
console.log(user);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The iterator follows the Management API pagination headers and stops when there are no more items.
|
|
56
|
+
It requests 100 items per page unless `page_size` is provided. When an audit log endpoint reports a
|
|
57
|
+
capped total, the iterator continues until it receives an empty page because the final page is
|
|
58
|
+
unknown. Breaking out of the loop stops further requests.
|
|
59
|
+
|
|
60
|
+
API error responses throw `ManagementApiPaginationError`, which exposes `status`, response metadata
|
|
61
|
+
and headers through `response`, and the parsed API error body as `cause`. The response body has
|
|
62
|
+
already been read. Network, timeout, and abort errors propagate unchanged.
|
|
63
|
+
|
|
41
64
|
#### Self-hosted / OSS
|
|
42
65
|
|
|
43
66
|
```ts
|
|
44
67
|
import { createManagementApi } from '@logto/api/management';
|
|
45
68
|
|
|
46
|
-
const { apiClient } = createManagementApi(
|
|
69
|
+
const { apiClient } = createManagementApi({
|
|
70
|
+
tenantId: 'default',
|
|
47
71
|
clientId: 'your-client-id',
|
|
48
72
|
clientSecret: 'your-client-secret',
|
|
49
73
|
baseUrl: 'https://your-logto-instance.com',
|
|
50
|
-
apiIndicator: 'https://your-logto-instance.com/api',
|
|
51
74
|
});
|
|
52
75
|
```
|
|
53
76
|
|
|
77
|
+
If the Management API indicator cannot be derived from a tenant ID, omit `tenantId` and provide
|
|
78
|
+
both `baseUrl` and `apiIndicator`.
|
|
79
|
+
|
|
80
|
+
#### Timeouts
|
|
81
|
+
|
|
82
|
+
Token fetches and Management API network requests have separate 10-second timeouts. The API request
|
|
83
|
+
timeout starts after token retrieval, so a request that needs a new token can use both timeout
|
|
84
|
+
periods. Use `tokenRequestTimeout` and `requestTimeout` to configure them in seconds. Set either option
|
|
85
|
+
to `0` or a negative value to disable its timeout.
|
|
86
|
+
|
|
87
|
+
Use `getTokenRequestSignal` to return a custom signal for each token request. The signal is composed
|
|
88
|
+
with `tokenRequestTimeout`, and the first one to abort cancels the token request. A per-request
|
|
89
|
+
`signal` is similarly composed with `requestTimeout` for the Management API request. Timeouts and
|
|
90
|
+
custom cancellation reject the API call instead of returning a response object.
|
|
91
|
+
|
|
92
|
+
#### Token refresh
|
|
93
|
+
|
|
94
|
+
After a Management API request returns `401`, the next request fetches a new token. If the replacement
|
|
95
|
+
token also receives a `401`, it remains cached until a request succeeds or the token expires. The SDK
|
|
96
|
+
does not automatically retry the failed API request.
|
|
97
|
+
|
|
54
98
|
#### Custom authentication
|
|
55
99
|
|
|
56
100
|
For advanced use cases where you need full control over the authentication logic, use `createApiClient`:
|
|
@@ -67,7 +111,7 @@ const client = createApiClient({
|
|
|
67
111
|
});
|
|
68
112
|
|
|
69
113
|
// Type-safe API calls
|
|
70
|
-
const response = await client.
|
|
114
|
+
const response = await client.get('/api/applications/{id}', {
|
|
71
115
|
params: { path: { id: 'your-app-id' } },
|
|
72
116
|
});
|
|
73
117
|
```
|
|
@@ -6,7 +6,7 @@ export type AccessToken = {
|
|
|
6
6
|
scope?: string;
|
|
7
7
|
};
|
|
8
8
|
export declare class ClientCredentialsError extends Error {
|
|
9
|
-
constructor(message: string);
|
|
9
|
+
constructor(message: string, options?: ErrorOptions);
|
|
10
10
|
}
|
|
11
11
|
export type ClientCredentialsOptions = {
|
|
12
12
|
clientId: string;
|
|
@@ -15,9 +15,23 @@ export type ClientCredentialsOptions = {
|
|
|
15
15
|
tokenParams?: Record<string, string>;
|
|
16
16
|
/**
|
|
17
17
|
* The time in seconds before the access token expires to consider it valid.
|
|
18
|
+
* A value greater than or equal to the token lifetime causes every call to fetch a new token,
|
|
19
|
+
* so cached-token invalidation protection cannot persist across calls.
|
|
18
20
|
* @default 60
|
|
19
21
|
*/
|
|
20
22
|
accessTokenExpiryLeeway?: Seconds;
|
|
23
|
+
/**
|
|
24
|
+
* The maximum time in seconds allowed for fetching an access token.
|
|
25
|
+
* Non-positive and infinite values disable the timeout. `NaN` uses the default value.
|
|
26
|
+
* @default 10
|
|
27
|
+
*/
|
|
28
|
+
tokenRequestTimeout?: Seconds;
|
|
29
|
+
/**
|
|
30
|
+
* Returns an optional signal for each token request. The factory is called once per actual token
|
|
31
|
+
* fetch, so concurrent callers sharing a fetch also share its signal. The signal is composed with
|
|
32
|
+
* `tokenRequestTimeout`, and the first signal to abort cancels the request.
|
|
33
|
+
*/
|
|
34
|
+
getTokenRequestSignal?: () => AbortSignal | undefined;
|
|
21
35
|
};
|
|
22
36
|
/**
|
|
23
37
|
* A class handles client credentials for API authentication. It caches the access token
|
|
@@ -26,12 +40,25 @@ export type ClientCredentialsOptions = {
|
|
|
26
40
|
export declare class ClientCredentials {
|
|
27
41
|
protected options: ClientCredentialsOptions;
|
|
28
42
|
protected accessToken?: AccessToken;
|
|
43
|
+
private accessTokenPromise?;
|
|
44
|
+
private awaitingValidationAfterRefresh;
|
|
29
45
|
get accessTokenExpiryLeeway(): Seconds;
|
|
46
|
+
get tokenRequestTimeout(): Seconds;
|
|
30
47
|
constructor(options: ClientCredentialsOptions);
|
|
31
48
|
/**
|
|
32
49
|
* Retrieves the access token, refreshing it if necessary.
|
|
33
|
-
*
|
|
50
|
+
* Concurrent calls share the same token request.
|
|
51
|
+
* @returns The access token and its metadata.
|
|
34
52
|
*/
|
|
35
53
|
getAccessToken(): Promise<AccessToken>;
|
|
54
|
+
/**
|
|
55
|
+
* Invalidates the cached token if it matches the token rejected by the API.
|
|
56
|
+
* A delayed response for an older token must not invalidate a newer cached token.
|
|
57
|
+
*/
|
|
58
|
+
invalidateAccessToken(token: string): void;
|
|
59
|
+
/** Allows a future 401 to invalidate the token after it receives a successful API response. */
|
|
60
|
+
markAccessTokenAsValid(token: string): void;
|
|
61
|
+
private fetchAccessToken;
|
|
62
|
+
private requestAccessToken;
|
|
36
63
|
}
|
|
37
64
|
export {};
|
|
@@ -1,9 +1,29 @@
|
|
|
1
|
+
import { createTimeoutSignal, getAbortReason, normalizeTimeout } from './timeout.js';
|
|
1
2
|
export class ClientCredentialsError extends Error {
|
|
2
|
-
constructor(message) {
|
|
3
|
-
super(message);
|
|
3
|
+
constructor(message, options) {
|
|
4
|
+
super(message, options);
|
|
4
5
|
this.name = 'ClientCredentialsError';
|
|
5
6
|
}
|
|
6
7
|
}
|
|
8
|
+
const createAbortPromise = (signal) => {
|
|
9
|
+
// eslint-disable-next-line no-use-extend-native/no-use-extend-native -- `Promise.withResolvers` is standard since ES2024; the rule's built-in method list predates it.
|
|
10
|
+
const { promise, reject } = Promise.withResolvers();
|
|
11
|
+
const handleAbort = () => {
|
|
12
|
+
reject(getAbortReason(signal, 'Token request aborted'));
|
|
13
|
+
};
|
|
14
|
+
if (signal.aborted) {
|
|
15
|
+
handleAbort();
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
signal.addEventListener('abort', handleAbort, { once: true });
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
promise,
|
|
22
|
+
clear: () => {
|
|
23
|
+
signal.removeEventListener('abort', handleAbort);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
};
|
|
7
27
|
/**
|
|
8
28
|
* A class handles client credentials for API authentication. It caches the access token
|
|
9
29
|
* and provides methods to retrieve it, ensuring the token is valid and refreshing it when necessary.
|
|
@@ -12,46 +32,113 @@ export class ClientCredentials {
|
|
|
12
32
|
get accessTokenExpiryLeeway() {
|
|
13
33
|
return this.options.accessTokenExpiryLeeway ?? 60;
|
|
14
34
|
}
|
|
35
|
+
get tokenRequestTimeout() {
|
|
36
|
+
return normalizeTimeout(this.options.tokenRequestTimeout);
|
|
37
|
+
}
|
|
15
38
|
constructor(options) {
|
|
16
39
|
this.options = options;
|
|
40
|
+
this.awaitingValidationAfterRefresh = false;
|
|
17
41
|
}
|
|
18
42
|
/**
|
|
19
43
|
* Retrieves the access token, refreshing it if necessary.
|
|
20
|
-
*
|
|
44
|
+
* Concurrent calls share the same token request.
|
|
45
|
+
* @returns The access token and its metadata.
|
|
21
46
|
*/
|
|
22
47
|
async getAccessToken() {
|
|
23
48
|
const now = new Date();
|
|
24
|
-
//
|
|
25
|
-
if (
|
|
26
|
-
this.accessToken.expiresAt
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
// Return the cached token if it is still valid.
|
|
50
|
+
if (this.accessToken?.expiresAt &&
|
|
51
|
+
this.accessToken.expiresAt > Math.floor(now.getTime() / 1000) + this.accessTokenExpiryLeeway) {
|
|
52
|
+
return this.accessToken;
|
|
53
|
+
}
|
|
54
|
+
if (this.accessToken) {
|
|
55
|
+
// A natural expiry starts a new invalidation cycle.
|
|
56
|
+
this.awaitingValidationAfterRefresh = false;
|
|
57
|
+
}
|
|
58
|
+
if (this.accessTokenPromise) {
|
|
59
|
+
return this.accessTokenPromise;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
this.accessTokenPromise = this.fetchAccessToken();
|
|
63
|
+
this.accessToken = await this.accessTokenPromise;
|
|
64
|
+
return this.accessToken;
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
this.accessTokenPromise = undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Invalidates the cached token if it matches the token rejected by the API.
|
|
72
|
+
* A delayed response for an older token must not invalidate a newer cached token.
|
|
73
|
+
*/
|
|
74
|
+
invalidateAccessToken(token) {
|
|
75
|
+
if (this.accessToken?.value === token && !this.awaitingValidationAfterRefresh) {
|
|
76
|
+
this.accessToken = undefined;
|
|
77
|
+
this.awaitingValidationAfterRefresh = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Allows a future 401 to invalidate the token after it receives a successful API response. */
|
|
81
|
+
markAccessTokenAsValid(token) {
|
|
82
|
+
if (this.accessToken?.value === token) {
|
|
83
|
+
this.awaitingValidationAfterRefresh = false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async fetchAccessToken() {
|
|
87
|
+
const timeout = createTimeoutSignal(this.tokenRequestTimeout, 'Token request timed out');
|
|
88
|
+
try {
|
|
89
|
+
const customSignal = this.options.getTokenRequestSignal?.();
|
|
90
|
+
const signal = customSignal
|
|
91
|
+
? AbortSignal.any([timeout.signal, customSignal])
|
|
92
|
+
: timeout.signal;
|
|
93
|
+
const abort = createAbortPromise(signal);
|
|
94
|
+
try {
|
|
95
|
+
// Guard against Response implementations that do not propagate request aborts to json().
|
|
96
|
+
return await Promise.race([this.requestAccessToken(signal), abort.promise]);
|
|
41
97
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
throw new ClientCredentialsError('Invalid response from token endpoint');
|
|
98
|
+
finally {
|
|
99
|
+
abort.clear();
|
|
45
100
|
}
|
|
46
|
-
|
|
47
|
-
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error instanceof ClientCredentialsError) {
|
|
104
|
+
throw error;
|
|
48
105
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
106
|
+
throw new ClientCredentialsError(`Failed to fetch access token: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
timeout.clear();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async requestAccessToken(signal) {
|
|
113
|
+
const now = new Date();
|
|
114
|
+
const response = await fetch(this.options.tokenEndpoint, {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
redirect: 'error',
|
|
117
|
+
signal,
|
|
118
|
+
headers: {
|
|
119
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
120
|
+
},
|
|
121
|
+
body: new URLSearchParams({
|
|
122
|
+
grant_type: 'client_credentials',
|
|
123
|
+
client_id: this.options.clientId,
|
|
124
|
+
client_secret: this.options.clientSecret,
|
|
125
|
+
...this.options.tokenParams,
|
|
126
|
+
}).toString(),
|
|
127
|
+
});
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
throw new ClientCredentialsError(`Failed to fetch access token: ${response.status} ${response.statusText}`);
|
|
130
|
+
}
|
|
131
|
+
const data = await response.json();
|
|
132
|
+
if (typeof data !== 'object' || data === null || !('access_token' in data)) {
|
|
133
|
+
throw new ClientCredentialsError('Invalid response from token endpoint');
|
|
134
|
+
}
|
|
135
|
+
if (!('expires_in' in data) || typeof data.expires_in !== 'number') {
|
|
136
|
+
throw new ClientCredentialsError('Invalid or missing expires_in in token response');
|
|
54
137
|
}
|
|
55
|
-
return
|
|
138
|
+
return {
|
|
139
|
+
value: String(data.access_token),
|
|
140
|
+
expiresAt: Math.floor(now.getTime() / 1000) + data.expires_in,
|
|
141
|
+
scope: 'scope' in data && typeof data.scope === 'string' ? data.scope : undefined,
|
|
142
|
+
};
|
|
56
143
|
}
|
|
57
144
|
}
|
package/lib/management.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type Client } from 'openapi-fetch';
|
|
2
2
|
import { ClientCredentials } from './client-credentials.js';
|
|
3
3
|
import { type paths } from './generated-types/management.js';
|
|
4
|
+
import { type PaginateMethod } from './pagination.js';
|
|
5
|
+
export { ManagementApiPaginationError } from './pagination.js';
|
|
4
6
|
/**
|
|
5
7
|
* Options for creating a Management API client.
|
|
6
8
|
*/
|
|
@@ -17,22 +19,52 @@ export type CreateManagementApiOptions = {
|
|
|
17
19
|
*/
|
|
18
20
|
clientSecret: string;
|
|
19
21
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
+
* The base URL for the Logto instance. Required when the tenant ID is omitted.
|
|
23
|
+
* Trailing slashes are ignored.
|
|
22
24
|
*/
|
|
23
25
|
baseUrl?: string;
|
|
24
26
|
/**
|
|
25
|
-
*
|
|
26
|
-
* Useful for testing or custom deployments.
|
|
27
|
+
* The resource indicator for the Management API. Required when the tenant ID is omitted.
|
|
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
|
};
|
|
50
|
+
/**
|
|
51
|
+
* Object options for creating a Management API client. Provide a tenant ID to derive the defaults,
|
|
52
|
+
* or provide both the base URL and API indicator directly.
|
|
53
|
+
*/
|
|
54
|
+
export type CreateManagementApiConfig = (CreateManagementApiOptions & {
|
|
55
|
+
tenantId: string;
|
|
56
|
+
}) | (CreateManagementApiOptions & {
|
|
57
|
+
tenantId?: never;
|
|
58
|
+
baseUrl: string;
|
|
59
|
+
apiIndicator: string;
|
|
60
|
+
});
|
|
30
61
|
/**
|
|
31
62
|
* Options for creating an API client with custom token authentication.
|
|
32
63
|
*/
|
|
33
64
|
export type CreateApiClientOptions = {
|
|
34
65
|
/**
|
|
35
66
|
* The base URL for the Management API.
|
|
67
|
+
* Trailing slashes are ignored.
|
|
36
68
|
*/
|
|
37
69
|
baseUrl: string;
|
|
38
70
|
/**
|
|
@@ -40,6 +72,13 @@ export type CreateApiClientOptions = {
|
|
|
40
72
|
* This function will be called for each request that requires authentication.
|
|
41
73
|
*/
|
|
42
74
|
getToken: () => Promise<string>;
|
|
75
|
+
/**
|
|
76
|
+
* The maximum time in seconds allowed for the API network request after `getToken()` resolves.
|
|
77
|
+
* It is composed with per-request signals, and a timeout rejects the API call.
|
|
78
|
+
* Non-positive and infinite values disable the timeout. `NaN` uses the default value.
|
|
79
|
+
* @default 10
|
|
80
|
+
*/
|
|
81
|
+
requestTimeout?: number;
|
|
43
82
|
};
|
|
44
83
|
/**
|
|
45
84
|
* Returns the base URL for the Management API based on the tenant ID.
|
|
@@ -59,6 +98,21 @@ export declare const getManagementApiIndicator: (tenantId: string) => string;
|
|
|
59
98
|
* This is used when requesting an access token for the Management API.
|
|
60
99
|
*/
|
|
61
100
|
export declare const allScope = "all";
|
|
101
|
+
type LowercaseHttpMethods = {
|
|
102
|
+
get: Client<paths>['GET'];
|
|
103
|
+
put: Client<paths>['PUT'];
|
|
104
|
+
post: Client<paths>['POST'];
|
|
105
|
+
delete: Client<paths>['DELETE'];
|
|
106
|
+
options: Client<paths>['OPTIONS'];
|
|
107
|
+
head: Client<paths>['HEAD'];
|
|
108
|
+
patch: Client<paths>['PATCH'];
|
|
109
|
+
trace: Client<paths>['TRACE'];
|
|
110
|
+
};
|
|
111
|
+
type PaginationMethod = {
|
|
112
|
+
paginate: PaginateMethod<paths>;
|
|
113
|
+
};
|
|
114
|
+
/** A typed Management API client with lowercase HTTP methods and pagination support. */
|
|
115
|
+
export type ManagementApiClient = Client<paths> & LowercaseHttpMethods & PaginationMethod;
|
|
62
116
|
/**
|
|
63
117
|
* Creates an API client with custom token authentication.
|
|
64
118
|
*
|
|
@@ -78,12 +132,12 @@ export declare const allScope = "all";
|
|
|
78
132
|
* getToken: async () => getYourToken(),
|
|
79
133
|
* });
|
|
80
134
|
*
|
|
81
|
-
* const response = await client.
|
|
135
|
+
* const response = await client.get('/api/applications/{id}', {
|
|
82
136
|
* params: { path: { id: 'app-id' } },
|
|
83
137
|
* });
|
|
84
138
|
* ```
|
|
85
139
|
*/
|
|
86
|
-
export declare function createApiClient(options: CreateApiClientOptions):
|
|
140
|
+
export declare function createApiClient(options: CreateApiClientOptions): ManagementApiClient;
|
|
87
141
|
type ManagementApiReturnType = {
|
|
88
142
|
/**
|
|
89
143
|
* The API client for the Management API.
|
|
@@ -91,14 +145,14 @@ type ManagementApiReturnType = {
|
|
|
91
145
|
* This client is configured to use the provided client credentials
|
|
92
146
|
* and will automatically include the access token in requests.
|
|
93
147
|
*/
|
|
94
|
-
apiClient:
|
|
148
|
+
apiClient: ManagementApiClient;
|
|
95
149
|
/**
|
|
96
150
|
* The client credentials instance used for authentication.
|
|
97
151
|
*/
|
|
98
152
|
clientCredentials: ClientCredentials;
|
|
99
153
|
};
|
|
100
154
|
/**
|
|
101
|
-
* Creates a Management API client
|
|
155
|
+
* Creates a Management API client from a tenant ID or an explicit base URL and API indicator.
|
|
102
156
|
*
|
|
103
157
|
* Before using this function, ensure that you have created a machine-to-machine application in
|
|
104
158
|
* Logto and granted it access to the Management API. See the documentation for more details:
|
|
@@ -108,35 +162,36 @@ type ManagementApiReturnType = {
|
|
|
108
162
|
* This function sets up the API client with the necessary authentication using client credentials.
|
|
109
163
|
* It will automatically handle token retrieval and renewal as needed.
|
|
110
164
|
*
|
|
111
|
-
* @param
|
|
112
|
-
*
|
|
113
|
-
* @param options The options
|
|
165
|
+
* @param tenantIdOrConfig A tenant ID, or object options containing a tenant ID or an explicit base
|
|
166
|
+
* URL and API indicator.
|
|
167
|
+
* @param options The client options when the first argument is a tenant ID.
|
|
114
168
|
* @returns An object containing the API client and client credentials instance.
|
|
115
169
|
* @example
|
|
116
170
|
* ```ts
|
|
117
171
|
* import { createManagementApi } from '@logto/api/management';
|
|
118
172
|
*
|
|
119
173
|
* // Logto Cloud example
|
|
120
|
-
* const { apiClient, clientCredentials } = createManagementApi(
|
|
174
|
+
* const { apiClient, clientCredentials } = createManagementApi({
|
|
175
|
+
* tenantId: 'my-tenant-id',
|
|
121
176
|
* clientId: 'my-client-id',
|
|
122
177
|
* clientSecret: 'my-client-secret',
|
|
123
178
|
* });
|
|
124
179
|
*
|
|
125
180
|
* // Use apiClient to make requests to the Management API
|
|
126
|
-
* const response = await apiClient.
|
|
181
|
+
* const response = await apiClient.get('/api/users');
|
|
127
182
|
* console.log(response.data);
|
|
128
183
|
* ```
|
|
129
184
|
*
|
|
130
185
|
* @example
|
|
131
186
|
* ```ts
|
|
132
187
|
* // OSS example
|
|
133
|
-
* const { apiClient, clientCredentials } = createManagementApi(
|
|
188
|
+
* const { apiClient, clientCredentials } = createManagementApi({
|
|
189
|
+
* tenantId: 'default',
|
|
134
190
|
* clientId: 'my-client-id',
|
|
135
191
|
* clientSecret: 'my-client-secret',
|
|
136
192
|
* baseUrl: 'https://my-oss-logto-instance.com',
|
|
137
|
-
* apiIndicator: 'https://default.logto.app/api',
|
|
138
193
|
* });
|
|
139
194
|
* ```
|
|
140
195
|
*/
|
|
196
|
+
export declare function createManagementApi(config: CreateManagementApiConfig): ManagementApiReturnType;
|
|
141
197
|
export declare function createManagementApi(tenantId: string, options: CreateManagementApiOptions): ManagementApiReturnType;
|
|
142
|
-
export {};
|
package/lib/management.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import createClient from 'openapi-fetch';
|
|
2
2
|
import { ClientCredentials } from './client-credentials.js';
|
|
3
|
+
import { createPaginate } from './pagination.js';
|
|
4
|
+
import { normalizeTimeout, toTimeoutMilliseconds } from './timeout.js';
|
|
5
|
+
export { ManagementApiPaginationError } from './pagination.js';
|
|
3
6
|
/**
|
|
4
7
|
* Returns the base URL for the Management API based on the tenant ID.
|
|
5
8
|
* @param tenantId The tenant ID to construct the base URL.
|
|
@@ -18,6 +21,29 @@ export const getManagementApiIndicator = (tenantId) => `${getBaseUrl(tenantId)}/
|
|
|
18
21
|
* This is used when requesting an access token for the Management API.
|
|
19
22
|
*/
|
|
20
23
|
export const allScope = 'all';
|
|
24
|
+
const bearerTokenPrefix = 'Bearer ';
|
|
25
|
+
const normalizeBaseUrl = (baseUrl) => baseUrl.replace(/\/+$/u, '');
|
|
26
|
+
const isWellKnownPath = (schemaPath) => schemaPath.includes('/.well-known/');
|
|
27
|
+
const createRequestTimeoutFetch = (requestTimeout) => async (request, requestInit) => {
|
|
28
|
+
const signal = AbortSignal.any([
|
|
29
|
+
request.signal,
|
|
30
|
+
AbortSignal.timeout(toTimeoutMilliseconds(requestTimeout)),
|
|
31
|
+
]);
|
|
32
|
+
return fetch(request, { ...requestInit, signal });
|
|
33
|
+
};
|
|
34
|
+
const addClientMethods = (client) =>
|
|
35
|
+
// eslint-disable-next-line @silverhand/fp/no-mutating-assign -- Keep the original client identity while adding convenience methods.
|
|
36
|
+
Object.assign(client, {
|
|
37
|
+
get: client.GET,
|
|
38
|
+
put: client.PUT,
|
|
39
|
+
post: client.POST,
|
|
40
|
+
delete: client.DELETE,
|
|
41
|
+
options: client.OPTIONS,
|
|
42
|
+
head: client.HEAD,
|
|
43
|
+
patch: client.PATCH,
|
|
44
|
+
trace: client.TRACE,
|
|
45
|
+
paginate: createPaginate(client.GET),
|
|
46
|
+
});
|
|
21
47
|
/**
|
|
22
48
|
* Creates an API client with custom token authentication.
|
|
23
49
|
*
|
|
@@ -37,71 +63,55 @@ export const allScope = 'all';
|
|
|
37
63
|
* getToken: async () => getYourToken(),
|
|
38
64
|
* });
|
|
39
65
|
*
|
|
40
|
-
* const response = await client.
|
|
66
|
+
* const response = await client.get('/api/applications/{id}', {
|
|
41
67
|
* params: { path: { id: 'app-id' } },
|
|
42
68
|
* });
|
|
43
69
|
* ```
|
|
44
70
|
*/
|
|
45
71
|
export function createApiClient(options) {
|
|
46
72
|
const { baseUrl, getToken } = options;
|
|
47
|
-
const
|
|
73
|
+
const requestTimeout = normalizeTimeout(options.requestTimeout);
|
|
74
|
+
const client = createClient({
|
|
75
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
76
|
+
...(requestTimeout > 0 && { fetch: createRequestTimeoutFetch(requestTimeout) }),
|
|
77
|
+
});
|
|
48
78
|
client.use({
|
|
49
79
|
async onRequest({ schemaPath, request }) {
|
|
50
|
-
if (schemaPath
|
|
80
|
+
if (isWellKnownPath(schemaPath)) {
|
|
51
81
|
return;
|
|
52
82
|
}
|
|
53
83
|
const token = await getToken();
|
|
54
|
-
request.headers.set('Authorization',
|
|
84
|
+
request.headers.set('Authorization', `${bearerTokenPrefix}${token}`);
|
|
55
85
|
return request;
|
|
56
86
|
},
|
|
57
87
|
});
|
|
58
|
-
return client;
|
|
88
|
+
return addClientMethods(client);
|
|
59
89
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
* // Use apiClient to make requests to the Management API
|
|
86
|
-
* const response = await apiClient.GET('/api/users');
|
|
87
|
-
* console.log(response.data);
|
|
88
|
-
* ```
|
|
89
|
-
*
|
|
90
|
-
* @example
|
|
91
|
-
* ```ts
|
|
92
|
-
* // OSS example
|
|
93
|
-
* const { apiClient, clientCredentials } = createManagementApi('default', {
|
|
94
|
-
* clientId: 'my-client-id',
|
|
95
|
-
* clientSecret: 'my-client-secret',
|
|
96
|
-
* baseUrl: 'https://my-oss-logto-instance.com',
|
|
97
|
-
* apiIndicator: 'https://default.logto.app/api',
|
|
98
|
-
* });
|
|
99
|
-
* ```
|
|
100
|
-
*/
|
|
101
|
-
export function createManagementApi(tenantId, options) {
|
|
102
|
-
const { clientId, clientSecret } = options;
|
|
103
|
-
const baseUrl = options.baseUrl ?? getBaseUrl(tenantId);
|
|
104
|
-
const apiIndicator = options.apiIndicator ?? getManagementApiIndicator(tenantId);
|
|
90
|
+
const resolveManagementApiArguments = (tenantIdOrConfig, options) => {
|
|
91
|
+
if (typeof tenantIdOrConfig === 'string') {
|
|
92
|
+
if (!options) {
|
|
93
|
+
throw new TypeError('Options are required when creating a Management API client by tenant ID');
|
|
94
|
+
}
|
|
95
|
+
return { tenantId: tenantIdOrConfig, options };
|
|
96
|
+
}
|
|
97
|
+
if (!tenantIdOrConfig) {
|
|
98
|
+
throw new TypeError('Provide a tenant ID or both baseUrl and apiIndicator');
|
|
99
|
+
}
|
|
100
|
+
return { tenantId: tenantIdOrConfig.tenantId, options: tenantIdOrConfig };
|
|
101
|
+
};
|
|
102
|
+
export function createManagementApi(tenantIdOrConfig, options) {
|
|
103
|
+
const { tenantId, options: resolvedOptions } = resolveManagementApiArguments(tenantIdOrConfig, options);
|
|
104
|
+
if (tenantId !== undefined && tenantId.trim().length === 0) {
|
|
105
|
+
throw new TypeError('Tenant ID must not be empty');
|
|
106
|
+
}
|
|
107
|
+
const { clientId, clientSecret, tokenRequestTimeout, getTokenRequestSignal, requestTimeout } = resolvedOptions;
|
|
108
|
+
const configuredBaseUrl = resolvedOptions.baseUrl ?? (tenantId === undefined ? undefined : getBaseUrl(tenantId));
|
|
109
|
+
const apiIndicator = resolvedOptions.apiIndicator ??
|
|
110
|
+
(tenantId === undefined ? undefined : getManagementApiIndicator(tenantId));
|
|
111
|
+
if (configuredBaseUrl === undefined || apiIndicator === undefined) {
|
|
112
|
+
throw new TypeError('Provide a tenant ID or both baseUrl and apiIndicator');
|
|
113
|
+
}
|
|
114
|
+
const baseUrl = normalizeBaseUrl(configuredBaseUrl);
|
|
105
115
|
const clientCredentials = new ClientCredentials({
|
|
106
116
|
clientId,
|
|
107
117
|
clientSecret,
|
|
@@ -110,17 +120,41 @@ export function createManagementApi(tenantId, options) {
|
|
|
110
120
|
resource: apiIndicator,
|
|
111
121
|
scope: allScope,
|
|
112
122
|
},
|
|
123
|
+
tokenRequestTimeout,
|
|
124
|
+
getTokenRequestSignal,
|
|
113
125
|
});
|
|
126
|
+
const warnedScopes = new Set();
|
|
114
127
|
const apiClient = createApiClient({
|
|
115
128
|
baseUrl,
|
|
129
|
+
requestTimeout,
|
|
116
130
|
getToken: async () => {
|
|
117
131
|
const { value, scope } = await clientCredentials.getAccessToken();
|
|
118
|
-
if (scope !== allScope) {
|
|
132
|
+
if (scope !== allScope && !warnedScopes.has(scope)) {
|
|
133
|
+
warnedScopes.add(scope);
|
|
119
134
|
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
135
|
}
|
|
121
136
|
return value;
|
|
122
137
|
},
|
|
123
138
|
});
|
|
139
|
+
apiClient.use({
|
|
140
|
+
onResponse({ schemaPath, request, response }) {
|
|
141
|
+
if (isWellKnownPath(schemaPath)) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const authorization = request.headers.get('Authorization');
|
|
145
|
+
if (!authorization?.startsWith(bearerTokenPrefix)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const accessToken = authorization.slice(bearerTokenPrefix.length);
|
|
149
|
+
if (response.status === 401) {
|
|
150
|
+
// Do not retry here because the request body may have been consumed.
|
|
151
|
+
clientCredentials.invalidateAccessToken(accessToken);
|
|
152
|
+
}
|
|
153
|
+
else if (response.ok) {
|
|
154
|
+
clientCredentials.markAccessTokenAsValid(accessToken);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
});
|
|
124
158
|
return {
|
|
125
159
|
apiClient,
|
|
126
160
|
clientCredentials,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Client, ClientPathsWithMethod, MaybeOptionalInit, MethodResponse } from 'openapi-fetch';
|
|
2
|
+
type GetOperation<Paths extends NonNullable<unknown>, Path extends keyof Paths> = Paths[Path] extends {
|
|
3
|
+
get: infer Operation;
|
|
4
|
+
} ? Operation : never;
|
|
5
|
+
type OperationQuery<Operation> = Operation extends {
|
|
6
|
+
parameters: {
|
|
7
|
+
query?: infer Query;
|
|
8
|
+
};
|
|
9
|
+
} ? NonNullable<Query> : never;
|
|
10
|
+
type SupportsPagination<Operation> = [OperationQuery<Operation>] extends [never] ? false : 'page' extends keyof OperationQuery<Operation> ? 'page_size' extends keyof OperationQuery<Operation> ? true : false : false;
|
|
11
|
+
type GetPath<Paths extends NonNullable<unknown>> = ClientPathsWithMethod<Client<Paths>, 'get'>;
|
|
12
|
+
type PaginatedGetPath<Paths extends NonNullable<unknown>> = {
|
|
13
|
+
[Path in GetPath<Paths>]: SupportsPagination<GetOperation<Paths, Path>> extends true ? MethodResponse<Client<Paths>, 'get', Path> extends readonly unknown[] ? Path : never : never;
|
|
14
|
+
}[GetPath<Paths>];
|
|
15
|
+
type PaginationItem<Paths extends NonNullable<unknown>, Path extends PaginatedGetPath<Paths>> = MethodResponse<Client<Paths>, 'get', Path> extends ReadonlyArray<infer Item> ? Item : never;
|
|
16
|
+
type GetInit<Paths extends NonNullable<unknown>, Path extends PaginatedGetPath<Paths>> = MaybeOptionalInit<{
|
|
17
|
+
get: GetOperation<Paths, Path>;
|
|
18
|
+
}, 'get'>;
|
|
19
|
+
type PaginationOptions<Paths extends NonNullable<unknown>, Path extends PaginatedGetPath<Paths>> = Omit<NonNullable<GetInit<Paths, Path>>, 'parseAs'> & {
|
|
20
|
+
parseAs?: never;
|
|
21
|
+
};
|
|
22
|
+
type PaginationInit<Paths extends NonNullable<unknown>, Path extends PaginatedGetPath<Paths>> = undefined extends GetInit<Paths, Path> ? [PaginationOptions<Paths, Path>?] : [PaginationOptions<Paths, Path>];
|
|
23
|
+
/** Iterates over items returned by a paginated Management API GET endpoint. */
|
|
24
|
+
export type PaginateMethod<Paths extends NonNullable<unknown>> = <Path extends PaginatedGetPath<Paths>>(path: Path, ...init: PaginationInit<Paths, Path>) => AsyncGenerator<PaginationItem<Paths, Path>, void, undefined>;
|
|
25
|
+
/** An error response returned while iterating over a paginated Management API endpoint. */
|
|
26
|
+
export declare class ManagementApiPaginationError extends Error {
|
|
27
|
+
/**
|
|
28
|
+
* The error response metadata and headers. Its body has already been read by openapi-fetch; the
|
|
29
|
+
* parsed error body is available as `cause`.
|
|
30
|
+
*/
|
|
31
|
+
readonly response: Response;
|
|
32
|
+
readonly status: number;
|
|
33
|
+
constructor(response: Response, options?: ErrorOptions);
|
|
34
|
+
}
|
|
35
|
+
/** Creates a typed pagination method backed by an openapi-fetch GET method. */
|
|
36
|
+
export declare const createPaginate: <Paths extends NonNullable<unknown>>(get: Client<Paths>["GET"]) => PaginateMethod<Paths>;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const defaultPageSize = 100;
|
|
2
|
+
const hasNextLink = (headers) => headers
|
|
3
|
+
.get('Link')
|
|
4
|
+
?.split(',')
|
|
5
|
+
.some((link) => /;\s*rel=(?:"next"|next)(?:\s*;|\s*$)/iu.test(link.trim())) ?? false;
|
|
6
|
+
const isTotalNumberCapped = (headers) => headers.get('Total-Number-Is-Capped')?.toLowerCase() === 'true';
|
|
7
|
+
/** An error response returned while iterating over a paginated Management API endpoint. */
|
|
8
|
+
export class ManagementApiPaginationError extends Error {
|
|
9
|
+
constructor(response, options) {
|
|
10
|
+
super(`Management API pagination request failed with status ${response.status}`, options);
|
|
11
|
+
this.name = 'ManagementApiPaginationError';
|
|
12
|
+
this.response = response;
|
|
13
|
+
this.status = response.status;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Creates a typed pagination method backed by an openapi-fetch GET method. */
|
|
17
|
+
export const createPaginate = (get) => {
|
|
18
|
+
// eslint-disable-next-line no-restricted-syntax -- The runtime adapter narrows openapi-fetch's generic GET method after the public type validates the path and options.
|
|
19
|
+
const getPage = get;
|
|
20
|
+
const paginate = (path, options = {}) => {
|
|
21
|
+
const { params = {}, parseAs, ...requestOptions } = options;
|
|
22
|
+
if (parseAs !== undefined) {
|
|
23
|
+
throw new TypeError('The paginate method only supports JSON array responses');
|
|
24
|
+
}
|
|
25
|
+
const query = params.query ?? {};
|
|
26
|
+
const firstPage = query.page === undefined ? 1 : query.page;
|
|
27
|
+
if (typeof firstPage !== 'number' || !Number.isSafeInteger(firstPage) || firstPage < 1) {
|
|
28
|
+
throw new RangeError('The page parameter must be a positive safe integer');
|
|
29
|
+
}
|
|
30
|
+
const pageSize = query.page_size ?? defaultPageSize;
|
|
31
|
+
const iterate = async function* () {
|
|
32
|
+
/* eslint-disable-next-line @silverhand/fp/no-let, @silverhand/fp/no-mutation -- Pagination advances one page at a time without retaining prior page responses. */
|
|
33
|
+
for (let page = firstPage; page <= Number.MAX_SAFE_INTEGER; page += 1) {
|
|
34
|
+
// eslint-disable-next-line no-await-in-loop -- Each request depends on the previous response's pagination headers.
|
|
35
|
+
const result = await getPage(path, {
|
|
36
|
+
...requestOptions,
|
|
37
|
+
params: {
|
|
38
|
+
...params,
|
|
39
|
+
query: {
|
|
40
|
+
...query,
|
|
41
|
+
page,
|
|
42
|
+
page_size: pageSize,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
if ('error' in result) {
|
|
47
|
+
throw new ManagementApiPaginationError(result.response, { cause: result.error });
|
|
48
|
+
}
|
|
49
|
+
if (!Array.isArray(result.data)) {
|
|
50
|
+
throw new TypeError('Expected a paginated Management API response to contain an array');
|
|
51
|
+
}
|
|
52
|
+
if (result.data.length === 0) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
yield* result.data;
|
|
56
|
+
if (!isTotalNumberCapped(result.response.headers) &&
|
|
57
|
+
!hasNextLink(result.response.headers)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
return iterate();
|
|
63
|
+
};
|
|
64
|
+
// eslint-disable-next-line no-restricted-syntax -- The public generic type adds generated path and item types to the runtime iterator.
|
|
65
|
+
return paginate;
|
|
66
|
+
};
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logto/api",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.44.0-beta.1",
|
|
4
4
|
"description": "Logto API types and clients.",
|
|
5
5
|
"author": "Silverhand Inc. <contact@silverhand.io>",
|
|
6
6
|
"homepage": "https://github.com/logto-io/logto#readme",
|
|
@@ -26,6 +26,18 @@
|
|
|
26
26
|
"bugs": {
|
|
27
27
|
"url": "https://github.com/logto-io/logto/issues"
|
|
28
28
|
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"precommit": "lint-staged",
|
|
31
|
+
"prepublishOnly": "pnpm generate-types && pnpm build",
|
|
32
|
+
"generate-types": "./generate-types.sh",
|
|
33
|
+
"build": "./check-types.sh && rm -rf lib/ && tsc -p tsconfig.build.json",
|
|
34
|
+
"prepack": "pnpm build",
|
|
35
|
+
"lint": "eslint --ext .ts src",
|
|
36
|
+
"lint:report": "pnpm lint --format json --output-file report.json",
|
|
37
|
+
"test": "vitest run src",
|
|
38
|
+
"test:watch": "vitest src --watch",
|
|
39
|
+
"test:ci": "pnpm run test --silent --coverage"
|
|
40
|
+
},
|
|
29
41
|
"engines": {
|
|
30
42
|
"node": "^22.14.0 || ^24.0.0"
|
|
31
43
|
},
|
|
@@ -49,15 +61,5 @@
|
|
|
49
61
|
],
|
|
50
62
|
"extends": "@silverhand"
|
|
51
63
|
},
|
|
52
|
-
"prettier": "@silverhand/eslint-config/.prettierrc"
|
|
53
|
-
|
|
54
|
-
"precommit": "lint-staged",
|
|
55
|
-
"generate-types": "./generate-types.sh",
|
|
56
|
-
"build": "./check-types.sh && rm -rf lib/ && tsc -p tsconfig.build.json",
|
|
57
|
-
"lint": "eslint --ext .ts src",
|
|
58
|
-
"lint:report": "pnpm lint --format json --output-file report.json",
|
|
59
|
-
"test": "vitest run src",
|
|
60
|
-
"test:watch": "vitest src --watch",
|
|
61
|
-
"test:ci": "pnpm run test --silent --coverage"
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
+
"prettier": "@silverhand/eslint-config/.prettierrc"
|
|
65
|
+
}
|