From ae3bda7179b56bfb5547c84e67a64a7025c44a20 Mon Sep 17 00:00:00 2001 From: wangsijie Date: Mon, 17 Feb 2025 20:05:20 +0800 Subject: [PATCH] feat(connector): add x connector (#7015) --- .changeset/strange-ghosts-draw.md | 5 + packages/connectors/connector-x/README.md | 71 ++++++++ packages/connectors/connector-x/logo.svg | 3 + packages/connectors/connector-x/package.json | 70 ++++++++ .../connectors/connector-x/src/constant.ts | 53 ++++++ .../connectors/connector-x/src/index.test.ts | 114 ++++++++++++ packages/connectors/connector-x/src/index.ts | 164 ++++++++++++++++++ packages/connectors/connector-x/src/mock.ts | 4 + packages/connectors/connector-x/src/types.ts | 30 ++++ packages/connectors/connector-x/src/utils.ts | 12 ++ pnpm-lock.yaml | 95 ++++++++-- 11 files changed, 604 insertions(+), 17 deletions(-) create mode 100644 .changeset/strange-ghosts-draw.md create mode 100644 packages/connectors/connector-x/README.md create mode 100644 packages/connectors/connector-x/logo.svg create mode 100644 packages/connectors/connector-x/package.json create mode 100644 packages/connectors/connector-x/src/constant.ts create mode 100644 packages/connectors/connector-x/src/index.test.ts create mode 100644 packages/connectors/connector-x/src/index.ts create mode 100644 packages/connectors/connector-x/src/mock.ts create mode 100644 packages/connectors/connector-x/src/types.ts create mode 100644 packages/connectors/connector-x/src/utils.ts diff --git a/.changeset/strange-ghosts-draw.md b/.changeset/strange-ghosts-draw.md new file mode 100644 index 000000000..652a679ee --- /dev/null +++ b/.changeset/strange-ghosts-draw.md @@ -0,0 +1,5 @@ +--- +"@logto/connector-x": minor +--- + +add X (Twitter) social connector diff --git a/packages/connectors/connector-x/README.md b/packages/connectors/connector-x/README.md new file mode 100644 index 000000000..f0d7a5776 --- /dev/null +++ b/packages/connectors/connector-x/README.md @@ -0,0 +1,71 @@ +# X connector + +The official Logto connector for X (formerly Twitter) social sign-in. + +**Table of contents** +- [X connector](#x-connector) + - [Get started](#get-started) + - [Create an app in the X Developer Portal](#create-an-app-in-the-x-developer-portal) + - [Configure your connector](#configure-your-connector) + - [Config types](#config-types) + - [Test X connector](#test-x-connector) + - [Reference](#reference) + +## Get started + +The X connector enables end-users to sign in to your application using their own X (formerly Twitter) accounts via the X OAuth 2.0 authentication protocol. + +## Create an app in the X Developer Portal + +Go to the [X Developer Portal](https://developer.x.com/en/portal/projects-and-apps) and sign in with your X account. If you don’t have an account, you can register for one. + +Then, create an app. + +**Step 1:** Navigate to the app creation section. + +Once signed in, go to the "Projects & Apps" section and click on **"Create App"** (or **"New App"**, depending on the interface). + +**Step 2:** Fill in the app details. + +Complete the form with the following information: + +- **App Name:** Provide a unique and descriptive name for your application. +- **Application Description:** (Optional) Add a brief description of what your app does. +- **Website URL:** Enter the URL of your application's homepage. +- **Callback URL / Redirect URI:** In our case, this will be `${your_logto_endpoint}/callback/${connector_id}`. e.g. `https://foo.logto.app/callback/${connector_id}`. The `connector_id` can be found on the top bar of the Logto Admin Console connector details page. + +**Step 3:** Select permissions and scopes. + +Choose the permissions that your app requires. For social sign-in via X, make sure you enable the necessary scopes `tweet.read` and `users.read`. + +**Step 4:** Save your app. + +Click **"Create"** or **"Save"** to register your app. + +After creation, navigate to your app’s **"Keys and tokens"** section to retrieve your **OAuth 2.0 Client ID and Client Secret**. + +## Configure your connector + +In your Logto connector configuration, fill out the following fields with the values obtained from your App's "Keys and tokens" page's "OAuth 2.0 Client ID and Client Secret" section: + +- **clientId:** Your App's Client ID. +- **clientSecret:** Your App's Client Secret. + +`scope` is a space-delimited list of [scopes](https://docs.x.com/x-api/users/user-lookup-me). If not provided, the default scope is `tweet.read users.read`. + +### Config types + +| Name | Type | +| ------------ | ------ | +| clientId | string | +| clientSecret | string | +| scope | string | + +## Test X connector + +That's it! The X connector should now be available for end-users to sign in with their X accounts. Don't forget to [Enable the connector in the sign-in experience](https://docs.logto.io/docs/recipes/configure-connectors/social-connector/enable-social-sign-in/). + +## Reference + +- [X Developer documentation](https://developer.x.com/en/docs) +- [X OAuth 2.0 Authorization Code Flow with PKCE](https://developer.x.com/en/docs/authentication/oauth-2-0/authorization-code) diff --git a/packages/connectors/connector-x/logo.svg b/packages/connectors/connector-x/logo.svg new file mode 100644 index 000000000..a07689ef4 --- /dev/null +++ b/packages/connectors/connector-x/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/connectors/connector-x/package.json b/packages/connectors/connector-x/package.json new file mode 100644 index 000000000..fe002beb9 --- /dev/null +++ b/packages/connectors/connector-x/package.json @@ -0,0 +1,70 @@ +{ + "name": "@logto/connector-x", + "version": "0.0.0", + "description": "X web connector implementation.", + "author": "Silverhand Inc. ", + "dependencies": { + "@logto/connector-kit": "workspace:^4.0.0", + "@silverhand/essentials": "^2.9.1", + "ky": "^1.2.3", + "query-string": "^9.0.0", + "snakecase-keys": "^8.0.1", + "zod": "^3.23.8" + }, + "main": "./lib/index.js", + "module": "./lib/index.js", + "exports": "./lib/index.js", + "license": "MPL-2.0", + "type": "module", + "files": [ + "lib", + "docs", + "logo.svg", + "logo-dark.svg" + ], + "scripts": { + "precommit": "lint-staged", + "check": "tsc --noEmit", + "build": "tsup", + "dev": "tsup --watch", + "lint": "eslint --ext .ts src", + "lint:report": "pnpm lint --format json --output-file report.json", + "test": "vitest src", + "test:ci": "pnpm run test --silent --coverage", + "prepublishOnly": "pnpm build" + }, + "engines": { + "node": "^20.9.0" + }, + "eslintConfig": { + "extends": "@silverhand", + "settings": { + "import/core-modules": [ + "@silverhand/essentials", + "got", + "nock", + "snakecase-keys", + "zod" + ] + } + }, + "prettier": "@silverhand/eslint-config/.prettierrc", + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@silverhand/eslint-config": "6.0.1", + "@silverhand/ts-config": "6.0.0", + "@types/node": "^20.11.20", + "@types/supertest": "^6.0.2", + "@vitest/coverage-v8": "^2.1.8", + "eslint": "^8.56.0", + "lint-staged": "^15.0.2", + "nock": "14.0.0-beta.15", + "prettier": "^3.0.0", + "supertest": "^7.0.0", + "tsup": "^8.3.0", + "typescript": "^5.5.3", + "vitest": "^2.1.8" + } +} diff --git a/packages/connectors/connector-x/src/constant.ts b/packages/connectors/connector-x/src/constant.ts new file mode 100644 index 000000000..b6ef87bb2 --- /dev/null +++ b/packages/connectors/connector-x/src/constant.ts @@ -0,0 +1,53 @@ +import type { ConnectorMetadata } from '@logto/connector-kit'; +import { ConnectorPlatform, ConnectorConfigFormItemType } from '@logto/connector-kit'; + +export const authorizationEndpoint = 'https://twitter.com/i/oauth2/authorize'; +export const defaultScope = 'tweet.read users.read'; +export const accessTokenEndpoint = 'https://api.twitter.com/2/oauth2/token'; +export const userInfoEndpoint = 'https://api.twitter.com/2/users/me'; + +export const defaultMetadata: ConnectorMetadata = { + id: 'x-universal', + target: 'x', + platform: ConnectorPlatform.Universal, + name: { + en: 'X', + 'zh-CN': 'X', + 'tr-TR': 'X', + ko: 'X', + }, + logo: './logo.svg', + logoDark: null, + description: { + en: 'X (formerly Twitter) is a social media platform for real-time conversation and information sharing.', + 'zh-CN': 'X(前身为 Twitter)是一个实时对话和信息分享的社交媒体平台。', + 'tr-TR': + 'X (eski adıyla Twitter), gerçek zamanlı sohbet ve bilgi paylaşımı için bir sosyal medya platformudur.', + ko: 'X(구 Twitter)는 실시간 대화와 정보 공유를 위한 소셜 미디어 플랫폼입니다.', + }, + readme: './README.md', + formItems: [ + { + key: 'clientId', + type: ConnectorConfigFormItemType.Text, + label: 'Client ID', + required: true, + }, + { + key: 'clientSecret', + type: ConnectorConfigFormItemType.Text, + label: 'Client Secret', + required: true, + }, + { + key: 'scope', + type: ConnectorConfigFormItemType.Text, + label: 'Scope', + required: false, + description: + "The `scope` determines permissions granted by the user's authorization. If you are not sure what to enter, do not worry, just leave it blank.", + }, + ], +}; + +export const defaultTimeout = 5000; diff --git a/packages/connectors/connector-x/src/index.test.ts b/packages/connectors/connector-x/src/index.test.ts new file mode 100644 index 000000000..d21484a23 --- /dev/null +++ b/packages/connectors/connector-x/src/index.test.ts @@ -0,0 +1,114 @@ +import nock from 'nock'; + +import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './constant.js'; +import createConnector, { getAccessToken } from './index.js'; +import { mockedConfig } from './mock.js'; + +const getConfig = vi.fn().mockResolvedValue(mockedConfig); +const setSession = vi.fn(); +const getSession = vi.fn().mockResolvedValue({ + codeVerifier: 'codeVerifier', +}); + +vi.mock('./utils.js', () => ({ + generateCodeVerifier: vi.fn().mockReturnValue('codeVerifier'), + generateCodeChallenge: vi.fn().mockReturnValue('codeChallenge'), +})); + +describe('getAuthorizationUri', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should get a valid uri by redirectUri and state', async () => { + const connector = await createConnector({ getConfig }); + const authorizationUri = await connector.getAuthorizationUri( + { + state: 'some_state', + redirectUri: 'http://localhost:3000/callback', + connectorId: 'some_connector_id', + connectorFactoryId: 'some_connector_factory_id', + jti: 'some_jti', + headers: {}, + }, + setSession + ); + expect(setSession).toHaveBeenCalledWith({ + codeVerifier: 'codeVerifier', + }); + expect(authorizationUri).toEqual( + `${authorizationEndpoint}?response_type=code&client_id=%3Cclient-id%3E&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&scope=tweet.read+users.read&state=some_state&code_challenge=codeChallenge&code_challenge_method=S256` + ); + }); +}); + +describe('getAccessToken', () => { + afterEach(() => { + nock.cleanAll(); + vi.clearAllMocks(); + }); + + it('should get an accessToken by exchanging with code', async () => { + nock(accessTokenEndpoint).post('').reply(200, { + access_token: 'access_token', + scope: 'scope', + token_type: 'token_type', + expires_in: 3600, + }); + const { access_token } = await getAccessToken( + mockedConfig, + 'code', + 'codeVerifier', + 'redirectUri' + ); + expect(access_token).toEqual('access_token'); + }); +}); + +describe('getUserInfo', () => { + beforeEach(() => { + nock(accessTokenEndpoint).post('').query(true).reply(200, { + access_token: 'access_token', + scope: 'scope', + token_type: 'token_type', + expires_in: 3600, + }); + }); + + afterEach(() => { + nock.cleanAll(); + vi.clearAllMocks(); + }); + + it('should get valid SocialUserInfo', async () => { + nock(userInfoEndpoint) + .get('') + .reply(200, { + data: { + id: '1', + name: 'monalisa', + }, + }); + const connector = await createConnector({ getConfig }); + const socialUserInfo = await connector.getUserInfo( + { code: 'code', redirectUri: 'http://localhost:3000/callback' }, + getSession + ); + expect(socialUserInfo).toStrictEqual({ + id: '1', + name: 'monalisa', + rawData: { + data: { + id: '1', + name: 'monalisa', + }, + }, + }); + }); + + it('throws unrecognized error', async () => { + nock(userInfoEndpoint).get('').reply(500); + const connector = await createConnector({ getConfig }); + await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toThrow(); + }); +}); diff --git a/packages/connectors/connector-x/src/index.ts b/packages/connectors/connector-x/src/index.ts new file mode 100644 index 000000000..deb729429 --- /dev/null +++ b/packages/connectors/connector-x/src/index.ts @@ -0,0 +1,164 @@ +import { conditional } from '@silverhand/essentials'; + +import { + ConnectorError, + ConnectorErrorCodes, + validateConfig, + ConnectorType, + jsonGuard, +} from '@logto/connector-kit'; +import type { + GetAuthorizationUri, + GetUserInfo, + SocialConnector, + CreateConnector, + GetConnectorConfig, +} from '@logto/connector-kit'; +import ky, { HTTPError } from 'ky'; + +import { + authorizationEndpoint, + accessTokenEndpoint, + defaultMetadata, + defaultTimeout, + defaultScope, + userInfoEndpoint, +} from './constant.js'; +import type { XConfig } from './types.js'; +import { + xConfigGuard, + userInfoResponseGuard, + authResponseGuard, + accessTokenResponseGuard, +} from './types.js'; +import { generateCodeVerifier, generateCodeChallenge } from './utils.js'; + +const getAuthorizationUri = + (getConfig: GetConnectorConfig): GetAuthorizationUri => + async ({ state, redirectUri }, setSession) => { + const config = await getConfig(defaultMetadata.id); + validateConfig(config, xConfigGuard); + + const codeVerifier = generateCodeVerifier(); + const codeChallenge = generateCodeChallenge(codeVerifier); + + await setSession({ codeVerifier }); + + const queryParams = new URLSearchParams({ + response_type: 'code', + client_id: config.clientId, + redirect_uri: redirectUri, + scope: config.scope ?? defaultScope, + state, + code_challenge: codeChallenge, + code_challenge_method: 'S256', + }); + + return `${authorizationEndpoint}?${queryParams.toString()}`; + }; + +export const getAccessToken = async ( + config: XConfig, + code: string, + codeVerifier: string, + redirectUri: string +) => { + const basicAuth = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64'); + const response = await ky + .post(accessTokenEndpoint, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${basicAuth}`, + }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: redirectUri, + client_id: config.clientId, + code_verifier: codeVerifier, + }).toString(), + timeout: defaultTimeout, + }) + .json(); + + return accessTokenResponseGuard.parse(response); +}; + +const getUserInfo = + (getConfig: GetConnectorConfig): GetUserInfo => + async (data, getSession) => { + const config = await getConfig(defaultMetadata.id); + validateConfig(config, xConfigGuard); + + const authResponseResult = authResponseGuard.safeParse(data); + + if (!authResponseResult.success) { + throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(data)); + } + + const { code, redirectUri } = authResponseResult.data; + const { codeVerifier } = await getSession(); + + if (!codeVerifier || typeof codeVerifier !== 'string') { + throw new ConnectorError(ConnectorErrorCodes.General, { + message: 'Cannot find `codeVerifier` from connector session.', + }); + } + + try { + const { access_token, token_type } = await getAccessToken( + config, + code, + codeVerifier, + redirectUri + ); + + const userInfo = await ky + .get(userInfoEndpoint, { + headers: { + Authorization: `${token_type} ${access_token}`, + }, + timeout: defaultTimeout, + }) + .json(); + const userInfoResult = userInfoResponseGuard.safeParse(userInfo); + + if (!userInfoResult.success) { + throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, userInfoResult.error); + } + + const { + data: { id, name }, + } = userInfoResult.data; + + return { + id, + name: conditional(name), + rawData: jsonGuard.parse(userInfo), + }; + } catch (error: unknown) { + if (error instanceof HTTPError) { + const { status, body: rawBody } = error.response; + + if (status === 401) { + throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid); + } + + throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(rawBody)); + } + + throw error; + } + }; + +const createXConnector: CreateConnector = async ({ getConfig }) => { + return { + metadata: defaultMetadata, + type: ConnectorType.Social, + configGuard: xConfigGuard, + getAuthorizationUri: getAuthorizationUri(getConfig), + getUserInfo: getUserInfo(getConfig), + }; +}; + +export default createXConnector; diff --git a/packages/connectors/connector-x/src/mock.ts b/packages/connectors/connector-x/src/mock.ts new file mode 100644 index 000000000..a52d77ccc --- /dev/null +++ b/packages/connectors/connector-x/src/mock.ts @@ -0,0 +1,4 @@ +export const mockedConfig = { + clientId: '', + clientSecret: '', +}; diff --git a/packages/connectors/connector-x/src/types.ts b/packages/connectors/connector-x/src/types.ts new file mode 100644 index 000000000..dda5dda21 --- /dev/null +++ b/packages/connectors/connector-x/src/types.ts @@ -0,0 +1,30 @@ +import { z } from 'zod'; + +export const xConfigGuard = z.object({ + clientId: z.string(), + clientSecret: z.string(), + scope: z.string().optional(), +}); + +export type XConfig = z.infer; + +export const userInfoResponseGuard = z.object({ + data: z.object({ + id: z.string(), + name: z.string().optional().nullable(), + }), +}); + +export type UserInfoResponse = z.infer; + +export const authResponseGuard = z.object({ + code: z.string(), + redirectUri: z.string(), +}); + +export const accessTokenResponseGuard = z.object({ + access_token: z.string(), + token_type: z.string(), + expires_in: z.number(), + scope: z.string(), +}); diff --git a/packages/connectors/connector-x/src/utils.ts b/packages/connectors/connector-x/src/utils.ts new file mode 100644 index 000000000..7ab27db7d --- /dev/null +++ b/packages/connectors/connector-x/src/utils.ts @@ -0,0 +1,12 @@ +import crypto from 'node:crypto'; + +export const generateCodeVerifier = () => { + const buffer = crypto.randomBytes(32); + return buffer.toString('base64url'); +}; + +export const generateCodeChallenge = (verifier: string) => { + const hash = crypto.createHash('sha256'); + hash.update(verifier); + return hash.digest('base64url'); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf9df20b5..8a5fb7466 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2743,6 +2743,67 @@ importers: specifier: ^2.1.9 version: 2.1.9(@types/node@20.10.4)(jsdom@20.0.2)(lightningcss@1.25.1)(sass@1.77.8) + packages/connectors/connector-x: + dependencies: + '@logto/connector-kit': + specifier: workspace:^4.0.0 + version: link:../../toolkit/connector-kit + '@silverhand/essentials': + specifier: ^2.9.1 + version: 2.9.2 + ky: + specifier: ^1.2.3 + version: 1.2.3 + query-string: + specifier: ^9.0.0 + version: 9.0.0 + snakecase-keys: + specifier: ^8.0.1 + version: 8.0.1 + zod: + specifier: ^3.23.8 + version: 3.23.8 + devDependencies: + '@silverhand/eslint-config': + specifier: 6.0.1 + version: 6.0.1(eslint@8.57.0)(prettier@3.0.0)(typescript@5.5.3) + '@silverhand/ts-config': + specifier: 6.0.0 + version: 6.0.0(typescript@5.5.3) + '@types/node': + specifier: ^20.11.20 + version: 20.12.7 + '@types/supertest': + specifier: ^6.0.2 + version: 6.0.2 + '@vitest/coverage-v8': + specifier: ^2.1.8 + version: 2.1.9(vitest@2.1.9(@types/node@20.12.7)(jsdom@20.0.2)(lightningcss@1.25.1)(sass@1.77.8)) + eslint: + specifier: ^8.56.0 + version: 8.57.0 + lint-staged: + specifier: ^15.0.2 + version: 15.0.2 + nock: + specifier: 14.0.0-beta.15 + version: 14.0.0-beta.15 + prettier: + specifier: ^3.0.0 + version: 3.0.0 + supertest: + specifier: ^7.0.0 + version: 7.0.0 + tsup: + specifier: ^8.3.0 + version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.5.1)(typescript@5.5.3)(yaml@2.4.5) + typescript: + specifier: ^5.5.3 + version: 5.5.3 + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@20.12.7)(jsdom@20.0.2)(lightningcss@1.25.1)(sass@1.77.8) + packages/connectors/connector-xiaomi: dependencies: '@logto/connector-kit': @@ -17651,7 +17712,7 @@ snapshots: graphemer: 1.4.0 ignore: 5.3.1 natural-compare: 1.4.0 - semver: 7.6.0 + semver: 7.6.3 ts-api-utils: 1.3.0(typescript@5.5.3) optionalDependencies: typescript: 5.5.3 @@ -17697,8 +17758,8 @@ snapshots: debug: 4.4.0 globby: 11.1.0 is-glob: 4.0.3 - minimatch: 9.0.4 - semver: 7.6.0 + minimatch: 9.0.5 + semver: 7.6.3 ts-api-utils: 1.3.0(typescript@5.5.3) optionalDependencies: typescript: 5.5.3 @@ -17714,7 +17775,7 @@ snapshots: '@typescript-eslint/types': 7.7.0 '@typescript-eslint/typescript-estree': 7.7.0(typescript@5.5.3) eslint: 8.57.0 - semver: 7.6.0 + semver: 7.6.3 transitivePeerDependencies: - supports-color - typescript @@ -19918,7 +19979,7 @@ snapshots: eslint-compat-utils@0.5.0(eslint@8.57.0): dependencies: eslint: 8.57.0 - semver: 7.6.0 + semver: 7.6.3 eslint-config-prettier@9.1.0(eslint@8.57.0): dependencies: @@ -20052,8 +20113,8 @@ snapshots: get-tsconfig: 4.7.3 globals: 14.0.0 ignore: 5.3.1 - minimatch: 9.0.4 - semver: 7.6.0 + minimatch: 9.0.5 + semver: 7.6.3 eslint-plugin-no-use-extend-native@0.5.0: dependencies: @@ -20114,7 +20175,7 @@ snapshots: eslint-plugin-unicorn@52.0.0(eslint@8.57.0): dependencies: - '@babel/helper-validator-identifier': 7.25.7 + '@babel/helper-validator-identifier': 7.25.9 '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@eslint/eslintrc': 2.1.4 ci-info: 4.0.0 @@ -20129,7 +20190,7 @@ snapshots: read-pkg-up: 7.0.1 regexp-tree: 0.1.27 regjsparser: 0.10.0 - semver: 7.6.0 + semver: 7.6.3 strip-indent: 3.0.0 transitivePeerDependencies: - supports-color @@ -21460,10 +21521,10 @@ snapshots: istanbul-lib-instrument@6.0.1: dependencies: '@babel/core': 7.24.9 - '@babel/parser': 7.24.8 + '@babel/parser': 7.26.3 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.6.0 + semver: 7.6.3 transitivePeerDependencies: - supports-color @@ -21475,7 +21536,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.3.4 + debug: 4.4.0 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -24073,9 +24134,9 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-sorting@8.0.2(postcss@8.4.39): + postcss-sorting@8.0.2(postcss@8.5.1): dependencies: - postcss: 8.4.39 + postcss: 8.5.1 postcss-value-parser@3.3.1: {} @@ -25310,8 +25371,8 @@ snapshots: stylelint-order@6.0.4(stylelint@15.11.0(typescript@5.5.3)): dependencies: - postcss: 8.4.39 - postcss-sorting: 8.0.2(postcss@8.4.39) + postcss: 8.5.1 + postcss-sorting: 8.0.2(postcss@8.5.1) stylelint: 15.11.0(typescript@5.5.3) stylelint-scss@6.2.1(stylelint@15.11.0(typescript@5.5.3)): @@ -25438,7 +25499,7 @@ snapshots: synckit@0.8.8: dependencies: '@pkgr/core': 0.1.1 - tslib: 2.8.0 + tslib: 2.8.1 table-layout@3.0.2: dependencies: