0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-27 21:39:16 -05:00

refactor(toolkit)!: split connector toolkit types (#4678)

* refactor(toolkit)!: split connector toolkit types

split connector toolkit types

* chore: add changeset

add changeset
This commit is contained in:
simeng-li 2023-10-18 13:53:21 +08:00 committed by GitHub
parent 8c0b55ab06
commit d24aaedf5b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 362 additions and 337 deletions

View file

@ -0,0 +1,6 @@
---
"@logto/connector-kit": major
---
major: Remove the deprecated enum MessageType, should all migrate using the new enum VerificationCodeType.
patch: Split the types for connectors into separate files.

View file

@ -1,8 +1,8 @@
import type { ZodType, ZodTypeDef } from 'zod';
import { ConnectorError, ConnectorErrorCodes } from './types.js';
import { ConnectorError, ConnectorErrorCodes } from './types/index.js';
export * from './types.js';
export * from './types/index.js';
export function validateConfig<Output, Input = Output>(
config: unknown,

View file

@ -1,335 +0,0 @@
import type { LanguageTag } from '@logto/language-kit';
import { isLanguageTag } from '@logto/language-kit';
import type Client from '@withtyped/client';
import type { BaseRoutes, Router } from '@withtyped/server';
import type { ZodType } from 'zod';
import { z } from 'zod';
// MARK: Foundation
export enum ConnectorType {
Email = 'Email',
Sms = 'Sms',
Social = 'Social',
}
export enum ConnectorPlatform {
Native = 'Native',
Universal = 'Universal',
Web = 'Web',
}
export const i18nPhrasesGuard: ZodType<I18nPhrases> = z
.object({ en: z.string() })
.and(z.record(z.string()))
.refine((i18nObject) => {
const keys = Object.keys(i18nObject);
if (!keys.includes('en')) {
return false;
}
for (const value of keys) {
if (!isLanguageTag(value)) {
return false;
}
}
return true;
});
type I18nPhrases = { en: string } & {
[K in Exclude<LanguageTag, 'en'>]?: string;
};
export enum ConnectorErrorCodes {
General = 'general',
InvalidMetadata = 'invalid_metadata',
UnexpectedType = 'unexpected_type',
InvalidConfigGuard = 'invalid_config_guard',
InvalidRequestParameters = 'invalid_request_parameters',
InsufficientRequestParameters = 'insufficient_request_parameters',
InvalidConfig = 'invalid_config',
InvalidResponse = 'invalid_response',
/** The template is not found for the given type. */
TemplateNotFound = 'template_not_found',
/** The template type is not supported by the connector. */
TemplateNotSupported = 'template_not_supported',
RateLimitExceeded = 'rate_limit_exceeded',
NotImplemented = 'not_implemented',
SocialAuthCodeInvalid = 'social_auth_code_invalid',
SocialAccessTokenInvalid = 'social_invalid_access_token',
SocialIdTokenInvalid = 'social_invalid_id_token',
AuthorizationFailed = 'authorization_failed',
}
export class ConnectorError extends Error {
public code: ConnectorErrorCodes;
public data: unknown;
constructor(code: ConnectorErrorCodes, data?: unknown) {
const message = `ConnectorError: ${data ? JSON.stringify(data) : code}`;
super(message);
this.code = code;
this.data = typeof data === 'string' ? { message: data } : data;
}
}
export enum VerificationCodeType {
SignIn = 'SignIn',
Register = 'Register',
ForgotPassword = 'ForgotPassword',
/** @deprecated */
Continue = 'Continue',
Generic = 'Generic',
/** @deprecated Use `Generic` type template for sending test sms/email use case */
Test = 'Test',
}
export const verificationCodeTypeGuard = z.nativeEnum(VerificationCodeType);
// Enum is string actually, keep this exported until GA for compatibility.
/** @deprecated Use `VerificationCodeType` instead. */
export enum MessageType {
SignIn = 'SignIn',
Register = 'Register',
ForgotPassword = 'ForgotPassword',
Continue = 'Continue',
Test = 'Test',
}
/** @deprecated Use `verificationCodeTypeGuard` instead. */
export const messageTypesGuard = verificationCodeTypeGuard;
export enum ConnectorConfigFormItemType {
Text = 'Text',
Number = 'Number',
MultilineText = 'MultilineText',
Switch = 'Switch',
Select = 'Select',
Json = 'Json',
}
const baseConfigFormItem = {
key: z.string(),
label: z.string(),
placeholder: z.string().optional(),
required: z.boolean().optional(),
defaultValue: z.unknown().optional(),
showConditions: z
.array(z.object({ targetKey: z.string(), expectValue: z.unknown().optional() }))
.optional(),
description: z.string().optional(),
tooltip: z.string().optional(),
isConfidential: z.boolean().optional(), // For `Text` type only.
};
const connectorConfigFormItemGuard = z.discriminatedUnion('type', [
z.object({
type: z.literal(ConnectorConfigFormItemType.Select),
selectItems: z.array(z.object({ value: z.string(), title: z.string() })),
...baseConfigFormItem,
}),
z.object({
type: z.enum([
ConnectorConfigFormItemType.Text,
ConnectorConfigFormItemType.Number,
ConnectorConfigFormItemType.MultilineText,
ConnectorConfigFormItemType.Switch,
ConnectorConfigFormItemType.Json,
]),
...baseConfigFormItem,
}),
]);
export type ConnectorConfigFormItem = z.infer<typeof connectorConfigFormItemGuard>;
export const connectorMetadataGuard = z
.object({
id: z.string(),
target: z.string(),
platform: z.nativeEnum(ConnectorPlatform).nullable(),
name: i18nPhrasesGuard,
logo: z.string(),
logoDark: z.string().nullable(),
description: i18nPhrasesGuard,
isStandard: z.boolean().optional(),
readme: z.string(),
configTemplate: z.string().optional(),
formItems: connectorConfigFormItemGuard.array().optional(),
})
.catchall(z.unknown());
export const configurableConnectorMetadataGuard = connectorMetadataGuard
.pick({
target: true,
name: true,
logo: true,
logoDark: true,
})
.partial();
export type ConnectorMetadata = z.infer<typeof connectorMetadataGuard>;
export type ConfigurableConnectorMetadata = z.infer<typeof configurableConnectorMetadataGuard>;
export const connectorSessionGuard = z
.object({
nonce: z.string(),
redirectUri: z.string(),
connectorId: z.string(),
connectorFactoryId: z.string(),
jti: z.string(),
state: z.string(),
})
.partial()
/**
* Accept arbitrary unspecified keys so developers who can not publish @logto/connector-kit can more flexibly utilize connector session.
*/
.catchall(z.unknown());
export type ConnectorSession = z.infer<typeof connectorSessionGuard>;
export type GetSession = () => Promise<ConnectorSession>;
export type SetSession = (storage: ConnectorSession) => Promise<void>;
export type BaseConnector<Type extends ConnectorType> = {
type: Type;
metadata: ConnectorMetadata;
configGuard: ZodType;
};
export type CreateConnector<
T extends AllConnector,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
U extends Router<any, BaseRoutes, string> = Router<any, BaseRoutes, string>,
> = (options: {
getConfig: GetConnectorConfig;
getCloudServiceClient?: GetCloudServiceClient<U>;
}) => Promise<T>;
export type GetConnectorConfig = (id: string) => Promise<unknown>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type GetCloudServiceClient<T extends Router<any, BaseRoutes, string>> = () => Promise<
Client<T>
>;
export type AllConnector = SmsConnector | EmailConnector | SocialConnector;
// MARK: SMS + Email connector
export type SmsConnector = BaseConnector<ConnectorType.Sms> & {
sendMessage: SendMessageFunction;
getUsage?: GetUsageFunction;
};
export type EmailConnector = BaseConnector<ConnectorType.Email> & {
sendMessage: SendMessageFunction;
getUsage?: GetUsageFunction;
};
export const urlRegEx =
/(https?:\/\/)?(?:www\.)?[\w#%+.:=@~-]{1,256}\.[\d()A-Za-z]{1,6}\b[\w#%&()+./:=?@~-]*/;
export const emailServiceBrandingGuard = z
.object({
senderName: z
.string()
.refine((address) => !urlRegEx.test(address), 'DO NOT include URL in the sender name!'),
companyInformation: z
.string()
.refine(
(address) => !urlRegEx.test(address),
'DO NOT include URL in the company information!'
),
appLogo: z.string().url(),
})
.partial();
export type EmailServiceBranding = z.infer<typeof emailServiceBrandingGuard>;
export type SendMessagePayload = {
/**
* The dynamic verification code to send.
*
* @example '123456'
*/
code: string;
};
export const sendMessagePayloadGuard = z.object({
code: z.string(),
}) satisfies z.ZodType<SendMessagePayload>;
export type SendMessageData = {
to: string;
type: VerificationCodeType;
payload: SendMessagePayload;
};
export const sendMessageDataGuard = z.object({
to: z.string(),
type: verificationCodeTypeGuard,
payload: sendMessagePayloadGuard,
}) satisfies z.ZodType<SendMessageData>;
export type SendMessageFunction = (data: SendMessageData, config?: unknown) => Promise<unknown>;
export type GetUsageFunction = (startFrom?: Date) => Promise<number>;
// MARK: Social connector
export type SocialConnector = BaseConnector<ConnectorType.Social> & {
getAuthorizationUri: GetAuthorizationUri;
getUserInfo: GetUserInfo;
validateSamlAssertion?: ValidateSamlAssertion;
};
// This type definition is for SAML connector
export type ValidateSamlAssertion = (
assertion: Record<string, unknown>,
getSession: GetSession,
setSession: SetSession
) => Promise<string>;
export type GetAuthorizationUri = (
payload: {
state: string;
redirectUri: string;
connectorId: string;
connectorFactoryId: string;
jti: string;
headers: { userAgent?: string };
},
setSession: SetSession
) => Promise<string>;
export const socialUserInfoGuard = z.object({
id: z.string(),
email: z.string().optional(),
phone: z.string().optional(),
name: z.string().optional(),
avatar: z.string().optional(),
});
export type SocialUserInfo = z.infer<typeof socialUserInfoGuard>;
export type GetUserInfo = (
data: unknown,
getSession: GetSession
) => Promise<SocialUserInfo & Record<string, string | boolean | number | undefined>>;
export enum DemoConnector {
Sms = 'logto-sms',
Social = 'logto-social-demo',
}
export const demoConnectorIds: readonly string[] = Object.freeze([
DemoConnector.Sms,
DemoConnector.Social,
]);
export enum ServiceConnector {
Email = 'logto-email',
}
export const serviceConnectorIds: readonly string[] = Object.freeze([ServiceConnector.Email]);

View file

@ -0,0 +1,44 @@
import { z } from 'zod';
export enum ConnectorConfigFormItemType {
Text = 'Text',
Number = 'Number',
MultilineText = 'MultilineText',
Switch = 'Switch',
Select = 'Select',
Json = 'Json',
}
const baseConfigFormItem = {
key: z.string(),
label: z.string(),
placeholder: z.string().optional(),
required: z.boolean().optional(),
defaultValue: z.unknown().optional(),
showConditions: z
.array(z.object({ targetKey: z.string(), expectValue: z.unknown().optional() }))
.optional(),
description: z.string().optional(),
tooltip: z.string().optional(),
isConfidential: z.boolean().optional(), // For `Text` type only.
};
export const connectorConfigFormItemGuard = z.discriminatedUnion('type', [
z.object({
type: z.literal(ConnectorConfigFormItemType.Select),
selectItems: z.array(z.object({ value: z.string(), title: z.string() })),
...baseConfigFormItem,
}),
z.object({
type: z.enum([
ConnectorConfigFormItemType.Text,
ConnectorConfigFormItemType.Number,
ConnectorConfigFormItemType.MultilineText,
ConnectorConfigFormItemType.Switch,
ConnectorConfigFormItemType.Json,
]),
...baseConfigFormItem,
}),
]);
export type ConnectorConfigFormItem = z.infer<typeof connectorConfigFormItemGuard>;

View file

@ -0,0 +1,32 @@
export enum ConnectorErrorCodes {
General = 'general',
InvalidMetadata = 'invalid_metadata',
UnexpectedType = 'unexpected_type',
InvalidConfigGuard = 'invalid_config_guard',
InvalidRequestParameters = 'invalid_request_parameters',
InsufficientRequestParameters = 'insufficient_request_parameters',
InvalidConfig = 'invalid_config',
InvalidResponse = 'invalid_response',
/** The template is not found for the given type. */
TemplateNotFound = 'template_not_found',
/** The template type is not supported by the connector. */
TemplateNotSupported = 'template_not_supported',
RateLimitExceeded = 'rate_limit_exceeded',
NotImplemented = 'not_implemented',
SocialAuthCodeInvalid = 'social_auth_code_invalid',
SocialAccessTokenInvalid = 'social_invalid_access_token',
SocialIdTokenInvalid = 'social_invalid_id_token',
AuthorizationFailed = 'authorization_failed',
}
export class ConnectorError extends Error {
public code: ConnectorErrorCodes;
public data: unknown;
constructor(code: ConnectorErrorCodes, data?: unknown) {
const message = `ConnectorError: ${data ? JSON.stringify(data) : code}`;
super(message);
this.code = code;
this.data = typeof data === 'string' ? { message: data } : data;
}
}

View file

@ -0,0 +1,19 @@
import type { ZodType } from 'zod';
import { type ConnectorMetadata } from './metadata.js';
export enum ConnectorType {
Email = 'Email',
Sms = 'Sms',
Social = 'Social',
}
/*
SocialConnector, EmailConnector, SmsConnector has dependency on BaseConnector,
so BaseConnector need be defined separately.
*/
export type BaseConnector<Type extends ConnectorType> = {
type: Type;
metadata: ConnectorMetadata;
configGuard: ZodType;
};

View file

@ -0,0 +1,46 @@
import type Client from '@withtyped/client';
import type { BaseRoutes, Router } from '@withtyped/server';
import { type SmsConnector, type EmailConnector } from './passwordless.js';
import { type SocialConnector } from './social.js';
export * from './config-form.js';
export * from './error.js';
export * from './metadata.js';
export * from './foundation.js';
export * from './passwordless.js';
export * from './social.js';
export type GetConnectorConfig = (id: string) => Promise<unknown>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type GetCloudServiceClient<T extends Router<any, BaseRoutes, string>> = () => Promise<
Client<T>
>;
export type CreateConnector<
T extends AllConnector,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
U extends Router<any, BaseRoutes, string> = Router<any, BaseRoutes, string>,
> = (options: {
getConfig: GetConnectorConfig;
getCloudServiceClient?: GetCloudServiceClient<U>;
}) => Promise<T>;
export type AllConnector = SmsConnector | EmailConnector | SocialConnector;
export enum DemoConnector {
Sms = 'logto-sms',
Social = 'logto-social-demo',
}
export const demoConnectorIds: readonly string[] = Object.freeze([
DemoConnector.Sms,
DemoConnector.Social,
]);
export enum ServiceConnector {
Email = 'logto-email',
}
export const serviceConnectorIds: readonly string[] = Object.freeze([ServiceConnector.Email]);

View file

@ -0,0 +1,76 @@
import type { LanguageTag } from '@logto/language-kit';
import { isLanguageTag } from '@logto/language-kit';
import type { ZodType } from 'zod';
import { z } from 'zod';
import { connectorConfigFormItemGuard } from './config-form.js';
export enum ConnectorPlatform {
Native = 'Native',
Universal = 'Universal',
Web = 'Web',
}
export const i18nPhrasesGuard: ZodType<I18nPhrases> = z
.object({ en: z.string() })
.and(z.record(z.string()))
.refine((i18nObject) => {
const keys = Object.keys(i18nObject);
if (!keys.includes('en')) {
return false;
}
for (const value of keys) {
if (!isLanguageTag(value)) {
return false;
}
}
return true;
});
type I18nPhrases = { en: string } & {
[K in Exclude<LanguageTag, 'en'>]?: string;
};
export const socialConnectorMetadataGuard = z.object({
// Social connector platform. TODO: @darcyYe considering remove the nullable and make all the social connector field optional
platform: z.nativeEnum(ConnectorPlatform).nullable(),
// Indicates custom connector that follows standard protocol. Currently supported standard connectors are OIDC, OAuth2, and SAML2
isStandard: z.boolean().optional(),
});
export const connectorMetadataGuard = z
.object({
// Unique connector factory id
id: z.string(),
/* Connector provider. Unique for each provider. Users can have only one social identity per provider
For Social connectors, it's manually set on connector creation
For SSO connectors, it's the same as the issuer
*/
target: z.string(),
name: i18nPhrasesGuard,
description: i18nPhrasesGuard,
logo: z.string(),
logoDark: z.string().nullable(),
readme: z.string(),
configTemplate: z.string().optional(), // Connector config template
formItems: connectorConfigFormItemGuard.array().optional(),
})
.merge(socialConnectorMetadataGuard)
.catchall(z.unknown());
export type ConnectorMetadata = z.infer<typeof connectorMetadataGuard>;
// Configurable connector metadata guard. Stored in DB metadata field
export const configurableConnectorMetadataGuard = connectorMetadataGuard
.pick({
target: true,
name: true,
logo: true,
logoDark: true,
})
.partial();
export type ConfigurableConnectorMetadata = z.infer<typeof configurableConnectorMetadataGuard>;

View file

@ -0,0 +1,74 @@
// MARK: SMS + Email connector
import { z } from 'zod';
import { type BaseConnector, type ConnectorType } from './foundation.js';
export enum VerificationCodeType {
SignIn = 'SignIn',
Register = 'Register',
ForgotPassword = 'ForgotPassword',
Generic = 'Generic',
/** @deprecated Use `Generic` type template for sending test sms/email use case */
Test = 'Test',
}
export const verificationCodeTypeGuard = z.nativeEnum(VerificationCodeType);
export type SendMessagePayload = {
/**
* The dynamic verification code to send.
* @example '123456'
*/
code: string;
};
export const sendMessagePayloadGuard = z.object({
code: z.string(),
}) satisfies z.ZodType<SendMessagePayload>;
export const urlRegEx =
/(https?:\/\/)?(?:www\.)?[\w#%+.:=@~-]{1,256}\.[\d()A-Za-z]{1,6}\b[\w#%&()+./:=?@~-]*/;
export const emailServiceBrandingGuard = z
.object({
senderName: z
.string()
.refine((address) => !urlRegEx.test(address), 'DO NOT include URL in the sender name!'),
companyInformation: z
.string()
.refine(
(address) => !urlRegEx.test(address),
'DO NOT include URL in the company information!'
),
appLogo: z.string().url(),
})
.partial();
export type EmailServiceBranding = z.infer<typeof emailServiceBrandingGuard>;
export type SendMessageData = {
to: string;
type: VerificationCodeType;
payload: SendMessagePayload;
};
export const sendMessageDataGuard = z.object({
to: z.string(),
type: verificationCodeTypeGuard,
payload: sendMessagePayloadGuard,
}) satisfies z.ZodType<SendMessageData>;
export type SendMessageFunction = (data: SendMessageData, config?: unknown) => Promise<unknown>;
export type GetUsageFunction = (startFrom?: Date) => Promise<number>;
export type SmsConnector = BaseConnector<ConnectorType.Sms> & {
sendMessage: SendMessageFunction;
getUsage?: GetUsageFunction;
};
export type EmailConnector = BaseConnector<ConnectorType.Email> & {
sendMessage: SendMessageFunction;
getUsage?: GetUsageFunction;
};

View file

@ -0,0 +1,63 @@
// MARK: Social connector
import { z } from 'zod';
import { type BaseConnector, type ConnectorType } from './foundation.js';
// This type definition is for SAML connector
export type ValidateSamlAssertion = (
assertion: Record<string, unknown>,
getSession: GetSession,
setSession: SetSession
) => Promise<string>;
export type GetAuthorizationUri = (
payload: {
state: string;
redirectUri: string;
connectorId: string;
connectorFactoryId: string;
jti: string;
headers: { userAgent?: string };
},
setSession: SetSession
) => Promise<string>;
export const socialUserInfoGuard = z.object({
id: z.string(),
email: z.string().optional(),
phone: z.string().optional(),
name: z.string().optional(),
avatar: z.string().optional(),
});
export type SocialUserInfo = z.infer<typeof socialUserInfoGuard>;
export type GetUserInfo = (
data: unknown,
getSession: GetSession
) => Promise<SocialUserInfo & Record<string, string | boolean | number | undefined>>;
export const connectorSessionGuard = z
.object({
nonce: z.string(),
redirectUri: z.string(),
connectorId: z.string(),
connectorFactoryId: z.string(),
jti: z.string(),
state: z.string(),
})
.partial()
// Accept arbitrary unspecified keys so developers who can not publish @logto/connector-kit can more flexibly utilize connector session.
.catchall(z.unknown());
export type ConnectorSession = z.infer<typeof connectorSessionGuard>;
export type GetSession = () => Promise<ConnectorSession>;
export type SetSession = (storage: ConnectorSession) => Promise<void>;
export type SocialConnector = BaseConnector<ConnectorType.Social> & {
getAuthorizationUri: GetAuthorizationUri;
getUserInfo: GetUserInfo;
validateSamlAssertion?: ValidateSamlAssertion;
};