From 69ec7b22e9d5a285123ba3286dbc7f576817d82f Mon Sep 17 00:00:00 2001 From: "{authemail@qq.com}" Date: Mon, 19 Dec 2022 17:50:35 +0800 Subject: [PATCH 1/6] feat(tencent-mail): add tencent mail connector --- packages/connector-tencent-email/README.md | 1 + .../docs/config-template.json | 72 +++++++++ .../connector-tencent-email/jest.config.ts | 1 + packages/connector-tencent-email/logo.svg | 12 ++ packages/connector-tencent-email/package.json | 56 +++++++ .../connector-tencent-email/src/constant.ts | 21 +++ packages/connector-tencent-email/src/http.ts | 123 +++++++++++++++ .../connector-tencent-email/src/index.test.ts | 89 +++++++++++ packages/connector-tencent-email/src/index.ts | 146 ++++++++++++++++++ packages/connector-tencent-email/src/mock.ts | 64 ++++++++ packages/connector-tencent-email/src/types.ts | 63 ++++++++ .../tsconfig.base.json | 10 ++ .../tsconfig.build.json | 5 + .../connector-tencent-email/tsconfig.json | 7 + .../tsconfig.test.json | 7 + pnpm-lock.yaml | 39 +++++ 16 files changed, 716 insertions(+) create mode 100644 packages/connector-tencent-email/README.md create mode 100644 packages/connector-tencent-email/docs/config-template.json create mode 100644 packages/connector-tencent-email/jest.config.ts create mode 100644 packages/connector-tencent-email/logo.svg create mode 100644 packages/connector-tencent-email/package.json create mode 100644 packages/connector-tencent-email/src/constant.ts create mode 100644 packages/connector-tencent-email/src/http.ts create mode 100644 packages/connector-tencent-email/src/index.test.ts create mode 100644 packages/connector-tencent-email/src/index.ts create mode 100644 packages/connector-tencent-email/src/mock.ts create mode 100644 packages/connector-tencent-email/src/types.ts create mode 100644 packages/connector-tencent-email/tsconfig.base.json create mode 100644 packages/connector-tencent-email/tsconfig.build.json create mode 100644 packages/connector-tencent-email/tsconfig.json create mode 100644 packages/connector-tencent-email/tsconfig.test.json diff --git a/packages/connector-tencent-email/README.md b/packages/connector-tencent-email/README.md new file mode 100644 index 00000000..178a19ef --- /dev/null +++ b/packages/connector-tencent-email/README.md @@ -0,0 +1 @@ +# Tencent mail connector diff --git a/packages/connector-tencent-email/docs/config-template.json b/packages/connector-tencent-email/docs/config-template.json new file mode 100644 index 00000000..670ff243 --- /dev/null +++ b/packages/connector-tencent-email/docs/config-template.json @@ -0,0 +1,72 @@ +{ + "accessKeyId": "", + "accessKeySecret": "", + "region": "ap-hongkong", + "fromAddress": "", + "fromName": "", + "replyAddress": "", + "templates": [ + { + "fromAddress": "", + "fromName": "", + "replyAddress": "", + "usageType": "SignIn", + "subject": "", + "templateId": "", + "params": [ + { + "name": "", + "value": "code or toAddress or fromAddress or fromName or replayAddress or subject" + }, + { + "name": "", + "value": "code or toAddress or fromAddress or fromName or replayAddress or subject" + } + ] + }, + { + "usageType": "Register", + "templateId": "", + "subject": "", + "params": [ + { + "name": "code", + "value": "code" + } + ] + }, + { + "usageType": "ForgotPassword", + "templateId": "", + "subject": "", + "params": [ + { + "name": "code", + "value": "code" + } + ] + }, + { + "usageType": "Continue", + "templateId": "", + "subject": "", + "params": [ + { + "name": "code", + "value": "code" + } + ] + }, + { + "usageType": "Test", + "templateId": "", + "subject": "", + "params": [ + { + "name": "code", + "value": "code" + } + ] + } + ] +} diff --git a/packages/connector-tencent-email/jest.config.ts b/packages/connector-tencent-email/jest.config.ts new file mode 100644 index 00000000..0a9aa1b2 --- /dev/null +++ b/packages/connector-tencent-email/jest.config.ts @@ -0,0 +1 @@ +export { default } from '@silverhand/jest-config'; diff --git a/packages/connector-tencent-email/logo.svg b/packages/connector-tencent-email/logo.svg new file mode 100644 index 00000000..cf2d8d90 --- /dev/null +++ b/packages/connector-tencent-email/logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/connector-tencent-email/package.json b/packages/connector-tencent-email/package.json new file mode 100644 index 00000000..6bf62304 --- /dev/null +++ b/packages/connector-tencent-email/package.json @@ -0,0 +1,56 @@ +{ + "name": "@logto/connector-tencent-email", + "version": "1.0.0-beta.13", + "description": "Tencent Email Service connector implementation.", + "main": "./lib/index.js", + "exports": "./lib/index.js", + "author": "Silverhand Inc. ", + "license": "MIT", + "files": [ + "lib", + "docs", + "logo.svg", + "README.md" + ], + "scripts": { + "precommit": "lint-staged", + "build": "rm -rf lib/ && ncc build src/index.ts -o lib", + "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": "jest", + "test:coverage": "jest --coverage --silent", + "prepack": "pnpm build" + }, + "dependencies": { + "@logto/connector-kit": "^1.0.0-beta.26", + "@silverhand/essentials": "^1.2.0", + "@silverhand/jest-config": "1.2.2", + "got": "^11.8.2", + "zod": "^3.14.3" + }, + "devDependencies": { + "@jest/types": "^28.1.3", + "@silverhand/eslint-config": "1.3.0", + "@silverhand/ts-config": "1.2.1", + "@types/jest": "^28.1.6", + "@types/node": "^16.3.1", + "@vercel/ncc": "^0.36.0", + "eslint": "^8.21.0", + "jest": "^28.1.3", + "lint-staged": "^13.0.0", + "nock": "^13.2.2", + "prettier": "^2.7.1", + "typescript": "^4.7.4" + }, + "engines": { + "node": "^16.0.0" + }, + "eslintConfig": { + "extends": "@silverhand" + }, + "prettier": "@silverhand/eslint-config/.prettierrc", + "publishConfig": { + "access": "public" + } +} diff --git a/packages/connector-tencent-email/src/constant.ts b/packages/connector-tencent-email/src/constant.ts new file mode 100644 index 00000000..4e67c6d6 --- /dev/null +++ b/packages/connector-tencent-email/src/constant.ts @@ -0,0 +1,21 @@ +import type { ConnectorMetadata } from '@logto/connector-kit'; + +export const endpoint = 'ses.tencentcloudapi.com'; + +export const defaultMetadata: ConnectorMetadata = { + id: 'tencent-email-service', + target: 'tencent-mail', + platform: null, + name: { + en: 'Tencent Mail Service', + 'zh-CN': '腾讯云邮件服务', + }, + logo: './logo.svg', + logoDark: null, + description: { + en: 'Tencent', + 'zh-CN': 'Tencent', + }, + readme: './README.md', + configTemplate: './docs/config-template.json', +}; diff --git a/packages/connector-tencent-email/src/http.ts b/packages/connector-tencent-email/src/http.ts new file mode 100644 index 00000000..4dac863c --- /dev/null +++ b/packages/connector-tencent-email/src/http.ts @@ -0,0 +1,123 @@ +import type { BinaryToTextEncoding } from 'crypto'; +import crypto from 'crypto'; + +import got from 'got'; + +import type { TencentErrorResponse, TencentSuccessResponse } from '@/types'; +import { tencentErrorResponse } from '@/types'; + +import { endpoint } from './constant'; + +function sha256Hmac(message: string, secret: string): string; +function sha256Hmac(message: string, secret: string, encoding: BinaryToTextEncoding): Buffer; + +function sha256Hmac(message: string, secret: string, encoding?: BinaryToTextEncoding) { + const hmac = crypto.createHmac('sha256', secret); + + return encoding ? hmac.update(message).digest(encoding) : hmac.update(message).digest(); +} + +function getHash(message: string, encoding: BinaryToTextEncoding = 'hex') { + const hash = crypto.createHash('sha256'); + + return hash.update(message).digest(encoding); +} + +function getDate(timestamp: number) { + const date = new Date(timestamp * 1000); + const year = date.getUTCFullYear(); + const month = date.getUTCMonth().toString().padStart(2, '0'); + const day = date.getUTCDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}`; +} + +export function isErrorResponse(response: unknown): response is TencentErrorResponse { + const result = tencentErrorResponse.safeParse(response); + + return result.success; +} + +export function request( + parameters: { + fromEmailAddress: string; + replyAddress: string; + destination: string; + template: { + templateId: string; + templateData: string; + }; + subject: string; + }, + config: { + secretId: string; + secretKey: string; + region: string; + } +) { + const { secretId, secretKey, region } = config; + const timestamp = Math.floor(Date.now() / 1000); + const date = getDate(timestamp); + const service = 'ses'; + + const firstPayload = { + FromEmailAddress: parameters.fromEmailAddress, + ReplyToAddresses: parameters.replyAddress, + Destination: [parameters.destination], + Template: { + TemplateID: Number(parameters.template.templateId), + TemplateData: parameters.template.templateData, + }, + Subject: parameters.subject, + }; + + const payload = JSON.stringify(firstPayload); + + const hashedRequestPayload = getHash(payload); + const signedHeaders = 'content-type;host'; + const httpRequestMethod = 'POST'; + const canonicalUri = '/'; + const canonicalQueryString = ''; + const canonicalHeaders = `content-type:application/json; charset=utf-8\nhost:${endpoint}\n`; + + const canonicalRequest = [ + httpRequestMethod, + canonicalUri, + canonicalQueryString, + canonicalHeaders, + signedHeaders, + hashedRequestPayload, + ].join('\n'); + + const algorithm = 'TC3-HMAC-SHA256'; + const hashedCanonicalRequest = getHash(canonicalRequest); + const credentialScope = `${date}/${service}/tc3_request`; + const stringToSign = [algorithm, timestamp, credentialScope, hashedCanonicalRequest].join('\n'); + + const secretDate = sha256Hmac(date, `TC3${secretKey}`); + const secretService = sha256Hmac(service, secretDate); + const secretSigning = sha256Hmac('tc3_request', secretService); + const signature = sha256Hmac(stringToSign, secretSigning, 'hex').toString(); + const credential = `${secretId}/${credentialScope}`; + + const authorization = [ + algorithm, + `Credential=${credential},`, + `SignedHeaders=${signedHeaders},`, + `Signature=${signature}`, + ].join(' '); + + return got.post(`https://${endpoint}`, { + headers: { + Authorization: authorization, + 'Content-Type': 'application/json; charset=utf-8', + Host: endpoint, + 'X-TC-Action': 'SendEmail', + 'X-TC-Timestamp': String(timestamp), + 'X-TC-Version': '2020-10-02', + 'X-TC-Region': region, + }, + body: payload, + responseType: 'json', + }); +} diff --git a/packages/connector-tencent-email/src/index.test.ts b/packages/connector-tencent-email/src/index.test.ts new file mode 100644 index 00000000..92a037b0 --- /dev/null +++ b/packages/connector-tencent-email/src/index.test.ts @@ -0,0 +1,89 @@ +import { ConnectorError, ConnectorErrorCodes, MessageTypes } from '@logto/connector-kit'; +import nock from 'nock'; + +import { endpoint } from '@/constant'; + +import createConnector from '.'; +import { errorConfig, mockedConfig, mockedOptionConfig } from './mock'; + +const getSuccess1Config = jest.fn().mockResolvedValue(mockedConfig); +const getSuccess2Config = jest.fn().mockResolvedValue(mockedOptionConfig); +const getErrorConfig = jest.fn().mockResolvedValue(errorConfig); + +describe('Tencent mail connector', () => { + it('should not throw errors using config definition method 1', async () => { + await expect(createConnector({ getConfig: getSuccess1Config })).resolves.not.toThrow(); + }); + + it('init without throwing errors, config define method 2', async () => { + await expect(createConnector({ getConfig: getSuccess2Config })).resolves.not.toThrow(); + }); + + it('throws with invalid config', async () => { + const connector = await createConnector({ getConfig: getErrorConfig }); + await expect( + connector.sendMessage({ + to: '', + type: MessageTypes.Register, + payload: { code: '' }, + }) + ).rejects.toThrow(); + }); +}); + +describe('Tencent mail connector params error', () => { + afterEach(() => { + nock.cleanAll(); + jest.clearAllMocks(); + }); + + it('sendMessage request error', async () => { + nock(`https://${endpoint}`) + .post('/') + .reply(200, { + Response: { + RequestId: '123456', + Error: { + Code: 'InvalidParameterValue.InvalidToAddress', + Message: 'Invalid to address.', + }, + }, + }); + + const connector = await createConnector({ getConfig: getSuccess1Config }); + + await expect( + connector.sendMessage({ + to: '', + type: MessageTypes.Test, + payload: { code: '' }, + }) + ).rejects.toThrowError( + new ConnectorError( + ConnectorErrorCodes.InvalidResponse, + 'Tencent email response error: Invalid to address.' + ) + ); + }); + + it('sendMessage request success', async () => { + nock(`https://${endpoint}`) + .post('/') + .reply(200, { + Response: { + RequestId: '123456', + MessageId: '123456', + }, + }); + + const connector = await createConnector({ getConfig: getSuccess1Config }); + + await expect( + connector.sendMessage({ + to: '', + type: MessageTypes.Test, + payload: { code: '' }, + }) + ).resolves.not.toThrow(); + }); +}); diff --git a/packages/connector-tencent-email/src/index.ts b/packages/connector-tencent-email/src/index.ts new file mode 100644 index 00000000..76632842 --- /dev/null +++ b/packages/connector-tencent-email/src/index.ts @@ -0,0 +1,146 @@ +import type { + CreateConnector, + EmailConnector, + GetConnectorConfig, + SendMessageFunction, +} from '@logto/connector-kit'; +import { + ConnectorError, + ConnectorErrorCodes, + ConnectorType, + validateConfig, +} from '@logto/connector-kit'; +import { assert } from '@silverhand/essentials'; +import { HTTPError } from 'got'; + +import { isErrorResponse, request } from '@/http'; +import type { MailTemplateType, SendTencentMailConfig, ValueNames } from '@/types'; +import { sendTencentMailConfigGuard } from '@/types'; + +import { defaultMetadata } from './constant'; + +function getTemplate(templates: MailTemplateType[], usageType: string): MailTemplateType { + const template = templates.find((template) => template.usageType === usageType); + assert( + template, + new ConnectorError( + ConnectorErrorCodes.TemplateNotFound, + `Template not found for type: ${usageType}` + ) + ); + + return template; +} + +function buildFromName(fromName: string, fromAddress: string): string { + return fromName.trim().length > 0 ? `${fromName} <${fromAddress}>` : fromAddress; +} + +function buildTemplatePayload( + template: MailTemplateType, + templateParameters: Record +): Record { + return template.params.reduce>((accumulator, parameter) => { + const { name, value } = parameter; + + return { + ...accumulator, + [name]: templateParameters[value], + }; + }, {}); +} + +const sendMessage = + ( + getConfig: GetConnectorConfig + ): SendMessageFunction => // eslint-disable-next-line complexity + async (data, inputConfig) => { + const { to, type, payload } = data; + const config = inputConfig ?? (await getConfig(defaultMetadata.id)); + validateConfig(config, sendTencentMailConfigGuard); + const { accessKeyId, accessKeySecret, region, fromAddress, fromName, replyAddress, templates } = + config; + + const mailTemplate: MailTemplateType = getTemplate(templates, type); + + const fromAddressValue = mailTemplate.fromAddress ?? fromAddress ?? ''; + + assert( + fromAddressValue.length > 0, + new ConnectorError( + ConnectorErrorCodes.InvalidConfig, + `mail params not found, fromAddress is required` + ) + ); + + const fromNameValue = mailTemplate.fromName ?? fromName ?? ''; + + const templateParameters: Record = { + code: payload.code, + toAddress: to, + fromAddress: fromAddressValue, + fromName: buildFromName(fromNameValue, fromAddressValue), + replyAddress: mailTemplate.replyAddress ?? replyAddress ?? '', + subject: mailTemplate.subject, + }; + + const emailTemplateParametersPayload = buildTemplatePayload(mailTemplate, templateParameters); + + try { + const response = await request( + { + fromEmailAddress: templateParameters.fromAddress, + replyAddress: templateParameters.replyAddress, + destination: templateParameters.toAddress, + subject: templateParameters.subject, + template: { + templateData: JSON.stringify(emailTemplateParametersPayload), + templateId: mailTemplate.templateId, + }, + }, + { + region, + secretId: accessKeyId, + secretKey: accessKeySecret, + } + ); + const { body: rawBody } = response; + + if (isErrorResponse(rawBody)) { + throw new ConnectorError( + ConnectorErrorCodes.InvalidResponse, + `Tencent email response error: ${rawBody.Response.Error.Message}` + ); + } + const { Response } = rawBody; + assert( + Response, + new ConnectorError(ConnectorErrorCodes.InvalidResponse, `Tencent email response not found`) + ); + + return response; + } catch (error: unknown) { + if (error instanceof ConnectorError) { + throw error; + } + + if (error instanceof HTTPError) { + throw new ConnectorError( + ConnectorErrorCodes.InvalidResponse, + `Tencent email response error: ${error.message}` + ); + } + throw new ConnectorError(ConnectorErrorCodes.General, `Tencent email unknown error`); + } + }; + +const createSendGridMailConnector: CreateConnector = async ({ getConfig }) => { + return { + metadata: defaultMetadata, + type: ConnectorType.Email, + configGuard: sendTencentMailConfigGuard, + sendMessage: sendMessage(getConfig), + }; +}; + +export default createSendGridMailConnector; diff --git a/packages/connector-tencent-email/src/mock.ts b/packages/connector-tencent-email/src/mock.ts new file mode 100644 index 00000000..6fb55822 --- /dev/null +++ b/packages/connector-tencent-email/src/mock.ts @@ -0,0 +1,64 @@ +import type { SendTencentMailConfig } from '@/types'; + +export const mockedConfig: SendTencentMailConfig = { + accessKeyId: 'some-access-key-id', + accessKeySecret: 'some-access-key-secret', + region: 'some-region', + fromAddress: 'some-from-address', + fromName: 'some-from-name', + replyAddress: 'some-replay-address', + templates: [ + { + usageType: 'Test', + subject: 'Logto Test Template', + templateId: '123456', + params: [ + { + name: 'code', + value: 'code', + }, + ], + }, + ], +}; + +export const mockedOptionConfig: SendTencentMailConfig = { + accessKeyId: 'some-access-key-id', + accessKeySecret: 'some-access-key-secret', + region: 'some-region', + templates: [ + { + fromAddress: 'some-from-address', + fromName: 'some-from-name', + replyAddress: 'some-replay-address', + usageType: 'Test', + subject: 'Logto Test Template', + templateId: '123456', + params: [ + { + name: 'code', + value: 'code', + }, + ], + }, + ], +}; + +export const errorConfig: SendTencentMailConfig = { + accessKeyId: 'some-access-key-id', + accessKeySecret: 'some-access-key-secret', + region: 'some-region', + templates: [ + { + usageType: 'Test', + subject: 'Logto Test Template', + templateId: '123456', + params: [ + { + name: 'code', + value: 'code', + }, + ], + }, + ], +}; diff --git a/packages/connector-tencent-email/src/types.ts b/packages/connector-tencent-email/src/types.ts new file mode 100644 index 00000000..adf5def8 --- /dev/null +++ b/packages/connector-tencent-email/src/types.ts @@ -0,0 +1,63 @@ +import { z } from 'zod'; + +export const fromInfo = z.object({ + fromAddress: z.string().optional(), + fromName: z.string().optional(), + replyAddress: z.string().optional(), +}); + +export const ValueNames = [ + 'code', + 'toAddress', + 'fromAddress', + 'fromName', + 'replyAddress', + 'subject', +] as const; + +export const mailTemplateParameters = z.object({ + name: z.string(), + value: z.enum(ValueNames), +}); + +export const mailTemplate = z + .object({ + subject: z.string(), + usageType: z.string(), + templateId: z.string(), + params: z.array(mailTemplateParameters), + }) + .merge(fromInfo); + +export type MailTemplateType = z.infer; + +export const sendTencentMailConfigGuard = z + .object({ + accessKeyId: z.string(), + accessKeySecret: z.string(), + region: z.string(), + templates: z.array(mailTemplate), + }) + .merge(fromInfo); + +export type SendTencentMailConfig = z.infer; + +export const tencentErrorResponse = z.object({ + Response: z.object({ + Error: z.object({ + Code: z.string(), + Message: z.string(), + }), + }), +}); + +export declare type TencentErrorResponse = z.infer; + +export const tencentSuccessResponse = z.object({ + Response: z.object({ + MessageId: z.string(), + RequestId: z.string(), + }), +}); + +export declare type TencentSuccessResponse = z.infer; diff --git a/packages/connector-tencent-email/tsconfig.base.json b/packages/connector-tencent-email/tsconfig.base.json new file mode 100644 index 00000000..848a915f --- /dev/null +++ b/packages/connector-tencent-email/tsconfig.base.json @@ -0,0 +1,10 @@ +{ + "extends": "@silverhand/ts-config/tsconfig.base", + "compilerOptions": { + "outDir": "lib", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + } +} diff --git a/packages/connector-tencent-email/tsconfig.build.json b/packages/connector-tencent-email/tsconfig.build.json new file mode 100644 index 00000000..d42923dd --- /dev/null +++ b/packages/connector-tencent-email/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.base", + "include": ["src"], + "exclude": ["src/**/*.test.ts"] +} diff --git a/packages/connector-tencent-email/tsconfig.json b/packages/connector-tencent-email/tsconfig.json new file mode 100644 index 00000000..71ff434a --- /dev/null +++ b/packages/connector-tencent-email/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.base", + "compilerOptions": { + "types": ["node", "jest"] + }, + "include": ["src", "jest.config.ts"] +} diff --git a/packages/connector-tencent-email/tsconfig.test.json b/packages/connector-tencent-email/tsconfig.test.json new file mode 100644 index 00000000..1424a155 --- /dev/null +++ b/packages/connector-tencent-email/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "isolatedModules": false, + "allowJs": true, + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71d34c73..372cc143 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1191,6 +1191,45 @@ importers: supertest: 6.2.4 typescript: 4.9.4 + packages/connector-tencent-email: + specifiers: + '@jest/types': ^28.1.3 + '@logto/connector-kit': ^1.0.0-beta.26 + '@silverhand/eslint-config': 1.3.0 + '@silverhand/essentials': ^1.2.0 + '@silverhand/jest-config': 1.2.2 + '@silverhand/ts-config': 1.2.1 + '@types/jest': ^28.1.6 + '@types/node': ^16.3.1 + '@vercel/ncc': ^0.34.0 + eslint: ^8.21.0 + got: ^11.8.2 + jest: ^28.1.3 + lint-staged: ^13.0.0 + nock: ^13.2.2 + prettier: ^2.7.1 + typescript: ^4.7.4 + zod: ^3.14.3 + dependencies: + '@logto/connector-kit': 1.0.0-beta.26_zod@3.18.0 + '@silverhand/essentials': 1.2.1 + '@silverhand/jest-config': 1.2.2_556mfp7b5dutuj2jcrj5i7zc5q + got: 11.8.5 + zod: 3.18.0 + devDependencies: + '@jest/types': 28.1.3 + '@silverhand/eslint-config': 1.3.0_er5sflmxjcrgfvrsp3qwdvxste + '@silverhand/ts-config': 1.2.1_typescript@4.8.2 + '@types/jest': 28.1.8 + '@types/node': 16.11.56 + '@vercel/ncc': 0.34.0 + eslint: 8.23.0 + jest: 28.1.3_@types+node@16.11.56 + lint-staged: 13.0.3 + nock: 13.2.9 + prettier: 2.7.1 + typescript: 4.8.2 + packages/connector-twilio-sms: specifiers: '@jest/types': ^29.3.1 From 9f613bf75e4c1438e036ca324d5f900655ba1d6a Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Wed, 21 Dec 2022 19:04:33 +0800 Subject: [PATCH 2/6] chore: add region sample values to config sample Co-Authored-By: Charles Zhao --- packages/connector-tencent-email/docs/config-template.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/connector-tencent-email/docs/config-template.json b/packages/connector-tencent-email/docs/config-template.json index 670ff243..5e1046ff 100644 --- a/packages/connector-tencent-email/docs/config-template.json +++ b/packages/connector-tencent-email/docs/config-template.json @@ -1,7 +1,7 @@ { "accessKeyId": "", "accessKeySecret": "", - "region": "ap-hongkong", + "region": "", "fromAddress": "", "fromName": "", "replyAddress": "", From d807dc0204e1f1c58fc8f7628328291d724c543c Mon Sep 17 00:00:00 2001 From: "{authemail@qq.com}" Date: Thu, 29 Dec 2022 16:25:21 +0800 Subject: [PATCH 3/6] feat(tencent-mail): sync master project struct --- .../connector-tencent-email/jest.config.ts | 1 - .../package.extend.json | 7 +++ packages/connector-tencent-email/package.json | 56 ------------------- .../tsconfig.base.json | 10 ---- .../tsconfig.build.json | 5 -- .../connector-tencent-email/tsconfig.json | 7 --- .../tsconfig.test.json | 7 --- 7 files changed, 7 insertions(+), 86 deletions(-) delete mode 100644 packages/connector-tencent-email/jest.config.ts create mode 100644 packages/connector-tencent-email/package.extend.json delete mode 100644 packages/connector-tencent-email/package.json delete mode 100644 packages/connector-tencent-email/tsconfig.base.json delete mode 100644 packages/connector-tencent-email/tsconfig.build.json delete mode 100644 packages/connector-tencent-email/tsconfig.json delete mode 100644 packages/connector-tencent-email/tsconfig.test.json diff --git a/packages/connector-tencent-email/jest.config.ts b/packages/connector-tencent-email/jest.config.ts deleted file mode 100644 index 0a9aa1b2..00000000 --- a/packages/connector-tencent-email/jest.config.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@silverhand/jest-config'; diff --git a/packages/connector-tencent-email/package.extend.json b/packages/connector-tencent-email/package.extend.json new file mode 100644 index 00000000..402613b0 --- /dev/null +++ b/packages/connector-tencent-email/package.extend.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json.schemastore.org/package", + "name": "@logto/connector-tencent-email", + "version": "1.0.0-beta.15", + "description": "Tencent Email connector implementation.", + "author": "StringKe" +} diff --git a/packages/connector-tencent-email/package.json b/packages/connector-tencent-email/package.json deleted file mode 100644 index 6bf62304..00000000 --- a/packages/connector-tencent-email/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "@logto/connector-tencent-email", - "version": "1.0.0-beta.13", - "description": "Tencent Email Service connector implementation.", - "main": "./lib/index.js", - "exports": "./lib/index.js", - "author": "Silverhand Inc. ", - "license": "MIT", - "files": [ - "lib", - "docs", - "logo.svg", - "README.md" - ], - "scripts": { - "precommit": "lint-staged", - "build": "rm -rf lib/ && ncc build src/index.ts -o lib", - "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": "jest", - "test:coverage": "jest --coverage --silent", - "prepack": "pnpm build" - }, - "dependencies": { - "@logto/connector-kit": "^1.0.0-beta.26", - "@silverhand/essentials": "^1.2.0", - "@silverhand/jest-config": "1.2.2", - "got": "^11.8.2", - "zod": "^3.14.3" - }, - "devDependencies": { - "@jest/types": "^28.1.3", - "@silverhand/eslint-config": "1.3.0", - "@silverhand/ts-config": "1.2.1", - "@types/jest": "^28.1.6", - "@types/node": "^16.3.1", - "@vercel/ncc": "^0.36.0", - "eslint": "^8.21.0", - "jest": "^28.1.3", - "lint-staged": "^13.0.0", - "nock": "^13.2.2", - "prettier": "^2.7.1", - "typescript": "^4.7.4" - }, - "engines": { - "node": "^16.0.0" - }, - "eslintConfig": { - "extends": "@silverhand" - }, - "prettier": "@silverhand/eslint-config/.prettierrc", - "publishConfig": { - "access": "public" - } -} diff --git a/packages/connector-tencent-email/tsconfig.base.json b/packages/connector-tencent-email/tsconfig.base.json deleted file mode 100644 index 848a915f..00000000 --- a/packages/connector-tencent-email/tsconfig.base.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "@silverhand/ts-config/tsconfig.base", - "compilerOptions": { - "outDir": "lib", - "baseUrl": ".", - "paths": { - "@/*": ["src/*"] - } - } -} diff --git a/packages/connector-tencent-email/tsconfig.build.json b/packages/connector-tencent-email/tsconfig.build.json deleted file mode 100644 index d42923dd..00000000 --- a/packages/connector-tencent-email/tsconfig.build.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "./tsconfig.base", - "include": ["src"], - "exclude": ["src/**/*.test.ts"] -} diff --git a/packages/connector-tencent-email/tsconfig.json b/packages/connector-tencent-email/tsconfig.json deleted file mode 100644 index 71ff434a..00000000 --- a/packages/connector-tencent-email/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "./tsconfig.base", - "compilerOptions": { - "types": ["node", "jest"] - }, - "include": ["src", "jest.config.ts"] -} diff --git a/packages/connector-tencent-email/tsconfig.test.json b/packages/connector-tencent-email/tsconfig.test.json deleted file mode 100644 index 1424a155..00000000 --- a/packages/connector-tencent-email/tsconfig.test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "./tsconfig", - "compilerOptions": { - "isolatedModules": false, - "allowJs": true, - } -} From 0409d4104bb55703cf02df9893f2f065adfd84f7 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Fri, 30 Dec 2022 19:32:56 +0800 Subject: [PATCH 4/6] chore: fix build error --- .../connector-tencent-email/src/index.test.ts | 11 ++-- pnpm-lock.yaml | 52 +++++++++++-------- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/packages/connector-tencent-email/src/index.test.ts b/packages/connector-tencent-email/src/index.test.ts index 92a037b0..faa1bc92 100644 --- a/packages/connector-tencent-email/src/index.test.ts +++ b/packages/connector-tencent-email/src/index.test.ts @@ -1,7 +1,6 @@ -import { ConnectorError, ConnectorErrorCodes, MessageTypes } from '@logto/connector-kit'; -import nock from 'nock'; - import { endpoint } from '@/constant'; +import { ConnectorError, ConnectorErrorCodes, VerificationCodeType } from '@logto/connector-kit'; +import nock from 'nock'; import createConnector from '.'; import { errorConfig, mockedConfig, mockedOptionConfig } from './mock'; @@ -24,7 +23,7 @@ describe('Tencent mail connector', () => { await expect( connector.sendMessage({ to: '', - type: MessageTypes.Register, + type: VerificationCodeType.Register, payload: { code: '' }, }) ).rejects.toThrow(); @@ -55,7 +54,7 @@ describe('Tencent mail connector params error', () => { await expect( connector.sendMessage({ to: '', - type: MessageTypes.Test, + type: VerificationCodeType.Test, payload: { code: '' }, }) ).rejects.toThrowError( @@ -81,7 +80,7 @@ describe('Tencent mail connector params error', () => { await expect( connector.sendMessage({ to: '', - type: MessageTypes.Test, + type: VerificationCodeType.Test, payload: { code: '' }, }) ).resolves.not.toThrow(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 372cc143..6f85a183 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1142,7 +1142,7 @@ importers: supertest: 6.2.4 typescript: 4.9.4 - packages/connector-tencent-sms: + packages/connector-tencent-email: specifiers: '@jest/types': ^29.3.1 '@logto/connector-kit': 1.0.0-beta.31 @@ -1168,7 +1168,7 @@ importers: zod: ^3.20.2 dependencies: '@logto/connector-kit': 1.0.0-beta.31 - '@silverhand/essentials': 1.3.0 + '@silverhand/essentials': 1.2.1 got: 11.8.5 snakecase-keys: 5.4.4 zod: 3.20.2 @@ -1191,44 +1191,54 @@ importers: supertest: 6.2.4 typescript: 4.9.4 - packages/connector-tencent-email: + packages/connector-tencent-sms: specifiers: - '@jest/types': ^28.1.3 - '@logto/connector-kit': ^1.0.0-beta.26 + '@jest/types': ^29.3.1 + '@logto/connector-kit': 1.0.0-beta.31 '@silverhand/eslint-config': 1.3.0 '@silverhand/essentials': ^1.2.0 '@silverhand/jest-config': 1.2.2 '@silverhand/ts-config': 1.2.1 - '@types/jest': ^28.1.6 + '@types/jest': ^29.2.4 '@types/node': ^16.3.1 - '@vercel/ncc': ^0.34.0 + '@types/supertest': ^2.0.11 + '@vercel/ncc': ^0.36.0 eslint: ^8.21.0 got: ^11.8.2 - jest: ^28.1.3 + iconv-lite: 0.6.3 + jest: ^29.3.1 + jest-matcher-specific-error: ^1.0.0 lint-staged: ^13.0.0 nock: ^13.2.2 prettier: ^2.7.1 - typescript: ^4.7.4 - zod: ^3.14.3 + snakecase-keys: ^5.1.0 + supertest: ^6.2.2 + typescript: ^4.9.4 + zod: ^3.20.2 dependencies: - '@logto/connector-kit': 1.0.0-beta.26_zod@3.18.0 - '@silverhand/essentials': 1.2.1 - '@silverhand/jest-config': 1.2.2_556mfp7b5dutuj2jcrj5i7zc5q + '@logto/connector-kit': 1.0.0-beta.31 + '@silverhand/essentials': 1.3.0 got: 11.8.5 - zod: 3.18.0 + snakecase-keys: 5.4.4 + zod: 3.20.2 devDependencies: - '@jest/types': 28.1.3 - '@silverhand/eslint-config': 1.3.0_er5sflmxjcrgfvrsp3qwdvxste - '@silverhand/ts-config': 1.2.1_typescript@4.8.2 - '@types/jest': 28.1.8 + '@jest/types': 29.3.1 + '@silverhand/eslint-config': 1.3.0_osxwdbbcucdkewmxwr46c5plia + '@silverhand/jest-config': 1.2.2_p6ekqnroyms5nhqbfxosryz7rm + '@silverhand/ts-config': 1.2.1_typescript@4.9.4 + '@types/jest': 29.2.4 '@types/node': 16.11.56 - '@vercel/ncc': 0.34.0 + '@types/supertest': 2.0.12 + '@vercel/ncc': 0.36.0 eslint: 8.23.0 - jest: 28.1.3_@types+node@16.11.56 + iconv-lite: 0.6.3 + jest: 29.3.1_@types+node@16.11.56 + jest-matcher-specific-error: 1.0.0 lint-staged: 13.0.3 nock: 13.2.9 prettier: 2.7.1 - typescript: 4.8.2 + supertest: 6.2.4 + typescript: 4.9.4 packages/connector-twilio-sms: specifiers: From efe520c3bfe9d047a9944e4d39727d866a393ca4 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Fri, 30 Dec 2022 21:04:30 +0800 Subject: [PATCH 5/6] fix: signature issue Co-Authored-By: Charles Zhao --- packages/connector-tencent-email/README.md | 46 ++++++++++++++++++- .../package.extend.json | 2 +- packages/connector-tencent-email/src/http.ts | 2 +- .../connector-tencent-email/src/index.test.ts | 3 +- packages/connector-tencent-email/src/index.ts | 4 +- 5 files changed, 51 insertions(+), 6 deletions(-) diff --git a/packages/connector-tencent-email/README.md b/packages/connector-tencent-email/README.md index 178a19ef..71989805 100644 --- a/packages/connector-tencent-email/README.md +++ b/packages/connector-tencent-email/README.md @@ -1 +1,45 @@ -# Tencent mail connector +# Tencent push mail service connector + +The official Logto connector for Tencent push email service. + +腾讯云邮件推送服务 Logto 官方连接器 [中文文档](#腾讯云邮件连接器) + +**Table of contents** + +- [Tencent push mail service connector](#tencent-push-mail-service-connector) +- [腾讯云邮件推送服务连接器](#腾讯云邮件推送服务连接器) + - [在腾讯云邮件推送服务控制台中配置邮件服务](#在腾讯云邮件推送服务控制台中配置邮件服务) + - [创建腾讯云账号](#创建腾讯云账号) + - [配置腾讯云邮件推送服务](#配置腾讯云邮件推送服务) + - [编写连接器的 JSON 配置](#编写连接器的-json-配置) + +# 腾讯云邮件推送服务连接器 + +腾讯云是亚洲地区一个重要的云服务厂商,提供了包括邮件推送服务在内的诸多云服务。 + +本连接器是 Logto 官方提供的腾讯云邮件连接器,帮助终端用户通过邮件验证码进行登录注册。 + +## 在腾讯云邮件推送服务控制台中配置邮件服务 + +> 💡 **Tip** +> +> 你可以跳过已经完成的部分。 + +### 创建腾讯云账号 + +前往 [腾讯云](https://cloud.tencent.com/) 并完成账号注册并进行实名认证。 + +### 配置腾讯云邮件推送服务 + +1. 使用刚刚注册的账号登录并前往 [邮件推送服务控制台](https://console.cloud.tencent.com/ses)。按照官方「快速入门」的步骤,逐步完成配置。 +2. 配置发信域名:腾讯云邮件推送支持所有行业标准的身份验证机制,包括域名密钥识别邮件 (DKIM)、发件人策略框架 (SPF)、基于域的邮件身份验证、报告和一致性 (DMARC)、邮件交换记录(MX record)。[官方文档](https://cloud.tencent.com/document/product/1288/60652) + - 首先需要另行购买域名,如:example.com + - 在发信域名配置页中,点击「新建」,输入你的发信域名,点击「提交」。 + - 之后返回至发信域名页面,在列表中选择你刚刚添加的域名,点击「验证」按钮。 + - 此时在弹出的发信域名配置弹窗中,按照文档提示在域名托管商的 DNS 设置中,添加如下解析记录(以 cloudflare 为例): + - DNS 设置完成后,点击「提交验证」按钮,如上述 DNS 设置成功则会看到「验证通过」状态。 +3. 配置发信地址:在发信地址配置页中,点击「新建」按钮,选择你在上一步绑定的域名,并指定发信地址的前缀,如:`noreply@example.com`。 +4. 创建发信模板:在发信模板配置页中,点击「新建」,输入模板名称,选择模板类型,并在邮件正文中使用 `{{变量名}}`作为模板的可替换内容。模板提交后需要通过审核之后才可使用。注:通常我们需要创建多个邮件模板,以满足我们在注册、登录、忘记密码等多个场合的不同需求。 +5. 发送测试邮件:点击官方「快速入门」的最后一步,来到[邮件发送](https://console.cloud.tencent.com/ses/send)页面。选择发信模板,输入主题和收件人等信息,点击「发送」按钮。如能成功收到测试邮件,那么恭喜,你已成功完成腾讯云邮件的设置。 + +### 编写连接器的 JSON 配置 \ No newline at end of file diff --git a/packages/connector-tencent-email/package.extend.json b/packages/connector-tencent-email/package.extend.json index 402613b0..87eb7072 100644 --- a/packages/connector-tencent-email/package.extend.json +++ b/packages/connector-tencent-email/package.extend.json @@ -2,6 +2,6 @@ "$schema": "https://json.schemastore.org/package", "name": "@logto/connector-tencent-email", "version": "1.0.0-beta.15", - "description": "Tencent Email connector implementation.", + "description": "Tencent email connector implementation.", "author": "StringKe" } diff --git a/packages/connector-tencent-email/src/http.ts b/packages/connector-tencent-email/src/http.ts index 4dac863c..f7b6dfa1 100644 --- a/packages/connector-tencent-email/src/http.ts +++ b/packages/connector-tencent-email/src/http.ts @@ -26,7 +26,7 @@ function getHash(message: string, encoding: BinaryToTextEncoding = 'hex') { function getDate(timestamp: number) { const date = new Date(timestamp * 1000); const year = date.getUTCFullYear(); - const month = date.getUTCMonth().toString().padStart(2, '0'); + const month = (date.getUTCMonth() + 1).toString().padStart(2, '0'); const day = date.getUTCDate().toString().padStart(2, '0'); return `${year}-${month}-${day}`; diff --git a/packages/connector-tencent-email/src/index.test.ts b/packages/connector-tencent-email/src/index.test.ts index faa1bc92..9a537eb6 100644 --- a/packages/connector-tencent-email/src/index.test.ts +++ b/packages/connector-tencent-email/src/index.test.ts @@ -1,7 +1,8 @@ -import { endpoint } from '@/constant'; import { ConnectorError, ConnectorErrorCodes, VerificationCodeType } from '@logto/connector-kit'; import nock from 'nock'; +import { endpoint } from '@/constant'; + import createConnector from '.'; import { errorConfig, mockedConfig, mockedOptionConfig } from './mock'; diff --git a/packages/connector-tencent-email/src/index.ts b/packages/connector-tencent-email/src/index.ts index 76632842..7fd0b2ee 100644 --- a/packages/connector-tencent-email/src/index.ts +++ b/packages/connector-tencent-email/src/index.ts @@ -134,7 +134,7 @@ const sendMessage = } }; -const createSendGridMailConnector: CreateConnector = async ({ getConfig }) => { +const createTencentMailConnector: CreateConnector = async ({ getConfig }) => { return { metadata: defaultMetadata, type: ConnectorType.Email, @@ -143,4 +143,4 @@ const createSendGridMailConnector: CreateConnector = async ({ ge }; }; -export default createSendGridMailConnector; +export default createTencentMailConnector; From 0b85ea503ac14105b65ef4862e38905674238087 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Fri, 30 Dec 2022 23:30:06 +0800 Subject: [PATCH 6/6] chore: update README Co-Authored-By: Charles Zhao --- packages/connector-tencent-email/README.md | 109 ++++++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/packages/connector-tencent-email/README.md b/packages/connector-tencent-email/README.md index 71989805..3c4a74f3 100644 --- a/packages/connector-tencent-email/README.md +++ b/packages/connector-tencent-email/README.md @@ -12,6 +12,9 @@ The official Logto connector for Tencent push email service. - [创建腾讯云账号](#创建腾讯云账号) - [配置腾讯云邮件推送服务](#配置腾讯云邮件推送服务) - [编写连接器的 JSON 配置](#编写连接器的-json-配置) + - [测试腾讯云邮件连接器](#测试腾讯云邮件连接器) + - [配置类型](#配置类型) + - [参考](#参考) # 腾讯云邮件推送服务连接器 @@ -37,9 +40,111 @@ The official Logto connector for Tencent push email service. - 在发信域名配置页中,点击「新建」,输入你的发信域名,点击「提交」。 - 之后返回至发信域名页面,在列表中选择你刚刚添加的域名,点击「验证」按钮。 - 此时在弹出的发信域名配置弹窗中,按照文档提示在域名托管商的 DNS 设置中,添加如下解析记录(以 cloudflare 为例): + image + - DNS 设置完成后,点击「提交验证」按钮,如上述 DNS 设置成功则会看到「验证通过」状态。 3. 配置发信地址:在发信地址配置页中,点击「新建」按钮,选择你在上一步绑定的域名,并指定发信地址的前缀,如:`noreply@example.com`。 4. 创建发信模板:在发信模板配置页中,点击「新建」,输入模板名称,选择模板类型,并在邮件正文中使用 `{{变量名}}`作为模板的可替换内容。模板提交后需要通过审核之后才可使用。注:通常我们需要创建多个邮件模板,以满足我们在注册、登录、忘记密码等多个场合的不同需求。 -5. 发送测试邮件:点击官方「快速入门」的最后一步,来到[邮件发送](https://console.cloud.tencent.com/ses/send)页面。选择发信模板,输入主题和收件人等信息,点击「发送」按钮。如能成功收到测试邮件,那么恭喜,你已成功完成腾讯云邮件的设置。 +5. 发送测试邮件:点击官方「快速入门」的最后一步,来到 [邮件发送](https://console.cloud.tencent.com/ses/send) 页面。选择发信模板,输入主题和收件人等信息,点击「发送」按钮。如能成功收到测试邮件,那么恭喜,你已成功完成腾讯云邮件的设置。 -### 编写连接器的 JSON 配置 \ No newline at end of file +### 编写连接器的 JSON 配置 + +1. 前往腾讯云 [API 密钥管理](https://console.cloud.tencent.com/cam/capi) 页面,如尚未生成过密钥,可以点击「新建密钥」按钮,完成身份认证之后系统会自动生成一对 `SecretId` 和 `SecretKey`,请注意妥善保管密钥避免泄漏而造成不必要的损失。 +2. 在 Logto Admin Console 的连接器页面,点击邮件连接器右侧的「配置」按钮,再在弹出的菜单中选择「腾讯云邮件服务」,点击「下一步」,进入配置页面。 +3. 在右侧的编辑器中,将配置模版中的字段值替换为真实字段。以下为最小可用的模板示例: + +```json +{ + "region": "ap-hongkong", + "accessKeyId": "", + "accessKeySecret": "", + "fromAddress": "", + "templates": [ + { + "params": [ + { + "name": "code", + "value": "code" + } + ], + "subject": "Sign-in with Logto", + "usageType": "SignIn", + "templateId": "" + }, + { + "params": [ + { + "name": "code", + "value": "code" + } + ], + "subject": "Sign-up with Logto", + "usageType": "Register", + "templateId": "" + }, + { + "params": [ + { + "name": "code", + "value": "code" + } + ], + "subject": "Reset your password in Logto", + "usageType": "ForgotPassword", + "templateId": "" + }, + { + "params": [ + { + "name": "code", + "value": "code" + } + ], + "subject": "Continue with Logto", + "usageType": "Continue", + "templateId": "" + }, + { + "params": [ + { + "name": "code", + "value": "code" + } + ], + "subject": "Test with Logto", + "usageType": "Test", + "templateId": "" + } + ] +} +``` + +### 测试腾讯云邮件连接器 + +你可以在「保存并完成」之前输入一个邮件地址并点按「发送」来测试配置是否可以正常工作。 + +大功告成!快去「登录体验」页面 [启用邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in/#%E5%9C%A8%E7%99%BB%E5%BD%95%E4%BD%93%E9%AA%8C%E4%B8%AD%E5%90%AF%E7%94%A8%E8%BF%9E%E6%8E%A5%E5%99%A8) +吧。 + +### 配置类型 + +| 名称 | 类型 | 必填 | +|-----------------|------------|-----------| +| accessKeyId | string | Yes | +| accessKeySecret | string | Yes | +| region | string | Yes | +| fromAddress | string | Yes | +| fromName | string | No | +| replyAddress | string | No | +| templates | Template[] | Yes | + +| 模板属性 | 类型 | 枚举值 | 必填 | +|--------------|-------------|--------------|----------| +| templateId | string | N/A | Yes | +| usageType | enum string | 'Register' \ 'SignIn' \ 'ForgotPassword' \ 'Continue' \ 'Test' | Yes | +| subject | string | N/A | Yes | +| params | object | N/A | Yes | + +## 参考 + +- [腾讯云文档中心 邮件推送](https://cloud.tencent.com/document/product/1288)