mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
127 lines
3.5 KiB
TypeScript
127 lines
3.5 KiB
TypeScript
import { assert } from '@silverhand/essentials';
|
|
import { HTTPError } from 'got';
|
|
|
|
import type {
|
|
GetConnectorConfig,
|
|
SendMessageFunction,
|
|
SmsConnector,
|
|
CreateConnector,
|
|
} from '@logto/connector-kit';
|
|
import {
|
|
ConnectorError,
|
|
ConnectorErrorCodes,
|
|
validateConfig,
|
|
ConnectorType,
|
|
parseJson,
|
|
} from '@logto/connector-kit';
|
|
|
|
import { defaultMetadata } from './constant.js';
|
|
import { sendSms } from './single-send-text.js';
|
|
import type { Template } from './types.js';
|
|
import { aliyunSmsConfigGuard, sendSmsResponseGuard } from './types.js';
|
|
|
|
const isChinaNumber = (to: string) => /^(\+86|0086|86)?\d{11}$/.test(to);
|
|
|
|
const getTemplateCode = ({ templateCode }: Template, to: string) => {
|
|
if (typeof templateCode === 'string') {
|
|
return templateCode;
|
|
}
|
|
|
|
return isChinaNumber(to) ? templateCode.china : templateCode.overseas;
|
|
};
|
|
|
|
const sendMessage =
|
|
(getConfig: GetConnectorConfig): SendMessageFunction =>
|
|
async (data, inputConfig) => {
|
|
const { to, type, payload } = data;
|
|
const config = inputConfig ?? (await getConfig(defaultMetadata.id));
|
|
validateConfig(config, aliyunSmsConfigGuard);
|
|
const { accessKeyId, accessKeySecret, signName, templates } = config;
|
|
const template = templates.find(({ usageType }) => usageType === type);
|
|
|
|
assert(
|
|
template,
|
|
new ConnectorError(
|
|
ConnectorErrorCodes.TemplateNotFound,
|
|
`Cannot find template for type: ${type}`
|
|
)
|
|
);
|
|
|
|
try {
|
|
const httpResponse = await sendSms(
|
|
{
|
|
AccessKeyId: accessKeyId,
|
|
PhoneNumbers: to,
|
|
SignName: signName,
|
|
TemplateCode: getTemplateCode(template, to),
|
|
TemplateParam: JSON.stringify(payload),
|
|
},
|
|
accessKeySecret
|
|
);
|
|
|
|
const { Code, Message, ...rest } = parseResponseString(httpResponse.body);
|
|
|
|
assert(
|
|
Code === 'OK',
|
|
new ConnectorError(
|
|
/**
|
|
* See https://help.aliyun.com/document_detail/101347.html for more details.
|
|
* Some errors (like rate limit) can be addressed by end users.
|
|
*/
|
|
Code === 'isv.BUSINESS_LIMIT_CONTROL'
|
|
? ConnectorErrorCodes.RateLimitExceeded
|
|
: ConnectorErrorCodes.General,
|
|
{
|
|
errorDescription: Message,
|
|
Code,
|
|
...rest,
|
|
}
|
|
)
|
|
);
|
|
|
|
return { Code, Message, ...rest };
|
|
} catch (error: unknown) {
|
|
if (error instanceof HTTPError) {
|
|
const {
|
|
response: { body: rawBody },
|
|
} = error;
|
|
|
|
assert(
|
|
typeof rawBody === 'string',
|
|
new ConnectorError(
|
|
ConnectorErrorCodes.InvalidResponse,
|
|
`Invalid response raw body type: ${typeof rawBody}`
|
|
)
|
|
);
|
|
|
|
const { Message, ...rest } = parseResponseString(rawBody);
|
|
throw new ConnectorError(ConnectorErrorCodes.General, {
|
|
errorDescription: Message,
|
|
...rest,
|
|
});
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const parseResponseString = (response: string) => {
|
|
const result = sendSmsResponseGuard.safeParse(parseJson(response));
|
|
|
|
if (!result.success) {
|
|
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error);
|
|
}
|
|
|
|
return result.data;
|
|
};
|
|
|
|
const createAliyunSmsConnector: CreateConnector<SmsConnector> = async ({ getConfig }) => {
|
|
return {
|
|
metadata: defaultMetadata,
|
|
type: ConnectorType.Sms,
|
|
configGuard: aliyunSmsConfigGuard,
|
|
sendMessage: sendMessage(getConfig),
|
|
};
|
|
};
|
|
|
|
export default createAliyunSmsConnector;
|