mirror of
https://github.com/logto-io/logto.git
synced 2025-03-10 22:22:45 -05:00
feat(core): wrap aliyun short message service connector (#670)
* feat(core): wrap Aliyun short message service connector * feat(core): connectors package.json private should be FALSE * feat(core): fix dependencies and remove query-string.stringify()
This commit is contained in:
parent
712cd795d0
commit
a06d3ee73c
25 changed files with 589 additions and 18 deletions
|
@ -10,7 +10,7 @@
|
|||
"lib",
|
||||
"docs"
|
||||
],
|
||||
"private": true,
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"precommit": "lint-staged",
|
||||
|
@ -30,7 +30,6 @@
|
|||
"dayjs": "^1.10.5",
|
||||
"got": "^11.8.2",
|
||||
"iconv-lite": "0.6.3",
|
||||
"query-string": "^7.0.1",
|
||||
"snakecase-keys": "^5.1.0",
|
||||
"zod": "^3.14.3"
|
||||
},
|
||||
|
|
|
@ -20,7 +20,6 @@ import {
|
|||
import { assert } from '@silverhand/essentials';
|
||||
import dayjs from 'dayjs';
|
||||
import got from 'got';
|
||||
import { stringify } from 'query-string';
|
||||
|
||||
import {
|
||||
alipayEndpoint,
|
||||
|
@ -61,12 +60,14 @@ export class AlipayConnector implements SocialConnector {
|
|||
|
||||
const redirect_uri = encodeURI(redirectUri);
|
||||
|
||||
return `${authorizationEndpoint}?${stringify({
|
||||
const queryParameters = new URLSearchParams({
|
||||
app_id,
|
||||
redirect_uri, // The variable `redirectUri` should match {appId, appSecret}
|
||||
scope,
|
||||
state,
|
||||
})}`;
|
||||
});
|
||||
|
||||
return `${authorizationEndpoint}?${queryParameters.toString()}`;
|
||||
};
|
||||
|
||||
public getAccessToken: GetAccessToken = async (code): Promise<AccessTokenObject> => {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"lib",
|
||||
"docs"
|
||||
],
|
||||
"private": true,
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"precommit": "lint-staged",
|
||||
|
|
2
packages/connector-aliyun-sms/README.md
Normal file
2
packages/connector-aliyun-sms/README.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
### Aliyun SMS README
|
||||
placeholder
|
33
packages/connector-aliyun-sms/docs/config-template.md
Normal file
33
packages/connector-aliyun-sms/docs/config-template.md
Normal file
|
@ -0,0 +1,33 @@
|
|||
```json
|
||||
{
|
||||
"accessKeyId": "<access-key-id>",
|
||||
"accessKeySecret": "<access-key-secret>",
|
||||
"signName": "<sign-name>",
|
||||
"templates": [
|
||||
{
|
||||
"type": 0,
|
||||
"usageType": "SIGN_IN",
|
||||
"code": "<temporary-passcode>",
|
||||
"name": "<sign-in-template-name>",
|
||||
"content": "<sign-in-template-content>",
|
||||
"remark": "<sign-in-template-remark>"
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"usageType": "REGISTER",
|
||||
"code": "<temporary-passcode>",
|
||||
"name": "<register-template-name>",
|
||||
"content": "<register-template-content>",
|
||||
"remark": "<register-template-remark>"
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"usageType": "TEST",
|
||||
"code": "<temporary-passcode>",
|
||||
"name": "<test-template-name>",
|
||||
"content": "<test-template-content>",
|
||||
"remark": "<test-template-remark>"
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
8
packages/connector-aliyun-sms/jest.config.ts
Normal file
8
packages/connector-aliyun-sms/jest.config.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { Config, merge } from '@logto/jest-config';
|
||||
|
||||
const config: Config.InitialOptions = merge({
|
||||
testEnvironment: 'node',
|
||||
setupFilesAfterEnv: ['jest-matcher-specific-error'],
|
||||
});
|
||||
|
||||
export default config;
|
56
packages/connector-aliyun-sms/package.json
Normal file
56
packages/connector-aliyun-sms/package.json
Normal file
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"name": "@logto/connector-aliyun-sms",
|
||||
"version": "0.1.0",
|
||||
"description": "Aliyun 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/jest-config": "^0.1.0",
|
||||
"@logto/shared": "^0.1.0",
|
||||
"@logto/schemas": "^0.1.0",
|
||||
"@silverhand/essentials": "^1.1.0",
|
||||
"got": "^11.8.2",
|
||||
"zod": "^3.14.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/types": "^27.5.1",
|
||||
"@silverhand/eslint-config": "^0.10.2",
|
||||
"@silverhand/ts-config": "^0.10.2",
|
||||
"@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": "^11.1.1",
|
||||
"prettier": "^2.3.2",
|
||||
"ts-jest": "^27.1.1",
|
||||
"tsc-watch": "^4.4.0",
|
||||
"typescript": "^4.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "@silverhand"
|
||||
},
|
||||
"prettier": "@silverhand/eslint-config/.prettierrc"
|
||||
}
|
50
packages/connector-aliyun-sms/src/constant.ts
Normal file
50
packages/connector-aliyun-sms/src/constant.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import path from 'path';
|
||||
|
||||
import { ConnectorMetadata, ConnectorType } from '@logto/connector-types';
|
||||
import { getFileContents } from '@logto/shared';
|
||||
|
||||
/**
|
||||
* @doc https://help.aliyun.com/document_detail/101414.html
|
||||
*/
|
||||
export interface SendSms {
|
||||
OutId?: string;
|
||||
PhoneNumbers: string; // 11 digits w/o prefix (can be multiple phone numbers with separator `,`)
|
||||
SignName: string; // Name of SMS signature
|
||||
SmsUpExtendCode?: string;
|
||||
TemplateCode: string; // Text message template ID
|
||||
TemplateParam?: string; // Stringified JSON (used to fill in text template)
|
||||
}
|
||||
|
||||
export const endpoint = 'https://dysmsapi.aliyuncs.com/';
|
||||
|
||||
export const staticConfigs = {
|
||||
Format: 'json',
|
||||
RegionId: 'cn-hangzhou',
|
||||
SignatureMethod: 'HMAC-SHA1',
|
||||
SignatureVersion: '1.0',
|
||||
Version: '2017-05-25',
|
||||
};
|
||||
|
||||
// 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 = {
|
||||
id: 'aliyun-sms',
|
||||
type: ConnectorType.SMS,
|
||||
name: {
|
||||
en: 'Aliyun Short Message Service',
|
||||
'zh-CN': '阿里云短信服务',
|
||||
},
|
||||
logo: './logo.png',
|
||||
description: {
|
||||
en: 'Short Message Service (SMS) has a batch sending feature and various API operations to send one-time password (OTP) messages, notification messages, and promotional messages to customers.',
|
||||
'zh-CN':
|
||||
'短信服务(Short Message Service)是指通过调用短信发送API,将指定短信内容发送给指定手机用户。',
|
||||
},
|
||||
readme: getFileContents(pathToReadmeFile, readmeContentFallback),
|
||||
configTemplate: getFileContents(pathToConfigTemplate, configTemplateFallback),
|
||||
};
|
56
packages/connector-aliyun-sms/src/index.test.ts
Normal file
56
packages/connector-aliyun-sms/src/index.test.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { GetConnectorConfig } from '@logto/connector-types';
|
||||
|
||||
import { AliyunSmsConnector, AliyunSmsConfig } from '.';
|
||||
import { mockedConnectorConfig, mockedValidConnectorConfig, phoneTest, codeTest } from './mock';
|
||||
import { sendSms } from './single-send-text';
|
||||
|
||||
const getConnectorConfig = jest.fn() as GetConnectorConfig<AliyunSmsConfig>;
|
||||
|
||||
const aliyunSmsMethods = new AliyunSmsConnector(getConnectorConfig);
|
||||
|
||||
jest.mock('./single-send-text');
|
||||
|
||||
describe('validateConfig()', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should pass on valid config', async () => {
|
||||
await expect(
|
||||
aliyunSmsMethods.validateConfig(mockedValidConnectorConfig)
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('throws if config is invalid', async () => {
|
||||
await expect(aliyunSmsMethods.validateConfig({})).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendMessage()', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call singleSendMail() and replace code in content', async () => {
|
||||
jest.spyOn(aliyunSmsMethods, 'getConfig').mockResolvedValueOnce(mockedConnectorConfig);
|
||||
await aliyunSmsMethods.sendMessage(phoneTest, 'SignIn', { code: codeTest });
|
||||
const { templates, ...credentials } = mockedConnectorConfig;
|
||||
expect(sendSms).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
AccessKeyId: credentials.accessKeyId,
|
||||
PhoneNumbers: phoneTest,
|
||||
SignName: credentials.signName,
|
||||
TemplateCode: 'code',
|
||||
TemplateParam: `{"code":"${codeTest}"}`,
|
||||
}),
|
||||
'accessKeySecret'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if template is missing', async () => {
|
||||
jest.spyOn(aliyunSmsMethods, 'getConfig').mockResolvedValueOnce(mockedConnectorConfig);
|
||||
await expect(
|
||||
aliyunSmsMethods.sendMessage(phoneTest, 'Register', { code: codeTest })
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
101
packages/connector-aliyun-sms/src/index.ts
Normal file
101
packages/connector-aliyun-sms/src/index.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
import {
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
ConnectorMetadata,
|
||||
SmsSendMessageFunction,
|
||||
ValidateConfig,
|
||||
SmsConnector,
|
||||
GetConnectorConfig,
|
||||
} from '@logto/connector-types';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import { Response } from 'got';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { defaultMetadata } from './constant';
|
||||
import { sendSms } from './single-send-text';
|
||||
import { SendSmsResponse } from './utils';
|
||||
/**
|
||||
* Details of SmsTemplateType can be found at:
|
||||
* https://next.api.aliyun.com/document/Dysmsapi/2017-05-25/QuerySmsTemplateList.
|
||||
*
|
||||
* For our use case is to send passcode sms for passwordless sign-in/up as well as
|
||||
* reset password, the default value of type code is set to be 2.
|
||||
*/
|
||||
enum SmsTemplateType {
|
||||
Notification = 0,
|
||||
Promotion = 1,
|
||||
Passcode = 2,
|
||||
InternationalMessage = 6,
|
||||
PureNumber = 7,
|
||||
}
|
||||
|
||||
/**
|
||||
* UsageType here is used to specify the use case of the template, can be either
|
||||
* 'Register', 'SignIn', 'ForgotPassword' or 'Test'.
|
||||
*
|
||||
* Type here in the template is used to specify the purpose of sending the sms,
|
||||
* can be either item in SmsTemplateType.
|
||||
* As the SMS is applied for sending passcode, the value should always be 2 in our case.
|
||||
*/
|
||||
const templateGuard = z.object({
|
||||
type: z.nativeEnum(SmsTemplateType).default(2),
|
||||
usageType: z.string(),
|
||||
code: z.string(),
|
||||
name: z.string().min(1).max(30),
|
||||
content: z.string().min(1).max(500),
|
||||
remark: z.string(),
|
||||
});
|
||||
|
||||
const configGuard = z.object({
|
||||
accessKeyId: z.string(),
|
||||
accessKeySecret: z.string(),
|
||||
signName: z.string(),
|
||||
templates: z.array(templateGuard),
|
||||
});
|
||||
|
||||
export type AliyunSmsConfig = z.infer<typeof configGuard>;
|
||||
|
||||
export class AliyunSmsConnector implements SmsConnector {
|
||||
public metadata: ConnectorMetadata = defaultMetadata;
|
||||
|
||||
public readonly getConfig: GetConnectorConfig<AliyunSmsConfig>;
|
||||
|
||||
constructor(getConnectorConfig: GetConnectorConfig<AliyunSmsConfig>) {
|
||||
this.getConfig = getConnectorConfig;
|
||||
}
|
||||
|
||||
public validateConfig: ValidateConfig = async (config: unknown) => {
|
||||
const result = configGuard.safeParse(config);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error.message);
|
||||
}
|
||||
};
|
||||
|
||||
public sendMessage: SmsSendMessageFunction<Response<SendSmsResponse>> = async (
|
||||
phone,
|
||||
type,
|
||||
{ code }
|
||||
) => {
|
||||
const config = await this.getConfig(this.metadata.id);
|
||||
await this.validateConfig(config);
|
||||
const { accessKeyId, accessKeySecret, signName, templates } = config;
|
||||
const template = templates.find(({ usageType }) => usageType === type);
|
||||
|
||||
assert(
|
||||
template,
|
||||
new ConnectorError(ConnectorErrorCodes.TemplateNotFound, `Cannot find template!`)
|
||||
);
|
||||
|
||||
return sendSms(
|
||||
{
|
||||
AccessKeyId: accessKeyId,
|
||||
PhoneNumbers: phone,
|
||||
SignName: signName,
|
||||
TemplateCode: template.code,
|
||||
TemplateParam: JSON.stringify({ code }),
|
||||
},
|
||||
accessKeySecret
|
||||
);
|
||||
};
|
||||
}
|
43
packages/connector-aliyun-sms/src/mock.ts
Normal file
43
packages/connector-aliyun-sms/src/mock.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
export const mockedConnectorConfig = {
|
||||
accessKeyId: 'accessKeyId',
|
||||
accessKeySecret: 'accessKeySecret',
|
||||
signName: 'signName',
|
||||
templates: [
|
||||
{
|
||||
type: 2,
|
||||
usageType: 'SignIn',
|
||||
code: 'code',
|
||||
name: 'name',
|
||||
content: 'content',
|
||||
remark: 'remark',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const mockedValidConnectorConfig = {
|
||||
accessKeyId: 'accessKeyId',
|
||||
accessKeySecret: 'accessKeySecret',
|
||||
signName: 'signName',
|
||||
templates: [],
|
||||
};
|
||||
|
||||
export const phoneTest = '13012345678';
|
||||
export const codeTest = '1234';
|
||||
|
||||
export const mockedParameters = {
|
||||
AccessKeyId: 'testid',
|
||||
AccountName: "<a%b'>",
|
||||
Action: 'SingleSendMail',
|
||||
AddressType: '1',
|
||||
Format: 'XML',
|
||||
HtmlBody: '4',
|
||||
RegionId: 'cn-hangzhou',
|
||||
ReplyToAddress: 'true',
|
||||
SignatureMethod: 'HMAC-SHA1',
|
||||
SignatureVersion: '1.0',
|
||||
Subject: '3',
|
||||
TagName: '2',
|
||||
ToAddress: '1@test.com',
|
||||
Version: '2015-11-23',
|
||||
};
|
||||
export const mockedRandomCode = 1235;
|
26
packages/connector-aliyun-sms/src/single-send-text.test.ts
Normal file
26
packages/connector-aliyun-sms/src/single-send-text.test.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { mockedRandomCode } from './mock';
|
||||
import { sendSms } from './single-send-text';
|
||||
import { request } from './utils';
|
||||
|
||||
jest.mock('./utils');
|
||||
|
||||
describe('sendSms', () => {
|
||||
it('should call request with action sendSms', async () => {
|
||||
const code = mockedRandomCode;
|
||||
|
||||
await sendSms(
|
||||
{
|
||||
AccessKeyId: '<access-key-id>',
|
||||
PhoneNumbers: '13912345678',
|
||||
SignName: '阿里云短信测试',
|
||||
TemplateCode: ' SMS_154950909',
|
||||
TemplateParam: JSON.stringify({ code }),
|
||||
},
|
||||
'<access-key-secret>'
|
||||
);
|
||||
const calledData = (request as jest.MockedFunction<typeof request>).mock.calls[0];
|
||||
expect(calledData).not.toBeUndefined();
|
||||
const payload = calledData?.[1];
|
||||
expect(payload).toHaveProperty('Action', 'SendSms');
|
||||
});
|
||||
});
|
18
packages/connector-aliyun-sms/src/single-send-text.ts
Normal file
18
packages/connector-aliyun-sms/src/single-send-text.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Response } from 'got';
|
||||
|
||||
import { SendSms, endpoint, staticConfigs } from './constant';
|
||||
import { PublicParameters, request, SendSmsResponse } from './utils';
|
||||
|
||||
/**
|
||||
* @doc https://help.aliyun.com/document_detail/101414.html
|
||||
*/
|
||||
export const sendSms = async (
|
||||
parameters: PublicParameters & SendSms,
|
||||
accessKeySecret: string
|
||||
): Promise<Response<SendSmsResponse>> => {
|
||||
return request<SendSmsResponse>(
|
||||
endpoint,
|
||||
{ Action: 'SendSms', ...staticConfigs, ...parameters },
|
||||
accessKeySecret
|
||||
);
|
||||
};
|
32
packages/connector-aliyun-sms/src/utils.test.ts
Normal file
32
packages/connector-aliyun-sms/src/utils.test.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import got from 'got';
|
||||
|
||||
import { mockedParameters } from './mock';
|
||||
import { getSignature, request } from './utils';
|
||||
|
||||
jest.mock('got');
|
||||
|
||||
describe('getSignature', () => {
|
||||
it('should get valid signature', () => {
|
||||
const parameters = {
|
||||
...mockedParameters,
|
||||
SignatureNonce: 'c1b2c332-4cfb-4a0f-b8cc-ebe622aa0a5c',
|
||||
Timestamp: '2016-10-20T06:27:56Z',
|
||||
};
|
||||
const signature = getSignature(parameters, 'testsecret', 'POST');
|
||||
expect(signature).toEqual('llJfXJjBW3OacrVgxxsITgYaYm0=');
|
||||
});
|
||||
});
|
||||
|
||||
describe('request', () => {
|
||||
it('should call axios.post with extended params', async () => {
|
||||
const parameters = mockedParameters;
|
||||
await request('http://test.endpoint.com', parameters, 'testsecret');
|
||||
const calledData = (got.post as jest.MockedFunction<typeof got.post>).mock.calls[0];
|
||||
expect(calledData).not.toBeUndefined();
|
||||
const payload = calledData?.[0].form as URLSearchParams;
|
||||
expect(payload.get('AccessKeyId')).toEqual('testid');
|
||||
expect(payload.get('Timestamp')).not.toBeNull();
|
||||
expect(payload.get('SignatureNonce')).not.toBeNull();
|
||||
expect(payload.get('Signature')).not.toBeNull();
|
||||
});
|
||||
});
|
79
packages/connector-aliyun-sms/src/utils.ts
Normal file
79
packages/connector-aliyun-sms/src/utils.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { createHmac } from 'crypto';
|
||||
|
||||
import got from 'got';
|
||||
|
||||
export type { Response } from 'got';
|
||||
export type SendSmsResponse = { BizId: string; Code: string; Message: string; RequestId: string };
|
||||
|
||||
// Aliyun has special escape rules.
|
||||
// https://help.aliyun.com/document_detail/29442.html
|
||||
const escaper = (string_: string) =>
|
||||
encodeURIComponent(string_)
|
||||
.replace(/\*/g, '%2A')
|
||||
.replace(/'/g, '%27')
|
||||
.replace(/!/g, '%21')
|
||||
.replace(/"/g, '%22')
|
||||
.replace(/\(/g, '%28')
|
||||
.replace(/\)/g, '%29')
|
||||
.replace(/\+/, '%2B');
|
||||
|
||||
export const getSignature = (
|
||||
parameters: Record<string, string>,
|
||||
secret: string,
|
||||
method: string
|
||||
) => {
|
||||
const canonicalizedQuery = Object.keys(parameters)
|
||||
.map((key) => {
|
||||
const value = parameters[key];
|
||||
|
||||
return value === undefined ? '' : `${escaper(key)}=${escaper(value)}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.slice()
|
||||
.sort()
|
||||
.join('&');
|
||||
|
||||
const stringToSign = `${method.toUpperCase()}&${escaper('/')}&${escaper(canonicalizedQuery)}`;
|
||||
|
||||
return createHmac('sha1', `${secret}&`).update(stringToSign).digest('base64');
|
||||
};
|
||||
|
||||
export interface PublicParameters {
|
||||
AccessKeyId: string;
|
||||
Format?: string; // 'json' or 'xml', default: 'json'
|
||||
RegionId?: string; // 'cn-hangzhou' | 'ap-southeast-1' | 'ap-southeast-2'
|
||||
Signature?: string;
|
||||
SignatureMethod?: string;
|
||||
SignatureNonce?: string;
|
||||
SignatureVersion?: string;
|
||||
Timestamp?: string;
|
||||
Version?: string;
|
||||
}
|
||||
|
||||
export const request = async <T>(
|
||||
url: string,
|
||||
parameters: PublicParameters & Record<string, string>,
|
||||
accessKeySecret: string
|
||||
) => {
|
||||
const finalParameters: Record<string, string> = {
|
||||
...parameters,
|
||||
SignatureNonce: String(Math.random()),
|
||||
Timestamp: new Date().toISOString(),
|
||||
};
|
||||
const signature = getSignature(finalParameters, accessKeySecret, 'POST');
|
||||
|
||||
const payload = new URLSearchParams();
|
||||
|
||||
for (const [key, value] of Object.entries(finalParameters)) {
|
||||
payload.append(key, value);
|
||||
}
|
||||
payload.append('Signature', signature);
|
||||
|
||||
return got.post<T>({
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
form: payload,
|
||||
});
|
||||
};
|
10
packages/connector-aliyun-sms/tsconfig.base.json
Normal file
10
packages/connector-aliyun-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-aliyun-sms/tsconfig.build.json
Normal file
5
packages/connector-aliyun-sms/tsconfig.build.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": "./tsconfig.base",
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
7
packages/connector-aliyun-sms/tsconfig.json
Normal file
7
packages/connector-aliyun-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-aliyun-sms/tsconfig.test.json
Normal file
6
packages/connector-aliyun-sms/tsconfig.test.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"isolatedModules": false
|
||||
}
|
||||
}
|
|
@ -29,7 +29,6 @@
|
|||
"@logto/schemas": "^0.1.0",
|
||||
"@silverhand/essentials": "^1.1.0",
|
||||
"got": "^11.8.2",
|
||||
"query-string": "^7.0.1",
|
||||
"zod": "^3.14.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -49,7 +49,7 @@ describe('facebook connector', () => {
|
|||
|
||||
const encodedRedirectUri = encodeURIComponent(redirectUri);
|
||||
expect(authorizationUri).toEqual(
|
||||
`${authorizationEndpoint}?client_id=${clientId}&redirect_uri=${encodedRedirectUri}&response_type=code&scope=email%2Cpublic_profile&state=${state}`
|
||||
`${authorizationEndpoint}?client_id=${clientId}&redirect_uri=${encodedRedirectUri}&response_type=code&state=${state}&scope=email%2Cpublic_profile`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,7 +16,6 @@ import {
|
|||
} from '@logto/connector-types';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import got, { RequestError as GotRequestError } from 'got';
|
||||
import { stringify } from 'query-string';
|
||||
|
||||
import {
|
||||
accessTokenEndpoint,
|
||||
|
@ -49,13 +48,15 @@ export class FacebookConnector implements SocialConnector {
|
|||
public getAuthorizationUri: GetAuthorizationUri = async (redirectUri, state) => {
|
||||
const config = await this.getConfig(this.metadata.id);
|
||||
|
||||
return `${authorizationEndpoint}?${stringify({
|
||||
const queryParameters = new URLSearchParams({
|
||||
client_id: config.clientId,
|
||||
redirect_uri: redirectUri,
|
||||
response_type: 'code',
|
||||
state,
|
||||
scope, // Only support fixed scope for v1.
|
||||
})}`;
|
||||
});
|
||||
|
||||
return `${authorizationEndpoint}?${queryParameters.toString()}`;
|
||||
};
|
||||
|
||||
public getAccessToken: GetAccessToken = async (code, redirectUri) => {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"private": true,
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"precommit": "lint-staged",
|
||||
|
|
|
@ -50,8 +50,6 @@ export type EmailMessageTypes = {
|
|||
|
||||
type SmsMessageTypes = EmailMessageTypes;
|
||||
|
||||
export type SendSmsResponse = { BizId: string; Code: string; Message: string; RequestId: string };
|
||||
|
||||
export type EmailSendMessageFunction<T = unknown> = (
|
||||
address: string,
|
||||
type: keyof EmailMessageTypes,
|
||||
|
|
49
pnpm-lock.yaml
generated
49
pnpm-lock.yaml
generated
|
@ -41,7 +41,6 @@ importers:
|
|||
lint-staged: ^11.1.1
|
||||
nock: ^13.2.2
|
||||
prettier: ^2.3.2
|
||||
query-string: ^7.0.1
|
||||
snakecase-keys: ^5.1.0
|
||||
supertest: ^6.2.2
|
||||
ts-jest: ^27.1.1
|
||||
|
@ -56,7 +55,6 @@ importers:
|
|||
dayjs: 1.10.7
|
||||
got: 11.8.3
|
||||
iconv-lite: 0.6.3
|
||||
query-string: 7.0.1
|
||||
snakecase-keys: 5.1.2
|
||||
zod: 3.14.3
|
||||
devDependencies:
|
||||
|
@ -122,6 +120,51 @@ importers:
|
|||
tsc-watch: 5.0.3_typescript@4.6.3
|
||||
typescript: 4.6.3
|
||||
|
||||
packages/connector-aliyun-sms:
|
||||
specifiers:
|
||||
'@jest/types': ^27.5.1
|
||||
'@logto/connector-types': ^0.1.0
|
||||
'@logto/jest-config': ^0.1.0
|
||||
'@logto/schemas': ^0.1.0
|
||||
'@logto/shared': ^0.1.0
|
||||
'@silverhand/eslint-config': ^0.10.2
|
||||
'@silverhand/essentials': ^1.1.0
|
||||
'@silverhand/ts-config': ^0.10.2
|
||||
'@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: ^11.1.1
|
||||
prettier: ^2.3.2
|
||||
ts-jest: ^27.1.1
|
||||
tsc-watch: ^4.4.0
|
||||
typescript: ^4.6.2
|
||||
zod: ^3.14.3
|
||||
dependencies:
|
||||
'@logto/connector-types': link:../connector-types
|
||||
'@logto/jest-config': link:../jest-config
|
||||
'@logto/schemas': link:../schemas
|
||||
'@logto/shared': link:../shared
|
||||
'@silverhand/essentials': 1.1.7
|
||||
got: 11.8.3
|
||||
zod: 3.14.3
|
||||
devDependencies:
|
||||
'@jest/types': 27.5.1
|
||||
'@silverhand/eslint-config': 0.10.2_bbe1a6794670f389df81805f22999709
|
||||
'@silverhand/ts-config': 0.10.2_typescript@4.6.3
|
||||
'@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: 11.2.6
|
||||
prettier: 2.5.1
|
||||
ts-jest: 27.1.1_9985e1834e803358b7be1e6ce5ca0eea
|
||||
tsc-watch: 4.5.0_typescript@4.6.3
|
||||
typescript: 4.6.3
|
||||
|
||||
packages/connector-facebook:
|
||||
specifiers:
|
||||
'@jest/types': ^27.5.1
|
||||
|
@ -141,7 +184,6 @@ importers:
|
|||
lint-staged: ^11.1.1
|
||||
nock: ^13.2.2
|
||||
prettier: ^2.3.2
|
||||
query-string: ^7.0.1
|
||||
ts-jest: ^27.1.1
|
||||
tsc-watch: ^5.0.0
|
||||
typescript: ^4.6.2
|
||||
|
@ -153,7 +195,6 @@ importers:
|
|||
'@logto/shared': link:../shared
|
||||
'@silverhand/essentials': 1.1.7
|
||||
got: 11.8.3
|
||||
query-string: 7.0.1
|
||||
zod: 3.14.3
|
||||
devDependencies:
|
||||
'@jest/types': 27.5.1
|
||||
|
|
Loading…
Add table
Reference in a new issue