diff --git a/.changeset/calm-ladybugs-doubt.md b/.changeset/calm-ladybugs-doubt.md new file mode 100644 index 000000000..df009bb90 --- /dev/null +++ b/.changeset/calm-ladybugs-doubt.md @@ -0,0 +1,5 @@ +--- +"@logto/core": patch +--- + +add summary and description to APIs diff --git a/packages/core/src/routes/admin-user/basic.openapi.json b/packages/core/src/routes/admin-user/basic.openapi.json index d0a3c57a2..9f8ea5977 100644 --- a/packages/core/src/routes/admin-user/basic.openapi.json +++ b/packages/core/src/routes/admin-user/basic.openapi.json @@ -1,4 +1,10 @@ { + "tags": [ + { + "name": "Users", + "description": "Endpoints for user management. Including creating, updating, deleting, and querying users with flexible filters. In addition to the endpoints, see [🧑‍🚀 Manage users](https://docs.logto.io/docs/recipes/manage-users/) for more insights." + } + ], "paths": { "/api/users/{userId}": { "get": { diff --git a/packages/core/src/routes/application.openapi.json b/packages/core/src/routes/application.openapi.json index ba6457ed2..f3da556db 100644 --- a/packages/core/src/routes/application.openapi.json +++ b/packages/core/src/routes/application.openapi.json @@ -2,7 +2,7 @@ "tags": [ { "name": "Applications", - "description": "Application represents a registered software program or service that has been authorized to access user information and perform actions on behalf of users within the system." + "description": "Application represents your registered software program or service that has been authorized to access user information and perform actions on behalf of users within the system. Currently, Logto supports four types of applications:\n\n- Traditional web\n\n- Single-page app\n- Native app\n- Machine-to-machine app.\n\nDepending on the application type, it may have different authentication flows and access to the system. See [🔗 Integrate Logto in your application](https://docs.logto.io/docs/recipes/integrate-logto/) to learn more about how to integrate Logto into your application.\n\nRole-based access control (RBAC) is supported for machine-to-machine applications. See [🔐 Role-based access control (RBAC)](https://docs.logto.io/docs/recipes/rbac/) to get started with role-based access control." } ], "paths": { diff --git a/packages/core/src/routes/authn.openapi.json b/packages/core/src/routes/authn.openapi.json index 41bbd4e86..54b0d1b65 100644 --- a/packages/core/src/routes/authn.openapi.json +++ b/packages/core/src/routes/authn.openapi.json @@ -1,4 +1,10 @@ { + "tags": [ + { + "name": "Authn", + "description": "Authentication endpoints for third-party integrations and identity providers." + } + ], "paths": { "/api/authn/hasura": { "get": { diff --git a/packages/core/src/routes/custom-phrases.openapi.json b/packages/core/src/routes/custom-phrases.openapi.json index 8cec2c270..83ee65768 100644 --- a/packages/core/src/routes/custom-phrases.openapi.json +++ b/packages/core/src/routes/custom-phrases.openapi.json @@ -2,7 +2,7 @@ "tags": [ { "name": "Custom phrases", - "description": "APIs for managing custom phrases that allow you to customize the phrases displayed when experiencing the sign-in flow." + "description": "Endpoints for managing custom phrases that allow you to customize the phrases displayed in the sign-in experience.\n\nSee [Localized language](https://docs.logto.io/docs/recipes/customize-sie/localized-language/) to learn more about custom phrases for localization." } ], "paths": { @@ -12,26 +12,27 @@ "description": "Get all custom phrases for all languages.", "responses": { "200": { - "description": "A list of custom phrases." + "description": "An array of custom phrases." } } } }, "/api/custom-phrases/{languageTag}": { "get": { - "summary": "Get custom phrases by language tag", + "summary": "Get custom phrases", "description": "Get custom phrases for the specified language tag.", "responses": { "200": { "description": "Custom phrases for the specified language tag." }, "404": { - "description": "Custom phrases not found for the specified language tag." + "description": "Custom phrases not found." } } }, "put": { - "summary": "Upsert custom phrases by language tag", + "summary": "Upsert custom phrases", + "description": "Upsert custom phrases for the specified language tag. Upsert means that if the custom phrases already exist, they will be updated. Otherwise, they will be created.", "requestBody": { "required": true, "content": { @@ -45,7 +46,7 @@ }, "responses": { "201": { - "description": "Custom phrases created/updated successfully." + "description": "Custom phrases created or updated successfully." }, "422": { "description": "Invalid translation structure." @@ -53,16 +54,17 @@ } }, "delete": { - "summary": "Delete custom phrases by language tag", + "summary": "Delete custom phrase", + "description": "Delete custom phrases for the specified language tag.", "responses": { "204": { "description": "Custom phrases deleted successfully." }, "404": { - "description": "Custom phrases not found for the specified language tag." + "description": "Custom phrases not found." }, "409": { - "description": "Cannot delete default language." + "description": "Cannot delete the default language." } } } diff --git a/packages/core/src/routes/dashboard.openapi.json b/packages/core/src/routes/dashboard.openapi.json index c029c5857..dd690f006 100644 --- a/packages/core/src/routes/dashboard.openapi.json +++ b/packages/core/src/routes/dashboard.openapi.json @@ -2,7 +2,7 @@ "tags": [ { "name": "Dashboard", - "description": "The APIs that power the dashboard page of Console to show the statistics of the current tenant." + "description": "Endpoints that power the dashboard page of Console to show the statistics of the current tenant." } ], "paths": { diff --git a/packages/core/src/routes/hooks.openapi.json b/packages/core/src/routes/hooks.openapi.json index 931a12b50..07f2e3f80 100644 --- a/packages/core/src/routes/hooks.openapi.json +++ b/packages/core/src/routes/hooks.openapi.json @@ -2,7 +2,7 @@ "tags": [ { "name": "Hooks", - "description": "Hook enables you to effortlessly receive real-time updates regarding specific events, such as user registration, sign-in, or password reset." + "description": "Hook enables you to effortlessly receive real-time updates regarding specific events, such as user registration, sign-in, or password reset. See [🪝 Webhooks] to get started and learn more." } ], "paths": { diff --git a/packages/core/src/routes/interaction/index.openapi.json b/packages/core/src/routes/interaction/index.openapi.json new file mode 100644 index 000000000..93e1d5ed4 --- /dev/null +++ b/packages/core/src/routes/interaction/index.openapi.json @@ -0,0 +1,8 @@ +{ + "tags": [ + { + "name": "Interaction", + "description": "Interaction endpoints are used to manage and process interactions for end-users, such as sign-in experience. Currently, all interaction endpoints are used internally as part of the authentication flow, and they are not useful to developers directly." + } + ] +} diff --git a/packages/core/src/routes/logto-config.openapi.json b/packages/core/src/routes/logto-config.openapi.json index 06cf1e4cd..772a248c3 100644 --- a/packages/core/src/routes/logto-config.openapi.json +++ b/packages/core/src/routes/logto-config.openapi.json @@ -2,7 +2,7 @@ "tags": [ { "name": "Configs", - "description": "Endpoints for managing Logto global configurations for the tenant." + "description": "Endpoints for managing Logto global configurations for the tenant, such as admin console config and OIDC signing keys.\n\nSee [🔑 Signing keys](https://docs.logto.io/docs/recipes/signing-keys-rotation/) to learn more about signing keys and key rotation." } ], "paths": { @@ -35,7 +35,7 @@ "/api/configs/oidc/{keyType}": { "get": { "summary": "Get OIDC keys", - "description": "Get OIDC keys by key type. The actual key will be redacted from the result.", + "description": "Get OIDC signing keys by key type. The actual key will be redacted from the result.", "parameters": [ { "in": "path", @@ -45,7 +45,7 @@ ], "responses": { "200": { - "description": "An array of OIDC keys for the given key type." + "description": "An array of OIDC signing keys for the given key type." } } } @@ -53,7 +53,7 @@ "/api/configs/oidc/{keyType}/{keyId}": { "delete": { "summary": "Delete OIDC key", - "description": "Delete an OIDC key by key type and key ID.", + "description": "Delete an OIDC signing key by key type and key ID.", "parameters": [ { "in": "path", @@ -100,7 +100,7 @@ }, "responses": { "200": { - "description": "An array of OIDC keys after rotation." + "description": "An array of OIDC signing keys after rotation." } } } diff --git a/packages/core/src/routes/organization/index.openapi.json b/packages/core/src/routes/organization/index.openapi.json index 96c4d0b79..03e48a230 100644 --- a/packages/core/src/routes/organization/index.openapi.json +++ b/packages/core/src/routes/organization/index.openapi.json @@ -2,7 +2,7 @@ "tags": [ { "name": "Organizations", - "description": "Organization is a concept that brings together multiple identities (mostly users). Logto supports multiple organizations, and each organization can have multiple users.\n\nEvery organization shares the same set (organization template) of roles and permissions. Each user can have different roles in different organizations." + "description": "Organization is a concept that brings together multiple identities (mostly users). Logto supports multiple organizations, and each organization can have multiple users.\n\nEvery organization shares the same set (organization template) of roles and permissions. Each user can have different roles in different organizations. See [🏢 Organizations (Multi-tenancy)](https://docs.logto.io/docs/recipes/organizations/) to get started with organizations and organization template." } ], "paths": { diff --git a/packages/core/src/routes/organization/roles.openapi.json b/packages/core/src/routes/organization/roles.openapi.json index 347c4f43a..11d49d29a 100644 --- a/packages/core/src/routes/organization/roles.openapi.json +++ b/packages/core/src/routes/organization/roles.openapi.json @@ -2,7 +2,7 @@ "tags": [ { "name": "Organization roles", - "description": "Organization roles are used to define a set of organization scopes that can be assigned to users. Every organization role is a part of the organization template.\n\nOrganization roles will only be meaningful within an organization context. For example, a user may have an `admin` role for organization A, but not for organization B." + "description": "Organization roles are used to define a set of organization scopes that can be assigned to users. Every organization role is a part of the organization template.\n\nOrganization roles will only be meaningful within an organization context. For example, a user may have an `admin` role for organization A, but not for organization B. See [🏢 Organizations (Multi-tenancy)](https://docs.logto.io/docs/recipes/organizations/) to get started with organizations and organization template." } ], "paths": { diff --git a/packages/core/src/routes/organization/scopes.openapi.json b/packages/core/src/routes/organization/scopes.openapi.json index c636c51e7..48f6835eb 100644 --- a/packages/core/src/routes/organization/scopes.openapi.json +++ b/packages/core/src/routes/organization/scopes.openapi.json @@ -2,7 +2,7 @@ "tags": [ { "name": "Organization scopes", - "description": "Organization scopes (permissions) are used to define actions that can be performed on a organization. Every organization scope is a part of the organization template.\n\nOrganization scopes will only be meaningful within an organization context. For example, a user may have a `read` scope for organization A, but not for organization B." + "description": "Organization scopes (permissions) are used to define actions that can be performed on a organization. Every organization scope is a part of the organization template.\n\nOrganization scopes will only be meaningful within an organization context. For example, a user may have a `read` scope for organization A, but not for organization B. See [🏢 Organizations (Multi-tenancy)](https://docs.logto.io/docs/recipes/organizations/) to get started with organizations and organization template." } ], "paths": { diff --git a/packages/core/src/routes/resource.openapi.json b/packages/core/src/routes/resource.openapi.json index 3471666fd..a07aa28fb 100644 --- a/packages/core/src/routes/resource.openapi.json +++ b/packages/core/src/routes/resource.openapi.json @@ -1,4 +1,10 @@ { + "tags": [ + { + "name": "Resources", + "description": "Resources (API resources) represent the APIs that you want to protect with Logto. Each resource has a unique indicator (URI) and a set of scopes (permissions). The resources will be used in the authorization process which conforms to [RFC 8707: Resource Indicators for OAuth 2.0](https://www.rfc-editor.org/rfc/rfc8707.html).\n\nSee [⚔️ Protect your API](https://docs.logto.io/docs/recipes/protect-your-api/) to learn more about how to define API resources and protect your APIs with Logto." + } + ], "paths": { "/api/resources": { "get": { diff --git a/packages/core/src/routes/role.openapi.json b/packages/core/src/routes/role.openapi.json index b272d4c89..5de4f772c 100644 --- a/packages/core/src/routes/role.openapi.json +++ b/packages/core/src/routes/role.openapi.json @@ -1,4 +1,10 @@ { + "tags": [ + { + "name": "Roles", + "description": "Role management for API resource RBAC (role-based access control). See [🔐 Role-based access control (RBAC)](https://docs.logto.io/docs/recipes/rbac/) to get started with role-based access control." + } + ], "paths": { "/api/roles": { "get": { diff --git a/packages/core/src/routes/sign-in-experience/index.openapi.json b/packages/core/src/routes/sign-in-experience/index.openapi.json index 7a5b60d68..19e9059d8 100644 --- a/packages/core/src/routes/sign-in-experience/index.openapi.json +++ b/packages/core/src/routes/sign-in-experience/index.openapi.json @@ -2,7 +2,7 @@ "tags": [ { "name": "Sign-in experience", - "description": "Set the Sign-in experience configuration to customize your sign-in experience." + "description": "Endpoints for customizing Logto sign-in experience. See [🎨 Customize sign-in experience](https://docs.logto.io/docs/recipes/customize-sie/) to learn more about how the configuration works and reflects on the user interface." } ], "paths": { diff --git a/packages/core/src/routes/status.openapi.json b/packages/core/src/routes/status.openapi.json index 9fcd46bb1..0ec0500f3 100644 --- a/packages/core/src/routes/status.openapi.json +++ b/packages/core/src/routes/status.openapi.json @@ -1,4 +1,10 @@ { + "tags": [ + { + "name": "Status", + "description": "Endpoints for health check." + } + ], "paths": { "/api/status": { "get": { diff --git a/packages/core/src/routes/swagger/index.openapi.json b/packages/core/src/routes/swagger/index.openapi.json new file mode 100644 index 000000000..6ccddc322 --- /dev/null +++ b/packages/core/src/routes/swagger/index.openapi.json @@ -0,0 +1,21 @@ +{ + "tags": [ + { + "name": "Swagger.json", + "description": "Endpoints for the Swagger JSON document." + } + ], + "paths": { + "/api/swagger.json": { + "get": { + "summary": "Get Swagger JSON", + "description": "The endpoint for the current JSON document. The JSON conforms to the [OpenAPI v3.0.1](https://spec.openapis.org/oas/v3.0.1) (a.k.a. Swagger) specification.", + "responses": { + "200": { + "description": "The JSON document." + } + } + } + } + } +} diff --git a/packages/core/src/routes/swagger/index.ts b/packages/core/src/routes/swagger/index.ts index 90367af59..4ccde8efa 100644 --- a/packages/core/src/routes/swagger/index.ts +++ b/packages/core/src/routes/swagger/index.ts @@ -25,6 +25,7 @@ import { findSupplementFiles, normalizePath, validateSupplement, + validateSwaggerDocument, } from './utils/general.js'; import { buildParameters, @@ -220,19 +221,22 @@ export default function swaggerRoutes ({ name: tag })), }; - if (EnvSet.values.isUnitTest) { - consoleLog.warn('Skip validating supplement documents in unit test.'); - } else { - for (const document of supplementDocuments) { - validateSupplement(baseDocument, document); - } - } - const data = supplementDocuments.reduce( (document, supplement) => deepmerge(document, supplement, { arrayMerge: mergeParameters }), baseDocument ); + if (EnvSet.values.isUnitTest) { + consoleLog.warn('Skip validating swagger document in unit test.'); + } + // Don't throw for integrity check in production as it has no benefit. + else if (!EnvSet.values.isProduction || EnvSet.values.isIntegrationTest) { + for (const document of supplementDocuments) { + validateSupplement(baseDocument, document); + } + validateSwaggerDocument(data); + } + ctx.body = { ...data, tags: data.tags?.slice().sort((tagA, tagB) => tagA.name.localeCompare(tagB.name)), diff --git a/packages/core/src/routes/swagger/utils/general.ts b/packages/core/src/routes/swagger/utils/general.ts index 3d2cecae8..b7c7ca5dc 100644 --- a/packages/core/src/routes/swagger/utils/general.ts +++ b/packages/core/src/routes/swagger/utils/general.ts @@ -6,6 +6,8 @@ import { isKeyInObject, type Optional } from '@silverhand/essentials'; import { OpenAPIV3 } from 'openapi-types'; import { z } from 'zod'; +import { consoleLog } from '#src/utils/console.js'; + const capitalize = (value: string) => value.charAt(0).toUpperCase() + value.slice(1); /** @@ -142,3 +144,60 @@ export const validateSupplement = ( ); } }; + +/** + * Check if the given OpenAPI document is valid for being served as the swagger document: + * + * - Every path + method combination must have a tag, summary, and description. + * - Every tag must have a description. + * + * @throws {TypeError} if the document is invalid. + */ +export const validateSwaggerDocument = (document: OpenAPIV3.Document) => { + for (const [path, operations] of Object.entries(document.paths)) { + if (path.startsWith('/api/interaction')) { + consoleLog.warn(`Path \`${path}\` is not documented. Do something!`); + continue; + } + + // This path is for admin tenant only, skip it. + if (path === '/api/.well-known/endpoints/{tenantId}') { + continue; + } + + if (!operations) { + continue; + } + + for (const method of Object.values(OpenAPIV3.HttpMethods)) { + const operation = operations[method]; + + if (!operation) { + continue; + } + + if (Array.isArray(operation)) { + throw new TypeError( + `Path \`${path}\` and operation \`${method}\` must be an object, not an array.` + ); + } + + assert( + operation.tags?.length, + `Path \`${path}\` and operation \`${method}\` must have at least one tag.` + ); + assert( + operation.summary, + `Path \`${path}\` and operation \`${method}\` must have a summary.` + ); + assert( + operation.description, + `Path \`${path}\` and operation \`${method}\` must have a description.` + ); + } + + for (const tag of document.tags ?? []) { + assert(tag.description, `Tag \`${tag.name}\` must have a description.`); + } + } +}; diff --git a/packages/core/src/routes/swagger/utils/parameters.ts b/packages/core/src/routes/swagger/utils/parameters.ts index 587cecdf5..57bd966d2 100644 --- a/packages/core/src/routes/swagger/utils/parameters.ts +++ b/packages/core/src/routes/swagger/utils/parameters.ts @@ -120,14 +120,33 @@ const isObjectArray = (value: unknown): value is Array> * Merge two arrays. If the two arrays are both object arrays, merge them with the following * rules: * - * - If the source array has an item with `name` and `in` properties, and the destination array - * also has an item with the same `name` and `in` properties, merge the two items with - * `deepmerge`. + * - If the source array has an item with `name` properties, and the destination array + * also has an item with the same `name` and `in` properties, merge the two items with + * `deepmerge`. It is assumed that the two items are using `name` for identifying the same + * parameter, and may use `in` to distinguish the same parameter in different places. * - Otherwise, append the item to the destination array (the default behavior of - * `deepmerge`). + * `deepmerge`). * * Otherwise, use `deepmerge` to merge the two arrays. * + * @example + * ```ts + * mergeParameters( + * [{ name: 'foo', in: 'query', required: true }, { name: 'bar', in: 'query', required: true }], + * [{ name: 'foo', in: 'query', required: false }] + * ); // [{ name: 'foo', in: 'query', required: false }, { name: 'bar', in: 'query', required: true }] + * + * mergeParameters( + * [{ name: 'foo', required: true }, { name: 'bar', required: true }], + * [{ name: 'foo', in: 'query', required: false }] + * ); + * // [ + * // { name: 'foo', required: true }, + * // { name: 'bar', required: true }, + * // { name: 'foo', in: 'query', required: false } + * // ] + * ``` + * * @param destination The destination array. * @param source The source array. * @returns The merged array. @@ -140,7 +159,7 @@ export const mergeParameters = (destination: unknown[], source: unknown[]) => { const result = destination.slice(); for (const item of source) { - if (!('name' in item) || !('in' in item)) { + if (!('name' in item)) { // eslint-disable-next-line @silverhand/fp/no-mutating-methods result.push(item); continue; diff --git a/packages/core/src/routes/user-assets.openapi.json b/packages/core/src/routes/user-assets.openapi.json new file mode 100644 index 000000000..5b2ebf350 --- /dev/null +++ b/packages/core/src/routes/user-assets.openapi.json @@ -0,0 +1,32 @@ +{ + "tags": [ + { + "name": "User assets", + "description": "Endpoints for managing user uploaded assets." + } + ], + "paths": { + "/api/user-assets/service-status": { + "get": { + "summary": "Get service status", + "description": "Get user assets service status.", + "responses": { + "200": { + "description": "An object containing the service status and metadata." + } + } + } + }, + "/api/user-assets": { + "post": { + "summary": "Upload asset", + "description": "Upload a user asset.", + "responses": { + "200": { + "description": "An object containing the uploaded asset metadata." + } + } + } + } + } +} diff --git a/packages/core/src/routes/verification-code.openapi.json b/packages/core/src/routes/verification-code.openapi.json index 14466ab1c..9997e7d70 100644 --- a/packages/core/src/routes/verification-code.openapi.json +++ b/packages/core/src/routes/verification-code.openapi.json @@ -2,7 +2,7 @@ "tags": [ { "name": "Verification codes", - "description": "Endpoints for handling verification codes.\n\nNote: before you call the endpoints, you need to setup your email/SMS connector first." + "description": "Endpoints for handling verification codes. It is helpful when building a custom profile page in your app. See [👤 User profile](https://docs.logto.io/docs/recipes/user-profile/#optional-validate-verification-code) for more details.\n\nNote: Before you call the endpoints, you need to setup your email/SMS connector first." } ], "paths": {