diff --git a/packages/connector-alipay-native/src/index.test.ts b/packages/connector-alipay-native/src/index.test.ts index f95e7ec02..11f584307 100644 --- a/packages/connector-alipay-native/src/index.test.ts +++ b/packages/connector-alipay-native/src/index.test.ts @@ -1,12 +1,16 @@ -import { ConnectorError, ConnectorErrorCodes, GetConnectorConfig } from '@logto/connector-types'; +import { + ConnectorError, + ConnectorErrorCodes, + GetConnectorConfig, + ValidateConfig, +} from '@logto/connector-types'; import nock from 'nock'; import AlipayNativeConnector from '.'; import { alipayEndpoint } from './constant'; import { mockedAlipayNativeConfig, mockedAlipayNativeConfigWithValidPrivateKey } from './mock'; -import { AlipayNativeConfig } from './types'; -const getConnectorConfig = jest.fn() as GetConnectorConfig; +const getConnectorConfig = jest.fn() as GetConnectorConfig; const alipayNativeMethods = new AlipayNativeConnector(getConnectorConfig); @@ -15,18 +19,30 @@ describe('validateConfig', () => { jest.clearAllMocks(); }); + /** + * Assertion functions always need explicit annotations. + * See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014 + */ + it('should pass on valid config', async () => { - await expect( - alipayNativeMethods.validateConfig(mockedAlipayNativeConfig) - ).resolves.not.toThrow(); + const validator: ValidateConfig = alipayNativeMethods.validateConfig; + expect(() => { + validator(mockedAlipayNativeConfig); + }).not.toThrow(); }); - it('should throw on empty config', async () => { - await expect(alipayNativeMethods.validateConfig({})).rejects.toThrowError(); + it('should fail on empty config', async () => { + const validator: ValidateConfig = alipayNativeMethods.validateConfig; + expect(() => { + validator({}); + }).toThrow(); }); - it('should throw when missing required properties', async () => { - await expect(alipayNativeMethods.validateConfig({ appId: 'appId' })).rejects.toThrowError(); + it('should fail when missing required properties', async () => { + const validator: ValidateConfig = alipayNativeMethods.validateConfig; + expect(() => { + validator({ appId: 'appId' }); + }).toThrow(); }); }); diff --git a/packages/connector-alipay-native/src/index.ts b/packages/connector-alipay-native/src/index.ts index 8dd8602bc..baaadbd33 100644 --- a/packages/connector-alipay-native/src/index.ts +++ b/packages/connector-alipay-native/src/index.ts @@ -13,7 +13,6 @@ import { ConnectorMetadata, GetAuthorizationUri, GetUserInfo, - ValidateConfig, SocialConnector, GetConnectorConfig, } from '@logto/connector-types'; @@ -44,23 +43,27 @@ import { signingParameters } from './utils'; export type { AlipayNativeConfig } from './types'; -export default class AlipayNativeConnector implements SocialConnector { +export default class AlipayNativeConnector implements SocialConnector { public metadata: ConnectorMetadata = defaultMetadata; private readonly signingParameters = signingParameters; - constructor(public readonly getConfig: GetConnectorConfig) {} + constructor(public readonly getConfig: GetConnectorConfig) {} - public validateConfig: ValidateConfig = async (config: unknown) => { + public validateConfig(config: unknown): asserts config is AlipayNativeConfig { const result = alipayNativeConfigGuard.safeParse(config); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error); } - }; + } public getAuthorizationUri: GetAuthorizationUri = async ({ state }) => { - const { appId } = await this.getConfig(this.metadata.id); + const config = await this.getConfig(this.metadata.id); + + this.validateConfig(config); + + const { appId } = config; const queryParameters = new URLSearchParams({ app_id: appId, state }); @@ -108,6 +111,9 @@ export default class AlipayNativeConnector implements SocialConnector { public getUserInfo: GetUserInfo = async (data) => { const { auth_code } = await this.authorizationCallbackHandler(data); const config = await this.getConfig(this.metadata.id); + + this.validateConfig(config); + const { accessToken } = await this.getAccessToken(auth_code, config); assert( diff --git a/packages/connector-alipay/src/index.test.ts b/packages/connector-alipay/src/index.test.ts index 41e1d8886..d6fff86bc 100644 --- a/packages/connector-alipay/src/index.test.ts +++ b/packages/connector-alipay/src/index.test.ts @@ -1,12 +1,16 @@ -import { ConnectorError, ConnectorErrorCodes, GetConnectorConfig } from '@logto/connector-types'; +import { + ConnectorError, + ConnectorErrorCodes, + GetConnectorConfig, + ValidateConfig, +} from '@logto/connector-types'; import nock from 'nock'; import AlipayConnector from '.'; import { alipayEndpoint, authorizationEndpoint } from './constant'; import { mockedAlipayConfig, mockedAlipayConfigWithValidPrivateKey } from './mock'; -import { AlipayConfig } from './types'; -const getConnectorConfig = jest.fn() as GetConnectorConfig; +const getConnectorConfig = jest.fn() as GetConnectorConfig; const alipayMethods = new AlipayConnector(getConnectorConfig); @@ -15,22 +19,34 @@ describe('validateConfig', () => { jest.clearAllMocks(); }); + /** + * Assertion functions always need explicit annotations. + * See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014 + */ + it('should pass on valid config', async () => { - await expect( - alipayMethods.validateConfig({ + const validator: ValidateConfig = alipayMethods.validateConfig; + expect(() => { + validator({ appId: 'appId', privateKey: 'privateKey', signType: 'RSA', - }) - ).resolves.not.toThrow(); + }); + }).not.toThrow(); }); - it('should throw on empty config', async () => { - await expect(alipayMethods.validateConfig({})).rejects.toThrowError(); + it('should fail on empty config', async () => { + const validator: ValidateConfig = alipayMethods.validateConfig; + expect(() => { + validator({}); + }).toThrow(); }); - it('should throw when missing required properties', async () => { - await expect(alipayMethods.validateConfig({ appId: 'appId' })).rejects.toThrowError(); + it('should fail when missing required properties', async () => { + const validator: ValidateConfig = alipayMethods.validateConfig; + expect(() => { + validator({ appId: 'appId' }); + }).toThrow(); }); }); diff --git a/packages/connector-alipay/src/index.ts b/packages/connector-alipay/src/index.ts index 461dc784c..a6c09e27e 100644 --- a/packages/connector-alipay/src/index.ts +++ b/packages/connector-alipay/src/index.ts @@ -11,7 +11,6 @@ import { ConnectorMetadata, GetAuthorizationUri, GetUserInfo, - ValidateConfig, SocialConnector, GetConnectorConfig, } from '@logto/connector-types'; @@ -44,23 +43,27 @@ import { signingParameters } from './utils'; export type { AlipayConfig } from './types'; -export default class AlipayConnector implements SocialConnector { +export default class AlipayConnector implements SocialConnector { public metadata: ConnectorMetadata = defaultMetadata; private readonly signingParameters = signingParameters; - constructor(public readonly getConfig: GetConnectorConfig) {} + constructor(public readonly getConfig: GetConnectorConfig) {} - public validateConfig: ValidateConfig = async (config: unknown) => { + public validateConfig(config: unknown): asserts config is AlipayConfig { const result = alipayConfigGuard.safeParse(config); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error); } - }; + } public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => { - const { appId: app_id } = await this.getConfig(this.metadata.id); + const config = await this.getConfig(this.metadata.id); + + this.validateConfig(config); + + const { appId: app_id } = config; const redirect_uri = encodeURI(redirectUri); @@ -116,6 +119,9 @@ export default class AlipayConnector implements SocialConnector { public getUserInfo: GetUserInfo = async (data) => { const { auth_code } = await this.authorizationCallbackHandler(data); const config = await this.getConfig(this.metadata.id); + + this.validateConfig(config); + const { accessToken } = await this.getAccessToken(auth_code, config); assert( diff --git a/packages/connector-aliyun-dm/src/index.test.ts b/packages/connector-aliyun-dm/src/index.test.ts index c54d3f5c2..76a667066 100644 --- a/packages/connector-aliyun-dm/src/index.test.ts +++ b/packages/connector-aliyun-dm/src/index.test.ts @@ -1,11 +1,10 @@ -import { GetConnectorConfig } from '@logto/connector-types'; +import { GetConnectorConfig, ValidateConfig } from '@logto/connector-types'; import AliyunDmConnector from '.'; import { mockedConfig } from './mock'; import { singleSendMail } from './single-send-mail'; -import { AliyunDmConfig } from './types'; -const getConnectorConfig = jest.fn() as GetConnectorConfig; +const getConnectorConfig = jest.fn() as GetConnectorConfig; const aliyunDmMethods = new AliyunDmConnector(getConnectorConfig); @@ -29,19 +28,28 @@ describe('validateConfig()', () => { jest.clearAllMocks(); }); + /** + * Assertion functions always need explicit annotations. + * See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014 + */ + it('should pass on valid config', async () => { - await expect( - aliyunDmMethods.validateConfig({ + const validator: ValidateConfig = aliyunDmMethods.validateConfig; + expect(() => { + validator({ accessKeyId: 'accessKeyId', accessKeySecret: 'accessKeySecret', accountName: 'accountName', templates: [], - }) - ).resolves.not.toThrow(); + }); + }).not.toThrow(); }); - it('throws if config is invalid', async () => { - await expect(aliyunDmMethods.validateConfig({})).rejects.toThrow(); + it('should fail if config is invalid', async () => { + const validator: ValidateConfig = aliyunDmMethods.validateConfig; + expect(() => { + validator({}); + }).toThrow(); }); }); diff --git a/packages/connector-aliyun-dm/src/index.ts b/packages/connector-aliyun-dm/src/index.ts index 826a2fc0a..a5413a717 100644 --- a/packages/connector-aliyun-dm/src/index.ts +++ b/packages/connector-aliyun-dm/src/index.ts @@ -3,7 +3,6 @@ import { ConnectorErrorCodes, ConnectorMetadata, EmailSendMessageFunction, - ValidateConfig, EmailConnector, GetConnectorConfig, } from '@logto/connector-types'; @@ -19,24 +18,24 @@ import { sendMailErrorResponseGuard, } from './types'; -export default class AliyunDmConnector implements EmailConnector { +export default class AliyunDmConnector implements EmailConnector { public metadata: ConnectorMetadata = defaultMetadata; + constructor(public readonly getConfig: GetConnectorConfig) {} - constructor(public readonly getConfig: GetConnectorConfig) {} - - public validateConfig: ValidateConfig = async (config: unknown) => { + public validateConfig(config: unknown): asserts config is AliyunDmConfig { const result = aliyunDmConfigGuard.safeParse(config); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error); } - }; + } // eslint-disable-next-line complexity public sendMessage: EmailSendMessageFunction = async (address, type, data, config) => { - const emailConfig = - (config as AliyunDmConfig | undefined) ?? (await this.getConfig(this.metadata.id)); - await this.validateConfig(emailConfig); + const emailConfig = config ?? (await this.getConfig(this.metadata.id)); + + this.validateConfig(emailConfig); + const { accessKeyId, accessKeySecret, accountName, fromAlias, templates } = emailConfig; const template = templates.find((template) => template.usageType === type); diff --git a/packages/connector-aliyun-sms/src/index.test.ts b/packages/connector-aliyun-sms/src/index.test.ts index 65db6ad21..31840ac73 100644 --- a/packages/connector-aliyun-sms/src/index.test.ts +++ b/packages/connector-aliyun-sms/src/index.test.ts @@ -1,11 +1,10 @@ -import { GetConnectorConfig } from '@logto/connector-types'; +import { GetConnectorConfig, ValidateConfig } from '@logto/connector-types'; import AliyunSmsConnector from '.'; import { mockedConnectorConfig, mockedValidConnectorConfig, phoneTest, codeTest } from './mock'; import { sendSms } from './single-send-text'; -import { AliyunSmsConfig } from './types'; -const getConnectorConfig = jest.fn() as GetConnectorConfig; +const getConnectorConfig = jest.fn() as GetConnectorConfig; const aliyunSmsMethods = new AliyunSmsConnector(getConnectorConfig); @@ -25,14 +24,23 @@ describe('validateConfig()', () => { jest.clearAllMocks(); }); + /** + * Assertion functions always need explicit annotations. + * See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014 + */ + it('should pass on valid config', async () => { - await expect( - aliyunSmsMethods.validateConfig(mockedValidConnectorConfig) - ).resolves.not.toThrow(); + const validator: ValidateConfig = aliyunSmsMethods.validateConfig; + expect(() => { + validator(mockedValidConnectorConfig); + }).not.toThrow(); }); - it('throws if config is invalid', async () => { - await expect(aliyunSmsMethods.validateConfig({})).rejects.toThrow(); + it('should fail if config is invalid', async () => { + const validator: ValidateConfig = aliyunSmsMethods.validateConfig; + expect(() => { + validator({}); + }).toThrow(); }); }); diff --git a/packages/connector-aliyun-sms/src/index.ts b/packages/connector-aliyun-sms/src/index.ts index e341ae0ee..020f16aed 100644 --- a/packages/connector-aliyun-sms/src/index.ts +++ b/packages/connector-aliyun-sms/src/index.ts @@ -3,7 +3,6 @@ import { ConnectorErrorCodes, ConnectorMetadata, SmsSendMessageFunction, - ValidateConfig, SmsConnector, GetConnectorConfig, } from '@logto/connector-types'; @@ -14,23 +13,23 @@ import { defaultMetadata } from './constant'; import { sendSms } from './single-send-text'; import { aliyunSmsConfigGuard, AliyunSmsConfig, sendSmsResponseGuard } from './types'; -export default class AliyunSmsConnector implements SmsConnector { +export default class AliyunSmsConnector implements SmsConnector { public metadata: ConnectorMetadata = defaultMetadata; + constructor(public readonly getConfig: GetConnectorConfig) {} - constructor(public readonly getConfig: GetConnectorConfig) {} - - public validateConfig: ValidateConfig = async (config: unknown) => { + public validateConfig(config: unknown): asserts config is AliyunSmsConfig { const result = aliyunSmsConfigGuard.safeParse(config); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error); } - }; + } public sendMessage: SmsSendMessageFunction = async (phone, type, { code }, config) => { - const smsConfig = - (config as AliyunSmsConfig | undefined) ?? (await this.getConfig(this.metadata.id)); - await this.validateConfig(smsConfig); + const smsConfig = config ?? (await this.getConfig(this.metadata.id)); + + this.validateConfig(smsConfig); + const { accessKeyId, accessKeySecret, signName, templates } = smsConfig; const template = templates.find(({ usageType }) => usageType === type); diff --git a/packages/connector-apple/src/index.test.ts b/packages/connector-apple/src/index.test.ts index a27862ad3..190bf903e 100644 --- a/packages/connector-apple/src/index.test.ts +++ b/packages/connector-apple/src/index.test.ts @@ -1,12 +1,16 @@ -import { ConnectorError, ConnectorErrorCodes, GetConnectorConfig } from '@logto/connector-types'; +import { + ConnectorError, + ConnectorErrorCodes, + GetConnectorConfig, + ValidateConfig, +} from '@logto/connector-types'; import { jwtVerify } from 'jose'; import AppleConnector from '.'; import { authorizationEndpoint } from './constant'; import { mockedConfig } from './mock'; -import { AppleConfig } from './types'; -const getConnectorConfig = jest.fn() as GetConnectorConfig; +const getConnectorConfig = jest.fn() as GetConnectorConfig; const appleMethods = new AppleConnector(getConnectorConfig); @@ -40,12 +44,23 @@ describe('validateConfig', () => { jest.clearAllMocks(); }); - it('should pass on valid config', async () => { - await expect(appleMethods.validateConfig({ clientId: 'clientId' })).resolves.not.toThrow(); + /** + * Assertion functions always need explicit annotations. + * See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014 + */ + + it('should be true on valid config', async () => { + const validator: ValidateConfig = appleMethods.validateConfig; + expect(() => { + validator({ clientId: 'clientId' }); + }).not.toThrow(); }); - it('should throw on empty config', async () => { - await expect(appleMethods.validateConfig({})).rejects.toThrowError(); + it('should be false on empty config', async () => { + const validator: ValidateConfig = appleMethods.validateConfig; + expect(() => { + validator({}); + }).toThrow(); }); }); diff --git a/packages/connector-apple/src/index.ts b/packages/connector-apple/src/index.ts index d0c7396f1..7a33c565c 100644 --- a/packages/connector-apple/src/index.ts +++ b/packages/connector-apple/src/index.ts @@ -1,7 +1,6 @@ import { ConnectorMetadata, GetAuthorizationUri, - ValidateConfig, GetUserInfo, ConnectorError, ConnectorErrorCodes, @@ -14,22 +13,24 @@ import { scope, defaultMetadata, jwksUri, issuer, authorizationEndpoint } from ' import { appleConfigGuard, AppleConfig, dataGuard } from './types'; // TO-DO: support nonce validation -export default class AppleConnector implements SocialConnector { +export default class AppleConnector implements SocialConnector { public metadata: ConnectorMetadata = defaultMetadata; - constructor(public readonly getConfig: GetConnectorConfig) {} + constructor(public readonly getConfig: GetConnectorConfig) {} - public validateConfig: ValidateConfig = async (config: unknown) => { + public validateConfig(config: unknown): asserts config is AppleConfig { const result = appleConfigGuard.safeParse(config); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error); } - }; + } public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => { const config = await this.getConfig(this.metadata.id); + this.validateConfig(config); + const queryParameters = new URLSearchParams({ client_id: config.clientId, redirect_uri: redirectUri, @@ -50,7 +51,11 @@ export default class AppleConnector implements SocialConnector { throw new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid); } - const { clientId } = await this.getConfig(this.metadata.id); + const config = await this.getConfig(this.metadata.id); + + this.validateConfig(config); + + const { clientId } = config; try { const { payload } = await jwtVerify(idToken, createRemoteJWKSet(new URL(jwksUri)), { diff --git a/packages/connector-facebook/src/index.test.ts b/packages/connector-facebook/src/index.test.ts index 08f93b2cd..b2cd7f503 100644 --- a/packages/connector-facebook/src/index.test.ts +++ b/packages/connector-facebook/src/index.test.ts @@ -1,12 +1,16 @@ -import { ConnectorError, ConnectorErrorCodes, GetConnectorConfig } from '@logto/connector-types'; +import { + ConnectorError, + ConnectorErrorCodes, + GetConnectorConfig, + ValidateConfig, +} from '@logto/connector-types'; import nock from 'nock'; import FacebookConnector from '.'; import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './constant'; import { clientId, clientSecret, code, dummyRedirectUri, fields, mockedConfig } from './mock'; -import { FacebookConfig } from './types'; -const getConnectorConfig = jest.fn() as GetConnectorConfig; +const getConnectorConfig = jest.fn() as GetConnectorConfig; const facebookMethods = new FacebookConnector(getConnectorConfig); @@ -20,16 +24,29 @@ describe('facebook connector', () => { jest.clearAllMocks(); }); + /** + * Assertion functions always need explicit annotations. + * See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014 + */ + it('should pass on valid config', async () => { - await expect( - facebookMethods.validateConfig({ clientId, clientSecret }) - ).resolves.not.toThrow(); + const validator: ValidateConfig = facebookMethods.validateConfig; + expect(() => { + validator({ clientId, clientSecret }); + }).not.toThrow(); }); - it('should throw on invalid config', async () => { - await expect(facebookMethods.validateConfig({})).rejects.toThrow(); - await expect(facebookMethods.validateConfig({ clientId })).rejects.toThrow(); - await expect(facebookMethods.validateConfig({ clientSecret })).rejects.toThrow(); + it('should fail on invalid config', async () => { + const validator: ValidateConfig = facebookMethods.validateConfig; + expect(() => { + validator({}); + }).toThrow(); + expect(() => { + validator({ clientId }); + }).toThrow(); + expect(() => { + validator({ clientSecret }); + }).toThrow(); }); }); diff --git a/packages/connector-facebook/src/index.ts b/packages/connector-facebook/src/index.ts index dd4fbcf55..fcd16b8e4 100644 --- a/packages/connector-facebook/src/index.ts +++ b/packages/connector-facebook/src/index.ts @@ -9,7 +9,6 @@ import { ConnectorMetadata, GetAuthorizationUri, GetUserInfo, - ValidateConfig, SocialConnector, GetConnectorConfig, codeWithRedirectDataGuard, @@ -33,22 +32,24 @@ import { userInfoResponseGuard, } from './types'; -export default class FacebookConnector implements SocialConnector { +export default class FacebookConnector implements SocialConnector { public metadata: ConnectorMetadata = defaultMetadata; - constructor(public readonly getConfig: GetConnectorConfig) {} + constructor(public readonly getConfig: GetConnectorConfig) {} - public validateConfig: ValidateConfig = async (config: unknown) => { + public validateConfig(config: unknown): asserts config is FacebookConfig { const result = facebookConfigGuard.safeParse(config); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error); } - }; + } public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => { const config = await this.getConfig(this.metadata.id); + this.validateConfig(config); + const queryParameters = new URLSearchParams({ client_id: config.clientId, redirect_uri: redirectUri, @@ -61,9 +62,11 @@ export default class FacebookConnector implements SocialConnector { }; public getAccessToken = async (code: string, redirectUri: string) => { - const { clientId: client_id, clientSecret: client_secret } = await this.getConfig( - this.metadata.id - ); + const config = await this.getConfig(this.metadata.id); + + this.validateConfig(config); + + const { clientId: client_id, clientSecret: client_secret } = config; const httpResponse = await got.get(accessTokenEndpoint, { searchParams: { diff --git a/packages/connector-github/src/index.test.ts b/packages/connector-github/src/index.test.ts index e0fa41803..31939be71 100644 --- a/packages/connector-github/src/index.test.ts +++ b/packages/connector-github/src/index.test.ts @@ -1,13 +1,17 @@ -import { ConnectorError, ConnectorErrorCodes, GetConnectorConfig } from '@logto/connector-types'; +import { + ConnectorError, + ConnectorErrorCodes, + GetConnectorConfig, + ValidateConfig, +} from '@logto/connector-types'; import nock from 'nock'; import * as qs from 'query-string'; import GithubConnector from '.'; import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './constant'; import { mockedConfig } from './mock'; -import { GithubConfig } from './types'; -const getConnectorConfig = jest.fn() as GetConnectorConfig; +const getConnectorConfig = jest.fn() as GetConnectorConfig; const githubMethods = new GithubConnector(getConnectorConfig); @@ -67,18 +71,30 @@ describe('validateConfig', () => { jest.clearAllMocks(); }); + /** + * Assertion functions always need explicit annotations. + * See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014 + */ + it('should pass on valid config', async () => { - await expect( - githubMethods.validateConfig({ clientId: 'clientId', clientSecret: 'clientSecret' }) - ).resolves.not.toThrow(); + const validator: ValidateConfig = githubMethods.validateConfig; + expect(() => { + validator({ clientId: 'clientId', clientSecret: 'clientSecret' }); + }).not.toThrow(); }); - it('should throw on empty config', async () => { - await expect(githubMethods.validateConfig({})).rejects.toThrowError(); + it('should fail on empty config', async () => { + const validator: ValidateConfig = githubMethods.validateConfig; + expect(() => { + validator({}); + }).toThrow(); }); - it('should throw when missing clientSecret', async () => { - await expect(githubMethods.validateConfig({ clientId: 'clientId' })).rejects.toThrowError(); + it('should fail when missing clientSecret', async () => { + const validator: ValidateConfig = githubMethods.validateConfig; + expect(() => { + validator({ clientId: 'clientId' }); + }).toThrow(); }); }); diff --git a/packages/connector-github/src/index.ts b/packages/connector-github/src/index.ts index caf97fb28..6cb14a36b 100644 --- a/packages/connector-github/src/index.ts +++ b/packages/connector-github/src/index.ts @@ -1,7 +1,6 @@ import { ConnectorMetadata, GetAuthorizationUri, - ValidateConfig, GetUserInfo, ConnectorError, ConnectorErrorCodes, @@ -29,22 +28,24 @@ import { userInfoResponseGuard, } from './types'; -export default class GithubConnector implements SocialConnector { +export default class GithubConnector implements SocialConnector { public metadata: ConnectorMetadata = defaultMetadata; - constructor(public readonly getConfig: GetConnectorConfig) {} + constructor(public readonly getConfig: GetConnectorConfig) {} - public validateConfig: ValidateConfig = async (config: unknown) => { + public validateConfig(config: unknown): asserts config is GithubConfig { const result = githubConfigGuard.safeParse(config); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error); } - }; + } public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => { const config = await this.getConfig(this.metadata.id); + this.validateConfig(config); + const queryParameters = new URLSearchParams({ client_id: config.clientId, redirect_uri: redirectUri, @@ -56,9 +57,11 @@ export default class GithubConnector implements SocialConnector { }; public getAccessToken = async (code: string) => { - const { clientId: client_id, clientSecret: client_secret } = await this.getConfig( - this.metadata.id - ); + const config = await this.getConfig(this.metadata.id); + + this.validateConfig(config); + + const { clientId: client_id, clientSecret: client_secret } = config; const httpResponse = await got.post({ url: accessTokenEndpoint, diff --git a/packages/connector-google/src/index.test.ts b/packages/connector-google/src/index.test.ts index be53d3d2f..4766725d6 100644 --- a/packages/connector-google/src/index.test.ts +++ b/packages/connector-google/src/index.test.ts @@ -1,12 +1,16 @@ -import { ConnectorError, ConnectorErrorCodes, GetConnectorConfig } from '@logto/connector-types'; +import { + ConnectorError, + ConnectorErrorCodes, + GetConnectorConfig, + ValidateConfig, +} from '@logto/connector-types'; import nock from 'nock'; import GoogleConnector from '.'; import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './constant'; import { mockedConfig } from './mock'; -import { GoogleConfig } from './types'; -const getConnectorConfig = jest.fn() as GetConnectorConfig; +const getConnectorConfig = jest.fn() as GetConnectorConfig; const googleMethods = new GoogleConnector(getConnectorConfig); @@ -20,18 +24,29 @@ describe('google connector', () => { jest.clearAllMocks(); }); + /** + * Assertion functions always need explicit annotations. + * See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014 + */ + it('should pass on valid config', async () => { - await expect( - googleMethods.validateConfig({ clientId: 'clientId', clientSecret: 'clientSecret' }) - ).resolves.not.toThrow(); + const validator: ValidateConfig = googleMethods.validateConfig; + expect(() => { + validator({ clientId: 'clientId', clientSecret: 'clientSecret' }); + }).not.toThrow(); }); - it('should throw on invalid config', async () => { - await expect(googleMethods.validateConfig({})).rejects.toThrow(); - await expect(googleMethods.validateConfig({ clientId: 'clientId' })).rejects.toThrow(); - await expect( - googleMethods.validateConfig({ clientSecret: 'clientSecret' }) - ).rejects.toThrow(); + it('should fail on invalid config', async () => { + const validator: ValidateConfig = googleMethods.validateConfig; + expect(() => { + validator({}); + }).toThrow(); + expect(() => { + validator({ clientId: 'clientId' }); + }).toThrow(); + expect(() => { + validator({ clientSecret: 'clientSecret' }); + }).toThrow(); }); }); diff --git a/packages/connector-google/src/index.ts b/packages/connector-google/src/index.ts index 05f27ea82..817569fe6 100644 --- a/packages/connector-google/src/index.ts +++ b/packages/connector-google/src/index.ts @@ -8,7 +8,6 @@ import { GetAuthorizationUri, GetUserInfo, ConnectorMetadata, - ValidateConfig, SocialConnector, GetConnectorConfig, codeWithRedirectDataGuard, @@ -31,22 +30,24 @@ import { userInfoResponseGuard, } from './types'; -export default class GoogleConnector implements SocialConnector { +export default class GoogleConnector implements SocialConnector { public metadata: ConnectorMetadata = defaultMetadata; - constructor(public readonly getConfig: GetConnectorConfig) {} + constructor(public readonly getConfig: GetConnectorConfig) {} - public validateConfig: ValidateConfig = async (config: unknown) => { + public validateConfig(config: unknown): asserts config is GoogleConfig { const result = googleConfigGuard.safeParse(config); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error); } - }; + } public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => { const config = await this.getConfig(this.metadata.id); + this.validateConfig(config); + const queryParameters = new URLSearchParams({ client_id: config.clientId, redirect_uri: redirectUri, @@ -59,7 +60,11 @@ export default class GoogleConnector implements SocialConnector { }; public getAccessToken = async (code: string, redirectUri: string) => { - const { clientId, clientSecret } = await this.getConfig(this.metadata.id); + const config = await this.getConfig(this.metadata.id); + + this.validateConfig(config); + + const { clientId, clientSecret } = config; // Noteļ¼šNeed to decodeURIComponent on code // https://stackoverflow.com/questions/51058256/google-api-node-js-invalid-grant-malformed-auth-code diff --git a/packages/connector-sendgrid-mail/src/index.test.ts b/packages/connector-sendgrid-mail/src/index.test.ts index 354458130..2154b2c4e 100644 --- a/packages/connector-sendgrid-mail/src/index.test.ts +++ b/packages/connector-sendgrid-mail/src/index.test.ts @@ -1,10 +1,10 @@ -import { GetConnectorConfig } from '@logto/connector-types'; +import { GetConnectorConfig, ValidateConfig } from '@logto/connector-types'; import SendGridMailConnector from '.'; import { mockedConfig } from './mock'; -import { ContextType, SendGridMailConfig } from './types'; +import { ContextType } from './types'; -const getConnectorConfig = jest.fn() as GetConnectorConfig; +const getConnectorConfig = jest.fn() as GetConnectorConfig; const sendGridMailMethods = new SendGridMailConnector(getConnectorConfig); @@ -19,9 +19,15 @@ describe('validateConfig()', () => { jest.clearAllMocks(); }); + /** + * Assertion functions always need explicit annotations. + * See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014 + */ + it('should pass on valid config', async () => { - await expect( - sendGridMailMethods.validateConfig({ + const validator: ValidateConfig = sendGridMailMethods.validateConfig; + expect(() => { + validator({ apiKey: 'apiKey', fromEmail: 'noreply@logto.test.io', fromName: 'Logto Test', @@ -33,11 +39,14 @@ describe('validateConfig()', () => { content: 'This is for testing purposes only. Your passcode is {{code}}.', }, ], - }) - ).resolves.not.toThrow(); + }); + }).not.toThrow(); }); - it('throws if config is invalid', async () => { - await expect(sendGridMailMethods.validateConfig({})).rejects.toThrow(); + it('should be false if config is invalid', async () => { + const validator: ValidateConfig = sendGridMailMethods.validateConfig; + expect(() => { + validator({}); + }).toThrow(); }); }); diff --git a/packages/connector-sendgrid-mail/src/index.ts b/packages/connector-sendgrid-mail/src/index.ts index 1dcc12f33..c2b5a55ee 100644 --- a/packages/connector-sendgrid-mail/src/index.ts +++ b/packages/connector-sendgrid-mail/src/index.ts @@ -3,7 +3,6 @@ import { ConnectorErrorCodes, ConnectorMetadata, EmailSendMessageFunction, - ValidateConfig, EmailConnector, GetConnectorConfig, } from '@logto/connector-types'; @@ -20,22 +19,23 @@ import { PublicParameters, } from './types'; -export default class SendGridMailConnector implements EmailConnector { +export default class SendGridMailConnector implements EmailConnector { public metadata: ConnectorMetadata = defaultMetadata; + constructor(public readonly getConfig: GetConnectorConfig) {} - constructor(public readonly getConfig: GetConnectorConfig) {} - - public validateConfig: ValidateConfig = async (config: unknown) => { + public validateConfig(config: unknown): asserts config is SendGridMailConfig { const result = sendGridMailConfigGuard.safeParse(config); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error); } - }; + } public sendMessage: EmailSendMessageFunction = async (address, type, data) => { const config = await this.getConfig(this.metadata.id); - await this.validateConfig(config); + + this.validateConfig(config); + const { apiKey, fromEmail, fromName, templates } = config; const template = templates.find((template) => template.usageType === type); diff --git a/packages/connector-smtp/src/index.test.ts b/packages/connector-smtp/src/index.test.ts index 03de6dd80..24d504d1c 100644 --- a/packages/connector-smtp/src/index.test.ts +++ b/packages/connector-smtp/src/index.test.ts @@ -1,9 +1,8 @@ -import { GetConnectorConfig } from '@logto/connector-types'; +import { GetConnectorConfig, ValidateConfig } from '@logto/connector-types'; import SmtpConnector from '.'; -import { SmtpConfig } from './types'; -const getConnectorConfig = jest.fn() as GetConnectorConfig; +const getConnectorConfig = jest.fn() as GetConnectorConfig; const smtpMethods = new SmtpConnector(getConnectorConfig); @@ -12,9 +11,15 @@ describe('validateConfig()', () => { jest.clearAllMocks(); }); + /** + * Assertion functions always need explicit annotations. + * See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014 + */ + it('should pass on valid config', async () => { - await expect( - smtpMethods.validateConfig({ + const validator: ValidateConfig = smtpMethods.validateConfig; + expect(() => { + validator({ host: 'smtp.testing.com', port: 80, password: 'password', @@ -46,11 +51,14 @@ describe('validateConfig()', () => { usageType: 'ForgotPassword', }, ], - }) - ).resolves.not.toThrow(); + }); + }).not.toThrow(); }); - it('throws if config is invalid', async () => { - await expect(smtpMethods.validateConfig({})).rejects.toThrow(); + it('should be false if config is invalid', async () => { + const validator: ValidateConfig = smtpMethods.validateConfig; + expect(() => { + validator({}); + }).toThrow(); }); }); diff --git a/packages/connector-smtp/src/index.ts b/packages/connector-smtp/src/index.ts index c477275eb..6d7c9875a 100644 --- a/packages/connector-smtp/src/index.ts +++ b/packages/connector-smtp/src/index.ts @@ -3,7 +3,6 @@ import { ConnectorErrorCodes, ConnectorMetadata, EmailSendMessageFunction, - ValidateConfig, EmailConnector, GetConnectorConfig, } from '@logto/connector-types'; @@ -14,22 +13,24 @@ import SMTPTransport from 'nodemailer/lib/smtp-transport'; import { defaultMetadata } from './constant'; import { ContextType, smtpConfigGuard, SmtpConfig } from './types'; -export default class SmtpConnector implements EmailConnector { +export default class SmtpConnector implements EmailConnector { public metadata: ConnectorMetadata = defaultMetadata; - constructor(public readonly getConfig: GetConnectorConfig) {} + constructor(public readonly getConfig: GetConnectorConfig) {} - public validateConfig: ValidateConfig = async (config: unknown) => { + public validateConfig(config: unknown): asserts config is SmtpConfig { const result = smtpConfigGuard.safeParse(config); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error); } - }; + } public sendMessage: EmailSendMessageFunction = async (address, type, data) => { const config = await this.getConfig(this.metadata.id); - await this.validateConfig(config); + + this.validateConfig(config); + const { host, port, username, password, fromEmail, replyTo, templates } = config; const template = templates.find((template) => template.usageType === type); diff --git a/packages/connector-twilio-sms/src/index.test.ts b/packages/connector-twilio-sms/src/index.test.ts index d6afc7c30..703cafd01 100644 --- a/packages/connector-twilio-sms/src/index.test.ts +++ b/packages/connector-twilio-sms/src/index.test.ts @@ -1,10 +1,9 @@ -import { GetConnectorConfig } from '@logto/connector-types'; +import { GetConnectorConfig, ValidateConfig } from '@logto/connector-types'; import TwilioSmsConnector from '.'; import { mockedConfig } from './mock'; -import { TwilioSmsConfig } from './types'; -const getConnectorConfig = jest.fn() as GetConnectorConfig; +const getConnectorConfig = jest.fn() as GetConnectorConfig; const twilioSmsMethods = new TwilioSmsConnector(getConnectorConfig); @@ -13,11 +12,22 @@ describe('validateConfig()', () => { jest.clearAllMocks(); }); + /** + * Assertion functions always need explicit annotations. + * See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014 + */ + it('should pass on valid config', async () => { - await expect(twilioSmsMethods.validateConfig(mockedConfig)).resolves.not.toThrow(); + const validator: ValidateConfig = twilioSmsMethods.validateConfig; + expect(() => { + validator(mockedConfig); + }).not.toThrow(); }); it('throws if config is invalid', async () => { - await expect(twilioSmsMethods.validateConfig({})).rejects.toThrow(); + const validator: ValidateConfig = twilioSmsMethods.validateConfig; + expect(() => { + validator({}); + }).toThrow(); }); }); diff --git a/packages/connector-twilio-sms/src/index.ts b/packages/connector-twilio-sms/src/index.ts index 42c831c70..a1006cc29 100644 --- a/packages/connector-twilio-sms/src/index.ts +++ b/packages/connector-twilio-sms/src/index.ts @@ -3,7 +3,6 @@ import { ConnectorErrorCodes, ConnectorMetadata, EmailSendMessageFunction, - ValidateConfig, SmsConnector, GetConnectorConfig, } from '@logto/connector-types'; @@ -13,22 +12,24 @@ import got, { HTTPError } from 'got'; import { defaultMetadata, endpoint } from './constant'; import { twilioSmsConfigGuard, TwilioSmsConfig, PublicParameters } from './types'; -export default class TwilioSmsConnector implements SmsConnector { +export default class TwilioSmsConnector implements SmsConnector { public metadata: ConnectorMetadata = defaultMetadata; - constructor(public readonly getConfig: GetConnectorConfig) {} + constructor(public readonly getConfig: GetConnectorConfig) {} - public validateConfig: ValidateConfig = async (config: unknown) => { + public validateConfig(config: unknown): asserts config is TwilioSmsConfig { const result = twilioSmsConfigGuard.safeParse(config); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error); } - }; + } public sendMessage: EmailSendMessageFunction = async (address, type, data) => { const config = await this.getConfig(this.metadata.id); - await this.validateConfig(config); + + this.validateConfig(config); + const { accountSID, authToken, fromMessagingServiceSID, templates } = config; const template = templates.find((template) => template.usageType === type); diff --git a/packages/connector-types/src/index.ts b/packages/connector-types/src/index.ts index c6a723dfb..10d9f6416 100644 --- a/packages/connector-types/src/index.ts +++ b/packages/connector-types/src/index.ts @@ -80,26 +80,26 @@ export type SmsSendMessageFunction = ( config?: Record ) => Promise; -export interface BaseConnector { +export interface BaseConnector { metadata: ConnectorMetadata; - validateConfig: ValidateConfig; getConfig: GetConnectorConfig; + validateConfig: ValidateConfig; } -export interface SmsConnector extends BaseConnector { +export interface SmsConnector extends BaseConnector { sendMessage: SmsSendMessageFunction; } -export interface EmailConnector extends BaseConnector { +export interface EmailConnector extends BaseConnector { sendMessage: EmailSendMessageFunction; } -export interface SocialConnector extends BaseConnector { +export interface SocialConnector extends BaseConnector { getAuthorizationUri: GetAuthorizationUri; getUserInfo: GetUserInfo; } -export type ValidateConfig> = (config: T) => Promise; +export type ValidateConfig = (config: unknown) => asserts config is T; export type GetAuthorizationUri = (payload: { state: string; @@ -110,7 +110,7 @@ export type GetUserInfo = ( data: unknown ) => Promise<{ id: string } & Record>; -export type GetConnectorConfig> = (id: string) => Promise; +export type GetConnectorConfig = (id: string) => Promise; export const codeDataGuard = z.object({ code: z.string(), diff --git a/packages/connector-wechat-native/src/index.test.ts b/packages/connector-wechat-native/src/index.test.ts index 60dd09c3d..bffa3286e 100644 --- a/packages/connector-wechat-native/src/index.test.ts +++ b/packages/connector-wechat-native/src/index.test.ts @@ -1,12 +1,16 @@ -import { ConnectorError, ConnectorErrorCodes, GetConnectorConfig } from '@logto/connector-types'; +import { + ConnectorError, + ConnectorErrorCodes, + GetConnectorConfig, + ValidateConfig, +} from '@logto/connector-types'; import nock from 'nock'; import WechatNativeConnector from '.'; import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './constant'; import { mockedConfig } from './mock'; -import { WechatNativeConfig } from './types'; -const getConnectorConfig = jest.fn() as GetConnectorConfig; +const getConnectorConfig = jest.fn() as GetConnectorConfig; const wechatNativeMethods = new WechatNativeConnector(getConnectorConfig); @@ -92,16 +96,30 @@ describe('getAccessToken', () => { }); describe('validateConfig', () => { + /** + * Assertion functions always need explicit annotations. + * See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014 + */ + it('should pass on valid config', async () => { - await expect( - wechatNativeMethods.validateConfig({ appId: 'appId', appSecret: 'appSecret' }) - ).resolves.not.toThrow(); + const validator: ValidateConfig = wechatNativeMethods.validateConfig; + expect(() => { + validator({ appId: 'appId', appSecret: 'appSecret' }); + }).not.toThrow(); }); - it('should throw on empty config', async () => { - await expect(wechatNativeMethods.validateConfig({})).rejects.toThrowError(); + + it('should fail on empty config', async () => { + const validator: ValidateConfig = wechatNativeMethods.validateConfig; + expect(() => { + validator({}); + }).toThrow(); }); - it('should throw when missing appSecret', async () => { - await expect(wechatNativeMethods.validateConfig({ appId: 'appId' })).rejects.toThrowError(); + + it('should fail when missing appSecret', async () => { + const validator: ValidateConfig = wechatNativeMethods.validateConfig; + expect(() => { + validator({ appId: 'appId' }); + }).toThrow(); }); }); diff --git a/packages/connector-wechat-native/src/index.ts b/packages/connector-wechat-native/src/index.ts index 8e5204ab9..bf50213f8 100644 --- a/packages/connector-wechat-native/src/index.ts +++ b/packages/connector-wechat-native/src/index.ts @@ -6,7 +6,6 @@ import { ConnectorMetadata, GetAuthorizationUri, - ValidateConfig, GetUserInfo, ConnectorError, ConnectorErrorCodes, @@ -35,21 +34,25 @@ import { WechatNativeConfig, } from './types'; -export default class WechatNativeConnector implements SocialConnector { +export default class WechatNativeConnector implements SocialConnector { public metadata: ConnectorMetadata = defaultMetadata; - constructor(public readonly getConfig: GetConnectorConfig) {} + constructor(public readonly getConfig: GetConnectorConfig) {} - public validateConfig: ValidateConfig = async (config: unknown) => { + public validateConfig(config: unknown): asserts config is WechatNativeConfig { const result = wechatNativeConfigGuard.safeParse(config); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error); } - }; + } public getAuthorizationUri: GetAuthorizationUri = async ({ state }) => { - const { appId, universalLinks } = await this.getConfig(this.metadata.id); + const config = await this.getConfig(this.metadata.id); + + this.validateConfig(config); + + const { appId, universalLinks } = config; const queryParameters = new URLSearchParams({ app_id: appId, @@ -65,7 +68,11 @@ export default class WechatNativeConnector implements SocialConnector { public getAccessToken = async ( code: string ): Promise<{ accessToken: string; openid: string }> => { - const { appId: appid, appSecret: secret } = await this.getConfig(this.metadata.id); + const config = await this.getConfig(this.metadata.id); + + this.validateConfig(config); + + const { appId: appid, appSecret: secret } = config; const httpResponse = await got.get(accessTokenEndpoint, { searchParams: { appid, secret, code, grant_type: 'authorization_code' }, diff --git a/packages/connector-wechat/src/index.test.ts b/packages/connector-wechat/src/index.test.ts index 05376b446..c4539899c 100644 --- a/packages/connector-wechat/src/index.test.ts +++ b/packages/connector-wechat/src/index.test.ts @@ -1,12 +1,16 @@ -import { ConnectorError, ConnectorErrorCodes, GetConnectorConfig } from '@logto/connector-types'; +import { + ConnectorError, + ConnectorErrorCodes, + GetConnectorConfig, + ValidateConfig, +} from '@logto/connector-types'; import nock from 'nock'; import WechatConnector from '.'; import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './constant'; import { mockedConfig } from './mock'; -import { WechatConfig } from './types'; -const getConnectorConfig = jest.fn() as GetConnectorConfig; +const getConnectorConfig = jest.fn() as GetConnectorConfig; const wechatMethods = new WechatConnector(getConnectorConfig); @@ -92,16 +96,30 @@ describe('getAccessToken', () => { }); describe('validateConfig', () => { + /** + * Assertion functions always need explicit annotations. + * See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014 + */ + it('should pass on valid config', async () => { - await expect( - wechatMethods.validateConfig({ appId: 'appId', appSecret: 'appSecret' }) - ).resolves.not.toThrow(); + const validator: ValidateConfig = wechatMethods.validateConfig; + expect(() => { + validator({ appId: 'appId', appSecret: 'appSecret' }); + }).not.toThrow(); }); - it('should throw on empty config', async () => { - await expect(wechatMethods.validateConfig({})).rejects.toThrowError(); + + it('should fail on empty config', async () => { + const validator: ValidateConfig = wechatMethods.validateConfig; + expect(() => { + validator({}); + }).toThrow(); }); - it('should throw when missing appSecret', async () => { - await expect(wechatMethods.validateConfig({ appId: 'appId' })).rejects.toThrowError(); + + it('should fail when missing appSecret', async () => { + const validator: ValidateConfig = wechatMethods.validateConfig; + expect(() => { + validator({ appId: 'appId' }); + }).toThrow(); }); }); diff --git a/packages/connector-wechat/src/index.ts b/packages/connector-wechat/src/index.ts index 51b616394..63b597c48 100644 --- a/packages/connector-wechat/src/index.ts +++ b/packages/connector-wechat/src/index.ts @@ -6,7 +6,6 @@ import { ConnectorMetadata, GetAuthorizationUri, - ValidateConfig, GetUserInfo, ConnectorError, ConnectorErrorCodes, @@ -36,21 +35,25 @@ import { WechatConfig, } from './types'; -export default class WechatConnector implements SocialConnector { +export default class WechatConnector implements SocialConnector { public metadata: ConnectorMetadata = defaultMetadata; - constructor(public readonly getConfig: GetConnectorConfig) {} + constructor(public readonly getConfig: GetConnectorConfig) {} - public validateConfig: ValidateConfig = async (config: unknown) => { + public validateConfig(config: unknown): asserts config is WechatConfig { const result = wechatConfigGuard.safeParse(config); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error); } - }; + } public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => { - const { appId } = await this.getConfig(this.metadata.id); + const config = await this.getConfig(this.metadata.id); + + this.validateConfig(config); + + const { appId } = config; const queryParameters = new URLSearchParams({ appid: appId, @@ -66,7 +69,11 @@ export default class WechatConnector implements SocialConnector { public getAccessToken = async ( code: string ): Promise<{ accessToken: string; openid: string }> => { - const { appId: appid, appSecret: secret } = await this.getConfig(this.metadata.id); + const config = await this.getConfig(this.metadata.id); + + this.validateConfig(config); + + const { appId: appid, appSecret: secret } = config; const httpResponse = await got.get(accessTokenEndpoint, { searchParams: { appid, secret, code, grant_type: 'authorization_code' }, diff --git a/packages/core/src/connectors/utilities/index.ts b/packages/core/src/connectors/utilities/index.ts index 707d21d1a..b710d9a58 100644 --- a/packages/core/src/connectors/utilities/index.ts +++ b/packages/core/src/connectors/utilities/index.ts @@ -1,14 +1,12 @@ -import { ArbitraryObject } from '@logto/schemas'; - import RequestError from '@/errors/RequestError'; import { findAllConnectors } from '@/queries/connector'; import assertThat from '@/utils/assert-that'; -export const getConnectorConfig = async (id: string): Promise => { +export const getConnectorConfig = async (id: string): Promise => { const connectors = await findAllConnectors(); const connector = connectors.find((connector) => connector.id === id); assertThat(connector, new RequestError({ code: 'entity.not_found', id, status: 404 })); - return connector.config as T; + return connector.config; }; diff --git a/packages/core/src/routes/connector.ts b/packages/core/src/routes/connector.ts index 1f66c2622..76e99de72 100644 --- a/packages/core/src/routes/connector.ts +++ b/packages/core/src/routes/connector.ts @@ -1,3 +1,4 @@ +import { ValidateConfig } from '@logto/connector-types'; import { arbitraryObjectGuard, ConnectorDto, Connectors, ConnectorType } from '@logto/schemas'; import { emailRegEx, phoneRegEx } from '@logto/shared'; import { object, string } from 'zod'; @@ -84,14 +85,22 @@ export default function connectorRoutes(router: T) { params: { id }, body: { enabled }, } = ctx.guard; + + const connectorInstance = await getConnectorInstanceById(id); const { connector: { config }, metadata, - validateConfig, - } = await getConnectorInstanceById(id); + } = connectorInstance; + + /** + * Assertion functions always need explicit annotations. + * See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014 + */ + // eslint-disable-next-line unicorn/consistent-destructuring + const validator: ValidateConfig = connectorInstance.validateConfig; if (enabled) { - await validateConfig(config); + validator(config); } // Only allow one enabled connector for SMS and Email. @@ -135,10 +144,19 @@ export default function connectorRoutes(router: T) { params: { id }, body, } = ctx.guard; - const { metadata, validateConfig } = await getConnectorInstanceById(id); + + const connectorInstance = await getConnectorInstanceById(id); + const { metadata } = connectorInstance; + + /** + * Assertion functions always need explicit annotations. + * See https://github.com/microsoft/TypeScript/issues/36931#issuecomment-589753014 + */ + // eslint-disable-next-line unicorn/consistent-destructuring + const validator: ValidateConfig = connectorInstance.validateConfig; if (body.config) { - await validateConfig(body.config); + validator(body.config); } const connector = await updateConnector({ set: body, where: { id }, jsonbMode: 'replace' }); @@ -187,10 +205,6 @@ export default function connectorRoutes(router: T) { }) ); - if (config) { - await connector.validateConfig(config); - } - await connector.sendMessage( subject, 'Test', diff --git a/packages/core/src/routes/connector.update.test.ts b/packages/core/src/routes/connector.update.test.ts index d20e62832..e7235e38c 100644 --- a/packages/core/src/routes/connector.update.test.ts +++ b/packages/core/src/routes/connector.update.test.ts @@ -38,7 +38,7 @@ const getConnectorInstanceByIdPlaceHolder = jest.fn(async (connectorId: string) sendMessage: sendMessagePlaceHolder, }; }); -const validateConfigPlaceHolder = jest.fn(); +const validateConfigPlaceHolder = jest.fn() as jest.MockedFunction; const sendMessagePlaceHolder = jest.fn(); jest.mock('@/queries/connector', () => ({ @@ -98,7 +98,7 @@ describe('connector PATCH routes', () => { }); it('enables one of the social connectors (with invalid config)', async () => { - validateConfigPlaceHolder.mockImplementationOnce(async () => { + validateConfigPlaceHolder.mockImplementationOnce(() => { throw new ConnectorError(ConnectorErrorCodes.InvalidConfig); }); getConnectorInstancesPlaceHolder.mockResolvedValueOnce([ @@ -187,7 +187,7 @@ describe('connector PATCH routes', () => { }); it('enables one of the email/sms connectors (with invalid config)', async () => { - validateConfigPlaceHolder.mockImplementationOnce(async () => { + validateConfigPlaceHolder.mockImplementationOnce(() => { throw new ConnectorError(ConnectorErrorCodes.InvalidConfig); }); getConnectorInstancesPlaceHolder.mockResolvedValueOnce([ @@ -247,7 +247,7 @@ describe('connector PATCH routes', () => { }); it('config validation fails', async () => { - validateConfigPlaceHolder.mockImplementationOnce(async () => { + validateConfigPlaceHolder.mockImplementationOnce(() => { throw new ConnectorError(ConnectorErrorCodes.InvalidConfig); }); getConnectorInstancesPlaceHolder.mockResolvedValueOnce([