mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
feat(connector-twilio-sms): add twilio sms connector (#881)
* feat(connector-twilio-sms): add twilio sms connector * feat(connector-twilio-sms): add twilio sms connector to initialization
This commit is contained in:
parent
17c63cd2d9
commit
d7ce13d260
17 changed files with 374 additions and 2 deletions
2
packages/connector-twilio-sms/README.md
Normal file
2
packages/connector-twilio-sms/README.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
### Twilio SMS README
|
||||
placeholder
|
21
packages/connector-twilio-sms/docs/config-template.md
Normal file
21
packages/connector-twilio-sms/docs/config-template.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
```json
|
||||
{
|
||||
"accountSID": "<account-sid>",
|
||||
"authToken": "<auth-token>",
|
||||
"fromMessagingServiceSID": "<from-messaging-service-sid>",
|
||||
"templates": [
|
||||
{
|
||||
"usageType": "SignIn",
|
||||
"content": "This is for sign-in purposes only. Your passcode is {{code}}.",
|
||||
},
|
||||
{
|
||||
"usageType": "Register",
|
||||
"content": "This is for registering purposes only. Your passcode is {{code}}.",
|
||||
},
|
||||
{
|
||||
"usageType": "Test",
|
||||
"content": "This is for testing purposes only. Your passcode is {{code}}.",
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
8
packages/connector-twilio-sms/jest.config.ts
Normal file
8
packages/connector-twilio-sms/jest.config.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { Config, merge } from '@silverhand/jest-config';
|
||||
|
||||
const config: Config.InitialOptions = merge({
|
||||
testEnvironment: 'node',
|
||||
setupFilesAfterEnv: ['jest-matcher-specific-error'],
|
||||
});
|
||||
|
||||
export default config;
|
55
packages/connector-twilio-sms/package.json
Normal file
55
packages/connector-twilio-sms/package.json
Normal file
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "@logto/connector-twilio-sms",
|
||||
"version": "0.1.0",
|
||||
"description": "Twilio SMS connector implementation.",
|
||||
"main": "./lib/index.js",
|
||||
"exports": "./lib/index.js",
|
||||
"author": "Silverhand Inc. <contact@silverhand.io>",
|
||||
"license": "MPL-2.0",
|
||||
"files": [
|
||||
"lib",
|
||||
"docs"
|
||||
],
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"precommit": "lint-staged",
|
||||
"build": "rm -rf lib/ && tsc -p tsconfig.build.json",
|
||||
"lint": "eslint --ext .ts src",
|
||||
"lint:report": "pnpm lint --format json --output-file report.json",
|
||||
"dev": "rm -rf lib/ && tsc-watch -p tsconfig.build.json --preserveWatchOutput --onSuccess \"node ./lib/index.js\"",
|
||||
"test": "jest",
|
||||
"test:coverage": "jest --coverage --silent",
|
||||
"prepack": "pnpm build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@logto/connector-types": "^0.1.0",
|
||||
"@logto/shared": "^0.1.0",
|
||||
"@silverhand/essentials": "^1.1.6",
|
||||
"@silverhand/jest-config": "^0.14.0",
|
||||
"got": "^11.8.2",
|
||||
"zod": "^3.14.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/types": "^27.5.1",
|
||||
"@silverhand/eslint-config": "^0.14.0",
|
||||
"@silverhand/ts-config": "^0.14.0",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/node": "^16.3.1",
|
||||
"eslint": "^8.10.0",
|
||||
"jest": "^27.5.1",
|
||||
"jest-matcher-specific-error": "^1.0.0",
|
||||
"lint-staged": "^12.0.0",
|
||||
"prettier": "^2.3.2",
|
||||
"ts-jest": "^27.1.1",
|
||||
"tsc-watch": "^5.0.0",
|
||||
"typescript": "^4.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "@silverhand"
|
||||
},
|
||||
"prettier": "@silverhand/eslint-config/.prettierrc"
|
||||
}
|
30
packages/connector-twilio-sms/src/constant.ts
Normal file
30
packages/connector-twilio-sms/src/constant.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import path from 'path';
|
||||
|
||||
import { ConnectorType, ConnectorMetadata } from '@logto/connector-types';
|
||||
import { getFileContents } from '@logto/shared';
|
||||
|
||||
export const endpoint = 'https://api.twilio.com/2010-04-01/Accounts/{{accountSID}}/Messages.json';
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
const currentPath = __dirname;
|
||||
const pathToReadmeFile = path.join(currentPath, '..', 'README.md');
|
||||
const pathToConfigTemplate = path.join(currentPath, '..', 'docs', 'config-template.md');
|
||||
const readmeContentFallback = 'Please check README.md file directory.';
|
||||
const configTemplateFallback = 'Please check config-template.md file directory.';
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
target: 'twilio-sms',
|
||||
type: ConnectorType.SMS,
|
||||
platform: null,
|
||||
name: {
|
||||
en: 'Twilio SMS Service',
|
||||
'zh-CN': 'Twilio 短信服务',
|
||||
},
|
||||
logo: './logo.png',
|
||||
description: {
|
||||
en: 'Messaging APIs for reliable SMS delivery.',
|
||||
'zh-CN': '可信赖的短信消息 API。',
|
||||
},
|
||||
readme: getFileContents(pathToReadmeFile, readmeContentFallback),
|
||||
configTemplate: getFileContents(pathToConfigTemplate, configTemplateFallback),
|
||||
};
|
23
packages/connector-twilio-sms/src/index.test.ts
Normal file
23
packages/connector-twilio-sms/src/index.test.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { GetConnectorConfig } from '@logto/connector-types';
|
||||
|
||||
import { TwilioSmsConnector } from '.';
|
||||
import { mockedConfig } from './mock';
|
||||
import { TwilioSmsConfig } from './types';
|
||||
|
||||
const getConnectorConfig = jest.fn() as GetConnectorConfig<TwilioSmsConfig>;
|
||||
|
||||
const twilioSmsMethods = new TwilioSmsConnector(getConnectorConfig);
|
||||
|
||||
describe('validateConfig()', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should pass on valid config', async () => {
|
||||
await expect(twilioSmsMethods.validateConfig(mockedConfig)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('throws if config is invalid', async () => {
|
||||
await expect(twilioSmsMethods.validateConfig({})).rejects.toThrow();
|
||||
});
|
||||
});
|
67
packages/connector-twilio-sms/src/index.ts
Normal file
67
packages/connector-twilio-sms/src/index.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import {
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
ConnectorMetadata,
|
||||
EmailSendMessageFunction,
|
||||
ValidateConfig,
|
||||
SmsConnector,
|
||||
GetConnectorConfig,
|
||||
} from '@logto/connector-types';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import got from 'got';
|
||||
|
||||
import { defaultMetadata, endpoint } from './constant';
|
||||
import { twilioSmsConfigGuard, SendSmsResponse, TwilioSmsConfig, PublicParameters } from './types';
|
||||
|
||||
export class TwilioSmsConnector implements SmsConnector {
|
||||
public metadata: ConnectorMetadata = defaultMetadata;
|
||||
|
||||
public readonly getConfig: GetConnectorConfig<TwilioSmsConfig>;
|
||||
|
||||
constructor(getConnectorConfig: GetConnectorConfig<TwilioSmsConfig>) {
|
||||
this.getConfig = getConnectorConfig;
|
||||
}
|
||||
|
||||
public validateConfig: ValidateConfig = async (config: unknown) => {
|
||||
const result = twilioSmsConfigGuard.safeParse(config);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error.message);
|
||||
}
|
||||
};
|
||||
|
||||
public sendMessage: EmailSendMessageFunction<SendSmsResponse> = async (address, type, data) => {
|
||||
const config = await this.getConfig(this.metadata.target, this.metadata.platform);
|
||||
await this.validateConfig(config);
|
||||
const { accountSID, authToken, fromMessagingServiceSID, templates } = config;
|
||||
const template = templates.find((template) => template.usageType === type);
|
||||
|
||||
assert(
|
||||
template,
|
||||
new ConnectorError(
|
||||
ConnectorErrorCodes.TemplateNotFound,
|
||||
`Cannot find template for type: ${type}`
|
||||
)
|
||||
);
|
||||
|
||||
const parameters: PublicParameters = {
|
||||
To: address,
|
||||
MessagingServiceSid: fromMessagingServiceSID,
|
||||
Body:
|
||||
typeof data.code === 'string'
|
||||
? template.content.replace(/{{code}}/g, data.code)
|
||||
: template.content,
|
||||
};
|
||||
|
||||
return got
|
||||
.post(endpoint.replace(/{{accountSID}}/g, accountSID), {
|
||||
headers: {
|
||||
Authorization:
|
||||
'Basic ' + Buffer.from([accountSID, authToken].join(':')).toString('base64'),
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams(parameters).toString(),
|
||||
})
|
||||
.json<SendSmsResponse>();
|
||||
};
|
||||
}
|
17
packages/connector-twilio-sms/src/mock.ts
Normal file
17
packages/connector-twilio-sms/src/mock.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { TwilioSmsConfig } from './types';
|
||||
|
||||
const mockedAccountSID = 'account-sid';
|
||||
const mockedAuthToken = 'auth-token';
|
||||
const mockedFromMessagingServiceSID = 'from-messaging-service-sid';
|
||||
|
||||
export const mockedConfig: TwilioSmsConfig = {
|
||||
accountSID: mockedAccountSID,
|
||||
authToken: mockedAuthToken,
|
||||
fromMessagingServiceSID: mockedFromMessagingServiceSID,
|
||||
templates: [
|
||||
{
|
||||
usageType: 'Test',
|
||||
content: 'This is for testing purposes only. Your passcode is {{code}}.',
|
||||
},
|
||||
],
|
||||
};
|
63
packages/connector-twilio-sms/src/types.ts
Normal file
63
packages/connector-twilio-sms/src/types.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { Nullable } from '@silverhand/essentials';
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* @doc https://www.twilio.com/docs/sms/send-messages
|
||||
*
|
||||
* @doc https://www.twilio.com/docs/phone-numbers
|
||||
* @doc https://www.twilio.com/phone-numbers/global-catalog
|
||||
* @doc https://en.wikipedia.org/wiki/E.164
|
||||
*/
|
||||
|
||||
export type PublicParameters = {
|
||||
To: string;
|
||||
MessagingServiceSid: string;
|
||||
Body: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* UsageType here is used to specify the use case of the template, can be either
|
||||
* 'Register', 'SignIn', 'ForgotPassword' or 'Test'.
|
||||
*/
|
||||
const templateGuard = z.object({
|
||||
usageType: z.string(),
|
||||
content: z.string(),
|
||||
});
|
||||
|
||||
export const twilioSmsConfigGuard = z.object({
|
||||
accountSID: z.string(),
|
||||
authToken: z.string(),
|
||||
fromMessagingServiceSID: z.string(),
|
||||
templates: z.array(templateGuard),
|
||||
});
|
||||
|
||||
export type TwilioSmsConfig = z.infer<typeof twilioSmsConfigGuard>;
|
||||
|
||||
export type SendSmsResponse = {
|
||||
account_sid: string;
|
||||
api_version: string;
|
||||
body: string;
|
||||
code: number;
|
||||
date_cereated: Nullable<string>;
|
||||
date_sent: Nullable<string>;
|
||||
date_updated: Nullable<string>;
|
||||
direction: string;
|
||||
error_code: Nullable<string>;
|
||||
error_message: Nullable<string>;
|
||||
from: Nullable<string>;
|
||||
message: Nullable<string>;
|
||||
messaging_service_sid: string;
|
||||
more_info: Nullable<string>;
|
||||
num_media: string;
|
||||
num_segments: string;
|
||||
price: Nullable<string>;
|
||||
price_unit: Nullable<string>;
|
||||
sid: string;
|
||||
status: number;
|
||||
subresource_uris: {
|
||||
media?: string;
|
||||
feedback?: string;
|
||||
};
|
||||
to: string;
|
||||
uri: string;
|
||||
};
|
10
packages/connector-twilio-sms/tsconfig.base.json
Normal file
10
packages/connector-twilio-sms/tsconfig.base.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "@silverhand/ts-config/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
5
packages/connector-twilio-sms/tsconfig.build.json
Normal file
5
packages/connector-twilio-sms/tsconfig.build.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": "./tsconfig.base",
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
7
packages/connector-twilio-sms/tsconfig.json
Normal file
7
packages/connector-twilio-sms/tsconfig.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "./tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "jest", "jest-matcher-specific-error"]
|
||||
},
|
||||
"include": ["src", "jest.config.ts"]
|
||||
}
|
6
packages/connector-twilio-sms/tsconfig.test.json
Normal file
6
packages/connector-twilio-sms/tsconfig.test.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"isolatedModules": false
|
||||
}
|
||||
}
|
|
@ -27,6 +27,7 @@
|
|||
"@logto/connector-github": "^0.1.0",
|
||||
"@logto/connector-google": "^0.1.0",
|
||||
"@logto/connector-sendgrid-email": "^0.1.0",
|
||||
"@logto/connector-twilio-sms": "^0.1.0",
|
||||
"@logto/connector-types": "^0.1.0",
|
||||
"@logto/connector-wechat": "^0.1.0",
|
||||
"@logto/connector-wechat-native": "^0.1.0",
|
||||
|
|
|
@ -67,6 +67,14 @@ const sendGridMailConnector = {
|
|||
config: {},
|
||||
createdAt: 1_646_382_233_111,
|
||||
};
|
||||
const twilioSmsConnector = {
|
||||
id: 'twilio-sms',
|
||||
target: 'twilio-sms',
|
||||
platform: null,
|
||||
enabled: false,
|
||||
config: {},
|
||||
createdAt: 1_646_382_233_000,
|
||||
};
|
||||
const wechatConnector = {
|
||||
id: 'wechat',
|
||||
target: 'wechat',
|
||||
|
@ -92,6 +100,7 @@ const connectors = [
|
|||
githubConnector,
|
||||
googleConnector,
|
||||
sendGridMailConnector,
|
||||
twilioSmsConnector,
|
||||
wechatConnector,
|
||||
wechatNativeConnector,
|
||||
];
|
||||
|
@ -116,8 +125,9 @@ describe('getConnectorInstances', () => {
|
|||
expect(connectorInstances[4]).toHaveProperty('connector', githubConnector);
|
||||
expect(connectorInstances[5]).toHaveProperty('connector', googleConnector);
|
||||
expect(connectorInstances[6]).toHaveProperty('connector', sendGridMailConnector);
|
||||
expect(connectorInstances[7]).toHaveProperty('connector', wechatConnector);
|
||||
expect(connectorInstances[8]).toHaveProperty('connector', wechatNativeConnector);
|
||||
expect(connectorInstances[7]).toHaveProperty('connector', twilioSmsConnector);
|
||||
expect(connectorInstances[8]).toHaveProperty('connector', wechatConnector);
|
||||
expect(connectorInstances[9]).toHaveProperty('connector', wechatNativeConnector);
|
||||
});
|
||||
|
||||
test('should throw if any required connector does not exist in DB', async () => {
|
||||
|
|
|
@ -5,6 +5,7 @@ import { FacebookConnector } from '@logto/connector-facebook';
|
|||
import { GithubConnector } from '@logto/connector-github';
|
||||
import { GoogleConnector } from '@logto/connector-google';
|
||||
import { SendGridMailConnector } from '@logto/connector-sendgrid-email';
|
||||
import { TwilioSmsConnector } from '@logto/connector-twilio-sms';
|
||||
import { WeChatConnector } from '@logto/connector-wechat';
|
||||
import { WeChatNativeConnector } from '@logto/connector-wechat-native';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
@ -23,6 +24,7 @@ const allConnectors: IConnector[] = [
|
|||
new GithubConnector(getConnectorConfig),
|
||||
new GoogleConnector(getConnectorConfig),
|
||||
new SendGridMailConnector(getConnectorConfig),
|
||||
new TwilioSmsConnector(getConnectorConfig),
|
||||
new WeChatConnector(getConnectorConfig),
|
||||
new WeChatNativeConnector(getConnectorConfig),
|
||||
];
|
||||
|
|
|
@ -349,6 +349,49 @@ importers:
|
|||
tsc-watch: 5.0.3_typescript@4.6.4
|
||||
typescript: 4.6.4
|
||||
|
||||
packages/connector-twilio-sms:
|
||||
specifiers:
|
||||
'@jest/types': ^27.5.1
|
||||
'@logto/connector-types': ^0.1.0
|
||||
'@logto/shared': ^0.1.0
|
||||
'@silverhand/eslint-config': ^0.14.0
|
||||
'@silverhand/essentials': ^1.1.6
|
||||
'@silverhand/jest-config': ^0.14.0
|
||||
'@silverhand/ts-config': ^0.14.0
|
||||
'@types/jest': ^27.4.1
|
||||
'@types/node': ^16.3.1
|
||||
eslint: ^8.10.0
|
||||
got: ^11.8.2
|
||||
jest: ^27.5.1
|
||||
jest-matcher-specific-error: ^1.0.0
|
||||
lint-staged: ^12.0.0
|
||||
prettier: ^2.3.2
|
||||
ts-jest: ^27.1.1
|
||||
tsc-watch: ^5.0.0
|
||||
typescript: ^4.6.2
|
||||
zod: ^3.14.3
|
||||
dependencies:
|
||||
'@logto/connector-types': link:../connector-types
|
||||
'@logto/shared': link:../shared
|
||||
'@silverhand/essentials': 1.1.7
|
||||
'@silverhand/jest-config': 0.14.0_53ggqi2i4rbcfjtktmjua6zili
|
||||
got: 11.8.3
|
||||
zod: 3.14.3
|
||||
devDependencies:
|
||||
'@jest/types': 27.5.1
|
||||
'@silverhand/eslint-config': 0.14.0_rqoong6vegs374egqglqjbgiwm
|
||||
'@silverhand/ts-config': 0.14.0_typescript@4.6.4
|
||||
'@types/jest': 27.4.1
|
||||
'@types/node': 16.11.12
|
||||
eslint: 8.10.0
|
||||
jest: 27.5.1
|
||||
jest-matcher-specific-error: 1.0.0
|
||||
lint-staged: 12.4.0
|
||||
prettier: 2.5.1
|
||||
ts-jest: 27.1.1_53ggqi2i4rbcfjtktmjua6zili
|
||||
tsc-watch: 5.0.3_typescript@4.6.4
|
||||
typescript: 4.6.4
|
||||
|
||||
packages/connector-types:
|
||||
specifiers:
|
||||
'@jest/types': ^27.5.1
|
||||
|
@ -599,6 +642,7 @@ importers:
|
|||
'@logto/connector-github': ^0.1.0
|
||||
'@logto/connector-google': ^0.1.0
|
||||
'@logto/connector-sendgrid-email': ^0.1.0
|
||||
'@logto/connector-twilio-sms': ^0.1.0
|
||||
'@logto/connector-types': ^0.1.0
|
||||
'@logto/connector-wechat': ^0.1.0
|
||||
'@logto/connector-wechat-native': ^0.1.0
|
||||
|
@ -671,6 +715,7 @@ importers:
|
|||
'@logto/connector-github': link:../connector-github
|
||||
'@logto/connector-google': link:../connector-google
|
||||
'@logto/connector-sendgrid-email': link:../connector-sendgrid-mail
|
||||
'@logto/connector-twilio-sms': link:../connector-twilio-sms
|
||||
'@logto/connector-types': link:../connector-types
|
||||
'@logto/connector-wechat': link:../connector-wechat
|
||||
'@logto/connector-wechat-native': link:../connector-wechat-native
|
||||
|
|
Loading…
Reference in a new issue