@logto/api 1.43.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 CHANGED
@@ -33,8 +33,9 @@ const { apiClient } = createManagementApi('your-tenant-id', {
33
33
  clientSecret: 'your-client-secret',
34
34
  });
35
35
 
36
- // Make API calls
37
- const response = await apiClient.GET('/api/users');
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
 
@@ -51,6 +52,24 @@ const { apiClient } = createManagementApi('default', {
51
52
  });
52
53
  ```
53
54
 
55
+ #### Timeouts
56
+
57
+ Token fetches and Management API network requests have separate 10-second timeouts. The API request
58
+ timeout starts after token retrieval, so a request that needs a new token can use both timeout
59
+ periods. Use `tokenRequestTimeout` and `requestTimeout` to configure them in seconds. Set either option
60
+ to `0` or a negative value to disable its timeout.
61
+
62
+ Use `getTokenRequestSignal` to return a custom signal for each token request. The signal is composed
63
+ with `tokenRequestTimeout`, and the first one to abort cancels the token request. A per-request
64
+ `signal` is similarly composed with `requestTimeout` for the Management API request. Timeouts and
65
+ custom cancellation reject the API call instead of returning a response object.
66
+
67
+ #### Token refresh
68
+
69
+ After a Management API request returns `401`, the next request fetches a new token. If the replacement
70
+ token also receives a `401`, it remains cached until a request succeeds or the token expires. The SDK
71
+ does not automatically retry the failed API request.
72
+
54
73
  #### Custom authentication
55
74
 
56
75
  For advanced use cases where you need full control over the authentication logic, use `createApiClient`:
@@ -67,7 +86,7 @@ const client = createApiClient({
67
86
  });
68
87
 
69
88
  // Type-safe API calls
70
- const response = await client.GET('/api/applications/{id}', {
89
+ const response = await client.get('/api/applications/{id}', {
71
90
  params: { path: { id: 'your-app-id' } },
72
91
  });
73
92
  ```
@@ -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
- * @returns The access token as a string.
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
- * @returns The access token as a string.
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
- // If the access token is not set or has expired, refresh it
25
- if (!this.accessToken?.expiresAt ||
26
- this.accessToken.expiresAt <= Math.floor(now.getTime() / 1000) + this.accessTokenExpiryLeeway) {
27
- const response = await fetch(this.options.tokenEndpoint, {
28
- method: 'POST',
29
- headers: {
30
- 'Content-Type': 'application/x-www-form-urlencoded',
31
- },
32
- body: new URLSearchParams({
33
- grant_type: 'client_credentials',
34
- client_id: this.options.clientId,
35
- client_secret: this.options.clientSecret,
36
- ...this.options.tokenParams,
37
- }).toString(),
38
- });
39
- if (!response.ok) {
40
- throw new ClientCredentialsError(`Failed to fetch access token: ${response.status} ${response.statusText}`);
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
- const data = await response.json();
43
- if (typeof data !== 'object' || data === null || !('access_token' in data)) {
44
- throw new ClientCredentialsError('Invalid response from token endpoint');
98
+ finally {
99
+ abort.clear();
45
100
  }
46
- if (!('expires_in' in data) || typeof data.expires_in !== 'number') {
47
- throw new ClientCredentialsError('Invalid or missing expires_in in token response');
101
+ }
102
+ catch (error) {
103
+ if (error instanceof ClientCredentialsError) {
104
+ throw error;
48
105
  }
49
- this.accessToken = {
50
- value: String(data.access_token),
51
- expiresAt: Math.floor(now.getTime() / 1000) + data.expires_in,
52
- scope: 'scope' in data && typeof data.scope === 'string' ? data.scope : undefined,
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 this.accessToken;
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
  }
@@ -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.GET('/api/applications/{id}', {
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): Client<paths>;
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: Client<paths>;
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.GET('/api/users');
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.GET('/api/applications/{id}', {
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 client = createClient({ baseUrl });
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.includes('/.well-known/')) {
77
+ if (isWellKnownPath(schemaPath)) {
51
78
  return;
52
79
  }
53
80
  const token = await getToken();
54
- request.headers.set('Authorization', `Bearer ${token}`);
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.GET('/api/users');
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,
@@ -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.43.0",
3
+ "version": "1.44.0-beta.0",
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",