mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
feat(connector): add x connector (#7015)
This commit is contained in:
parent
5735d9ddfc
commit
ae3bda7179
11 changed files with 604 additions and 17 deletions
5
.changeset/strange-ghosts-draw.md
Normal file
5
.changeset/strange-ghosts-draw.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@logto/connector-x": minor
|
||||
---
|
||||
|
||||
add X (Twitter) social connector
|
71
packages/connectors/connector-x/README.md
Normal file
71
packages/connectors/connector-x/README.md
Normal file
|
@ -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)
|
3
packages/connectors/connector-x/logo.svg
Normal file
3
packages/connectors/connector-x/logo.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100" height="100" viewBox="0 0 50 50">
|
||||
<path d="M 11 4 C 7.134 4 4 7.134 4 11 L 4 39 C 4 42.866 7.134 46 11 46 L 39 46 C 42.866 46 46 42.866 46 39 L 46 11 C 46 7.134 42.866 4 39 4 L 11 4 z M 13.085938 13 L 21.023438 13 L 26.660156 21.009766 L 33.5 13 L 36 13 L 27.789062 22.613281 L 37.914062 37 L 29.978516 37 L 23.4375 27.707031 L 15.5 37 L 13 37 L 22.308594 26.103516 L 13.085938 13 z M 16.914062 15 L 31.021484 35 L 34.085938 35 L 19.978516 15 L 16.914062 15 z"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 544 B |
70
packages/connectors/connector-x/package.json
Normal file
70
packages/connectors/connector-x/package.json
Normal file
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"name": "@logto/connector-x",
|
||||
"version": "0.0.0",
|
||||
"description": "X web connector implementation.",
|
||||
"author": "Silverhand Inc. <contact@silverhand.io>",
|
||||
"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"
|
||||
}
|
||||
}
|
53
packages/connectors/connector-x/src/constant.ts
Normal file
53
packages/connectors/connector-x/src/constant.ts
Normal file
|
@ -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;
|
114
packages/connectors/connector-x/src/index.test.ts
Normal file
114
packages/connectors/connector-x/src/index.test.ts
Normal file
|
@ -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();
|
||||
});
|
||||
});
|
164
packages/connectors/connector-x/src/index.ts
Normal file
164
packages/connectors/connector-x/src/index.ts
Normal file
|
@ -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<SocialConnector> = async ({ getConfig }) => {
|
||||
return {
|
||||
metadata: defaultMetadata,
|
||||
type: ConnectorType.Social,
|
||||
configGuard: xConfigGuard,
|
||||
getAuthorizationUri: getAuthorizationUri(getConfig),
|
||||
getUserInfo: getUserInfo(getConfig),
|
||||
};
|
||||
};
|
||||
|
||||
export default createXConnector;
|
4
packages/connectors/connector-x/src/mock.ts
Normal file
4
packages/connectors/connector-x/src/mock.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export const mockedConfig = {
|
||||
clientId: '<client-id>',
|
||||
clientSecret: '<client-secret>',
|
||||
};
|
30
packages/connectors/connector-x/src/types.ts
Normal file
30
packages/connectors/connector-x/src/types.ts
Normal file
|
@ -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<typeof xConfigGuard>;
|
||||
|
||||
export const userInfoResponseGuard = z.object({
|
||||
data: z.object({
|
||||
id: z.string(),
|
||||
name: z.string().optional().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type UserInfoResponse = z.infer<typeof userInfoResponseGuard>;
|
||||
|
||||
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(),
|
||||
});
|
12
packages/connectors/connector-x/src/utils.ts
Normal file
12
packages/connectors/connector-x/src/utils.ts
Normal file
|
@ -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');
|
||||
};
|
95
pnpm-lock.yaml
generated
95
pnpm-lock.yaml
generated
|
@ -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:
|
||||
|
|
Loading…
Add table
Reference in a new issue