diff --git a/packages/connectors/connector-dingtalk-web/README.md b/packages/connectors/connector-dingtalk-web/README.md
new file mode 100644
index 000000000..b2df1b138
--- /dev/null
+++ b/packages/connectors/connector-dingtalk-web/README.md
@@ -0,0 +1,144 @@
+# DingTalk Web Connector
+
+The official Logto connector for DingTalk social sign-in in web apps.
+
+钉钉 web 应用社交登录官方 Logto 连接器 [中文文档](#钉钉网页连接器)
+
+**Table of contents**
+
+- [DingTalk Web Connector](#dingtalk-web-connector)
+ - [Get Started](#get-started)
+ - [Create a Web App in the DingTalk Open Platform](#create-a-web-app-in-the-dingtalk-open-platform)
+ - [Register a DingTalk Developer Account](#register-a-dingtalk-developer-account)
+ - [Create an Application](#create-an-application)
+ - [Configure Permissions](#configure-permissions)
+ - [Configure Your Connector](#configure-your-connector)
+ - [Config Types](#config-types)
+ - [Test DingTalk Connector](#test-dingtalk-connector)
+ - [Support](#support)
+- [钉钉网页连接器](#钉钉网页连接器)
+ - [开始上手](#开始上手)
+ - [在钉钉开放平台新建一个应用](#在钉钉开放平台新建一个应用)
+ - [注册钉钉开发者账号](#注册钉钉开发者账号)
+ - [创建应用](#创建应用)
+ - [配置权限](#配置权限)
+ - [配置你的连接器](#配置你的连接器)
+ - [配置类型](#配置类型)
+ - [测试钉钉连接器](#测试钉钉连接器)
+ - [支持](#支持)
+
+## Get started
+
+The DingTalk web connector is designed for desktop web applications. It uses the OAuth 2.0 authentication flow.
+
+## Create a web app in the DingTalk Open Platform
+
+> 💡 **Tip**
+>
+> You can skip some sections if you have already finished.
+
+### Register a DingTalk developer account
+
+If you do not have a DingTalk developer account, please register at the [DingTalk Open Platform](https://open.dingtalk.com).
+
+### Create an application
+
+1. In the [DingTalk Developer Console](https://open-dev.dingtalk.com/console/index), click "Create Application"
+2. Choose "Self-built Application", fill in the application name and basic information, and click "Create"
+3. In the left navigation bar, select "Development Configuration" -> "Security Settings", find and configure the "Redirect URL" `${your_logto_origin}/callback/${connector_id}`. You can find the `connector_id` on the connector details page after adding the respective connector in the management console
+4. In the left navigation bar, select "Basic Information" -> "Credentials and Basic Information" to get the "Client ID" and "Client Secret"
+5. In the left navigation bar, select "Application Release" -> "Version Management and Release", create and release the first version to activate the "Client ID" and "Client Secret"
+
+> ℹ️ **Note**
+> If the application does not release a version, the obtained "Client ID" and "Client Secret" cannot be used, or requests will fail.
+
+### Configure permissions
+
+1. In "Development Configuration" -> "Permission Management", select `Contact.User.Read` and `Contact.User.mobile` permissions and authorize them
+2. After confirming the permission configuration, click "Save" and publish the application
+
+## Configure your connector
+
+Fill out the `clientId` and `clientSecret` field with _Client ID(formerly AppKey and SuiteKey)_ and _Client Secret(formerly AppKey and SuiteKey)_ you've got from OAuth app detail pages mentioned in the previous section.
+
+`scope` currently supports two values: `openid` and `openid corpid`. `openid` allows obtaining the user's `userid` after authorization, while `openid corpid` allows obtaining both the user's `id` and the organization `id` selected during the login process. The values should be space-delimited. Note: URL encoding is required.
+
+### Config types
+
+| Name | Type |
+|--------------|--------|
+| clientId | string |
+| clientSecret | string |
+| scope | string |
+
+## Test DingTalk connector
+
+That's it. The DingTalk connector should be available now. Don't forget to [Enable connector in sign-in experience](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-social-sign-in).
+
+Once DingTalk web connector is enabled, you can sign in to your app again to see if it works.
+
+> ℹ️ **Note**
+> Please ensure strict compliance with the usage specifications and development guidelines of the DingTalk Open Platform during the development process.
+
+## Support
+
+If you have any questions or need further assistance, please visit the [DingTalk Developer Documentation](https://open.dingtalk.com/document/orgapp/obtain-identity-credentials) or contact DingTalk technical support.
+
+# 钉钉网页连接器
+
+## 开始上手
+
+钉钉网页连接器是为桌面网页应用设计的。它采用了 OAuth 2.0 认证流程。
+
+## 在钉钉开放平台新建一个应用
+
+> 💡 **Tip**
+>
+> 你可以跳过已经完成的部分。
+
+### 注册钉钉开发者账号
+
+如果你还没有钉钉开发者账号,请在 [钉钉开放平台](https://open.dingtalk.com) 注册。
+
+### 创建应用
+
+1. 在 [钉钉开发者后台](https://open-dev.dingtalk.com/console/index) 中,点击「创建应用」
+2. 选择「自建应用」,填写应用名称和基本信息,点击「创建」
+3. 在左侧导航栏选择「开发配置」->「安全设置」,找到并配置「重定向 URL」 `${your_logto_origin}/callback/${connector_id}`。其中 `connector_id` 在管理控制台添加了相应的连接器之后,可以在连接器的详情页中找到
+4. 在左侧导航栏选择「基础信息」->「凭证与基础信息」中可以获取「Client ID」、「Client Secret」
+5. 在左侧导航栏选择「应用发布」->「版本管理与发布」,创建并发布第一个版本,以使「Client ID」、「Client Secret」生效
+
+> ℹ️ **Note**
+> 应用不发布版本,所获取的「Client ID」、「Client Secret」 均无法使用,或请求错误。
+
+### 配置权限
+
+1. 在「开发配置」->「权限管理」中,选择`通讯录个人信息读权限`和`个人手机号信息`权限并进行授权
+2. 确认权限配置后,点击「保存」并发布应用
+
+## 配置你的连接器
+
+在 clientId 和 clientSecret 字段中填入你在上一个部分中提到的 OAuth 应用详情页面获取的 Client ID(原 AppKey 和 SuiteKey) 和 Client Secret(原 AppKey 和 SuiteKey) 。
+
+scope 目前支持两种值:openid 和 openid corpid。openid 授权后可以获取用户的 userid,而 openid corpid 授权后可以获取用户的 id 和登录过程中用户选择的组织 id。这些值应以空格分隔。注意:需要进行 URL 编码。
+
+### 配置类型
+
+| Name | Type |
+|--------------|--------|
+| clientId | string |
+| clientSecret | string |
+| scope | string |
+
+## 测试钉钉连接器
+
+大功告成。别忘了 [在登录体验中启用本连接器](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-social-sign-in)。
+
+在钉钉web连接器启用后,你可以构建并运行你的应用看看是否生效。
+
+> ℹ️ **Note**
+> 请确保在开发过程中,严格遵守钉钉开放平台的使用规范和开发指南。
+
+## 支持
+
+如有任何问题或需进一步帮助,请访问 [钉钉开发者文档](https://open.dingtalk.com/document/orgapp/obtain-identity-credentials) 或联系钉钉技术支持。
diff --git a/packages/connectors/connector-dingtalk-web/logo.svg b/packages/connectors/connector-dingtalk-web/logo.svg
new file mode 100644
index 000000000..0d15a9e4e
--- /dev/null
+++ b/packages/connectors/connector-dingtalk-web/logo.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/packages/connectors/connector-dingtalk-web/package.json b/packages/connectors/connector-dingtalk-web/package.json
new file mode 100644
index 000000000..b37aabbfc
--- /dev/null
+++ b/packages/connectors/connector-dingtalk-web/package.json
@@ -0,0 +1,76 @@
+{
+ "name": "@logto/connector-dingtalk-web",
+ "version": "1.0.0",
+ "description": "Dingtalk web connector implementation.",
+ "dependencies": {
+ "@logto/connector-kit": "workspace:^3.0.0",
+ "@silverhand/essentials": "^2.9.1",
+ "dayjs": "^1.10.5",
+ "got": "^14.0.0",
+ "iconv-lite": "^0.6.3",
+ "snakecase-keys": "^8.0.0",
+ "zod": "^3.22.4"
+ },
+ "devDependencies": {
+ "@rollup/plugin-commonjs": "^25.0.7",
+ "@rollup/plugin-json": "^6.1.0",
+ "@rollup/plugin-node-resolve": "^15.2.3",
+ "@rollup/plugin-typescript": "^11.1.6",
+ "@shopify/jest-koa-mocks": "^5.0.0",
+ "@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": "^1.4.0",
+ "eslint": "^8.56.0",
+ "lint-staged": "^15.0.2",
+ "nock": "^13.3.1",
+ "prettier": "^3.0.0",
+ "rollup": "^4.12.0",
+ "rollup-plugin-output-size": "^1.3.0",
+ "supertest": "^7.0.0",
+ "typescript": "^5.3.3",
+ "vitest": "^1.4.0"
+ },
+ "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",
+ "build:test": "rm -rf lib/ && tsc -p tsconfig.test.json --sourcemap",
+ "build": "rm -rf lib/ && tsc -p tsconfig.build.json --noEmit && rollup -c",
+ "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental",
+ "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"
+ }
+}
diff --git a/packages/connectors/connector-dingtalk-web/src/constant.ts b/packages/connectors/connector-dingtalk-web/src/constant.ts
new file mode 100644
index 000000000..f05d1046e
--- /dev/null
+++ b/packages/connectors/connector-dingtalk-web/src/constant.ts
@@ -0,0 +1,60 @@
+import type { ConnectorMetadata } from '@logto/connector-kit';
+import { ConnectorConfigFormItemType, ConnectorPlatform } from '@logto/connector-kit';
+
+// https://open.dingtalk.com/document/orgapp-server/use-dingtalk-account-to-log-on-to-third-party-websites-1
+export const authorizationEndpoint = 'https://login.dingtalk.com/oauth2/auth';
+// https://open.dingtalk.com/document/isvapp/obtain-user-token
+export const accessTokenEndpoint = 'https://api.dingtalk.com/v1.0/oauth2/userAccessToken';
+// https://open.dingtalk.com/document/isvapp/dingtalk-retrieve-user-information
+// To obtain the current authorized person's information, the unionId parameter can be set to "me".
+export const userInfoEndpoint = 'https://api.dingtalk.com/v1.0/contact/users/me';
+export const scope = 'openid';
+
+export const defaultMetadata: ConnectorMetadata = {
+ id: 'dingtalk-web',
+ target: 'dingtalk',
+ platform: ConnectorPlatform.Web,
+ name: {
+ en: 'DingTalk',
+ 'zh-CN': '钉钉',
+ 'tr-TR': 'DingTalk',
+ ko: 'DingTalk',
+ },
+ logo: './logo.svg',
+ logoDark: null,
+ description: {
+ en: 'DingTalk is an enterprise-level intelligent mobile office platform launched by Alibaba Group.',
+ 'zh-CN': '钉钉是一个由阿里巴巴集团推出的企业级智能移动办公平台。',
+ 'tr-TR':
+ 'DingTalk, Alibaba Grubu tarafından piyasaya sürülen kurumsal düzeyde akıllı mobil ofis platformudur.',
+ ko: '딩톡은 알리바바 그룹이 출시한 기업용 지능형 모바일 오피스 플랫폼입니다.',
+ },
+ readme: './README.md',
+ formItems: [
+ {
+ key: 'clientId',
+ label: 'Client ID',
+ required: true,
+ type: ConnectorConfigFormItemType.Text,
+ placeholder: '',
+ },
+ {
+ key: 'clientSecret',
+ label: 'Client Secret',
+ required: true,
+ type: ConnectorConfigFormItemType.Text,
+ placeholder: '',
+ },
+ {
+ key: 'scope',
+ type: ConnectorConfigFormItemType.Text,
+ label: 'Scope',
+ required: false,
+ placeholder: '',
+ 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-dingtalk-web/src/index.test.ts b/packages/connectors/connector-dingtalk-web/src/index.test.ts
new file mode 100644
index 000000000..ba8eb7710
--- /dev/null
+++ b/packages/connectors/connector-dingtalk-web/src/index.test.ts
@@ -0,0 +1,119 @@
+import nock from 'nock';
+
+import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
+
+import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './constant.js';
+import createConnector, { getAccessToken } from './index.js';
+import { mockedConfig } from './mock.js';
+
+const getConfig = vi.fn().mockResolvedValue(mockedConfig);
+
+describe('Dingtalk connector', () => {
+ describe('getAuthorizationUri', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should get a valid authorizationUri with 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: {},
+ },
+ vi.fn()
+ );
+ expect(authorizationUri).toEqual(
+ `${authorizationEndpoint}?client_id=%3Cclient-id%3E&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&response_type=code&scope=openid&state=some_state&prompt=consent`
+ );
+ });
+ });
+
+ describe('getAccessToken', () => {
+ afterEach(() => {
+ nock.cleanAll();
+ vi.clearAllMocks();
+ });
+
+ it('should get an accessToken by exchanging with code', async () => {
+ nock(accessTokenEndpoint).post('').reply(200, {
+ accessToken: 'accessToken',
+ refreshToken: 'scope',
+ expires_in: 7200,
+ corpId: 'corpId',
+ });
+
+ const { accessToken } = await getAccessToken('code', mockedConfig);
+ expect(accessToken).toEqual('accessToken');
+ });
+ });
+
+ describe('getUserInfo', () => {
+ beforeEach(() => {
+ nock(accessTokenEndpoint).post('').reply(200, {
+ accessToken: 'accessToken',
+ refreshToken: 'scope',
+ expires_in: 7200,
+ corpId: 'corpId',
+ });
+ });
+
+ afterEach(() => {
+ nock.cleanAll();
+ vi.clearAllMocks();
+ });
+
+ it('should get valid SocialUserInfo', async () => {
+ nock(userInfoEndpoint).get('').reply(200, {
+ nick: 'zhangsan',
+ avatarUrl: 'https://xxx',
+ mobile: '150xxxx9144',
+ openId: '123',
+ unionId: '123',
+ email: 'zhangsan@alibaba-inc.com',
+ stateCode: '86',
+ });
+ const connector = await createConnector({ getConfig });
+ const socialUserInfo = await connector.getUserInfo(
+ {
+ code: 'code',
+ },
+ vi.fn()
+ );
+ expect(socialUserInfo).toStrictEqual({
+ id: '123',
+ avatar: 'https://xxx',
+ email: 'zhangsan@alibaba-inc.com',
+ name: 'zhangsan',
+ phone: '86150xxxx9144',
+ rawData: {
+ nick: 'zhangsan',
+ avatarUrl: 'https://xxx',
+ mobile: '150xxxx9144',
+ openId: '123',
+ unionId: '123',
+ email: 'zhangsan@alibaba-inc.com',
+ stateCode: '86',
+ },
+ });
+ });
+
+ it('throws SocialAccessTokenInvalid error if remote response code is 400', async () => {
+ nock(userInfoEndpoint).get('').reply(400);
+ const connector = await createConnector({ getConfig });
+ await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toStrictEqual(
+ new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
+ );
+ });
+
+ 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-dingtalk-web/src/index.ts b/packages/connectors/connector-dingtalk-web/src/index.ts
new file mode 100644
index 000000000..1ecd56e08
--- /dev/null
+++ b/packages/connectors/connector-dingtalk-web/src/index.ts
@@ -0,0 +1,159 @@
+/**
+ * DingTalk OAuth2 Connector
+ * https://open.dingtalk.com/document/orgapp/obtain-identity-credentials#title-4up-u8w-5ug
+ */
+
+import { assert } from '@silverhand/essentials';
+import { got, HTTPError } from 'got';
+
+import type {
+ GetAuthorizationUri,
+ GetUserInfo,
+ GetConnectorConfig,
+ CreateConnector,
+ SocialConnector,
+} from '@logto/connector-kit';
+import {
+ ConnectorError,
+ ConnectorErrorCodes,
+ validateConfig,
+ ConnectorType,
+ parseJson,
+} from '@logto/connector-kit';
+
+import {
+ authorizationEndpoint,
+ accessTokenEndpoint,
+ userInfoEndpoint,
+ scope as defaultScope,
+ defaultMetadata,
+ defaultTimeout,
+} from './constant.js';
+import type { DingtalkConfig } from './types.js';
+import {
+ dingtalkConfigGuard,
+ accessTokenResponseGuard,
+ userInfoResponseGuard,
+ authResponseGuard,
+} from './types.js';
+
+const getAuthorizationUri =
+ (getConfig: GetConnectorConfig): GetAuthorizationUri =>
+ async ({ state, redirectUri }) => {
+ const config = await getConfig(defaultMetadata.id);
+ validateConfig(config, dingtalkConfigGuard);
+
+ const { clientId, scope } = config;
+
+ const queryParameters = new URLSearchParams({
+ client_id: clientId,
+ redirect_uri: encodeURI(redirectUri), // The variable `redirectUri` should match {clientId, clientSecret}
+ response_type: 'code',
+ scope: scope ?? defaultScope,
+ state,
+ prompt: 'consent',
+ });
+
+ return `${authorizationEndpoint}?${queryParameters.toString()}`;
+ };
+
+export const getAccessToken = async (
+ code: string,
+ config: DingtalkConfig
+): Promise<{ accessToken: string }> => {
+ const { clientId, clientSecret } = config;
+
+ const httpResponse = await got.post(accessTokenEndpoint, {
+ json: {
+ clientId,
+ clientSecret,
+ code,
+ grantType: 'authorization_code',
+ },
+ timeout: { request: defaultTimeout },
+ });
+
+ const result = accessTokenResponseGuard.safeParse(parseJson(httpResponse.body));
+
+ if (!result.success) {
+ throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
+ }
+
+ const { accessToken } = result.data;
+ assert(accessToken, new ConnectorError(ConnectorErrorCodes.InvalidResponse));
+
+ return { accessToken };
+};
+
+const getUserInfo =
+ (getConfig: GetConnectorConfig): GetUserInfo =>
+ async (data) => {
+ const { code } = await authorizationCallbackHandler(data);
+ const config = await getConfig(defaultMetadata.id);
+ validateConfig(config, dingtalkConfigGuard);
+ const { accessToken } = await getAccessToken(code, config);
+
+ try {
+ const httpResponse = await got.get(userInfoEndpoint, {
+ headers: {
+ 'x-acs-dingtalk-access-token': accessToken,
+ },
+ timeout: { request: defaultTimeout },
+ });
+ const rawData = parseJson(httpResponse.body);
+ const result = userInfoResponseGuard.safeParse(rawData);
+
+ if (!result.success) {
+ throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
+ }
+
+ const { nick: name, avatarUrl: avatar, unionId: id, email, mobile, stateCode } = result.data;
+ return {
+ id,
+ avatar,
+ phone: stateCode && mobile ? `${stateCode}${mobile}` : undefined,
+ email,
+ name,
+ rawData,
+ };
+ } catch (error: unknown) {
+ return getUserInfoErrorHandler(error);
+ }
+ };
+
+const getUserInfoErrorHandler = (error: unknown) => {
+ // https://open.dingtalk.com/document/personalapp/error-code-2#title-m5s-krt-vds
+ if (error instanceof HTTPError) {
+ const { statusCode, body: rawBody } = error.response;
+
+ if (statusCode === 400) {
+ throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
+ }
+
+ throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(rawBody));
+ }
+
+ throw error;
+};
+
+const authorizationCallbackHandler = async (parameterObject: unknown) => {
+ const result = authResponseGuard.safeParse(parameterObject);
+
+ if (!result.success) {
+ throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject));
+ }
+
+ return result.data;
+};
+
+const createDingtalkConnector: CreateConnector = async ({ getConfig }) => {
+ return {
+ metadata: defaultMetadata,
+ type: ConnectorType.Social,
+ configGuard: dingtalkConfigGuard,
+ getAuthorizationUri: getAuthorizationUri(getConfig),
+ getUserInfo: getUserInfo(getConfig),
+ };
+};
+
+export default createDingtalkConnector;
diff --git a/packages/connectors/connector-dingtalk-web/src/mock.ts b/packages/connectors/connector-dingtalk-web/src/mock.ts
new file mode 100644
index 000000000..2361ae0a1
--- /dev/null
+++ b/packages/connectors/connector-dingtalk-web/src/mock.ts
@@ -0,0 +1,5 @@
+export const mockedConfig = {
+ clientId: '',
+ clientSecret: '',
+ scode: '',
+};
diff --git a/packages/connectors/connector-dingtalk-web/src/types.ts b/packages/connectors/connector-dingtalk-web/src/types.ts
new file mode 100644
index 000000000..7bf76dea5
--- /dev/null
+++ b/packages/connectors/connector-dingtalk-web/src/types.ts
@@ -0,0 +1,33 @@
+import { z } from 'zod';
+
+export const dingtalkConfigGuard = z.object({
+ clientId: z.string(),
+ clientSecret: z.string(),
+ scope: z.string().optional(),
+});
+
+export type DingtalkConfig = z.infer;
+
+export const accessTokenResponseGuard = z.object({
+ accessToken: z.string(),
+ refreshToken: z.string().optional(),
+ expireIn: z.number().optional(), // In seconds
+ corpId: z.string().optional(),
+});
+
+// https://open.dingtalk.com/document/isvapp/dingtalk-retrieve-user-information
+export const userInfoResponseGuard = z.object({
+ nick: z.string().optional(),
+ avatarUrl: z.string().optional(),
+ mobile: z.string().optional(),
+ openId: z.string().optional(), // DingTalk no longer recommends using OpenId for integration. Instead, use unionId.
+ unionId: z.string(),
+ email: z.string().optional(),
+ stateCode: z.string().optional(),
+});
+
+export type UserInfoResponse = z.infer;
+
+export type UserInfoResponseMessageParser = (userInfo: Partial) => void;
+
+export const authResponseGuard = z.object({ code: z.string() });
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6e63a4582..7c90b2fe8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -754,6 +754,88 @@ importers:
specifier: ^1.4.0
version: 1.4.0(@types/node@20.11.20)(jsdom@20.0.2)(sass@1.56.1)
+ packages/connectors/connector-dingtalk-web:
+ dependencies:
+ '@logto/connector-kit':
+ specifier: workspace:^3.0.0
+ version: link:../../toolkit/connector-kit
+ '@silverhand/essentials':
+ specifier: ^2.9.1
+ version: 2.9.1
+ dayjs:
+ specifier: ^1.10.5
+ version: 1.11.6
+ got:
+ specifier: ^14.0.0
+ version: 14.0.0
+ iconv-lite:
+ specifier: ^0.6.3
+ version: 0.6.3
+ snakecase-keys:
+ specifier: ^8.0.0
+ version: 8.0.0
+ zod:
+ specifier: ^3.22.4
+ version: 3.22.4
+ devDependencies:
+ '@rollup/plugin-commonjs':
+ specifier: ^25.0.7
+ version: 25.0.7(rollup@4.14.3)
+ '@rollup/plugin-json':
+ specifier: ^6.1.0
+ version: 6.1.0(rollup@4.14.3)
+ '@rollup/plugin-node-resolve':
+ specifier: ^15.2.3
+ version: 15.2.3(rollup@4.14.3)
+ '@rollup/plugin-typescript':
+ specifier: ^11.1.6
+ version: 11.1.6(rollup@4.14.3)(tslib@2.6.2)(typescript@5.3.3)
+ '@shopify/jest-koa-mocks':
+ specifier: ^5.0.0
+ version: 5.0.0
+ '@silverhand/eslint-config':
+ specifier: 6.0.1
+ version: 6.0.1(eslint@8.57.0)(prettier@3.0.0)(typescript@5.3.3)
+ '@silverhand/ts-config':
+ specifier: 6.0.0
+ version: 6.0.0(typescript@5.3.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: ^1.4.0
+ version: 1.4.0(vitest@1.4.0(@types/node@20.12.7)(jsdom@20.0.2)(sass@1.56.1))
+ eslint:
+ specifier: ^8.56.0
+ version: 8.57.0
+ lint-staged:
+ specifier: ^15.0.2
+ version: 15.0.2
+ nock:
+ specifier: ^13.3.1
+ version: 13.3.1
+ prettier:
+ specifier: ^3.0.0
+ version: 3.0.0
+ rollup:
+ specifier: ^4.12.0
+ version: 4.14.3
+ rollup-plugin-output-size:
+ specifier: ^1.3.0
+ version: 1.3.0(rollup@4.14.3)
+ supertest:
+ specifier: ^7.0.0
+ version: 7.0.0
+ typescript:
+ specifier: ^5.3.3
+ version: 5.3.3
+ vitest:
+ specifier: ^1.4.0
+ version: 1.4.0(@types/node@20.12.7)(jsdom@20.0.2)(sass@1.56.1)
+
packages/connectors/connector-discord:
dependencies:
'@logto/connector-kit':
@@ -15602,7 +15684,7 @@ snapshots:
deepmerge: 4.3.1
is-builtin-module: 3.2.1
is-module: 1.0.0
- resolve: 1.22.2
+ resolve: 1.22.8
optionalDependencies:
rollup: 4.14.3