From 3fa2b796e6db282d509d6e352bf91dc9efd0b63e Mon Sep 17 00:00:00 2001 From: u0x01 <233355@gmail.com> Date: Mon, 30 Dec 2024 20:02:12 +0800 Subject: [PATCH] feat(connector): add Xiaomi social login connector (#6905) * feat(connector): add Xiaomi social login connector * chore: update README and pnpm lock * chore: update changeset * fix(connector): fix connector-xiaomi test fail & enhance error handling * refactor(connector): remove unnecessary logs and code --------- Co-authored-by: Charles Zhao --- .changeset/rotten-lizards-buy.md | 5 + .../connectors/connector-xiaomi/README.md | 69 +++++++ .../connector-xiaomi/README.zh-CN.md | 71 +++++++ packages/connectors/connector-xiaomi/logo.svg | 22 +++ .../connectors/connector-xiaomi/package.json | 69 +++++++ .../connector-xiaomi/src/constant.ts | 59 ++++++ .../connector-xiaomi/src/index.test.ts | 133 +++++++++++++ .../connectors/connector-xiaomi/src/index.ts | 181 ++++++++++++++++++ .../connectors/connector-xiaomi/src/mock.ts | 26 +++ .../connectors/connector-xiaomi/src/types.ts | 48 +++++ pnpm-lock.yaml | 57 +++++- 11 files changed, 739 insertions(+), 1 deletion(-) create mode 100644 .changeset/rotten-lizards-buy.md create mode 100644 packages/connectors/connector-xiaomi/README.md create mode 100644 packages/connectors/connector-xiaomi/README.zh-CN.md create mode 100644 packages/connectors/connector-xiaomi/logo.svg create mode 100644 packages/connectors/connector-xiaomi/package.json create mode 100644 packages/connectors/connector-xiaomi/src/constant.ts create mode 100644 packages/connectors/connector-xiaomi/src/index.test.ts create mode 100644 packages/connectors/connector-xiaomi/src/index.ts create mode 100644 packages/connectors/connector-xiaomi/src/mock.ts create mode 100644 packages/connectors/connector-xiaomi/src/types.ts diff --git a/.changeset/rotten-lizards-buy.md b/.changeset/rotten-lizards-buy.md new file mode 100644 index 000000000..9b5f03bcb --- /dev/null +++ b/.changeset/rotten-lizards-buy.md @@ -0,0 +1,5 @@ +--- +"@logto/connector-xiaomi": minor +--- + +add Xiaomi social connector diff --git a/packages/connectors/connector-xiaomi/README.md b/packages/connectors/connector-xiaomi/README.md new file mode 100644 index 000000000..63830b008 --- /dev/null +++ b/packages/connectors/connector-xiaomi/README.md @@ -0,0 +1,69 @@ +# Xiaomi social connector + +The official Logto connector for Xiaomi social sign-in. [中文文档](https://github.com/logto-io/logto/tree/master/packages/connectors/connector-xiaomi/README.zh-CN.md) + +**Table of contents** + +- [Xiaomi social connector](#xiaomi-social-connector) + - [Get started](#get-started) + - [Configure Xiaomi OAuth application](#configure-xiaomi-oauth-application) + - [Scopes description](#scopes-description) + - [Test Xiaomi connector](#test-xiaomi-connector) + - [References](#references) + +## Get started + +1. Create a developer account at [Xiaomi Open Platform](https://dev.mi.com/) +2. Visit [Xiaomi Account Service](https://dev.mi.com/passport/oauth2/applist) +3. Create a new application if you don't have one + +## Configure Xiaomi OAuth application + +1. Visit [Xiaomi Account Service](https://dev.mi.com/passport/oauth2/applist) +2. Configure OAuth settings: + - Open the application you want to use for login, click on "Callback URL" (if you haven't edited the callback URL, it will display as "Enabled") + - Add authorization callback URL: `${your_logto_origin}/callback/${connector_id}` + - `connector_id` can be found on the top of the connector details page in Logto Console +3. Get `AppID` and `AppSecret` from the application details page +4. Fill in the `clientId` and `clientSecret` fields in Logto Console with the values from step 3 +5. Optional configuration: + - `skipConfirm`: Whether to skip the Xiaomi authorization confirmation page when user is already logged in to Xiaomi account, defaults to false + +## Scopes description + +By default, the connector requests the following scope: + +- `1`: Read user profile + +Available scopes: + +| Scope Value | Description | API Interface | +|-------------|-------------|---------------| +| 1 | Get user profile | user/profile | +| 3 | Get user open_id | user/openIdV2 | +| 1000 | Get Xiaomi router info | Mi Router | +| 1001 | Access all Xiaomi router info | Mi Router | +| 2001 | Access Xiaomi cloud calendar | Mi Cloud | +| 2002 | Access Xiaomi cloud alarm | Mi Cloud | +| 6000 | Use Mi Home smart home service | Mi Home | +| 6002 | Add third-party devices to Mi Home | Mi Home | +| 6003 | Alexa control Xiaomi devices | Mi Home | +| 6004 | Third-party service access to Xiaomi devices | Mi Home | +| 7000 | Follow Yellow Pages service account | Mi Cloud | +| 11000 | Get Xiaomi cloud photos | Mi Cloud | +| 12001 | Save app data to Mi Cloud | Mi Cloud | +| 12005 | Use health ECG service | Health | +| 16000 | Get Mi Wallet passes | app/get_pass | +| 20000 | Enable XiaoAI voice service | XiaoAI | +| 40000 | Enable cloud AI service | Internal Use | + +Multiple scopes can be configured by separating them with spaces, e.g.: `1 3 6000`. + +## Test Xiaomi connector + +That's it. Don't forget to [Enable social sign-in](https://docs.logto.io/connectors/social-connectors#enable-social-sign-in) in the sign-in experience. + +## References + +- [Xiaomi OAuth 2.0 Documentation](https://dev.mi.com/xiaomihyperos/documentation/detail?pId=1708) +- [Xiaomi Get User Profile Documentation](https://dev.mi.com/xiaomihyperos/documentation/detail?pId=1517) diff --git a/packages/connectors/connector-xiaomi/README.zh-CN.md b/packages/connectors/connector-xiaomi/README.zh-CN.md new file mode 100644 index 000000000..b7f8fb337 --- /dev/null +++ b/packages/connectors/connector-xiaomi/README.zh-CN.md @@ -0,0 +1,71 @@ +# 小米社交连接器 + +小米社交登录 Logto 官方连接器 [中文文档](#小米社交连接器) + +**目录** + +- [小米社交连接器](#小米社交连接器) + - [开始上手](#开始上手) + - [配置小米 OAuth 应用](#配置小米-oauth-应用) + - [权限范围说明](#权限范围说明) + - [测试小米连接器](#测试小米连接器) + - [参考](#参考) + +小米是一家全球知名的科技公司,提供包括智能手机、智能家居等在内的多种产品和服务。本连接器可以帮助终端用户使用小米账号登录你的应用。 + +## 开始上手 + +1. 在[小米开放平台](https://dev.mi.com/)创建开发者账号 +2. 访问[小米帐号服务](https://dev.mi.com/passport/oauth2/applist) +3. 创建一个新应用(如果还没有) + +## 配置小米 OAuth 应用 + +1. 访问[小米帐号服务](https://dev.mi.com/passport/oauth2/applist) +2. 配置 OAuth 设置: + - 打开要用于登录的应用,点击「回调地址」(如果没有编辑过回调地址,会显示为「启用」) + - 添加授权回调地址: `${your_logto_origin}/callback/${connector_id}` + - `connector_id` 可以在 Logto 管理控制台连接器详情页顶部找到 +3. 从应用详情页获取 `AppID` 和 `AppSecret` +4. 将第 3 步获取的值填入 Logto 管理控制台的 `clientId` 和 `clientSecret` 字段 +5. 可选配置: + - `skipConfirm`: 在用户已登录小米账号的情况下,是否跳过小米授权确认页面,默认为 false + +## 权限范围说明 + +默认情况下,连接器请求以下权限: + +- `1`: 读取用户资料 + +可配置的权限范围: + +| 权限值 | 描述 | API 接口 | +|-------|------|----------| +| 1 | 获取用户资料 | user/profile | +| 3 | 获取用户 open_id | user/openIdV2 | +| 1000 | 获取小米路由器信息 | 路由器 | +| 1001 | 访问所有小米路由器信息 | 路由器 | +| 2001 | 访问小米云日历 | 小米云 | +| 2002 | 访问小米云闹钟 | 小米云 | +| 6000 | 使用米家智能家居服务 | 米家 | +| 6002 | 添加第三方设备到米家 | 米家 | +| 6003 | Alexa 控制小米设备 | 米家 | +| 6004 | 第三方服务访问小米设备 | 米家 | +| 7000 | 关注黄页服务号 | 小米云 | +| 11000 | 获取小米云相册 | 小米云 | +| 12001 | 保存应用数据到小米云 | 小米云 | +| 12005 | 使用健康心电图服务 | 健康 | +| 16000 | 获取小米卡包卡券 | app/get_pass | +| 20000 | 启用小爱智能语音服务 | 小爱 | +| 40000 | 启用云端 AI 服务 | 内部使用 | + +可以通过空格分隔配置多个权限范围,例如: `1 3 6000`。 + +## 测试小米连接器 + +大功告成!别忘了在[登录体验](https://docs.logto.io/zh-CN/connectors/social-connectors#enable-social-sign-in)中启用该连接器。 + +## 参考 + +- [小米 OAuth 2.0 文档](https://dev.mi.com/xiaomihyperos/documentation/detail?pId=1708) +- [小米获取用户信息文档](https://dev.mi.com/xiaomihyperos/documentation/detail?pId=1517) diff --git a/packages/connectors/connector-xiaomi/logo.svg b/packages/connectors/connector-xiaomi/logo.svg new file mode 100644 index 000000000..27c845325 --- /dev/null +++ b/packages/connectors/connector-xiaomi/logo.svg @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/packages/connectors/connector-xiaomi/package.json b/packages/connectors/connector-xiaomi/package.json new file mode 100644 index 000000000..dfec997d9 --- /dev/null +++ b/packages/connectors/connector-xiaomi/package.json @@ -0,0 +1,69 @@ +{ + "name": "@logto/connector-xiaomi", + "version": "1.0.0", + "description": "Xiaomi web connector implementation.", + "author": "Silverhand Inc. ", + "dependencies": { + "@logto/connector-kit": "workspace:^4.0.0", + "@silverhand/essentials": "^2.9.1", + "ky": "^1.2.3", + "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:fix": "eslint --ext .ts src --fix", + "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-xiaomi/src/constant.ts b/packages/connectors/connector-xiaomi/src/constant.ts new file mode 100644 index 000000000..337d12936 --- /dev/null +++ b/packages/connectors/connector-xiaomi/src/constant.ts @@ -0,0 +1,59 @@ +import type { ConnectorMetadata } from '@logto/connector-kit'; +import { ConnectorPlatform, ConnectorConfigFormItemType } from '@logto/connector-kit'; + +export const authorizationEndpoint = 'https://account.xiaomi.com/oauth2/authorize'; +export const accessTokenEndpoint = 'https://account.xiaomi.com/oauth2/token'; +export const userInfoEndpoint = 'https://open.account.xiaomi.com/user/profile'; + +// Default scope is read user profile +export const defaultScope = '1'; + +export const defaultMetadata: ConnectorMetadata = { + id: 'xiaomi-universal', + target: 'xiaomi', + platform: ConnectorPlatform.Universal, + name: { + en: 'Xiaomi', + 'zh-CN': '小米', + }, + logo: './logo.svg', + logoDark: null, + description: { + en: 'Xiaomi is a Chinese electronics company.', + 'zh-CN': '小米是一家中国的电子产品公司。', + }, + readme: './README.md', + formItems: [ + { + key: 'clientId', + type: ConnectorConfigFormItemType.Text, + label: 'Client ID', + required: true, + placeholder: '', + }, + { + key: 'clientSecret', + type: ConnectorConfigFormItemType.Text, + label: 'Client Secret', + required: true, + placeholder: '', + }, + { + key: 'scope', + type: ConnectorConfigFormItemType.Text, + label: 'Scope', + required: false, + placeholder: '', + description: 'The scope determines permissions granted by the user.', + }, + { + key: 'skipConfirm', + type: ConnectorConfigFormItemType.Switch, + label: 'Skip Auth Confirm Page', + required: false, + description: 'Skip the Xiaomi auth confirm page when the user is already logged in.', + }, + ], +}; + +export const defaultTimeout = 5000; diff --git a/packages/connectors/connector-xiaomi/src/index.test.ts b/packages/connectors/connector-xiaomi/src/index.test.ts new file mode 100644 index 000000000..07c86edad --- /dev/null +++ b/packages/connectors/connector-xiaomi/src/index.test.ts @@ -0,0 +1,133 @@ +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, mockedAccessTokenResponse, mockedUserInfoResponse } from './mock.js'; + +const getConfig = vi.fn().mockResolvedValue(mockedConfig); + +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: {}, + }, + vi.fn() + ); + expect(authorizationUri).toEqual( + `${authorizationEndpoint}?client_id=%3Cclient-id%3E&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&response_type=code&state=some_state&scope=1&skip_confirm=true` + ); + }); +}); + +describe('getAccessToken', () => { + afterEach(() => { + nock.cleanAll(); + vi.clearAllMocks(); + }); + it('should get an accessToken by exchanging with code', async () => { + nock(accessTokenEndpoint) + .post('') + .reply(200, `&&&START&&&${JSON.stringify(mockedAccessTokenResponse)}`); + const { accessToken } = await getAccessToken(mockedConfig, { code: 'code' }, ''); + expect(accessToken).toEqual('access_token'); + }); + it('throws SocialAuthCodeInvalid error if accessToken not found in response', async () => { + nock(accessTokenEndpoint) + .post('') + .reply(200, '&&&START&&&{"error":96010,"error_description":"invalid redirect uri"}'); + await expect(getAccessToken(mockedConfig, { code: 'code' }, '')).rejects.toStrictEqual( + new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid) + ); + }); +}); + +describe('getUserInfo', () => { + beforeEach(() => { + nock(accessTokenEndpoint) + .post('') + .reply(200, `&&&START&&&${JSON.stringify(mockedAccessTokenResponse)}`); + }); + + afterEach(() => { + nock.cleanAll(); + vi.clearAllMocks(); + }); + + it('should get valid SocialUserInfo', async () => { + nock(userInfoEndpoint).get('').query(true).reply(200, mockedUserInfoResponse); + const connector = await createConnector({ getConfig }); + const socialUserInfo = await connector.getUserInfo( + { + code: 'valid_code', + redirectUri: 'http://localhost:3000/callback', + }, + vi.fn() + ); + expect(socialUserInfo).toStrictEqual({ + id: 'union_id', + avatar: 'https://avatar.example.com/user.jpg', + name: 'Test User', + rawData: mockedUserInfoResponse, + }); + }); + + it('throws SocialAccessTokenInvalid error if remote response code is 403', async () => { + // Mock the userinfo endpoint to return 403 + nock(userInfoEndpoint).get('').query(true).reply(403, { + // eslint-disable-next-line unicorn/numeric-separators-style + code: 96008, + description: 'token invalid or expired', + result: 'error', + }); + + const connector = await createConnector({ getConfig }); + await expect( + connector.getUserInfo( + { + code: 'some_code', + redirectUri: 'http://localhost:3000/callback', + }, + vi.fn() + ) + ).rejects.toThrow(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)); + }); + + it('throws General error if remote response code is 401', async () => { + const errorResponse = { + // eslint-disable-next-line unicorn/numeric-separators-style + code: 96012, + description: 'server rejected auth request', + result: 'error', + }; + nock(userInfoEndpoint).get('').query(true).reply(401, JSON.stringify(errorResponse)); + + const connector = await createConnector({ getConfig }); + await expect( + connector.getUserInfo( + { + code: 'some_code', + redirectUri: 'http://localhost:3000/callback', + }, + vi.fn() + ) + ).rejects.toThrow( + new ConnectorError(ConnectorErrorCodes.General, { + code: errorResponse.code, + description: errorResponse.description, + }) + ); + }); +}); diff --git a/packages/connectors/connector-xiaomi/src/index.ts b/packages/connectors/connector-xiaomi/src/index.ts new file mode 100644 index 000000000..aad3d8849 --- /dev/null +++ b/packages/connectors/connector-xiaomi/src/index.ts @@ -0,0 +1,181 @@ +import { assert, conditional } from '@silverhand/essentials'; + +import { + ConnectorError, + ConnectorErrorCodes, + validateConfig, + ConnectorType, + jsonGuard, + type GetAuthorizationUri, + type GetUserInfo, + type SocialConnector, + type CreateConnector, + type GetConnectorConfig, +} from '@logto/connector-kit'; +import ky, { HTTPError } from 'ky'; + +import { + authorizationEndpoint, + accessTokenEndpoint, + userInfoEndpoint, + defaultScope, + defaultMetadata, + defaultTimeout, +} from './constant.js'; +import type { XiaomiConfig } from './types.js'; +import { + xiaomiConfigGuard, + accessTokenResponseGuard, + userInfoResponseGuard, + authorizationCallbackErrorGuard, + authResponseGuard, + getUserInfoErrorGuard, +} from './types.js'; + +const authorizationCallbackHandler = async (parameterObject: unknown) => { + const result = authResponseGuard.safeParse(parameterObject); + + if (!result.success) { + const parsedError = authorizationCallbackErrorGuard.safeParse(parameterObject); + + if (!parsedError.success) { + throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject)); + } + + const { error, error_description } = parsedError.data; + + throw new ConnectorError(ConnectorErrorCodes.General, { + error, + errorDescription: error_description, + }); + } + + return result.data; +}; + +const getAuthorizationUri = + (getConfig: GetConnectorConfig): GetAuthorizationUri => + async ({ state, redirectUri }) => { + const config = await getConfig(defaultMetadata.id); + validateConfig(config, xiaomiConfigGuard); + + const queryParameters = new URLSearchParams({ + client_id: config.clientId, + redirect_uri: redirectUri, + response_type: 'code', + state, + scope: config.scope ?? defaultScope, + skip_confirm: String(config.skipConfirm ?? false), + }); + + return `${authorizationEndpoint}?${queryParameters.toString()}`; + }; + +export const getAccessToken = async ( + config: XiaomiConfig, + codeObject: { code: string }, + redirectUri: string +) => { + const { code } = codeObject; + const { clientId: client_id, clientSecret: client_secret } = config; + + const formData = new URLSearchParams({ + client_id, + client_secret, + code, + grant_type: 'authorization_code', + redirect_uri: redirectUri, + }); + + const httpResponse = await ky + .post(accessTokenEndpoint, { + body: formData, + timeout: defaultTimeout, + }) + .text(); + + const jsonResponse = jsonGuard.parse(JSON.parse(httpResponse.replace('&&&START&&&', ''))); + + const result = accessTokenResponseGuard.safeParse(jsonResponse); + + if (!result.success) { + throw new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid); + } + + const { access_token: accessToken } = result.data; + + assert(accessToken, new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid)); + + return { accessToken }; +}; + +const getUserInfo = + (getConfig: GetConnectorConfig): GetUserInfo => + async (data) => { + const { code, redirectUri } = await authorizationCallbackHandler(data); + const config = await getConfig(defaultMetadata.id); + validateConfig(config, xiaomiConfigGuard); + const { accessToken } = await getAccessToken(config, { code }, redirectUri); + + try { + const response = await ky + .get(userInfoEndpoint, { + searchParams: { + clientId: config.clientId, + token: accessToken, + }, + timeout: defaultTimeout, + }) + .json(); + + const userInfoResult = userInfoResponseGuard.safeParse(response); + + if (!userInfoResult.success) { + throw new ConnectorError(ConnectorErrorCodes.InvalidResponse); + } + + const { + data: { miliaoNick, unionId, miliaoIcon }, + } = userInfoResult.data; + + return { + id: unionId, + avatar: conditional(miliaoIcon), + name: conditional(miliaoNick), + rawData: jsonGuard.parse(response), + }; + } catch (error: unknown) { + if (error instanceof HTTPError) { + const errorBody: unknown = await error.response.json(); + const parsedError = getUserInfoErrorGuard.safeParse(errorBody); + + if (!parsedError.success) { + throw new ConnectorError(ConnectorErrorCodes.General, parsedError.error); + } + + const { code, description } = parsedError.data; + if (error.response.status === 403) { + throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid); + } + + throw new ConnectorError(ConnectorErrorCodes.General, { + code, + description, + }); + } + + throw error; + } + }; + +const createXiaomiConnector: CreateConnector = async ({ getConfig }) => { + return { + metadata: defaultMetadata, + type: ConnectorType.Social, + configGuard: xiaomiConfigGuard, + getAuthorizationUri: getAuthorizationUri(getConfig), + getUserInfo: getUserInfo(getConfig), + }; +}; + +export default createXiaomiConnector; diff --git a/packages/connectors/connector-xiaomi/src/mock.ts b/packages/connectors/connector-xiaomi/src/mock.ts new file mode 100644 index 000000000..38ea272d8 --- /dev/null +++ b/packages/connectors/connector-xiaomi/src/mock.ts @@ -0,0 +1,26 @@ +export const mockedConfig = { + clientId: '', + clientSecret: '', + redirectUri: 'http://localhost:3000/callback', + skipConfirm: true, +}; + +export const mockedAccessTokenResponse = { + access_token: 'access_token', + expires_in: 3600, + refresh_token: 'refresh_token', + scope: '1', + openId: 'openId', + union_id: 'union_id', +}; + +export const mockedUserInfoResponse = { + result: 'ok', + code: 0, + description: 'no error', + data: { + miliaoNick: 'Test User', + unionId: 'union_id', + miliaoIcon: 'https://avatar.example.com/user.jpg', + }, +}; diff --git a/packages/connectors/connector-xiaomi/src/types.ts b/packages/connectors/connector-xiaomi/src/types.ts new file mode 100644 index 000000000..ed2043ca9 --- /dev/null +++ b/packages/connectors/connector-xiaomi/src/types.ts @@ -0,0 +1,48 @@ +import { z } from 'zod'; + +export const xiaomiConfigGuard = z.object({ + clientId: z.string(), + clientSecret: z.string(), + scope: z.string().optional(), + redirectUri: z.string().optional(), + skipConfirm: z.boolean().optional(), +}); + +export type XiaomiConfig = z.infer; + +export const accessTokenResponseGuard = z.object({ + access_token: z.string(), + expires_in: z.number(), + refresh_token: z.string(), + scope: z.string(), + openId: z.string(), + union_id: z.string(), +}); + +export type AccessTokenResponse = z.infer; + +export const userInfoResponseGuard = z.object({ + result: z.string(), + code: z.number(), + description: z.string(), + data: z.object({ + unionId: z.string(), + miliaoNick: z.string().optional(), + miliaoIcon: z.string().optional(), + }), +}); + +export type UserInfoResponse = z.infer; + +export const authorizationCallbackErrorGuard = z.object({ + error: z.string(), + error_description: z.string(), +}); + +export const authResponseGuard = z.object({ code: z.string(), redirectUri: z.string() }); + +export const getUserInfoErrorGuard = z.object({ + code: z.number(), + description: z.string(), + result: z.string(), +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36de13de5..ca630024b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2739,6 +2739,61 @@ importers: specifier: ^2.1.8 version: 2.1.8(@types/node@20.10.4)(jsdom@20.0.2)(lightningcss@1.25.1)(sass@1.77.8) + packages/connectors/connector-xiaomi: + 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 + 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.8(vitest@2.1.8(@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.4.49)(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.8(@types/node@20.12.7)(jsdom@20.0.2)(lightningcss@1.25.1)(sass@1.77.8) + packages/console: devDependencies: '@fontsource/roboto-mono': @@ -16382,7 +16437,7 @@ snapshots: '@logto/client@2.7.2': dependencies: '@logto/js': 4.1.4 - '@silverhand/essentials': 2.9.1 + '@silverhand/essentials': 2.9.2 camelcase-keys: 7.0.2 jose: 5.9.6