mirror of
https://github.com/logto-io/logto.git
synced 2025-02-24 22:05:56 -05:00
feat(connector): add slack connector (#7021)
This commit is contained in:
parent
5086f4bd23
commit
e4eeccd36e
10 changed files with 522 additions and 0 deletions
5
.changeset/rude-books-applaud.md
Normal file
5
.changeset/rude-books-applaud.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@logto/connector-slack": minor
|
||||
---
|
||||
|
||||
add slack social connector
|
59
packages/connectors/connector-slack/README.md
Normal file
59
packages/connectors/connector-slack/README.md
Normal file
|
@ -0,0 +1,59 @@
|
|||
# Slack connector
|
||||
|
||||
The official Logto connector for Slack social sign-in.
|
||||
|
||||
**Table of contents**
|
||||
- [Slack connector](#slack-connector)
|
||||
- [Get started](#get-started)
|
||||
- [Set up Slack App](#set-up-slack-app)
|
||||
- [Configure your connector](#configure-your-connector)
|
||||
- [Config types](#config-types)
|
||||
- [Test Slack connector](#test-slack-connector)
|
||||
- [Reference](#reference)
|
||||
|
||||
## Get started
|
||||
|
||||
The Slack connector enables end-users to sign in to your application using their own Slack accounts via the Slack OAuth 2.0 authentication protocol.
|
||||
|
||||
## Set up Slack App
|
||||
|
||||
Go to the [Slack API: Applications](https://api.slack.com/apps) and sign in with your Slack account. If you don’t have an account, you can register for one.
|
||||
|
||||
Then, create an app.
|
||||
|
||||
**Step 1:** Find `Client ID` and `Client Secret`.
|
||||
|
||||
You can find the `Client ID` and `Client Secret` on the **"Basic Information"** section.
|
||||
|
||||
**Step 2:** Set up redirect URLs.
|
||||
|
||||
Go to the **"OAuth & Permissions"** section, you can find the **"Redirect URLs"** form.
|
||||
|
||||
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.
|
||||
|
||||
You can refer to the [Slack API documentation](https://api.slack.com/authentication/sign-in-with-slack) for more details.
|
||||
|
||||
## 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 OpenID scopes. If not provided, the default scope is `openid profile`.
|
||||
|
||||
### Config types
|
||||
|
||||
| Name | Type |
|
||||
| ------------ | ------ |
|
||||
| clientId | string |
|
||||
| clientSecret | string |
|
||||
| scope | string |
|
||||
|
||||
## Test Slack connector
|
||||
|
||||
That's it! The Slack connector should now be available for end-users to sign in with their Slack 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
|
||||
|
||||
- [Slack API: Sign in with Slack](https://api.slack.com/authentication/sign-in-with-slack)
|
1
packages/connectors/connector-slack/logo.svg
Normal file
1
packages/connectors/connector-slack/logo.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg enable-background="new 0 0 2447.6 2452.5" viewBox="0 0 2447.6 2452.5" xmlns="http://www.w3.org/2000/svg"><g clip-rule="evenodd" fill-rule="evenodd"><path d="m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z" fill="#36c5f0"/><path d="m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z" fill="#2eb67d"/><path d="m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z" fill="#ecb22e"/><path d="m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0" fill="#e01e5a"/></g></svg>
|
After Width: | Height: | Size: 1.4 KiB |
70
packages/connectors/connector-slack/package.json
Normal file
70
packages/connectors/connector-slack/package.json
Normal file
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"name": "@logto/connector-slack",
|
||||
"version": "0.0.0",
|
||||
"description": "Slack 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"
|
||||
}
|
||||
}
|
52
packages/connectors/connector-slack/src/constant.ts
Normal file
52
packages/connectors/connector-slack/src/constant.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import type { ConnectorMetadata } from '@logto/connector-kit';
|
||||
import { ConnectorPlatform, ConnectorConfigFormItemType } from '@logto/connector-kit';
|
||||
|
||||
export const authorizationEndpoint = 'https://slack.com/openid/connect/authorize';
|
||||
export const defaultScope = 'openid profile email';
|
||||
export const accessTokenEndpoint = 'https://slack.com/api/openid.connect.token';
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'slack-universal',
|
||||
target: 'slack',
|
||||
platform: ConnectorPlatform.Universal,
|
||||
name: {
|
||||
en: 'Slack',
|
||||
'zh-CN': 'Slack',
|
||||
'tr-TR': 'Slack',
|
||||
ko: 'Slack',
|
||||
},
|
||||
logo: './logo.svg',
|
||||
logoDark: null,
|
||||
description: {
|
||||
en: 'Slack is a team communication platform for real-time conversation and information sharing.',
|
||||
'zh-CN': 'Slack 是一个团队沟通平台,用于实时对话和信息共享。',
|
||||
'tr-TR':
|
||||
'Slack, gerçek zamanlı sohbet ve bilgi paylaşımı için bir takım iletişim platformudur.',
|
||||
ko: 'Slack은 실시간 대화와 정보 공유를 위한 팀 통신 플랫폼입니다.',
|
||||
},
|
||||
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;
|
94
packages/connectors/connector-slack/src/index.test.ts
Normal file
94
packages/connectors/connector-slack/src/index.test.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
import nock from 'nock';
|
||||
|
||||
import { accessTokenEndpoint, authorizationEndpoint } 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 redirectUri = 'http://localhost:3000/callback';
|
||||
const getSession = vi.fn().mockResolvedValue({
|
||||
redirectUri,
|
||||
});
|
||||
|
||||
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,
|
||||
connectorId: 'some_connector_id',
|
||||
connectorFactoryId: 'some_connector_factory_id',
|
||||
jti: 'some_jti',
|
||||
headers: {},
|
||||
},
|
||||
setSession
|
||||
);
|
||||
expect(setSession).toHaveBeenCalledWith({
|
||||
redirectUri,
|
||||
});
|
||||
expect(authorizationUri).toEqual(
|
||||
`${authorizationEndpoint}?response_type=code&client_id=%3Cclient-id%3E&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&scope=openid+profile+email&state=some_state`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessToken', () => {
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should get an accessToken by exchanging with code', async () => {
|
||||
nock(accessTokenEndpoint).post('').reply(200, {
|
||||
ok: true,
|
||||
access_token: 'access_token',
|
||||
token_type: 'token_type',
|
||||
id_token: 'id_token',
|
||||
});
|
||||
const { access_token, id_token } = await getAccessToken(mockedConfig, 'code', redirectUri);
|
||||
expect(access_token).toEqual('access_token');
|
||||
expect(id_token).toEqual('id_token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should get valid SocialUserInfo', async () => {
|
||||
nock(accessTokenEndpoint).post('').query(true).reply(200, {
|
||||
ok: true,
|
||||
access_token: 'access_token',
|
||||
token_type: 'token_type',
|
||||
id_token:
|
||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicGljdHVyZSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vanBob2RvZS5qcGcifQ.',
|
||||
});
|
||||
const connector = await createConnector({ getConfig });
|
||||
const socialUserInfo = await connector.getUserInfo({ code: 'code', redirectUri }, getSession);
|
||||
expect(socialUserInfo).toStrictEqual({
|
||||
id: '1234567890',
|
||||
name: 'John Doe',
|
||||
image: 'https://example.com/jphodoe.jpg',
|
||||
email: undefined,
|
||||
rawData: {
|
||||
sub: '1234567890',
|
||||
name: 'John Doe',
|
||||
picture: 'https://example.com/jphodoe.jpg',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('throws unrecognized error', async () => {
|
||||
nock(accessTokenEndpoint).post('').reply(500);
|
||||
const connector = await createConnector({ getConfig });
|
||||
await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toThrow();
|
||||
});
|
||||
});
|
145
packages/connectors/connector-slack/src/index.ts
Normal file
145
packages/connectors/connector-slack/src/index.ts
Normal file
|
@ -0,0 +1,145 @@
|
|||
import { conditional } from '@silverhand/essentials';
|
||||
|
||||
import {
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
validateConfig,
|
||||
ConnectorType,
|
||||
parseJson,
|
||||
} 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,
|
||||
} from './constant.js';
|
||||
import {
|
||||
slackConfigGuard,
|
||||
authResponseGuard,
|
||||
accessTokenResponseGuard,
|
||||
type SlackConfig,
|
||||
userInfoResponseGuard,
|
||||
} from './types.js';
|
||||
|
||||
const getAuthorizationUri =
|
||||
(getConfig: GetConnectorConfig): GetAuthorizationUri =>
|
||||
async ({ state, redirectUri }, setSession) => {
|
||||
const config = await getConfig(defaultMetadata.id);
|
||||
validateConfig(config, slackConfigGuard);
|
||||
|
||||
await setSession({
|
||||
redirectUri,
|
||||
});
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
response_type: 'code',
|
||||
client_id: config.clientId,
|
||||
redirect_uri: redirectUri,
|
||||
scope: config.scope ?? defaultScope,
|
||||
state,
|
||||
});
|
||||
|
||||
return `${authorizationEndpoint}?${queryParams.toString()}`;
|
||||
};
|
||||
|
||||
export const getAccessToken = async (config: SlackConfig, code: string, redirectUri: string) => {
|
||||
const queryParams = new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
redirect_uri: redirectUri,
|
||||
client_id: config.clientId,
|
||||
client_secret: config.clientSecret,
|
||||
});
|
||||
|
||||
const response = await ky
|
||||
.post(accessTokenEndpoint, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: queryParams.toString(),
|
||||
timeout: defaultTimeout,
|
||||
})
|
||||
.json();
|
||||
|
||||
return accessTokenResponseGuard.parse(response);
|
||||
};
|
||||
|
||||
const getUserInfo =
|
||||
(getConfig: GetConnectorConfig): GetUserInfo =>
|
||||
async (data, getSession) => {
|
||||
const config = await getConfig(defaultMetadata.id);
|
||||
validateConfig(config, slackConfigGuard);
|
||||
|
||||
const authResponseResult = authResponseGuard.safeParse(data);
|
||||
|
||||
if (!authResponseResult.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(data));
|
||||
}
|
||||
|
||||
const { code } = authResponseResult.data;
|
||||
const { redirectUri } = await getSession();
|
||||
|
||||
if (!redirectUri) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, {
|
||||
message: 'Cannot find `redirectUri` from connector session.',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const { id_token } = await getAccessToken(config, code, redirectUri);
|
||||
|
||||
const [, payload] = id_token.split('.');
|
||||
|
||||
if (!payload) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
|
||||
}
|
||||
|
||||
const decodedPayload = Buffer.from(payload, 'base64').toString('utf8');
|
||||
const rawData = parseJson(decodedPayload);
|
||||
const userInfo = userInfoResponseGuard.parse(rawData);
|
||||
|
||||
const { sub, name, picture, email, email_verified } = userInfo;
|
||||
|
||||
return {
|
||||
id: sub,
|
||||
name: conditional(name),
|
||||
rawData,
|
||||
image: conditional(picture),
|
||||
email: conditional(email_verified && email),
|
||||
};
|
||||
} 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 createSlackConnector: CreateConnector<SocialConnector> = async ({ getConfig }) => {
|
||||
return {
|
||||
metadata: defaultMetadata,
|
||||
type: ConnectorType.Social,
|
||||
configGuard: slackConfigGuard,
|
||||
getAuthorizationUri: getAuthorizationUri(getConfig),
|
||||
getUserInfo: getUserInfo(getConfig),
|
||||
};
|
||||
};
|
||||
|
||||
export default createSlackConnector;
|
4
packages/connectors/connector-slack/src/mock.ts
Normal file
4
packages/connectors/connector-slack/src/mock.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export const mockedConfig = {
|
||||
clientId: '<client-id>',
|
||||
clientSecret: '<client-secret>',
|
||||
};
|
31
packages/connectors/connector-slack/src/types.ts
Normal file
31
packages/connectors/connector-slack/src/types.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const slackConfigGuard = z.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
scope: z.string().optional(),
|
||||
});
|
||||
|
||||
export type SlackConfig = z.infer<typeof slackConfigGuard>;
|
||||
|
||||
export const userInfoResponseGuard = z.object({
|
||||
sub: z.string(),
|
||||
name: z.string(),
|
||||
picture: z.string().optional().nullable(),
|
||||
email: z.string().optional().nullable(),
|
||||
email_verified: z.boolean().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({
|
||||
ok: z.boolean(),
|
||||
access_token: z.string(),
|
||||
token_type: z.string(),
|
||||
id_token: z.string(),
|
||||
});
|
61
pnpm-lock.yaml
generated
61
pnpm-lock.yaml
generated
|
@ -2276,6 +2276,67 @@ importers:
|
|||
specifier: ^2.1.9
|
||||
version: 2.1.9(@types/node@20.11.20)(jsdom@20.0.2)(lightningcss@1.25.1)(sass@1.77.8)
|
||||
|
||||
packages/connectors/connector-slack:
|
||||
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-smsaero:
|
||||
dependencies:
|
||||
'@logto/connector-kit':
|
||||
|
|
Loading…
Add table
Reference in a new issue