diff --git a/packages/core/src/routes/connector/config-testing.ts b/packages/core/src/routes/connector/config-testing.ts index b2363a85c..40253b21e 100644 --- a/packages/core/src/routes/connector/config-testing.ts +++ b/packages/core/src/routes/connector/config-testing.ts @@ -4,22 +4,36 @@ import { type SmsConnector, type EmailConnector, demoConnectorIds, + ServiceConnector, VerificationCodeType, } from '@logto/connector-kit'; import { phoneRegEx, emailRegEx } from '@logto/core-kit'; -import { jsonObjectGuard, ConnectorType } from '@logto/schemas'; +import { jsonObjectGuard, ConnectorType, type JsonObject } from '@logto/schemas'; import { string, object } from 'zod'; import RequestError from '#src/errors/RequestError/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; import assertThat from '#src/utils/assert-that.js'; import { loadConnectorFactories } from '#src/utils/connectors/index.js'; +import { getCloudConnectionEndpoints } from '#src/utils/endpoint.js'; import type { AuthedRouter, RouterInitArgs } from '../types.js'; export default function connectorConfigTestingRoutes( - ...[router]: RouterInitArgs + ...[router, { libraries }]: RouterInitArgs ) { + const { + logtoConfigs: { getCloudConnectionData }, + } = libraries; + const configPatcher = async (factoryId: string, config?: JsonObject) => { + if (!config || ServiceConnector.Email !== factoryId) { + return config; + } + const endpoints = getCloudConnectionEndpoints(); + const credentials = await getCloudConnectionData(); + return { ...endpoints, ...credentials, ...config }; + }; + router.post( '/connectors/:factoryId/test', koaGuard({ @@ -35,7 +49,7 @@ export default function connectorConfigTestingRoutes( params: { factoryId }, body, } = ctx.guard; - const { phone, email, config } = body; + const { phone, email, config: originalConfig } = body; const subject = phone ?? email; assertThat(subject, new RequestError({ code: 'guard.invalid_input' })); @@ -64,6 +78,7 @@ export default function connectorConfigTestingRoutes( rawConnector: { sendMessage }, } = await buildRawConnector(connectorFactory); + const config = await configPatcher(factoryId, originalConfig); await sendMessage( { to: subject, diff --git a/packages/core/src/routes/connector/index.ts b/packages/core/src/routes/connector/index.ts index 68f1e152b..fca4fc78f 100644 --- a/packages/core/src/routes/connector/index.ts +++ b/packages/core/src/routes/connector/index.ts @@ -1,11 +1,17 @@ /* eslint-disable max-lines */ import { buildRawConnector } from '@logto/cli/lib/connector/index.js'; -import { demoConnectorIds, validateConfig } from '@logto/connector-kit'; +import { + ServiceConnector, + demoConnectorIds, + emailServiceBrandingGuard, + validateConfig, +} from '@logto/connector-kit'; import { connectorFactoryResponseGuard, Connectors, ConnectorType, connectorResponseGuard, + type JsonObject, } from '@logto/schemas'; import { buildIdGenerator } from '@logto/shared'; import cleanDeep from 'clean-deep'; @@ -22,6 +28,7 @@ import { buildExtraInfoFromEmailServiceData, } from '#src/utils/connectors/index.js'; import { checkSocialConnectorTargetAndPlatformUniqueness } from '#src/utils/connectors/platform.js'; +import { getCloudConnectionEndpoints } from '#src/utils/endpoint.js'; import type { AuthedRouter, RouterInitArgs } from '../types.js'; @@ -30,6 +37,20 @@ import connectorConfigTestingRoutes from './config-testing.js'; const generateConnectorId = buildIdGenerator(12); +const configPruner = (factoryId: string, config: JsonObject): JsonObject => { + if (ServiceConnector.Email !== factoryId) { + return config; + } + + /** + * Can not use `pick()` from @silverhand/essentials since the pick returns 'undefined' and + * this is value is not accepted by `config` return type JsonObject. + */ + return Object.fromEntries( + Object.keys(emailServiceBrandingGuard.shape).map((key) => [key, config[key] ?? null]) + ); +}; + export default function connectorRoutes( ...[router, tenant]: RouterInitArgs ) { @@ -44,6 +65,7 @@ export default function connectorRoutes( const { getLogtoConnectorById, getLogtoConnectors } = tenant.connectors; const { signInExperiences: { removeUnavailableSocialConnectorTargets }, + logtoConfigs: { getCloudConnectionData }, } = tenant.libraries; // Will accept other source of `extraInfo` in the future. @@ -55,6 +77,15 @@ export default function connectorRoutes( return cleanDeep(extraInfo, { emptyObjects: false }); }; + const configPatcher = async (factoryId: string, config?: JsonObject) => { + if (!config || ServiceConnector.Email !== factoryId) { + return config; + } + const endpoints = getCloudConnectionEndpoints(); + const credentials = await getCloudConnectionData(); + return { ...endpoints, ...credentials, ...config }; + }; + router.post( '/connectors', koaGuard({ @@ -71,7 +102,7 @@ export default function connectorRoutes( }), async (ctx, next) => { const { - body: { id: proposedId, connectorId, metadata, config, syncProfile }, + body: { id: proposedId, connectorId, metadata, config: originalConfig, syncProfile }, } = ctx.guard; const connectorFactories = await loadConnectorFactories(); @@ -130,6 +161,7 @@ export default function connectorRoutes( ); } + const config = await configPatcher(connectorFactory.metadata.id, originalConfig); if (config) { const { rawConnector } = await buildRawConnector(connectorFactory); validateConfig(config, rawConnector.configGuard); @@ -164,7 +196,10 @@ export default function connectorRoutes( } const connector = await getLogtoConnectorById(insertConnectorId); - ctx.body = await transpileLogtoConnector(connector, buildExtraInfo(connector.metadata.id)); + ctx.body = await transpileLogtoConnector(connector, { + extraInfo: buildExtraInfo(connector.metadata.id), + configPruner: (config) => configPruner(connectorFactory.metadata.id, config), + }); return next(); } @@ -200,7 +235,10 @@ export default function connectorRoutes( ctx.body = await Promise.all( filteredConnectors.map(async (connector) => - transpileLogtoConnector(connector, buildExtraInfo(connector.metadata.id)) + transpileLogtoConnector(connector, { + extraInfo: buildExtraInfo(connector.metadata.id), + configPruner: (config) => configPruner(connector.metadata.id, config), + }) ) ); @@ -224,7 +262,10 @@ export default function connectorRoutes( // Hide demo connector assertThat(!demoConnectorIds.includes(connector.metadata.id), 'connector.not_found'); - ctx.body = await transpileLogtoConnector(connector, buildExtraInfo(connector.metadata.id)); + ctx.body = await transpileLogtoConnector(connector, { + extraInfo: buildExtraInfo(connector.metadata.id), + configPruner: (config) => configPruner(connector.metadata.id, config), + }); return next(); } @@ -287,7 +328,7 @@ export default function connectorRoutes( async (ctx, next) => { const { params: { id }, - body: { config, metadata, syncProfile }, + body: { config: originalConfig, metadata, syncProfile }, } = ctx.guard; const { type, validateConfig, metadata: originalMetadata } = await getLogtoConnectorById(id); @@ -314,6 +355,7 @@ export default function connectorRoutes( ); } + const config = await configPatcher(originalMetadata.id, originalConfig); if (config) { validateConfig(config); } @@ -324,7 +366,10 @@ export default function connectorRoutes( jsonbMode: 'replace', }); const connector = await getLogtoConnectorById(id); - ctx.body = await transpileLogtoConnector(connector, buildExtraInfo(connector.metadata.id)); + ctx.body = await transpileLogtoConnector(connector, { + extraInfo: buildExtraInfo(connector.metadata.id), + configPruner: (config) => configPruner(connector.metadata.id, config), + }); return next(); } diff --git a/packages/core/src/utils/connectors/index.ts b/packages/core/src/utils/connectors/index.ts index dec79a471..a71f7d2a4 100644 --- a/packages/core/src/utils/connectors/index.ts +++ b/packages/core/src/utils/connectors/index.ts @@ -14,7 +14,12 @@ import { type SmsConnector, ServiceConnector, } from '@logto/connector-kit'; -import type { ConnectorFactoryResponse, ConnectorResponse, EmailServiceData } from '@logto/schemas'; +import type { + ConnectorFactoryResponse, + ConnectorResponse, + EmailServiceData, + JsonObject, +} from '@logto/schemas'; import { findPackage } from '@logto/shared'; import { conditional, deduplicate, pick, trySafe, type Optional } from '@silverhand/essentials'; @@ -30,7 +35,10 @@ export const isPasswordlessLogtoConnector = ( export const transpileLogtoConnector = async ( connector: LogtoConnector, - extraInfo?: ConnectorResponse['extraInfo'] + payload?: { + extraInfo?: ConnectorResponse['extraInfo']; + configPruner?: (config: JsonObject) => JsonObject; + } ): Promise => { const usagePayload = conditional( /** Should do the check in advance since only passwordless connectors could have `getUsage` method. */ @@ -44,6 +52,8 @@ export const transpileLogtoConnector = async ( /** Temporarily block entering Logto email connector as well until this feature is ready for prod. */ const isDemo = demoConnectorIds.includes(id) || serviceConnectorIds.includes(id); + const { extraInfo, configPruner } = payload ?? {}; + return { type, ...metadata, @@ -51,7 +61,7 @@ export const transpileLogtoConnector = async ( isDemo, extraInfo, // Hide demo connector config - config: isDemo ? {} : config, + config: isDemo ? {} : configPruner ? configPruner(config) : config, ...usagePayload, }; }; diff --git a/packages/core/src/utils/endpoint.ts b/packages/core/src/utils/endpoint.ts index 7d3541d19..2924f171d 100644 --- a/packages/core/src/utils/endpoint.ts +++ b/packages/core/src/utils/endpoint.ts @@ -2,9 +2,7 @@ import { appendPath } from '@silverhand/essentials'; import { EnvSet } from '#src/env-set/index.js'; -/** Will use this method in upcoming changes. */ -// eslint-disable-next-line import/no-unused-modules -export const getCloudConnectionEndpoints = async () => { +export const getCloudConnectionEndpoints = () => { const { cloudUrlSet, adminUrlSet } = EnvSet.values; return { tokenEndpoint: appendPath(adminUrlSet.endpoint, 'oidc/token').toString(),