@logto/api 1.44.0-beta.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 CHANGED
@@ -27,8 +27,8 @@ 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
- // For Logto Cloud
31
- const { apiClient } = createManagementApi('your-tenant-id', {
30
+ const { apiClient } = createManagementApi({
31
+ tenantId: 'your-tenant-id',
32
32
  clientId: 'your-client-id',
33
33
  clientSecret: 'your-client-secret',
34
34
  });
@@ -39,19 +39,44 @@ const response = await apiClient.get('/api/users');
39
39
  console.log(response.data);
40
40
  ```
41
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
+
42
64
  #### Self-hosted / OSS
43
65
 
44
66
  ```ts
45
67
  import { createManagementApi } from '@logto/api/management';
46
68
 
47
- const { apiClient } = createManagementApi('default', {
69
+ const { apiClient } = createManagementApi({
70
+ tenantId: 'default',
48
71
  clientId: 'your-client-id',
49
72
  clientSecret: 'your-client-secret',
50
73
  baseUrl: 'https://your-logto-instance.com',
51
- apiIndicator: 'https://your-logto-instance.com/api',
52
74
  });
53
75
  ```
54
76
 
77
+ If the Management API indicator cannot be derived from a tenant ID, omit `tenantId` and provide
78
+ both `baseUrl` and `apiIndicator`.
79
+
55
80
  #### Timeouts
56
81
 
57
82
  Token fetches and Management API network requests have separate 10-second timeouts. The API request
@@ -1,5 +1 @@
1
- /**
2
- * This file was auto-generated by openapi-typescript.
3
- * Do not make direct changes to the file.
4
- */
5
1
  export {};
@@ -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,14 +19,12 @@ export type CreateManagementApiOptions = {
17
19
  */
18
20
  clientSecret: string;
19
21
  /**
20
- * Override the base URL generated from the tenant ID.
21
- * Useful for testing or custom deployments.
22
+ * The base URL for the Logto instance. Required when the tenant ID is omitted.
22
23
  * Trailing slashes are ignored.
23
24
  */
24
25
  baseUrl?: string;
25
26
  /**
26
- * Override the API indicator for the management API.
27
- * Useful for testing or custom deployments.
27
+ * The resource indicator for the Management API. Required when the tenant ID is omitted.
28
28
  */
29
29
  apiIndicator?: string;
30
30
  /**
@@ -47,6 +47,17 @@ export type CreateManagementApiOptions = {
47
47
  */
48
48
  requestTimeout?: number;
49
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
+ });
50
61
  /**
51
62
  * Options for creating an API client with custom token authentication.
52
63
  */
@@ -97,8 +108,11 @@ type LowercaseHttpMethods = {
97
108
  patch: Client<paths>['PATCH'];
98
109
  trace: Client<paths>['TRACE'];
99
110
  };
100
- /** A typed Management API client with lowercase HTTP methods. */
101
- export type ManagementApiClient = Client<paths> & LowercaseHttpMethods;
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;
102
116
  /**
103
117
  * Creates an API client with custom token authentication.
104
118
  *
@@ -138,7 +152,7 @@ type ManagementApiReturnType = {
138
152
  clientCredentials: ClientCredentials;
139
153
  };
140
154
  /**
141
- * Creates a Management API client with the specified tenant ID and options.
155
+ * Creates a Management API client from a tenant ID or an explicit base URL and API indicator.
142
156
  *
143
157
  * Before using this function, ensure that you have created a machine-to-machine application in
144
158
  * Logto and granted it access to the Management API. See the documentation for more details:
@@ -148,16 +162,17 @@ type ManagementApiReturnType = {
148
162
  * This function sets up the API client with the necessary authentication using client credentials.
149
163
  * It will automatically handle token retrieval and renewal as needed.
150
164
  *
151
- * @param tenantId The tenant ID for which to create the Management API client. For OSS deployments,
152
- * you can pass any string as the tenant ID, for example, 'default'.
153
- * @param options The options for creating the Management API client, including client ID and secret.
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.
154
168
  * @returns An object containing the API client and client credentials instance.
155
169
  * @example
156
170
  * ```ts
157
171
  * import { createManagementApi } from '@logto/api/management';
158
172
  *
159
173
  * // Logto Cloud example
160
- * const { apiClient, clientCredentials } = createManagementApi('my-tenant-id', {
174
+ * const { apiClient, clientCredentials } = createManagementApi({
175
+ * tenantId: 'my-tenant-id',
161
176
  * clientId: 'my-client-id',
162
177
  * clientSecret: 'my-client-secret',
163
178
  * });
@@ -170,13 +185,13 @@ type ManagementApiReturnType = {
170
185
  * @example
171
186
  * ```ts
172
187
  * // OSS example
173
- * const { apiClient, clientCredentials } = createManagementApi('default', {
188
+ * const { apiClient, clientCredentials } = createManagementApi({
189
+ * tenantId: 'default',
174
190
  * clientId: 'my-client-id',
175
191
  * clientSecret: 'my-client-secret',
176
192
  * baseUrl: 'https://my-oss-logto-instance.com',
177
- * apiIndicator: 'https://default.logto.app/api',
178
193
  * });
179
194
  * ```
180
195
  */
196
+ export declare function createManagementApi(config: CreateManagementApiConfig): ManagementApiReturnType;
181
197
  export declare function createManagementApi(tenantId: string, options: CreateManagementApiOptions): ManagementApiReturnType;
182
- export {};
package/lib/management.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import createClient from 'openapi-fetch';
2
2
  import { ClientCredentials } from './client-credentials.js';
3
+ import { createPaginate } from './pagination.js';
3
4
  import { normalizeTimeout, toTimeoutMilliseconds } from './timeout.js';
5
+ export { ManagementApiPaginationError } from './pagination.js';
4
6
  /**
5
7
  * Returns the base URL for the Management API based on the tenant ID.
6
8
  * @param tenantId The tenant ID to construct the base URL.
@@ -29,8 +31,8 @@ const createRequestTimeoutFetch = (requestTimeout) => async (request, requestIni
29
31
  ]);
30
32
  return fetch(request, { ...requestInit, signal });
31
33
  };
32
- const addLowercaseHttpMethods = (client) =>
33
- // eslint-disable-next-line @silverhand/fp/no-mutating-assign -- Keep the original client identity while adding lowercase aliases.
34
+ const addClientMethods = (client) =>
35
+ // eslint-disable-next-line @silverhand/fp/no-mutating-assign -- Keep the original client identity while adding convenience methods.
34
36
  Object.assign(client, {
35
37
  get: client.GET,
36
38
  put: client.PUT,
@@ -40,6 +42,7 @@ Object.assign(client, {
40
42
  head: client.HEAD,
41
43
  patch: client.PATCH,
42
44
  trace: client.TRACE,
45
+ paginate: createPaginate(client.GET),
43
46
  });
44
47
  /**
45
48
  * Creates an API client with custom token authentication.
@@ -82,53 +85,33 @@ export function createApiClient(options) {
82
85
  return request;
83
86
  },
84
87
  });
85
- return addLowercaseHttpMethods(client);
88
+ return addClientMethods(client);
86
89
  }
87
- /**
88
- * Creates a Management API client with the specified tenant ID and options.
89
- *
90
- * Before using this function, ensure that you have created a machine-to-machine application in
91
- * Logto and granted it access to the Management API. See the documentation for more details:
92
- *
93
- * https://a.logto.io/m2m-mapi
94
- *
95
- * This function sets up the API client with the necessary authentication using client credentials.
96
- * It will automatically handle token retrieval and renewal as needed.
97
- *
98
- * @param tenantId The tenant ID for which to create the Management API client. For OSS deployments,
99
- * you can pass any string as the tenant ID, for example, 'default'.
100
- * @param options The options for creating the Management API client, including client ID and secret.
101
- * @returns An object containing the API client and client credentials instance.
102
- * @example
103
- * ```ts
104
- * import { createManagementApi } from '@logto/api/management';
105
- *
106
- * // Logto Cloud example
107
- * const { apiClient, clientCredentials } = createManagementApi('my-tenant-id', {
108
- * clientId: 'my-client-id',
109
- * clientSecret: 'my-client-secret',
110
- * });
111
- *
112
- * // Use apiClient to make requests to the Management API
113
- * const response = await apiClient.get('/api/users');
114
- * console.log(response.data);
115
- * ```
116
- *
117
- * @example
118
- * ```ts
119
- * // OSS example
120
- * const { apiClient, clientCredentials } = createManagementApi('default', {
121
- * clientId: 'my-client-id',
122
- * clientSecret: 'my-client-secret',
123
- * baseUrl: 'https://my-oss-logto-instance.com',
124
- * apiIndicator: 'https://default.logto.app/api',
125
- * });
126
- * ```
127
- */
128
- export function createManagementApi(tenantId, options) {
129
- const { clientId, clientSecret, tokenRequestTimeout, getTokenRequestSignal, requestTimeout } = options;
130
- const baseUrl = normalizeBaseUrl(options.baseUrl ?? getBaseUrl(tenantId));
131
- 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);
132
115
  const clientCredentials = new ClientCredentials({
133
116
  clientId,
134
117
  clientSecret,
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logto/api",
3
- "version": "1.44.0-beta.0",
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
- "scripts": {
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
+ }