From a1ad8e3a1e06cab30341ed67e865217326a45237 Mon Sep 17 00:00:00 2001 From: neo773 <62795688+neo773@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:03:04 +0530 Subject: [PATCH] Outbound message domains (#14557) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Félix Malfait Co-authored-by: Félix Malfait --- .../src/generated-metadata/graphql.ts | 242 +++++ .../twenty-front/src/generated/graphql.ts | 53 + .../modules/app/components/SettingsRoutes.tsx | 26 +- .../client-config/hooks/useClientConfig.ts | 6 + .../states/isEmailingDomainsEnabledState.ts | 6 + .../client-config/types/ClientConfig.ts | 1 + .../components/SettingsDnsRecordsTable.tsx | 122 +++ .../components/SettingsDomainRecords.tsx | 119 +-- .../SettingsEmailingDomainRowDropdownMenu.tsx | 78 ++ ...tingsEmailingDomainVerificationRecords.tsx | 68 ++ .../graphql/mutations/createEmailingDomain.ts | 24 + .../graphql/mutations/deleteEmailingDomain.ts | 7 + .../graphql/mutations/verifyEmailingDomain.ts | 15 + .../graphql/queries/getAllEmailingDomains.ts | 21 + .../settings/domains/SettingsDomains.tsx | 19 +- .../SettingsEmailingDomainDetail.tsx | 60 ++ .../SettingsEmailingDomains.tsx | 77 ++ .../SettingsNewEmailingDomain.tsx | 157 +++ .../utils/getEmailingDomainStatusColor.ts | 18 + .../utils/getEmailingDomainStatusText.ts | 17 + .../settingsEmailingDomainFormSchema.ts | 18 + .../src/testing/mock-data/config.ts | 1 + packages/twenty-server/package.json | 1 + ...758388517321-createEmailingDomainEntity.ts | 31 + .../engine/core-modules/core-engine.module.ts | 2 + .../providers/aws-ses-client.provider.ts | 43 + .../services/aws-ses-driver.service.ts | 263 +++++ .../services/aws-ses-handle-error.service.ts | 98 ++ .../aws-ses/types/aws-ses-error.type.ts | 8 + .../drivers/emailing-domain-driver.factory.ts | 75 ++ .../emailing-domain-driver.exception.ts | 11 + .../interfaces/driver-config.interface.ts | 14 + .../emailing-domain-driver.interface.ts | 27 + .../drivers/types/emailing-domain.ts | 10 + .../drivers/types/verifications-record.ts | 6 + .../dtos/emailing-domain.dto.ts | 45 + .../dtos/verification-record.dto.ts | 16 + .../emailing-domain/emailing-domain.entity.ts | 71 ++ .../emailing-domain/emailing-domain.module.ts | 27 + .../emailing-domain.resolver.ts | 67 ++ .../services/emailing-domain.service.ts | 173 ++++ .../enums/feature-flag-key.enum.ts | 1 + .../twenty-config/config-variables.ts | 44 + .../config-variables-group-metadata.ts | 5 + .../enums/config-variables-group.enum.ts | 1 + .../workspace/workspace.entity.ts | 6 +- .../workspace-entity-manager.spec.ts | 1 + .../core/utils/seed-feature-flags.util.ts | 7 +- .../twenty-shared/src/types/SettingsPath.ts | 2 + yarn.lock | 946 ++++++++++++++++++ 50 files changed, 3053 insertions(+), 103 deletions(-) create mode 100644 packages/twenty-front/src/modules/client-config/states/isEmailingDomainsEnabledState.ts create mode 100644 packages/twenty-front/src/modules/settings/components/SettingsDnsRecordsTable.tsx create mode 100644 packages/twenty-front/src/modules/settings/emailing-domains/components/SettingsEmailingDomainRowDropdownMenu.tsx create mode 100644 packages/twenty-front/src/modules/settings/emailing-domains/components/SettingsEmailingDomainVerificationRecords.tsx create mode 100644 packages/twenty-front/src/modules/settings/emailing-domains/graphql/mutations/createEmailingDomain.ts create mode 100644 packages/twenty-front/src/modules/settings/emailing-domains/graphql/mutations/deleteEmailingDomain.ts create mode 100644 packages/twenty-front/src/modules/settings/emailing-domains/graphql/mutations/verifyEmailingDomain.ts create mode 100644 packages/twenty-front/src/modules/settings/emailing-domains/graphql/queries/getAllEmailingDomains.ts create mode 100644 packages/twenty-front/src/pages/settings/emailing-domains/SettingsEmailingDomainDetail.tsx create mode 100644 packages/twenty-front/src/pages/settings/emailing-domains/SettingsEmailingDomains.tsx create mode 100644 packages/twenty-front/src/pages/settings/emailing-domains/SettingsNewEmailingDomain.tsx create mode 100644 packages/twenty-front/src/pages/settings/emailing-domains/utils/getEmailingDomainStatusColor.ts create mode 100644 packages/twenty-front/src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts create mode 100644 packages/twenty-front/src/pages/settings/emailing-domains/validation-schemas/settingsEmailingDomainFormSchema.ts create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1758388517321-createEmailingDomainEntity.ts create mode 100644 packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/aws-ses/providers/aws-ses-client.provider.ts create mode 100644 packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-driver.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-handle-error.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/aws-ses/types/aws-ses-error.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/emailing-domain-driver.factory.ts create mode 100644 packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/exceptions/emailing-domain-driver.exception.ts create mode 100644 packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/interfaces/driver-config.interface.ts create mode 100644 packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/interfaces/emailing-domain-driver.interface.ts create mode 100644 packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/types/emailing-domain.ts create mode 100644 packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/types/verifications-record.ts create mode 100644 packages/twenty-server/src/engine/core-modules/emailing-domain/dtos/emailing-domain.dto.ts create mode 100644 packages/twenty-server/src/engine/core-modules/emailing-domain/dtos/verification-record.dto.ts create mode 100644 packages/twenty-server/src/engine/core-modules/emailing-domain/emailing-domain.entity.ts create mode 100644 packages/twenty-server/src/engine/core-modules/emailing-domain/emailing-domain.module.ts create mode 100644 packages/twenty-server/src/engine/core-modules/emailing-domain/emailing-domain.resolver.ts create mode 100644 packages/twenty-server/src/engine/core-modules/emailing-domain/services/emailing-domain.service.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 0be195a770..9dd4823233 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -488,6 +488,7 @@ export enum ConfigVariableType { export enum ConfigVariablesGroup { AnalyticsConfig = 'AnalyticsConfig', + AwsSesSettings = 'AwsSesSettings', BillingConfig = 'BillingConfig', CaptchaConfig = 'CaptchaConfig', CloudflareConfig = 'CloudflareConfig', @@ -1034,6 +1035,29 @@ export type EmailPasswordResetLink = { success: Scalars['Boolean']; }; +export type EmailingDomain = { + __typename?: 'EmailingDomain'; + createdAt: Scalars['DateTime']; + domain: Scalars['String']; + driver: EmailingDomainDriver; + id: Scalars['UUID']; + status: EmailingDomainStatus; + updatedAt: Scalars['DateTime']; + verificationRecords?: Maybe>; + verifiedAt?: Maybe; +}; + +export enum EmailingDomainDriver { + AWS_SES = 'AWS_SES' +} + +export enum EmailingDomainStatus { + FAILED = 'FAILED', + PENDING = 'PENDING', + TEMPORARY_FAILURE = 'TEMPORARY_FAILURE', + VERIFIED = 'VERIFIED' +} + export type ExecuteServerlessFunctionInput = { /** Id of the serverless function to execute */ id: Scalars['UUID']; @@ -1065,6 +1089,7 @@ export enum FeatureFlagKey { IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED', IS_DATABASE_EVENT_TRIGGER_ENABLED = 'IS_DATABASE_EVENT_TRIGGER_ENABLED', IS_DYNAMIC_SEARCH_FIELDS_ENABLED = 'IS_DYNAMIC_SEARCH_FIELDS_ENABLED', + IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED', IS_GROUP_BY_ENABLED = 'IS_GROUP_BY_ENABLED', IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED', IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED', @@ -1476,6 +1501,7 @@ export type Mutation = { createCoreViewSort: CoreViewSort; createDatabaseConfigVariable: Scalars['Boolean']; createDraftFromWorkflowVersion: WorkflowVersion; + createEmailingDomain: EmailingDomain; createFile: File; createOIDCIdentityProvider: SetupSsoOutput; createObjectEvent: Analytics; @@ -1504,6 +1530,7 @@ export type Mutation = { deleteCoreViewSort: Scalars['Boolean']; deleteCurrentWorkspace: Workspace; deleteDatabaseConfigVariable: Scalars['Boolean']; + deleteEmailingDomain: Scalars['Boolean']; deleteFile: File; deleteOneAgent: Agent; deleteOneField: Field; @@ -1613,6 +1640,7 @@ export type Mutation = { upsertPermissionFlags: Array; userLookupAdminPanel: UserLookup; validateApprovedAccessDomain: ApprovedAccessDomain; + verifyEmailingDomain: EmailingDomain; verifyTwoFactorAuthenticationMethodForAuthenticatedUser: VerifyTwoFactorAuthenticationMethodOutput; }; @@ -1725,6 +1753,12 @@ export type MutationCreateDraftFromWorkflowVersionArgs = { }; +export type MutationCreateEmailingDomainArgs = { + domain: Scalars['String']; + driver: EmailingDomainDriver; +}; + + export type MutationCreateFileArgs = { file: Scalars['Upload']; }; @@ -1863,6 +1897,11 @@ export type MutationDeleteDatabaseConfigVariableArgs = { }; +export type MutationDeleteEmailingDomainArgs = { + id: Scalars['String']; +}; + + export type MutationDeleteFileArgs = { fileId: Scalars['UUID']; }; @@ -2409,6 +2448,11 @@ export type MutationValidateApprovedAccessDomainArgs = { }; +export type MutationVerifyEmailingDomainArgs = { + id: Scalars['String']; +}; + + export type MutationVerifyTwoFactorAuthenticationMethodForAuthenticatedUserArgs = { otp: Scalars['String']; }; @@ -2742,6 +2786,7 @@ export type Query = { getCoreViewSorts: Array; getCoreViews: Array; getDatabaseConfigVariable: ConfigVariable; + getEmailingDomains: Array; getIndicatorHealthStatus: AdminPanelHealthServiceData; getMeteredProductsUsage: Array; getPageLayout?: Maybe; @@ -3899,6 +3944,14 @@ export type ValidatePasswordResetToken = { id: Scalars['UUID']; }; +export type VerificationRecord = { + __typename?: 'VerificationRecord'; + key: Scalars['String']; + priority?: Maybe; + type: Scalars['String']; + value: Scalars['String']; +}; + export type VerifyTwoFactorAuthenticationMethodOutput = { __typename?: 'VerifyTwoFactorAuthenticationMethodOutput'; success: Scalars['Boolean']; @@ -4916,6 +4969,33 @@ export type FindManyPublicDomainsQueryVariables = Exact<{ [key: string]: never; export type FindManyPublicDomainsQuery = { __typename?: 'Query', findManyPublicDomains: Array<{ __typename?: 'PublicDomain', id: string, domain: string, isValidated: boolean, createdAt: string }> }; +export type CreateEmailingDomainMutationVariables = Exact<{ + domain: Scalars['String']; + driver: EmailingDomainDriver; +}>; + + +export type CreateEmailingDomainMutation = { __typename?: 'Mutation', createEmailingDomain: { __typename?: 'EmailingDomain', id: string, domain: string, driver: EmailingDomainDriver, status: EmailingDomainStatus, verifiedAt?: string | null, createdAt: string, updatedAt: string, verificationRecords?: Array<{ __typename?: 'VerificationRecord', type: string, key: string, value: string, priority?: number | null }> | null } }; + +export type DeleteEmailingDomainMutationVariables = Exact<{ + id: Scalars['String']; +}>; + + +export type DeleteEmailingDomainMutation = { __typename?: 'Mutation', deleteEmailingDomain: boolean }; + +export type VerifyEmailingDomainMutationVariables = Exact<{ + id: Scalars['String']; +}>; + + +export type VerifyEmailingDomainMutation = { __typename?: 'Mutation', verifyEmailingDomain: { __typename?: 'EmailingDomain', id: string, domain: string, driver: EmailingDomainDriver, status: EmailingDomainStatus, verifiedAt?: string | null, createdAt: string, updatedAt: string } }; + +export type GetEmailingDomainsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetEmailingDomainsQuery = { __typename?: 'Query', getEmailingDomains: Array<{ __typename?: 'EmailingDomain', id: string, domain: string, driver: EmailingDomainDriver, status: EmailingDomainStatus, verifiedAt?: string | null, createdAt: string, updatedAt: string, verificationRecords?: Array<{ __typename?: 'VerificationRecord', type: string, key: string, value: string, priority?: number | null }> | null }> }; + export type UpdateLabPublicFeatureFlagMutationVariables = Exact<{ input: UpdateLabPublicFeatureFlagInput; }>; @@ -10015,6 +10095,168 @@ export function useFindManyPublicDomainsLazyQuery(baseOptions?: Apollo.LazyQuery export type FindManyPublicDomainsQueryHookResult = ReturnType; export type FindManyPublicDomainsLazyQueryHookResult = ReturnType; export type FindManyPublicDomainsQueryResult = Apollo.QueryResult; +export const CreateEmailingDomainDocument = gql` + mutation CreateEmailingDomain($domain: String!, $driver: EmailingDomainDriver!) { + createEmailingDomain(domain: $domain, driver: $driver) { + id + domain + driver + status + verifiedAt + verificationRecords { + type + key + value + priority + } + createdAt + updatedAt + } +} + `; +export type CreateEmailingDomainMutationFn = Apollo.MutationFunction; + +/** + * __useCreateEmailingDomainMutation__ + * + * To run a mutation, you first call `useCreateEmailingDomainMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateEmailingDomainMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [createEmailingDomainMutation, { data, loading, error }] = useCreateEmailingDomainMutation({ + * variables: { + * domain: // value for 'domain' + * driver: // value for 'driver' + * }, + * }); + */ +export function useCreateEmailingDomainMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateEmailingDomainDocument, options); + } +export type CreateEmailingDomainMutationHookResult = ReturnType; +export type CreateEmailingDomainMutationResult = Apollo.MutationResult; +export type CreateEmailingDomainMutationOptions = Apollo.BaseMutationOptions; +export const DeleteEmailingDomainDocument = gql` + mutation DeleteEmailingDomain($id: String!) { + deleteEmailingDomain(id: $id) +} + `; +export type DeleteEmailingDomainMutationFn = Apollo.MutationFunction; + +/** + * __useDeleteEmailingDomainMutation__ + * + * To run a mutation, you first call `useDeleteEmailingDomainMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useDeleteEmailingDomainMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [deleteEmailingDomainMutation, { data, loading, error }] = useDeleteEmailingDomainMutation({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useDeleteEmailingDomainMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(DeleteEmailingDomainDocument, options); + } +export type DeleteEmailingDomainMutationHookResult = ReturnType; +export type DeleteEmailingDomainMutationResult = Apollo.MutationResult; +export type DeleteEmailingDomainMutationOptions = Apollo.BaseMutationOptions; +export const VerifyEmailingDomainDocument = gql` + mutation VerifyEmailingDomain($id: String!) { + verifyEmailingDomain(id: $id) { + id + domain + driver + status + verifiedAt + createdAt + updatedAt + } +} + `; +export type VerifyEmailingDomainMutationFn = Apollo.MutationFunction; + +/** + * __useVerifyEmailingDomainMutation__ + * + * To run a mutation, you first call `useVerifyEmailingDomainMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useVerifyEmailingDomainMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [verifyEmailingDomainMutation, { data, loading, error }] = useVerifyEmailingDomainMutation({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useVerifyEmailingDomainMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(VerifyEmailingDomainDocument, options); + } +export type VerifyEmailingDomainMutationHookResult = ReturnType; +export type VerifyEmailingDomainMutationResult = Apollo.MutationResult; +export type VerifyEmailingDomainMutationOptions = Apollo.BaseMutationOptions; +export const GetEmailingDomainsDocument = gql` + query GetEmailingDomains { + getEmailingDomains { + id + domain + driver + status + verifiedAt + verificationRecords { + type + key + value + priority + } + createdAt + updatedAt + } +} + `; + +/** + * __useGetEmailingDomainsQuery__ + * + * To run a query within a React component, call `useGetEmailingDomainsQuery` and pass it any options that fit your needs. + * When your component renders, `useGetEmailingDomainsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetEmailingDomainsQuery({ + * variables: { + * }, + * }); + */ +export function useGetEmailingDomainsQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetEmailingDomainsDocument, options); + } +export function useGetEmailingDomainsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetEmailingDomainsDocument, options); + } +export type GetEmailingDomainsQueryHookResult = ReturnType; +export type GetEmailingDomainsLazyQueryHookResult = ReturnType; +export type GetEmailingDomainsQueryResult = Apollo.QueryResult; export const UpdateLabPublicFeatureFlagDocument = gql` mutation UpdateLabPublicFeatureFlag($input: UpdateLabPublicFeatureFlagInput!) { updateLabPublicFeatureFlag(input: $input) { diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 53195c30ef..6ea8044763 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -488,6 +488,7 @@ export enum ConfigVariableType { export enum ConfigVariablesGroup { AnalyticsConfig = 'AnalyticsConfig', + AwsSesSettings = 'AwsSesSettings', BillingConfig = 'BillingConfig', CaptchaConfig = 'CaptchaConfig', CloudflareConfig = 'CloudflareConfig', @@ -998,6 +999,29 @@ export type EmailPasswordResetLink = { success: Scalars['Boolean']; }; +export type EmailingDomain = { + __typename?: 'EmailingDomain'; + createdAt: Scalars['DateTime']; + domain: Scalars['String']; + driver: EmailingDomainDriver; + id: Scalars['UUID']; + status: EmailingDomainStatus; + updatedAt: Scalars['DateTime']; + verificationRecords?: Maybe>; + verifiedAt?: Maybe; +}; + +export enum EmailingDomainDriver { + AWS_SES = 'AWS_SES' +} + +export enum EmailingDomainStatus { + FAILED = 'FAILED', + PENDING = 'PENDING', + TEMPORARY_FAILURE = 'TEMPORARY_FAILURE', + VERIFIED = 'VERIFIED' +} + export type ExecuteServerlessFunctionInput = { /** Id of the serverless function to execute */ id: Scalars['UUID']; @@ -1029,6 +1053,7 @@ export enum FeatureFlagKey { IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED', IS_DATABASE_EVENT_TRIGGER_ENABLED = 'IS_DATABASE_EVENT_TRIGGER_ENABLED', IS_DYNAMIC_SEARCH_FIELDS_ENABLED = 'IS_DYNAMIC_SEARCH_FIELDS_ENABLED', + IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED', IS_GROUP_BY_ENABLED = 'IS_GROUP_BY_ENABLED', IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED', IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED', @@ -1433,6 +1458,7 @@ export type Mutation = { createCoreViewSort: CoreViewSort; createDatabaseConfigVariable: Scalars['Boolean']; createDraftFromWorkflowVersion: WorkflowVersion; + createEmailingDomain: EmailingDomain; createFile: File; createOIDCIdentityProvider: SetupSsoOutput; createObjectEvent: Analytics; @@ -1460,6 +1486,7 @@ export type Mutation = { deleteCoreViewSort: Scalars['Boolean']; deleteCurrentWorkspace: Workspace; deleteDatabaseConfigVariable: Scalars['Boolean']; + deleteEmailingDomain: Scalars['Boolean']; deleteFile: File; deleteOneAgent: Agent; deleteOneField: Field; @@ -1564,6 +1591,7 @@ export type Mutation = { upsertPermissionFlags: Array; userLookupAdminPanel: UserLookup; validateApprovedAccessDomain: ApprovedAccessDomain; + verifyEmailingDomain: EmailingDomain; verifyTwoFactorAuthenticationMethodForAuthenticatedUser: VerifyTwoFactorAuthenticationMethodOutput; }; @@ -1676,6 +1704,12 @@ export type MutationCreateDraftFromWorkflowVersionArgs = { }; +export type MutationCreateEmailingDomainArgs = { + domain: Scalars['String']; + driver: EmailingDomainDriver; +}; + + export type MutationCreateFileArgs = { file: Scalars['Upload']; }; @@ -1799,6 +1833,11 @@ export type MutationDeleteDatabaseConfigVariableArgs = { }; +export type MutationDeleteEmailingDomainArgs = { + id: Scalars['String']; +}; + + export type MutationDeleteFileArgs = { fileId: Scalars['UUID']; }; @@ -2320,6 +2359,11 @@ export type MutationValidateApprovedAccessDomainArgs = { }; +export type MutationVerifyEmailingDomainArgs = { + id: Scalars['String']; +}; + + export type MutationVerifyTwoFactorAuthenticationMethodForAuthenticatedUserArgs = { otp: Scalars['String']; }; @@ -2650,6 +2694,7 @@ export type Query = { getCoreViewSorts: Array; getCoreViews: Array; getDatabaseConfigVariable: ConfigVariable; + getEmailingDomains: Array; getIndicatorHealthStatus: AdminPanelHealthServiceData; getMeteredProductsUsage: Array; getPageLayout?: Maybe; @@ -3727,6 +3772,14 @@ export type ValidatePasswordResetToken = { id: Scalars['UUID']; }; +export type VerificationRecord = { + __typename?: 'VerificationRecord'; + key: Scalars['String']; + priority?: Maybe; + type: Scalars['String']; + value: Scalars['String']; +}; + export type VerifyTwoFactorAuthenticationMethodOutput = { __typename?: 'VerifyTwoFactorAuthenticationMethodOutput'; success: Scalars['Boolean']; diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 059d623167..8840a43ca9 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -3,9 +3,9 @@ import { Route, Routes } from 'react-router-dom'; import { SettingsProtectedRouteWrapper } from '@/settings/components/SettingsProtectedRouteWrapper'; import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader'; +import { SettingPublicDomain } from '@/settings/domains/components/SettingPublicDomain'; import { SettingsPath } from 'twenty-shared/types'; import { PermissionFlagType } from '~/generated/graphql'; -import { SettingPublicDomain } from '@/settings/domains/components/SettingPublicDomain'; const SettingsGraphQLPlayground = lazy(() => import( @@ -310,6 +310,22 @@ const SettingsSecurityApprovedAccessDomain = lazy(() => ), ); +const SettingsNewEmailingDomain = lazy(() => + import('~/pages/settings/emailing-domains/SettingsNewEmailingDomain').then( + (module) => ({ + default: module.SettingsNewEmailingDomain, + }), + ), +); + +const SettingsEmailingDomainDetail = lazy(() => + import('~/pages/settings/emailing-domains/SettingsEmailingDomainDetail').then( + (module) => ({ + default: module.SettingsEmailingDomainDetail, + }), + ), +); + const SettingsAdmin = lazy(() => import('~/pages/settings/admin-panel/SettingsAdmin').then((module) => ({ default: module.SettingsAdmin, @@ -429,6 +445,14 @@ export const SettingsRoutes = ({ /> } /> } /> + } + /> + } + /> } diff --git a/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts b/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts index 262da88689..d396454768 100644 --- a/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts +++ b/packages/twenty-front/src/modules/client-config/hooks/useClientConfig.ts @@ -11,6 +11,7 @@ import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabl import { isAttachmentPreviewEnabledState } from '@/client-config/states/isAttachmentPreviewEnabledState'; import { isConfigVariablesInDbEnabledState } from '@/client-config/states/isConfigVariablesInDbEnabledState'; import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState'; +import { isEmailingDomainsEnabledState } from '@/client-config/states/isEmailingDomainsEnabledState'; import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState'; import { isGoogleCalendarEnabledState } from '@/client-config/states/isGoogleCalendarEnabledState'; import { isGoogleMessagingEnabledState } from '@/client-config/states/isGoogleMessagingEnabledState'; @@ -105,6 +106,9 @@ export const useClientConfig = (): UseClientConfigResult => { const setIsImapSmtpCaldavEnabled = useSetRecoilState( isImapSmtpCaldavEnabledState, ); + const setIsEmailingDomainsEnabled = useSetRecoilState( + isEmailingDomainsEnabledState, + ); const setAppVersion = useSetRecoilState(appVersionState); @@ -179,6 +183,7 @@ export const useClientConfig = (): UseClientConfigResult => { setCalendarBookingPageId(clientConfig?.calendarBookingPageId ?? null); setIsImapSmtpCaldavEnabled(clientConfig?.isImapSmtpCaldavEnabled); + setIsEmailingDomainsEnabled(clientConfig?.isEmailingDomainsEnabled); } catch (err) { const error = err instanceof Error ? err : new Error('Failed to fetch client config'); @@ -211,6 +216,7 @@ export const useClientConfig = (): UseClientConfigResult => { setIsEmailVerificationRequired, setIsImapSmtpCaldavEnabled, setIsMultiWorkspaceEnabled, + setIsEmailingDomainsEnabled, setLabPublicFeatureFlags, setMicrosoftCalendarEnabled, setMicrosoftMessagingEnabled, diff --git a/packages/twenty-front/src/modules/client-config/states/isEmailingDomainsEnabledState.ts b/packages/twenty-front/src/modules/client-config/states/isEmailingDomainsEnabledState.ts new file mode 100644 index 0000000000..41597cea97 --- /dev/null +++ b/packages/twenty-front/src/modules/client-config/states/isEmailingDomainsEnabledState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui/utilities'; + +export const isEmailingDomainsEnabledState = createState({ + key: 'isEmailingDomainsEnabled', + defaultValue: false, +}); diff --git a/packages/twenty-front/src/modules/client-config/types/ClientConfig.ts b/packages/twenty-front/src/modules/client-config/types/ClientConfig.ts index 1a092357a9..53b602a0f2 100644 --- a/packages/twenty-front/src/modules/client-config/types/ClientConfig.ts +++ b/packages/twenty-front/src/modules/client-config/types/ClientConfig.ts @@ -32,6 +32,7 @@ export type ClientConfig = { isMicrosoftMessagingEnabled: boolean; isMultiWorkspaceEnabled: boolean; isImapSmtpCaldavEnabled: boolean; + isEmailingDomainsEnabled: boolean; publicFeatureFlags: Array; sentry: Sentry; signInPrefilled: boolean; diff --git a/packages/twenty-front/src/modules/settings/components/SettingsDnsRecordsTable.tsx b/packages/twenty-front/src/modules/settings/components/SettingsDnsRecordsTable.tsx new file mode 100644 index 0000000000..3fedcb3e6d --- /dev/null +++ b/packages/twenty-front/src/modules/settings/components/SettingsDnsRecordsTable.tsx @@ -0,0 +1,122 @@ +import { Table } from '@/ui/layout/table/components/Table'; +import { TableCell } from '@/ui/layout/table/components/TableCell'; +import { TableHeader } from '@/ui/layout/table/components/TableHeader'; +import { TableRow } from '@/ui/layout/table/components/TableRow'; +import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; +import { capitalize, isDefined } from 'twenty-shared/utils'; +import { OverflowingTextWithTooltip, Status } from 'twenty-ui/display'; +import { type ThemeColor } from 'twenty-ui/theme'; +import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; + +type RecordStatus = { + status: string; + statusColor: ThemeColor; +}; + +type DnsRecordBase = { + type: string; + key: string; + value: string; + priority?: number | null; + ttl?: string; +}; + +type DnsRecord = DnsRecordBase | (DnsRecordBase & RecordStatus); + +type SettingsDnsRecordsTableProps = { + records: DnsRecord[]; +}; + +const StyledTableRow = styled(TableRow)` + & > * { + min-width: 0; + max-width: 100%; + overflow: hidden; + } +`; + +const StyledTableCell = styled(TableCell)` + font-family: monospace; +`; + +export const SettingsDnsRecordsTable = ({ + records, +}: SettingsDnsRecordsTableProps) => { + const { copyToClipboard } = useCopyToClipboard(); + + if (!records || records.length === 0) { + return null; + } + + const hasTtlRecords = records.some((record) => isDefined(record.ttl)); + const hasStatusRecords = records.some((record) => 'status' in record); + const hasPriorityRecords = records.some((record) => + isDefined(record.priority), + ); + + const buildGridColumns = () => { + const baseColumns = ['max-content', '1fr', '1fr']; + + if (hasPriorityRecords) baseColumns.push('max-content'); + if (hasTtlRecords) baseColumns.push('max-content'); + if (hasStatusRecords) baseColumns.push('max-content'); + + return baseColumns.join(' '); + }; + + const gridAutoColumns = buildGridColumns(); + + return ( + + + {t`Type`} + {t`Key`} + {t`Value`} + {hasPriorityRecords && ( + {t`Priority`} + )} + {hasTtlRecords && {t`TTL`}} + {hasStatusRecords && ( + {t`Status`} + )} + + + {records.map((record) => ( + + {record.type} + { + copyToClipboard(record.key || ''); + }} + > + + + + { + copyToClipboard(record.value); + }} + > + + + + {hasPriorityRecords && ( + {record.priority} + )} + {hasTtlRecords && {record.ttl}} + {hasStatusRecords && ( + + {'status' in record ? ( + + ) : null} + + )} + + ))} +
+ ); +}; diff --git a/packages/twenty-front/src/modules/settings/domains/components/SettingsDomainRecords.tsx b/packages/twenty-front/src/modules/settings/domains/components/SettingsDomainRecords.tsx index 4d884e4ea7..e809e9aaad 100644 --- a/packages/twenty-front/src/modules/settings/domains/components/SettingsDomainRecords.tsx +++ b/packages/twenty-front/src/modules/settings/domains/components/SettingsDomainRecords.tsx @@ -1,84 +1,39 @@ -import { Table } from '@/ui/layout/table/components/Table'; -import { TableBody } from '@/ui/layout/table/components/TableBody'; -import { TableCell } from '@/ui/layout/table/components/TableCell'; -import { TableHeader } from '@/ui/layout/table/components/TableHeader'; -import { TableRow } from '@/ui/layout/table/components/TableRow'; -import styled from '@emotion/styled'; -import { Button } from 'twenty-ui/input'; -import { useDebouncedCallback } from 'use-debounce'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { SettingsDnsRecordsTable } from '@/settings/components/SettingsDnsRecordsTable'; +import { useRecoilValue } from 'recoil'; +import { H2Title } from 'twenty-ui/display'; +import { Section } from 'twenty-ui/layout'; +import { type ThemeColor } from 'twenty-ui/theme'; import { type DomainRecord, type DomainValidRecords, } from '~/generated/graphql'; -import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; -import { capitalize } from 'twenty-shared/utils'; -import { useRecoilValue } from 'recoil'; -import { type ThemeColor } from 'twenty-ui/theme'; -import { Status } from 'twenty-ui/display'; -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; -import { customDomainRecordsState } from '@/settings/domains/states/customDomainRecordsState'; - -const StyledTable = styled(Table)` - border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; -`; - -const StyledTableCell = styled(TableCell)` - overflow: hidden; - display: block; - padding: 0 ${({ theme }) => theme.spacing(3)} 0 0; - - &:first-of-type { - padding-left: 0; - } - - &:last-of-type { - padding-right: 0; - } -`; - -const StyledButton = styled(Button)` - border: 1px solid ${({ theme }) => theme.border.color.medium}; - color: ${({ theme }) => theme.font.color.tertiary}; - font-weight: ${({ theme }) => theme.font.weight.regular}; - height: ${({ theme }) => theme.spacing(6)}; - overflow: hidden; - user-select: text; - width: 100%; -`; export const SettingsDomainRecords = ({ records, }: { records: DomainValidRecords['records']; }) => { - const { customDomainRecords } = useRecoilValue(customDomainRecordsState); const currentWorkspace = useRecoilValue(currentWorkspaceState); - const { copyToClipboard } = useCopyToClipboard(); - - const copyToClipboardDebounced = useDebouncedCallback( - (value: string) => copyToClipboard(value), - 200, - ); - const rowsDefinitions = [ { name: 'Domain Setup', validationType: 'redirection' as const }, { name: 'Secure Connection', validationType: 'ssl' as const }, ]; - const defaultValues: { status: string; color: ThemeColor } = - currentWorkspace?.customDomain === customDomainRecords?.domain + const defaultValues: { status: string; statusColor: ThemeColor } = + currentWorkspace?.customDomain ? { status: 'success', - color: 'green', + statusColor: 'green', } : { status: 'loading', - color: 'gray', + statusColor: 'gray', }; - const rows = rowsDefinitions.map< - { name: string; status: string; color: ThemeColor } & DomainRecord + const transformedRecords = rowsDefinitions.map< + { statusColor: ThemeColor } & DomainRecord >((row) => { const record = records.find( ({ validationType }) => validationType === row.validationType, @@ -89,55 +44,23 @@ export const SettingsDomainRecords = ({ } return { - name: row.name, - color: + statusColor: record && record.status === 'error' ? 'red' : record && record.status === 'pending' ? 'yellow' - : defaultValues.color, + : defaultValues.statusColor, ...record, }; }); return ( - - - Name - Type - Value - - - - {rows.map((row) => ( - - - copyToClipboardDebounced(row.key)} - type="button" - /> - - - copyToClipboardDebounced(row.type.toUpperCase())} - type="button" - /> - - - copyToClipboardDebounced(row.value)} - type="button" - /> - - - - - - ))} - - +
+ + +
); }; diff --git a/packages/twenty-front/src/modules/settings/emailing-domains/components/SettingsEmailingDomainRowDropdownMenu.tsx b/packages/twenty-front/src/modules/settings/emailing-domains/components/SettingsEmailingDomainRowDropdownMenu.tsx new file mode 100644 index 0000000000..dd0506bf91 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/emailing-domains/components/SettingsEmailingDomainRowDropdownMenu.tsx @@ -0,0 +1,78 @@ +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; +import { ApolloError } from '@apollo/client'; +import { t } from '@lingui/core/macro'; +import { IconDotsVertical, IconTrash } from 'twenty-ui/display'; +import { LightIconButton } from 'twenty-ui/input'; +import { MenuItem } from 'twenty-ui/navigation'; +import { + type GetEmailingDomainsQuery, + useDeleteEmailingDomainMutation, + useGetEmailingDomainsQuery, +} from '~/generated-metadata/graphql'; + +type SettingsEmailingDomainRowDropdownMenuProps = { + emailingDomain: GetEmailingDomainsQuery['getEmailingDomains'][0]; +}; + +export const SettingsEmailingDomainRowDropdownMenu = ({ + emailingDomain, +}: SettingsEmailingDomainRowDropdownMenuProps) => { + const dropdownId = `settings-emailing-domain-row-${emailingDomain.id}`; + + const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar(); + + const { closeDropdown } = useCloseDropdown(); + + const { refetch: refetchEmailingDomains } = useGetEmailingDomainsQuery(); + + const [deleteEmailingDomainMutation] = useDeleteEmailingDomainMutation(); + + const handleDeleteEmailingDomain = async () => { + try { + await deleteEmailingDomainMutation({ + variables: { + id: emailingDomain.id, + }, + }); + + enqueueSuccessSnackBar({ + message: t`Emailing domain deleted successfully`, + }); + + await refetchEmailingDomains(); + } catch (error) { + enqueueErrorSnackBar({ + ...(error instanceof ApolloError ? { apolloError: error } : {}), + }); + } + }; + + return ( + + } + dropdownComponents={ + + + { + handleDeleteEmailingDomain(); + closeDropdown(dropdownId); + }} + /> + + + } + /> + ); +}; diff --git a/packages/twenty-front/src/modules/settings/emailing-domains/components/SettingsEmailingDomainVerificationRecords.tsx b/packages/twenty-front/src/modules/settings/emailing-domains/components/SettingsEmailingDomainVerificationRecords.tsx new file mode 100644 index 0000000000..90c22ccfa7 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/emailing-domains/components/SettingsEmailingDomainVerificationRecords.tsx @@ -0,0 +1,68 @@ +import { SettingsDnsRecordsTable } from '@/settings/components/SettingsDnsRecordsTable'; + +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { ApolloError } from '@apollo/client'; +import { t } from '@lingui/core/macro'; +import { H2Title, IconRefresh } from 'twenty-ui/display'; +import { Button } from 'twenty-ui/input'; + +import { Section } from 'twenty-ui/layout'; +import { + type EmailingDomain, + useVerifyEmailingDomainMutation, +} from '~/generated-metadata/graphql'; + +type SettingsEmailingDomainVerificationRecordsProps = { + domain: EmailingDomain; +}; + +export const SettingsEmailingDomainVerificationRecords = ({ + domain, +}: SettingsEmailingDomainVerificationRecordsProps) => { + const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); + + const [verifyEmailingDomainMutation, { loading: isVerifying }] = + useVerifyEmailingDomainMutation(); + + if (!domain.verificationRecords || domain.verificationRecords.length === 0) { + return null; + } + + const handleVerifyEmailingDomain = async () => { + try { + await verifyEmailingDomainMutation({ + variables: { + id: domain.id, + }, + }); + enqueueSuccessSnackBar({ + message: t`Started verification process`, + }); + } catch (error) { + enqueueErrorSnackBar({ + ...(error instanceof ApolloError ? { apolloError: error } : {}), + }); + } + }; + + return ( +
+ + } + /> + +
+ ); +}; diff --git a/packages/twenty-front/src/modules/settings/emailing-domains/graphql/mutations/createEmailingDomain.ts b/packages/twenty-front/src/modules/settings/emailing-domains/graphql/mutations/createEmailingDomain.ts new file mode 100644 index 0000000000..f434b887ea --- /dev/null +++ b/packages/twenty-front/src/modules/settings/emailing-domains/graphql/mutations/createEmailingDomain.ts @@ -0,0 +1,24 @@ +import { gql } from '@apollo/client'; + +export const CREATE_EMAILING_DOMAIN = gql` + mutation CreateEmailingDomain( + $domain: String! + $driver: EmailingDomainDriver! + ) { + createEmailingDomain(domain: $domain, driver: $driver) { + id + domain + driver + status + verifiedAt + verificationRecords { + type + key + value + priority + } + createdAt + updatedAt + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/emailing-domains/graphql/mutations/deleteEmailingDomain.ts b/packages/twenty-front/src/modules/settings/emailing-domains/graphql/mutations/deleteEmailingDomain.ts new file mode 100644 index 0000000000..98523e9260 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/emailing-domains/graphql/mutations/deleteEmailingDomain.ts @@ -0,0 +1,7 @@ +import { gql } from '@apollo/client'; + +export const DELETE_EMAILING_DOMAIN = gql` + mutation DeleteEmailingDomain($id: String!) { + deleteEmailingDomain(id: $id) + } +`; diff --git a/packages/twenty-front/src/modules/settings/emailing-domains/graphql/mutations/verifyEmailingDomain.ts b/packages/twenty-front/src/modules/settings/emailing-domains/graphql/mutations/verifyEmailingDomain.ts new file mode 100644 index 0000000000..b0a4858623 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/emailing-domains/graphql/mutations/verifyEmailingDomain.ts @@ -0,0 +1,15 @@ +import { gql } from '@apollo/client'; + +export const VERIFY_EMAILING_DOMAIN = gql` + mutation VerifyEmailingDomain($id: String!) { + verifyEmailingDomain(id: $id) { + id + domain + driver + status + verifiedAt + createdAt + updatedAt + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/emailing-domains/graphql/queries/getAllEmailingDomains.ts b/packages/twenty-front/src/modules/settings/emailing-domains/graphql/queries/getAllEmailingDomains.ts new file mode 100644 index 0000000000..5d6d800b8d --- /dev/null +++ b/packages/twenty-front/src/modules/settings/emailing-domains/graphql/queries/getAllEmailingDomains.ts @@ -0,0 +1,21 @@ +import { gql } from '@apollo/client'; + +export const GET_ALL_EMAILING_DOMAINS = gql` + query GetEmailingDomains { + getEmailingDomains { + id + domain + driver + status + verifiedAt + verificationRecords { + type + key + value + priority + } + createdAt + updatedAt + } + } +`; diff --git a/packages/twenty-front/src/pages/settings/domains/SettingsDomains.tsx b/packages/twenty-front/src/pages/settings/domains/SettingsDomains.tsx index fbbba80847..7f795d7f8b 100644 --- a/packages/twenty-front/src/pages/settings/domains/SettingsDomains.tsx +++ b/packages/twenty-front/src/pages/settings/domains/SettingsDomains.tsx @@ -2,16 +2,18 @@ import styled from '@emotion/styled'; import { Trans, useLingui } from '@lingui/react/macro'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; +import { SettingsPublicDomainsListCard } from '@/settings/domains/components/SettingsPublicDomainsListCard'; import { SettingsWorkspaceDomainCard } from '@/settings/domains/components/SettingsWorkspaceDomainCard'; import { SettingsApprovedAccessDomainsListCard } from '@/settings/security/components/approvedAccessDomains/SettingsApprovedAccessDomainsListCard'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { SettingsPath } from 'twenty-shared/types'; import { getSettingsPath } from 'twenty-shared/utils'; import { H2Title } from 'twenty-ui/display'; import { Section } from 'twenty-ui/layout'; -import { SettingsPublicDomainsListCard } from '@/settings/domains/components/SettingsPublicDomainsListCard'; -import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap'; import { FeatureFlagKey } from '~/generated/graphql'; +import { SettingsEmailingDomains } from '~/pages/settings/emailing-domains/SettingsEmailingDomains'; const StyledMainContent = styled.div` display: flex; @@ -31,6 +33,10 @@ export const SettingsDomains = () => { const isPublicDomainEnabled = featureFlags[FeatureFlagKey.IS_PUBLIC_DOMAIN_ENABLED]; + const isEmailingDomainsEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_EMAILING_DOMAIN_ENABLED, + ); + return ( { /> + {isEmailingDomainsEnabled && ( + + + + + )} {isPublicDomainEnabled && ( { + const { domainId } = useParams<{ domainId: string }>(); + + const { data, loading, error } = useQuery( + GET_ALL_EMAILING_DOMAINS, + { + skip: !domainId, + }, + ); + + const emailingDomain = data?.getEmailingDomains?.find( + (domain) => domain.id === domainId, + ); + + if (loading) { + return
Loading...
; + } + + if (isDefined(error) || !isDefined(emailingDomain)) { + return Domain not found; + } + + return ( + Workspace, + href: getSettingsPath(SettingsPath.Workspace), + }, + { + children: Emailing Domains, + href: getSettingsPath(SettingsPath.Domains), + }, + { children: emailingDomain.domain }, + ]} + > + + {emailingDomain.verificationRecords && + emailingDomain.verificationRecords.length > 0 && ( + + )} + + + ); +}; diff --git a/packages/twenty-front/src/pages/settings/emailing-domains/SettingsEmailingDomains.tsx b/packages/twenty-front/src/pages/settings/emailing-domains/SettingsEmailingDomains.tsx new file mode 100644 index 0000000000..69a29de757 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/emailing-domains/SettingsEmailingDomains.tsx @@ -0,0 +1,77 @@ +import { Link, useNavigate } from 'react-router-dom'; + +import { SettingsCard } from '@/settings/components/SettingsCard'; +import { SettingsListCard } from '@/settings/components/SettingsListCard'; + +import { SettingsEmailingDomainRowDropdownMenu } from '@/settings/emailing-domains/components/SettingsEmailingDomainRowDropdownMenu'; + +import styled from '@emotion/styled'; +import { useLingui } from '@lingui/react/macro'; +import { useRecoilValue } from 'recoil'; +import { SettingsPath } from 'twenty-shared/types'; +import { getSettingsPath } from 'twenty-shared/utils'; +import { IconMail, Status } from 'twenty-ui/display'; +import { useGetEmailingDomainsQuery } from '~/generated-metadata/graphql'; +import { dateLocaleState } from '~/localization/states/dateLocaleState'; +import { getColorByEmailingDomainStatus } from '~/pages/settings/emailing-domains/utils/getEmailingDomainStatusColor'; +import { getTextByEmailingDomainStatus } from '~/pages/settings/emailing-domains/utils/getEmailingDomainStatusText'; +import { beautifyPastDateRelativeToNow } from '~/utils/date-utils'; + +const StyledLink = styled(Link)` + text-decoration: none; +`; + +export const SettingsEmailingDomains = () => { + const { t } = useLingui(); + const { localeCatalog } = useRecoilValue(dateLocaleState); + const navigate = useNavigate(); + + const { data, loading: isLoading } = useGetEmailingDomainsQuery(); + const emailingDomains = data?.getEmailingDomains ?? []; + + const getItemDescription = (createdAt: string) => { + const beautifyPastDateRelative = beautifyPastDateRelativeToNow( + createdAt, + localeCatalog, + ); + return t`Added ${beautifyPastDateRelative}`; + }; + + return isLoading || !emailingDomains.length ? ( + + } /> + + ) : ( + <> + domain ?? ''} + getItemDescription={({ createdAt }) => getItemDescription(createdAt)} + RowIcon={IconMail} + onRowClick={(emailingDomain) => { + navigate( + getSettingsPath(SettingsPath.EmailingDomainDetail, { + domainId: emailingDomain.id, + }), + ); + }} + RowRightComponent={({ item: emailingDomain }) => ( + <> + + + + )} + hasFooter + footerButtonLabel="Add Emailing Domain" + onFooterButtonClick={() => + navigate(getSettingsPath(SettingsPath.NewEmailingDomain)) + } + /> + + ); +}; diff --git a/packages/twenty-front/src/pages/settings/emailing-domains/SettingsNewEmailingDomain.tsx b/packages/twenty-front/src/pages/settings/emailing-domains/SettingsNewEmailingDomain.tsx new file mode 100644 index 0000000000..fe41ab704b --- /dev/null +++ b/packages/twenty-front/src/pages/settings/emailing-domains/SettingsNewEmailingDomain.tsx @@ -0,0 +1,157 @@ +import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; +import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { ApolloError } from '@apollo/client'; +import { Trans, useLingui } from '@lingui/react/macro'; +import { useState } from 'react'; +import { SettingsPath } from 'twenty-shared/types'; +import { getSettingsPath, isDefined } from 'twenty-shared/utils'; +import { H2Title } from 'twenty-ui/display'; +import { Section } from 'twenty-ui/layout'; +import { + EmailingDomainDriver, + useCreateEmailingDomainMutation, +} from '~/generated-metadata/graphql'; +import { useNavigateSettings } from '~/hooks/useNavigateSettings'; +import { + settingsEmailingDomainFormSchema, + type SettingsEmailingDomainFormValues, +} from '~/pages/settings/emailing-domains/validation-schemas/settingsEmailingDomainFormSchema'; + +type FieldErrors = Partial< + Record +>; +export const SettingsNewEmailingDomain = () => { + const navigate = useNavigateSettings(); + const { t } = useLingui(); + const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); + + const [formValues, setFormValues] = + useState({ + driver: EmailingDomainDriver.AWS_SES, + domain: '', + }); + + const [fieldErrors, setFieldErrors] = useState({}); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [createEmailingDomain] = useCreateEmailingDomainMutation(); + + const validateForm = (): boolean => { + const result = settingsEmailingDomainFormSchema.safeParse(formValues); + + if (!result.success) { + setFieldErrors(result.error?.flatten().fieldErrors as FieldErrors); + return false; + } + + setFieldErrors({}); + return true; + }; + + const handleFieldChange = ( + field: keyof SettingsEmailingDomainFormValues, + value: string | EmailingDomainDriver, + ) => { + setFormValues((prev) => ({ + ...prev, + [field]: value, + })); + + if (isDefined(fieldErrors[field])) { + setFieldErrors((prev) => ({ + ...prev, + [field]: undefined, + })); + } + }; + + const canSave = !isSubmitting; + + const handleSave = async () => { + if (!validateForm()) { + return; + } + + setIsSubmitting(true); + + try { + await createEmailingDomain({ + variables: { + domain: formValues.domain, + driver: formValues.driver, + }, + onCompleted: (data) => { + enqueueSuccessSnackBar({ + message: t`Emailing domain created successfully. Please verify the domain to start using it.`, + }); + if (!data.createEmailingDomain?.id) return; + + navigate(SettingsPath.EmailingDomainDetail, { + domainId: data.createEmailingDomain.id, + }); + }, + onError: (error) => { + enqueueErrorSnackBar({ + apolloError: error instanceof ApolloError ? error : undefined, + }); + }, + }); + } catch (error) { + enqueueErrorSnackBar({ + apolloError: error instanceof ApolloError ? error : undefined, + }); + } finally { + setIsSubmitting(false); + } + }; + + return ( + navigate(SettingsPath.Domains)} + onSave={handleSave} + isSaveDisabled={!canSave} + /> + } + links={[ + { + children: Workspace, + href: getSettingsPath(SettingsPath.Workspace), + }, + { + children: Domains, + href: getSettingsPath(SettingsPath.Domains), + }, + { + children: Emailing Domains, + href: getSettingsPath(SettingsPath.Domains), + }, + { children: New Emailing Domain }, + ]} + > + +
+ + handleFieldChange('domain', value)} + fullWidth + placeholder="yourdomain.com" + error={fieldErrors.domain} + /> +
+
+
+ ); +}; diff --git a/packages/twenty-front/src/pages/settings/emailing-domains/utils/getEmailingDomainStatusColor.ts b/packages/twenty-front/src/pages/settings/emailing-domains/utils/getEmailingDomainStatusColor.ts new file mode 100644 index 0000000000..c149dd89ed --- /dev/null +++ b/packages/twenty-front/src/pages/settings/emailing-domains/utils/getEmailingDomainStatusColor.ts @@ -0,0 +1,18 @@ +import { EmailingDomainStatus } from '~/generated/graphql'; + +export const getColorByEmailingDomainStatus = ( + status: EmailingDomainStatus, +) => { + switch (status) { + case EmailingDomainStatus.VERIFIED: + return 'turquoise'; + case EmailingDomainStatus.PENDING: + return 'orange'; + case EmailingDomainStatus.TEMPORARY_FAILURE: + return 'orange'; + case EmailingDomainStatus.FAILED: + return 'red'; + default: + return 'gray'; + } +}; diff --git a/packages/twenty-front/src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts b/packages/twenty-front/src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts new file mode 100644 index 0000000000..ab539f678f --- /dev/null +++ b/packages/twenty-front/src/pages/settings/emailing-domains/utils/getEmailingDomainStatusText.ts @@ -0,0 +1,17 @@ +import { t } from '@lingui/core/macro'; +import { EmailingDomainStatus } from '~/generated/graphql'; + +export const getTextByEmailingDomainStatus = (status: EmailingDomainStatus) => { + switch (status) { + case EmailingDomainStatus.VERIFIED: + return t`Verified`; + case EmailingDomainStatus.PENDING: + return t`Pending`; + case EmailingDomainStatus.TEMPORARY_FAILURE: + return t`Temporary Failure`; + case EmailingDomainStatus.FAILED: + return t`Failed`; + default: + return t`Unknown`; + } +}; diff --git a/packages/twenty-front/src/pages/settings/emailing-domains/validation-schemas/settingsEmailingDomainFormSchema.ts b/packages/twenty-front/src/pages/settings/emailing-domains/validation-schemas/settingsEmailingDomainFormSchema.ts new file mode 100644 index 0000000000..5dcc072aba --- /dev/null +++ b/packages/twenty-front/src/pages/settings/emailing-domains/validation-schemas/settingsEmailingDomainFormSchema.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; +import { EmailingDomainDriver } from '~/generated-metadata/graphql'; + +export const settingsEmailingDomainFormSchema = z.object({ + domain: z + .string() + .min(1, 'Domain is required') + .regex( + /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])\.[a-zA-Z]{2,}$/, + 'Invalid domain format. Please enter a valid domain name.', + ) + .max(256, 'Domain must be less than 256 characters.'), + driver: z.nativeEnum(EmailingDomainDriver), +}); + +export type SettingsEmailingDomainFormValues = z.infer< + typeof settingsEmailingDomainFormSchema +>; diff --git a/packages/twenty-front/src/testing/mock-data/config.ts b/packages/twenty-front/src/testing/mock-data/config.ts index b7af81ce5b..e92854299a 100644 --- a/packages/twenty-front/src/testing/mock-data/config.ts +++ b/packages/twenty-front/src/testing/mock-data/config.ts @@ -56,4 +56,5 @@ export const mockedClientConfig: ClientConfig = { isConfigVariablesInDbEnabled: false, isImapSmtpCaldavEnabled: false, isTwoFactorAuthenticationEnabled: false, + isEmailingDomainsEnabled: false, }; diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index dc8ba1e168..765c521530 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -21,6 +21,7 @@ "@ai-sdk/xai": "^2.0.19", "@aws-sdk/client-lambda": "3.825.0", "@aws-sdk/client-s3": "3.825.0", + "@aws-sdk/client-sesv2": "^3.888.0", "@aws-sdk/client-sts": "3.825.0", "@aws-sdk/credential-providers": "3.825.0", "@babel/preset-env": "7.26.9", diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1758388517321-createEmailingDomainEntity.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1758388517321-createEmailingDomainEntity.ts new file mode 100644 index 0000000000..e3139be253 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1758388517321-createEmailingDomainEntity.ts @@ -0,0 +1,31 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class CreateEmailingDomainEntity1758388517321 + implements MigrationInterface +{ + name = 'CreateEmailingDomainEntity1758388517321'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "core"."emailingDomain_driver_enum" AS ENUM('AWS_SES')`, + ); + await queryRunner.query( + `CREATE TYPE "core"."emailingDomain_status_enum" AS ENUM('PENDING', 'VERIFIED', 'FAILED', 'TEMPORARY_FAILURE')`, + ); + await queryRunner.query( + `CREATE TABLE "core"."emailingDomain" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "domain" character varying NOT NULL, "driver" "core"."emailingDomain_driver_enum" NOT NULL, "status" "core"."emailingDomain_status_enum" NOT NULL DEFAULT 'PENDING', "verificationRecords" jsonb, "verifiedAt" TIMESTAMP WITH TIME ZONE, "workspaceId" uuid NOT NULL, CONSTRAINT "IDX_EMAILING_DOMAIN_DOMAIN_WORKSPACE_ID_UNIQUE" UNIQUE ("domain", "workspaceId"), CONSTRAINT "PK_dca7032537b5d307f8cc6d74f1d" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "core"."emailingDomain" ADD CONSTRAINT "FK_793a938bef2aae0a2129f78951f" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."emailingDomain" DROP CONSTRAINT "FK_793a938bef2aae0a2129f78951f"`, + ); + await queryRunner.query(`DROP TABLE "core"."emailingDomain"`); + await queryRunner.query(`DROP TYPE "core"."emailingDomain_status_enum"`); + await queryRunner.query(`DROP TYPE "core"."emailingDomain_driver_enum"`); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts index 38eeead330..8f17e42bf0 100644 --- a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts +++ b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts @@ -20,6 +20,7 @@ import { captchaModuleFactory } from 'src/engine/core-modules/captcha/captcha.mo import { CloudflareModule } from 'src/engine/core-modules/cloudflare/cloudflare.module'; import { DnsManagerModule } from 'src/engine/core-modules/dns-manager/dns-manager.module'; import { EmailModule } from 'src/engine/core-modules/email/email.module'; +import { EmailingDomainModule } from 'src/engine/core-modules/emailing-domain/emailing-domain.module'; import { ExceptionHandlerModule } from 'src/engine/core-modules/exception-handler/exception-handler.module'; import { exceptionHandlerModuleFactory } from 'src/engine/core-modules/exception-handler/exception-handler.module-factory'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; @@ -83,6 +84,7 @@ import { FileModule } from './file/file.module'; WorkspaceInvitationModule, WorkspaceSSOModule, ApprovedAccessDomainModule, + EmailingDomainModule, PublicDomainModule, CloudflareModule, DnsManagerModule, diff --git a/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/aws-ses/providers/aws-ses-client.provider.ts b/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/aws-ses/providers/aws-ses-client.provider.ts new file mode 100644 index 0000000000..b80011882e --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/aws-ses/providers/aws-ses-client.provider.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@nestjs/common'; + +import { + SESv2Client as SESClient, + type SESv2ClientConfig as SESClientConfig, +} from '@aws-sdk/client-sesv2'; + +import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; + +@Injectable() +export class AwsSesClientProvider { + private sesClient: SESClient | null = null; + + constructor(private readonly twentyConfigService: TwentyConfigService) {} + + public getSESClient(): SESClient { + if (!this.sesClient) { + const config: SESClientConfig = { + region: this.twentyConfigService.get('AWS_SES_REGION'), + }; + + const accessKeyId = this.twentyConfigService.get('AWS_SES_ACCESS_KEY_ID'); + const secretAccessKey = this.twentyConfigService.get( + 'AWS_SES_SECRET_ACCESS_KEY', + ); + const sessionToken = this.twentyConfigService.get( + 'AWS_SES_SESSION_TOKEN', + ); + + if (accessKeyId && secretAccessKey && sessionToken) { + config.credentials = { + accessKeyId, + secretAccessKey, + sessionToken, + }; + } + + this.sesClient = new SESClient(config); + } + + return this.sesClient; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-driver.service.ts b/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-driver.service.ts new file mode 100644 index 0000000000..ecc23b455a --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-driver.service.ts @@ -0,0 +1,263 @@ +import { Logger } from '@nestjs/common'; + +import { + CreateEmailIdentityCommand, + CreateTenantCommand, + CreateTenantResourceAssociationCommand, + GetEmailIdentityCommand, + PutEmailIdentityDkimAttributesCommand, +} from '@aws-sdk/client-sesv2'; + +import { type AwsSesDriverConfig } from 'src/engine/core-modules/emailing-domain/drivers/interfaces/driver-config.interface'; +import { + type DomainStatusInput, + type DomainVerificationInput, + type EmailingDomainDriverInterface, + type EmailingDomainVerificationResult, +} from 'src/engine/core-modules/emailing-domain/drivers/interfaces/emailing-domain-driver.interface'; + +import { type AwsSesClientProvider } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/providers/aws-ses-client.provider'; +import { type AwsSesHandleErrorService } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-handle-error.service'; +import { EmailingDomainStatus } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain'; +import { type VerificationRecord } from 'src/engine/core-modules/emailing-domain/dtos/verification-record.dto'; + +export class AwsSesDriver implements EmailingDomainDriverInterface { + private readonly logger = new Logger(AwsSesDriver.name); + + constructor( + private readonly config: AwsSesDriverConfig, + private readonly awsSesClientProvider: AwsSesClientProvider, + private readonly awsSesHandleErrorService: AwsSesHandleErrorService, + ) {} + + async verifyDomain( + input: DomainVerificationInput, + ): Promise { + try { + this.logger.log(`Starting domain verification for: ${input.domain}`); + + const tenantName = this.generateTenantName(input.workspaceId); + + await this.ensureTenantExists(tenantName); + + const { isVerified, verificationRecords } = + await this.createOrUpdateEmailIdentity(input.domain, tenantName); + + if (isVerified) { + await this.enableDkimSigning(input.domain); + } + + return { + status: isVerified + ? EmailingDomainStatus.VERIFIED + : EmailingDomainStatus.PENDING, + verifiedAt: isVerified ? new Date() : null, + verificationRecords, + }; + } catch (error) { + this.logger.error(`Failed to verify domain ${input.domain}: ${error}`); + this.awsSesHandleErrorService.handleAwsSesError(error, 'verifyDomain'); + } + } + + async getDomainStatus( + input: DomainStatusInput, + ): Promise { + try { + this.logger.log(`Getting domain status for: ${input.domain}`); + + const sesClient = this.awsSesClientProvider.getSESClient(); + + const getIdentityCommand = new GetEmailIdentityCommand({ + EmailIdentity: input.domain, + }); + + const identityResponse = await sesClient.send(getIdentityCommand); + + const status = this.determineVerificationStatus(identityResponse); + const isFullyVerified = status === EmailingDomainStatus.VERIFIED; + const verificationRecords = this.buildVerificationRecords( + input.domain, + identityResponse.DkimAttributes?.Tokens || [], + ); + + return { + status, + verifiedAt: isFullyVerified ? new Date() : null, + verificationRecords, + }; + } catch (error) { + if (error.name === 'NotFoundException') { + return { + status: EmailingDomainStatus.FAILED, + verifiedAt: null, + verificationRecords: [], + }; + } + + this.logger.error( + `Failed to get domain status ${input.domain}: ${error}`, + ); + this.awsSesHandleErrorService.handleAwsSesError(error, 'getDomainStatus'); + } + } + + private generateTenantName(workspaceId: string): string { + return `twenty-workspace-${workspaceId}`; + } + + private async ensureTenantExists(tenantName: string): Promise { + const sesClient = this.awsSesClientProvider.getSESClient(); + + try { + await sesClient.send(new CreateTenantCommand({ TenantName: tenantName })); + this.logger.log(`Created tenant: ${tenantName}`); + } catch (error) { + if (error.name === 'AlreadyExistsException') { + this.logger.log(`Tenant already exists: ${tenantName}`); + + return; + } + throw error; + } + } + + private async createOrUpdateEmailIdentity( + domain: string, + tenantName: string, + ): Promise<{ + isVerified: boolean; + verificationRecords: VerificationRecord[]; + }> { + const sesClient = this.awsSesClientProvider.getSESClient(); + + try { + const getIdentityCommand = new GetEmailIdentityCommand({ + EmailIdentity: domain, + }); + const existingIdentity = await sesClient.send(getIdentityCommand); + + const isVerified = existingIdentity.VerifiedForSendingStatus === true; + const verificationRecords = this.buildVerificationRecords( + domain, + existingIdentity.DkimAttributes?.Tokens || [], + ); + + if (!isVerified) { + await this.associateResourceWithTenant(domain, tenantName); + } + + return { isVerified, verificationRecords }; + } catch (error) { + if (error.name === 'NotFoundException') { + return await this.createNewEmailIdentity(domain, tenantName); + } + throw error; + } + } + + private async createNewEmailIdentity( + domain: string, + tenantName: string, + ): Promise<{ + isVerified: boolean; + verificationRecords: VerificationRecord[]; + }> { + const sesClient = this.awsSesClientProvider.getSESClient(); + + const createCommand = new CreateEmailIdentityCommand({ + EmailIdentity: domain, + Tags: [{ Key: 'Tenant', Value: tenantName }], + }); + + const createResponse = await sesClient.send(createCommand); + const dkimTokens = createResponse.DkimAttributes?.Tokens || []; + + await this.associateResourceWithTenant(domain, tenantName); + + const verificationRecords = this.buildVerificationRecords( + domain, + dkimTokens, + ); + + return { + isVerified: false, + verificationRecords, + }; + } + + private async associateResourceWithTenant( + domain: string, + tenantName: string, + ): Promise { + const sesClient = this.awsSesClientProvider.getSESClient(); + + try { + await sesClient.send( + new CreateTenantResourceAssociationCommand({ + TenantName: tenantName, + ResourceArn: `arn:aws:ses:${this.config.region}:${this.config.accountId}:identity/${domain}`, + }), + ); + this.logger.log(`Associated domain ${domain} with tenant ${tenantName}`); + } catch (error) { + if (error.name === 'AlreadyExistsException') { + this.logger.log( + `Domain ${domain} already associated with tenant ${tenantName}`, + ); + + return; + } + throw error; + } + } + + private async enableDkimSigning(domain: string): Promise { + const sesClient = this.awsSesClientProvider.getSESClient(); + + const dkimCommand = new PutEmailIdentityDkimAttributesCommand({ + EmailIdentity: domain, + SigningEnabled: true, + }); + + await sesClient.send(dkimCommand); + this.logger.log(`Enabled DKIM signing for domain: ${domain}`); + } + + private buildVerificationRecords( + domain: string, + dkimTokens: string[], + ): VerificationRecord[] { + return dkimTokens.map((token) => ({ + type: 'CNAME' as const, + key: `${token}._domainkey.${domain}`, + value: `${token}.dkim.amazonses.com`, + })); + } + + private determineVerificationStatus(identityResponse: { + VerifiedForSendingStatus?: boolean; + DkimAttributes?: { + SigningEnabled?: boolean; + Status?: string; + }; + }): EmailingDomainStatus { + const isVerified = identityResponse.VerifiedForSendingStatus === true; + const isDkimEnabled = + identityResponse.DkimAttributes?.SigningEnabled === true; + const dkimStatus = identityResponse.DkimAttributes?.Status; + + if (isVerified && isDkimEnabled && dkimStatus === 'SUCCESS') { + return EmailingDomainStatus.VERIFIED; + } + + if ( + identityResponse.VerifiedForSendingStatus === false || + dkimStatus === 'FAILED' + ) { + return EmailingDomainStatus.FAILED; + } + + return EmailingDomainStatus.PENDING; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-handle-error.service.ts b/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-handle-error.service.ts new file mode 100644 index 0000000000..53fe1b0fee --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-handle-error.service.ts @@ -0,0 +1,98 @@ +import { Injectable } from '@nestjs/common'; + +import { type AwsSesError } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/types/aws-ses-error.type'; +import { + EmailingDomainDriverException, + EmailingDomainDriverExceptionCode, +} from 'src/engine/core-modules/emailing-domain/drivers/exceptions/emailing-domain-driver.exception'; + +@Injectable() +export class AwsSesHandleErrorService { + public handleAwsSesError(error: AwsSesError, context?: string): never { + const name = error?.name ?? 'UnknownError'; + const message = error?.message ?? 'No message'; + const httpStatus = error?.$metadata?.httpStatusCode; + const suffix = context ? ` (${context})` : ''; + + if (this.isTemporary(name, httpStatus)) { + throw new EmailingDomainDriverException( + `AWS SES temporary error${suffix}: ${message}`, + EmailingDomainDriverExceptionCode.TEMPORARY_ERROR, + ); + } + + if (this.isInsufficientPermissions(name, httpStatus)) { + throw new EmailingDomainDriverException( + `AWS SES insufficient permissions${suffix}: ${message}`, + EmailingDomainDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + } + + if (this.isConfigurationError(name, httpStatus)) { + throw new EmailingDomainDriverException( + `AWS SES configuration error${suffix}: ${message}`, + EmailingDomainDriverExceptionCode.CONFIGURATION_ERROR, + ); + } + + throw new EmailingDomainDriverException( + `AWS SES error${suffix}: ${name} - ${message}`, + EmailingDomainDriverExceptionCode.UNKNOWN, + ); + } + + private isTemporary(name: string, httpStatus?: number): boolean { + if (httpStatus && httpStatus >= 500) { + return true; + } + + if ( + name === 'ThrottlingException' || + name === 'ServiceUnavailable' || + name === 'InternalFailure' || + name === 'RequestTimeout' || + name === 'TooManyRequestsException' + ) { + return true; + } + + return false; + } + + private isInsufficientPermissions( + name: string, + httpStatus?: number, + ): boolean { + if (httpStatus === 403) { + return true; + } + + if ( + name === 'AccessDeniedException' || + name === 'AccountSuspendedException' + ) { + return true; + } + + return false; + } + + private isConfigurationError(name: string, httpStatus?: number): boolean { + if (httpStatus === 400) { + return true; + } + + if ( + name === 'InvalidParameterValue' || + name === 'InvalidParameterCombination' || + name === 'MissingParameter' || + name === 'MessageRejected' || + name === 'MailFromDomainNotVerifiedException' || + name === 'FromEmailAddressNotVerifiedException' + ) { + return true; + } + + return false; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/aws-ses/types/aws-ses-error.type.ts b/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/aws-ses/types/aws-ses-error.type.ts new file mode 100644 index 0000000000..712565d252 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/aws-ses/types/aws-ses-error.type.ts @@ -0,0 +1,8 @@ +export type AwsSesError = { + name?: string; + message?: string; + $metadata?: { + httpStatusCode?: number; + requestId?: string; + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/emailing-domain-driver.factory.ts b/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/emailing-domain-driver.factory.ts new file mode 100644 index 0000000000..31d244b982 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/emailing-domain-driver.factory.ts @@ -0,0 +1,75 @@ +import { Injectable } from '@nestjs/common'; + +import { type AwsSesDriverConfig } from 'src/engine/core-modules/emailing-domain/drivers/interfaces/driver-config.interface'; +import { type EmailingDomainDriverInterface } from 'src/engine/core-modules/emailing-domain/drivers/interfaces/emailing-domain-driver.interface'; + +import { AwsSesClientProvider } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/providers/aws-ses-client.provider'; +import { AwsSesDriver } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-driver.service'; +import { AwsSesHandleErrorService } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-handle-error.service'; +import { EmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain'; +import { DriverFactoryBase } from 'src/engine/core-modules/twenty-config/dynamic-factory.base'; +import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum'; +import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; + +@Injectable() +export class EmailingDomainDriverFactory extends DriverFactoryBase { + constructor( + twentyConfigService: TwentyConfigService, + private readonly awsSesClientProvider: AwsSesClientProvider, + private readonly awsSesHandleErrorService: AwsSesHandleErrorService, + ) { + super(twentyConfigService); + } + + protected buildConfigKey(): string { + const driver = EmailingDomainDriver.AWS_SES; + + if (driver === EmailingDomainDriver.AWS_SES) { + const awsConfigHash = this.getConfigGroupHash( + ConfigVariablesGroup.AwsSesSettings, + ); + + return `aws-ses|${awsConfigHash}`; + } + + throw new Error(`Unsupported emailing domain driver: ${driver}`); + } + + protected createDriver(): EmailingDomainDriverInterface { + const driver = EmailingDomainDriver.AWS_SES; + + switch (driver) { + case EmailingDomainDriver.AWS_SES: { + const region = this.twentyConfigService.get('AWS_SES_REGION'); + const accountId = this.twentyConfigService.get('AWS_SES_ACCOUNT_ID'); + const accessKeyId = this.twentyConfigService.get( + 'AWS_SES_ACCESS_KEY_ID', + ); + const secretAccessKey = this.twentyConfigService.get( + 'AWS_SES_SECRET_ACCESS_KEY', + ); + const sessionToken = this.twentyConfigService.get( + 'AWS_SES_SESSION_TOKEN', + ); + + const awsConfig: AwsSesDriverConfig = { + driver: EmailingDomainDriver.AWS_SES, + region, + accountId, + accessKeyId, + secretAccessKey, + sessionToken, + }; + + return new AwsSesDriver( + awsConfig, + this.awsSesClientProvider, + this.awsSesHandleErrorService, + ); + } + + default: + throw new Error(`Invalid emailing domain driver: ${driver}`); + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/exceptions/emailing-domain-driver.exception.ts b/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/exceptions/emailing-domain-driver.exception.ts new file mode 100644 index 0000000000..3fa2b2316b --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/exceptions/emailing-domain-driver.exception.ts @@ -0,0 +1,11 @@ +import { CustomException } from 'src/utils/custom-exception'; + +export class EmailingDomainDriverException extends CustomException {} + +export enum EmailingDomainDriverExceptionCode { + NOT_FOUND = 'NOT_FOUND', + TEMPORARY_ERROR = 'TEMPORARY_ERROR', + INSUFFICIENT_PERMISSIONS = 'INSUFFICIENT_PERMISSIONS', + CONFIGURATION_ERROR = 'CONFIGURATION_ERROR', + UNKNOWN = 'UNKNOWN', +} diff --git a/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/interfaces/driver-config.interface.ts b/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/interfaces/driver-config.interface.ts new file mode 100644 index 0000000000..98376ff3b2 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/interfaces/driver-config.interface.ts @@ -0,0 +1,14 @@ +import { type EmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain'; + +export interface BaseDriverConfig { + driver: EmailingDomainDriver; +} + +export interface AwsSesDriverConfig extends BaseDriverConfig { + driver: EmailingDomainDriver.AWS_SES; + region: string; + accountId: string; + accessKeyId?: string; + secretAccessKey?: string; + sessionToken?: string; +} diff --git a/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/interfaces/emailing-domain-driver.interface.ts b/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/interfaces/emailing-domain-driver.interface.ts new file mode 100644 index 0000000000..9bae0a5dca --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/interfaces/emailing-domain-driver.interface.ts @@ -0,0 +1,27 @@ +import { type EmailingDomainStatus } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain'; +import { type VerificationRecord } from 'src/engine/core-modules/emailing-domain/drivers/types/verifications-record'; + +export type DomainVerificationInput = { + domain: string; + workspaceId: string; +}; + +export type DomainStatusInput = { + domain: string; + workspaceId: string; +}; + +export type EmailingDomainVerificationResult = { + status: EmailingDomainStatus; + verificationRecords: VerificationRecord[]; + verifiedAt: Date | null; +}; + +export interface EmailingDomainDriverInterface { + verifyDomain( + input: DomainVerificationInput, + ): Promise; + getDomainStatus( + input: DomainStatusInput, + ): Promise; +} diff --git a/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/types/emailing-domain.ts b/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/types/emailing-domain.ts new file mode 100644 index 0000000000..b32f2a87dd --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/types/emailing-domain.ts @@ -0,0 +1,10 @@ +export enum EmailingDomainDriver { + AWS_SES = 'AWS_SES', +} + +export enum EmailingDomainStatus { + PENDING = 'PENDING', + VERIFIED = 'VERIFIED', + FAILED = 'FAILED', + TEMPORARY_FAILURE = 'TEMPORARY_FAILURE', +} diff --git a/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/types/verifications-record.ts b/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/types/verifications-record.ts new file mode 100644 index 0000000000..2e5b7c93f9 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/emailing-domain/drivers/types/verifications-record.ts @@ -0,0 +1,6 @@ +export type VerificationRecord = { + type: 'TXT' | 'CNAME' | 'MX'; + key: string; + value: string; + priority?: number; +}; diff --git a/packages/twenty-server/src/engine/core-modules/emailing-domain/dtos/emailing-domain.dto.ts b/packages/twenty-server/src/engine/core-modules/emailing-domain/dtos/emailing-domain.dto.ts new file mode 100644 index 0000000000..7f0e3927d3 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/emailing-domain/dtos/emailing-domain.dto.ts @@ -0,0 +1,45 @@ +import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; + +import { IDField } from '@ptc-org/nestjs-query-graphql'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { + EmailingDomainDriver, + EmailingDomainStatus, +} from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain'; +import { VerificationRecord } from 'src/engine/core-modules/emailing-domain/dtos/verification-record.dto'; + +registerEnumType(EmailingDomainDriver, { + name: 'EmailingDomainDriver', +}); + +registerEnumType(EmailingDomainStatus, { + name: 'EmailingDomainStatus', +}); + +@ObjectType('EmailingDomain') +export class EmailingDomainDto { + @IDField(() => UUIDScalarType) + id: string; + + @Field(() => Date) + createdAt: Date; + + @Field(() => Date) + updatedAt: Date; + + @Field(() => String) + domain: string; + + @Field(() => EmailingDomainDriver) + driver: EmailingDomainDriver; + + @Field(() => EmailingDomainStatus) + status: EmailingDomainStatus; + + @Field(() => [VerificationRecord], { nullable: true }) + verificationRecords: VerificationRecord[] | null; + + @Field(() => Date, { nullable: true }) + verifiedAt: Date | null; +} diff --git a/packages/twenty-server/src/engine/core-modules/emailing-domain/dtos/verification-record.dto.ts b/packages/twenty-server/src/engine/core-modules/emailing-domain/dtos/verification-record.dto.ts new file mode 100644 index 0000000000..292e878f9a --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/emailing-domain/dtos/verification-record.dto.ts @@ -0,0 +1,16 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class VerificationRecord { + @Field(() => String) + type: 'TXT' | 'CNAME' | 'MX'; + + @Field(() => String) + key: string; + + @Field(() => String) + value: string; + + @Field(() => Number, { nullable: true }) + priority?: number; +} diff --git a/packages/twenty-server/src/engine/core-modules/emailing-domain/emailing-domain.entity.ts b/packages/twenty-server/src/engine/core-modules/emailing-domain/emailing-domain.entity.ts new file mode 100644 index 0000000000..079d973fbc --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/emailing-domain/emailing-domain.entity.ts @@ -0,0 +1,71 @@ +import { ObjectType } from '@nestjs/graphql'; + +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + Unique, + UpdateDateColumn, +} from 'typeorm'; + +import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; + +import { + EmailingDomainDriver, + EmailingDomainStatus, +} from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain'; +import { VerificationRecord } from 'src/engine/core-modules/emailing-domain/drivers/types/verifications-record'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; + +@Entity({ name: 'emailingDomain', schema: 'core' }) +@ObjectType() +@Unique('IDX_EMAILING_DOMAIN_DOMAIN_WORKSPACE_ID_UNIQUE', [ + 'domain', + 'workspaceId', +]) +export class EmailingDomain { + @PrimaryGeneratedColumn('uuid') + id: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @Column({ type: 'varchar', nullable: false }) + domain: string; + + @Column({ + type: 'enum', + enum: Object.values(EmailingDomainDriver), + nullable: false, + }) + driver: EmailingDomainDriver; + + @Column({ + type: 'enum', + enum: Object.values(EmailingDomainStatus), + default: EmailingDomainStatus.PENDING, + nullable: false, + }) + status: EmailingDomainStatus; + + @Column({ type: 'jsonb', nullable: true }) + verificationRecords: VerificationRecord[]; + + @Column({ type: 'timestamptz', nullable: true }) + verifiedAt: Date | null; + + @Column({ nullable: false }) + workspaceId: string; + + @ManyToOne(() => Workspace, (workspace) => workspace.emailingDomains, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'workspaceId' }) + workspace: Relation; +} diff --git a/packages/twenty-server/src/engine/core-modules/emailing-domain/emailing-domain.module.ts b/packages/twenty-server/src/engine/core-modules/emailing-domain/emailing-domain.module.ts new file mode 100644 index 0000000000..ef81ac1b70 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/emailing-domain/emailing-domain.module.ts @@ -0,0 +1,27 @@ +import { Module } from '@nestjs/common'; + +import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; + +import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; +import { AwsSesClientProvider } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/providers/aws-ses-client.provider'; +import { AwsSesHandleErrorService } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-handle-error.service'; +import { EmailingDomainDriverFactory } from 'src/engine/core-modules/emailing-domain/drivers/emailing-domain-driver.factory'; +import { EmailingDomain } from 'src/engine/core-modules/emailing-domain/emailing-domain.entity'; +import { EmailingDomainResolver } from 'src/engine/core-modules/emailing-domain/emailing-domain.resolver'; +import { EmailingDomainService } from 'src/engine/core-modules/emailing-domain/services/emailing-domain.service'; + +@Module({ + imports: [ + TypeORMModule, + NestjsQueryTypeOrmModule.forFeature([EmailingDomain]), + ], + exports: [EmailingDomainService], + providers: [ + EmailingDomainService, + EmailingDomainResolver, + EmailingDomainDriverFactory, + AwsSesClientProvider, + AwsSesHandleErrorService, + ], +}) +export class EmailingDomainModule {} diff --git a/packages/twenty-server/src/engine/core-modules/emailing-domain/emailing-domain.resolver.ts b/packages/twenty-server/src/engine/core-modules/emailing-domain/emailing-domain.resolver.ts new file mode 100644 index 0000000000..0f8f85f676 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/emailing-domain/emailing-domain.resolver.ts @@ -0,0 +1,67 @@ +import { UseGuards, UsePipes } from '@nestjs/common'; +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; + +import { EmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain'; +import { EmailingDomainDto } from 'src/engine/core-modules/emailing-domain/dtos/emailing-domain.dto'; +import { EmailingDomainService } from 'src/engine/core-modules/emailing-domain/services/emailing-domain.service'; +import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; + +@UseGuards(WorkspaceAuthGuard) +@UsePipes(ResolverValidationPipe) +@Resolver(() => EmailingDomainDto) +export class EmailingDomainResolver { + constructor(private readonly emailingDomainService: EmailingDomainService) {} + + @Mutation(() => EmailingDomainDto) + async createEmailingDomain( + @Args('domain') domain: string, + @Args('driver') driver: EmailingDomainDriver, + @AuthWorkspace() currentWorkspace: Workspace, + ): Promise { + const emailingDomain = + await this.emailingDomainService.createEmailingDomain( + domain, + driver, + currentWorkspace, + ); + + return emailingDomain; + } + + @Mutation(() => Boolean) + async deleteEmailingDomain( + @Args('id') id: string, + @AuthWorkspace() currentWorkspace: Workspace, + ): Promise { + await this.emailingDomainService.deleteEmailingDomain(currentWorkspace, id); + + return true; + } + + @Mutation(() => EmailingDomainDto) + async verifyEmailingDomain( + @Args('id') id: string, + @AuthWorkspace() currentWorkspace: Workspace, + ): Promise { + const emailingDomain = + await this.emailingDomainService.verifyEmailingDomain( + currentWorkspace, + id, + ); + + return emailingDomain; + } + + @Query(() => [EmailingDomainDto]) + async getEmailingDomains( + @AuthWorkspace() currentWorkspace: Workspace, + ): Promise { + const emailingDomains = + await this.emailingDomainService.getEmailingDomains(currentWorkspace); + + return emailingDomains; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/emailing-domain/services/emailing-domain.service.ts b/packages/twenty-server/src/engine/core-modules/emailing-domain/services/emailing-domain.service.ts new file mode 100644 index 0000000000..ae187a9dc4 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/emailing-domain/services/emailing-domain.service.ts @@ -0,0 +1,173 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { EmailingDomainDriverFactory } from 'src/engine/core-modules/emailing-domain/drivers/emailing-domain-driver.factory'; +import { + EmailingDomainDriver, + EmailingDomainStatus, +} from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain'; +import { EmailingDomain } from 'src/engine/core-modules/emailing-domain/emailing-domain.entity'; +import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; + +@Injectable() +export class EmailingDomainService { + constructor( + @InjectRepository(EmailingDomain) + private readonly emailingDomainRepository: Repository, + private readonly emailingDomainDriverFactory: EmailingDomainDriverFactory, + ) {} + + async createEmailingDomain( + domain: string, + driver: EmailingDomainDriver, + workspace: Workspace, + ): Promise { + const existingDomain = await this.emailingDomainRepository.findOneBy({ + domain, + workspaceId: workspace.id, + }); + + if (existingDomain) { + throw new Error('Emailing domain already exists for this workspace'); + } + + const driverInstance = this.emailingDomainDriverFactory.getCurrentDriver(); + const verificationResult = await driverInstance.verifyDomain({ + domain, + workspaceId: workspace.id, + }); + + const domainToCreate = { + domain, + driver, + workspaceId: workspace.id, + ...verificationResult, + }; + + const savedDomain = + await this.emailingDomainRepository.save(domainToCreate); + + return savedDomain; + } + + async deleteEmailingDomain( + workspace: Workspace, + emailingDomainId: string, + ): Promise { + const emailingDomain = await this.emailingDomainRepository.findOneBy({ + id: emailingDomainId, + workspaceId: workspace.id, + }); + + if (!emailingDomain) { + throw new Error('Emailing domain not found'); + } + + await this.emailingDomainRepository.delete({ + id: emailingDomain.id, + }); + } + + async getEmailingDomains(workspace: Workspace): Promise { + return await this.emailingDomainRepository.find({ + where: { + workspaceId: workspace.id, + }, + order: { + createdAt: 'DESC', + }, + }); + } + + async getEmailingDomain( + workspace: Workspace, + emailingDomainId: string, + ): Promise { + return await this.emailingDomainRepository.findOneBy({ + id: emailingDomainId, + workspaceId: workspace.id, + }); + } + + async verifyEmailingDomain( + workspace: Workspace, + emailingDomainId: string, + ): Promise { + const emailingDomain = await this.getEmailingDomain( + workspace, + emailingDomainId, + ); + + if (!emailingDomain) { + throw new Error('Emailing domain not found'); + } + + if (emailingDomain.status === EmailingDomainStatus.VERIFIED) { + throw new Error('Emailing domain is already verified'); + } + + const driver = this.emailingDomainDriverFactory.getCurrentDriver(); + const verificationResult = await driver.verifyDomain({ + domain: emailingDomain.domain, + workspaceId: emailingDomain.workspaceId, + }); + + const updatedDomain = await this.emailingDomainRepository.save({ + ...emailingDomain, + ...verificationResult, + }); + + return updatedDomain; + } + + async syncEmailingDomain( + workspace: Workspace, + emailingDomainId: string, + ): Promise { + const emailingDomain = await this.getEmailingDomain( + workspace, + emailingDomainId, + ); + + if (!emailingDomain) { + throw new Error('Emailing domain not found'); + } + + await this.emailingDomainRepository.update( + { + id: emailingDomainId, + }, + { + verificationRecords: emailingDomain.verificationRecords, + status: EmailingDomainStatus.PENDING, + }, + ); + + try { + const driver = this.emailingDomainDriverFactory.getCurrentDriver(); + const statusResult = await driver.getDomainStatus({ + domain: emailingDomain.domain, + workspaceId: emailingDomain.workspaceId, + }); + + const updatedDomain = await this.emailingDomainRepository.save({ + ...emailingDomain, + ...statusResult, + }); + + return updatedDomain; + } catch (error) { + await this.emailingDomainRepository.update( + { id: emailingDomainId }, + { + verificationRecords: emailingDomain.verificationRecords, + status: emailingDomain.status, + }, + ); + + throw error; + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts index 80c25dbb91..a835ea2a43 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts @@ -18,5 +18,6 @@ export enum FeatureFlagKey { IS_CALENDAR_VIEW_ENABLED = 'IS_CALENDAR_VIEW_ENABLED', IS_GROUP_BY_ENABLED = 'IS_GROUP_BY_ENABLED', IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED', + IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED', IS_DYNAMIC_SEARCH_FIELDS_ENABLED = 'IS_DYNAMIC_SEARCH_FIELDS_ENABLED', } diff --git a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts index e34a1087f4..fda94ae4ec 100644 --- a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts @@ -1211,6 +1211,50 @@ export class ConfigVariables { }) @ValidateIf((env) => env.IS_MAPS_AND_ADDRESS_AUTOCOMPLETE_ENABLED) GOOGLE_MAP_API_KEY: string; + + @ConfigVariablesMetadata({ + group: ConfigVariablesGroup.AwsSesSettings, + description: 'AWS region', + type: ConfigVariableType.STRING, + }) + @IsAWSRegion() + @IsOptional() + AWS_SES_REGION: AwsRegion; + + @ConfigVariablesMetadata({ + group: ConfigVariablesGroup.AwsSesSettings, + isSensitive: true, + description: 'AWS access key ID', + type: ConfigVariableType.STRING, + }) + @IsOptional() + AWS_SES_ACCESS_KEY_ID: string; + + @ConfigVariablesMetadata({ + group: ConfigVariablesGroup.AwsSesSettings, + isSensitive: true, + description: 'AWS session token', + type: ConfigVariableType.STRING, + }) + @IsOptional() + AWS_SES_SESSION_TOKEN: string; + + @ConfigVariablesMetadata({ + group: ConfigVariablesGroup.AwsSesSettings, + isSensitive: true, + description: 'AWS secret access key', + type: ConfigVariableType.STRING, + }) + @IsOptional() + AWS_SES_SECRET_ACCESS_KEY: string; + + @ConfigVariablesMetadata({ + group: ConfigVariablesGroup.AwsSesSettings, + description: 'AWS Account ID for SES ARN construction', + type: ConfigVariableType.STRING, + }) + @IsOptional() + AWS_SES_ACCOUNT_ID: string; } export const validate = (config: Record): ConfigVariables => { diff --git a/packages/twenty-server/src/engine/core-modules/twenty-config/constants/config-variables-group-metadata.ts b/packages/twenty-server/src/engine/core-modules/twenty-config/constants/config-variables-group-metadata.ts index 84d98e3421..ecc913b806 100644 --- a/packages/twenty-server/src/engine/core-modules/twenty-config/constants/config-variables-group-metadata.ts +++ b/packages/twenty-server/src/engine/core-modules/twenty-config/constants/config-variables-group-metadata.ts @@ -125,4 +125,9 @@ export const CONFIG_VARIABLES_GROUP_METADATA: Record< 'These have been set to sensible default so you probably don’t need to change them unless you have a specific use-case.', isHiddenOnLoad: true, }, + [ConfigVariablesGroup.AwsSesSettings]: { + position: 2100, + description: 'Configure AWS SES settings for emailing domains', + isHiddenOnLoad: true, + }, }; diff --git a/packages/twenty-server/src/engine/core-modules/twenty-config/enums/config-variables-group.enum.ts b/packages/twenty-server/src/engine/core-modules/twenty-config/enums/config-variables-group.enum.ts index 3587f6d637..6faef1b56d 100644 --- a/packages/twenty-server/src/engine/core-modules/twenty-config/enums/config-variables-group.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/twenty-config/enums/config-variables-group.enum.ts @@ -19,4 +19,5 @@ export enum ConfigVariablesGroup { AnalyticsConfig = 'audit-config', TokensDuration = 'tokens-duration', TwoFactorAuthentication = 'two-factor-authentication', + AwsSesSettings = 'aws-ses-settings', } diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts index e473df5bfb..db80d44617 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts @@ -19,9 +19,11 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/ import { ApiKey } from 'src/engine/core-modules/api-key/api-key.entity'; import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity'; import { ApprovedAccessDomain } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity'; +import { EmailingDomain } from 'src/engine/core-modules/emailing-domain/emailing-domain.entity'; import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity'; import { PostgresCredentials } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.entity'; +import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity'; import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { ViewFieldDTO } from 'src/engine/core-modules/view/dtos/view-field.dto'; @@ -41,7 +43,6 @@ import { AgentHandoffEntity } from 'src/engine/metadata-modules/agent/agent-hand import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity'; import { AgentDTO } from 'src/engine/metadata-modules/agent/dtos/agent.dto'; import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto'; -import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity'; registerEnumType(WorkspaceActivationStatus, { name: 'WorkspaceActivationStatus', @@ -116,6 +117,9 @@ export class Workspace { ) approvedAccessDomains: Relation; + @OneToMany(() => EmailingDomain, (emailingDomain) => emailingDomain.workspace) + emailingDomains: Relation; + @OneToMany(() => PublicDomain, (publicDomain) => publicDomain.workspace) publicDomains: Relation; diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index 5fee9d6a7a..87cba55243 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -141,6 +141,7 @@ describe('WorkspaceEntityManager', () => { IS_CALENDAR_VIEW_ENABLED: false, IS_GROUP_BY_ENABLED: false, IS_PUBLIC_DOMAIN_ENABLED: false, + IS_EMAILING_DOMAIN_ENABLED: false, IS_DYNAMIC_SEARCH_FIELDS_ENABLED: false, }, eventEmitterService: { diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts index dd255ee69b..c89c299a60 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts @@ -83,7 +83,12 @@ export const seedFeatureFlags = async ( { key: FeatureFlagKey.IS_PUBLIC_DOMAIN_ENABLED, workspaceId: workspaceId, - value: false, + value: true, + }, + { + key: FeatureFlagKey.IS_EMAILING_DOMAIN_ENABLED, + workspaceId: workspaceId, + value: true, }, { key: FeatureFlagKey.IS_DYNAMIC_SEARCH_FIELDS_ENABLED, diff --git a/packages/twenty-shared/src/types/SettingsPath.ts b/packages/twenty-shared/src/types/SettingsPath.ts index 36774efa5e..95e79c5a00 100644 --- a/packages/twenty-shared/src/types/SettingsPath.ts +++ b/packages/twenty-shared/src/types/SettingsPath.ts @@ -25,6 +25,8 @@ export enum SettingsPath { Domain = 'domains/domain', PublicDomain = 'domains/public-domain', NewApprovedAccessDomain = 'domains/approved-access-domain/new', + NewEmailingDomain = 'domains/emailing-domain/new', + EmailingDomainDetail = 'domains/emailing-domain/:domainId', Releases = 'releases', AI = 'ai', AINewAgent = 'ai/new-agent', diff --git a/yarn.lock b/yarn.lock index 5749bd03a0..ce5a1c9779 100644 --- a/yarn.lock +++ b/yarn.lock @@ -917,6 +917,54 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/client-sesv2@npm:^3.888.0": + version: 3.888.0 + resolution: "@aws-sdk/client-sesv2@npm:3.888.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:3.888.0" + "@aws-sdk/credential-provider-node": "npm:3.888.0" + "@aws-sdk/middleware-host-header": "npm:3.887.0" + "@aws-sdk/middleware-logger": "npm:3.887.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.887.0" + "@aws-sdk/middleware-user-agent": "npm:3.888.0" + "@aws-sdk/region-config-resolver": "npm:3.887.0" + "@aws-sdk/signature-v4-multi-region": "npm:3.888.0" + "@aws-sdk/types": "npm:3.887.0" + "@aws-sdk/util-endpoints": "npm:3.887.0" + "@aws-sdk/util-user-agent-browser": "npm:3.887.0" + "@aws-sdk/util-user-agent-node": "npm:3.888.0" + "@smithy/config-resolver": "npm:^4.2.1" + "@smithy/core": "npm:^3.11.0" + "@smithy/fetch-http-handler": "npm:^5.2.1" + "@smithy/hash-node": "npm:^4.1.1" + "@smithy/invalid-dependency": "npm:^4.1.1" + "@smithy/middleware-content-length": "npm:^4.1.1" + "@smithy/middleware-endpoint": "npm:^4.2.1" + "@smithy/middleware-retry": "npm:^4.2.1" + "@smithy/middleware-serde": "npm:^4.1.1" + "@smithy/middleware-stack": "npm:^4.1.1" + "@smithy/node-config-provider": "npm:^4.2.1" + "@smithy/node-http-handler": "npm:^4.2.1" + "@smithy/protocol-http": "npm:^5.2.1" + "@smithy/smithy-client": "npm:^4.6.1" + "@smithy/types": "npm:^4.5.0" + "@smithy/url-parser": "npm:^4.1.1" + "@smithy/util-base64": "npm:^4.1.0" + "@smithy/util-body-length-browser": "npm:^4.1.0" + "@smithy/util-body-length-node": "npm:^4.1.0" + "@smithy/util-defaults-mode-browser": "npm:^4.1.1" + "@smithy/util-defaults-mode-node": "npm:^4.1.1" + "@smithy/util-endpoints": "npm:^3.1.1" + "@smithy/util-middleware": "npm:^4.1.1" + "@smithy/util-retry": "npm:^4.1.1" + "@smithy/util-utf8": "npm:^4.1.0" + tslib: "npm:^2.6.2" + checksum: 10c0/90e15a3eccd85c69c81fc31f1d14b4cade73f0176ca86856d5d81aa40825ef0e35e9d50ce68e221b9ce7609785f33a081c48029cba8499d208bf4c6c57643051 + languageName: node + linkType: hard + "@aws-sdk/client-sso@npm:3.825.0": version: 3.825.0 resolution: "@aws-sdk/client-sso@npm:3.825.0" @@ -963,6 +1011,52 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/client-sso@npm:3.888.0": + version: 3.888.0 + resolution: "@aws-sdk/client-sso@npm:3.888.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:3.888.0" + "@aws-sdk/middleware-host-header": "npm:3.887.0" + "@aws-sdk/middleware-logger": "npm:3.887.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.887.0" + "@aws-sdk/middleware-user-agent": "npm:3.888.0" + "@aws-sdk/region-config-resolver": "npm:3.887.0" + "@aws-sdk/types": "npm:3.887.0" + "@aws-sdk/util-endpoints": "npm:3.887.0" + "@aws-sdk/util-user-agent-browser": "npm:3.887.0" + "@aws-sdk/util-user-agent-node": "npm:3.888.0" + "@smithy/config-resolver": "npm:^4.2.1" + "@smithy/core": "npm:^3.11.0" + "@smithy/fetch-http-handler": "npm:^5.2.1" + "@smithy/hash-node": "npm:^4.1.1" + "@smithy/invalid-dependency": "npm:^4.1.1" + "@smithy/middleware-content-length": "npm:^4.1.1" + "@smithy/middleware-endpoint": "npm:^4.2.1" + "@smithy/middleware-retry": "npm:^4.2.1" + "@smithy/middleware-serde": "npm:^4.1.1" + "@smithy/middleware-stack": "npm:^4.1.1" + "@smithy/node-config-provider": "npm:^4.2.1" + "@smithy/node-http-handler": "npm:^4.2.1" + "@smithy/protocol-http": "npm:^5.2.1" + "@smithy/smithy-client": "npm:^4.6.1" + "@smithy/types": "npm:^4.5.0" + "@smithy/url-parser": "npm:^4.1.1" + "@smithy/util-base64": "npm:^4.1.0" + "@smithy/util-body-length-browser": "npm:^4.1.0" + "@smithy/util-body-length-node": "npm:^4.1.0" + "@smithy/util-defaults-mode-browser": "npm:^4.1.1" + "@smithy/util-defaults-mode-node": "npm:^4.1.1" + "@smithy/util-endpoints": "npm:^3.1.1" + "@smithy/util-middleware": "npm:^4.1.1" + "@smithy/util-retry": "npm:^4.1.1" + "@smithy/util-utf8": "npm:^4.1.0" + tslib: "npm:^2.6.2" + checksum: 10c0/8798cad059c5f29baf191d240c9b49a2dd7dce855e151de2e3532b8c8cff7b717fdebc93689ec37759baf89e8cfdaa6029803206cba4cfb59eb7a2848873ac58 + languageName: node + linkType: hard + "@aws-sdk/client-sts@npm:3.825.0": version: 3.825.0 resolution: "@aws-sdk/client-sts@npm:3.825.0" @@ -1033,6 +1127,29 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/core@npm:3.888.0": + version: 3.888.0 + resolution: "@aws-sdk/core@npm:3.888.0" + dependencies: + "@aws-sdk/types": "npm:3.887.0" + "@aws-sdk/xml-builder": "npm:3.887.0" + "@smithy/core": "npm:^3.11.0" + "@smithy/node-config-provider": "npm:^4.2.1" + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/protocol-http": "npm:^5.2.1" + "@smithy/signature-v4": "npm:^5.1.3" + "@smithy/smithy-client": "npm:^4.6.1" + "@smithy/types": "npm:^4.5.0" + "@smithy/util-base64": "npm:^4.1.0" + "@smithy/util-body-length-browser": "npm:^4.1.0" + "@smithy/util-middleware": "npm:^4.1.1" + "@smithy/util-utf8": "npm:^4.1.0" + fast-xml-parser: "npm:5.2.5" + tslib: "npm:^2.6.2" + checksum: 10c0/48e904a79b85cf5bceeb88b73c632c140750e6c9643c78bc38229168d7609207f3bc8c98801783a2a0457fc6da148f5ba96226201d9bda12df6fa1b11d8b68dd + languageName: node + linkType: hard + "@aws-sdk/credential-provider-cognito-identity@npm:3.825.0": version: 3.825.0 resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.825.0" @@ -1059,6 +1176,19 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-env@npm:3.888.0": + version: 3.888.0 + resolution: "@aws-sdk/credential-provider-env@npm:3.888.0" + dependencies: + "@aws-sdk/core": "npm:3.888.0" + "@aws-sdk/types": "npm:3.887.0" + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/09c4fb216031ba9695a0966692bacf8bce6e65fa52454293bcf87533cf5c872e5f322922fd72bfa180a69ab53df33d58aafd8aa93ddb419c83b12f020bf0c78f + languageName: node + linkType: hard + "@aws-sdk/credential-provider-http@npm:3.825.0": version: 3.825.0 resolution: "@aws-sdk/credential-provider-http@npm:3.825.0" @@ -1077,6 +1207,24 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-http@npm:3.888.0": + version: 3.888.0 + resolution: "@aws-sdk/credential-provider-http@npm:3.888.0" + dependencies: + "@aws-sdk/core": "npm:3.888.0" + "@aws-sdk/types": "npm:3.887.0" + "@smithy/fetch-http-handler": "npm:^5.2.1" + "@smithy/node-http-handler": "npm:^4.2.1" + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/protocol-http": "npm:^5.2.1" + "@smithy/smithy-client": "npm:^4.6.1" + "@smithy/types": "npm:^4.5.0" + "@smithy/util-stream": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10c0/b8f66d2b80b1118ac047c54c2d6607dc9e042c904080f52926ae8a38e10b9571837e433be94a763cfb560dd9011e5078bdf15cd06ef5074d6dd2303819a445ab + languageName: node + linkType: hard + "@aws-sdk/credential-provider-ini@npm:3.825.0": version: 3.825.0 resolution: "@aws-sdk/credential-provider-ini@npm:3.825.0" @@ -1098,6 +1246,27 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-ini@npm:3.888.0": + version: 3.888.0 + resolution: "@aws-sdk/credential-provider-ini@npm:3.888.0" + dependencies: + "@aws-sdk/core": "npm:3.888.0" + "@aws-sdk/credential-provider-env": "npm:3.888.0" + "@aws-sdk/credential-provider-http": "npm:3.888.0" + "@aws-sdk/credential-provider-process": "npm:3.888.0" + "@aws-sdk/credential-provider-sso": "npm:3.888.0" + "@aws-sdk/credential-provider-web-identity": "npm:3.888.0" + "@aws-sdk/nested-clients": "npm:3.888.0" + "@aws-sdk/types": "npm:3.887.0" + "@smithy/credential-provider-imds": "npm:^4.0.7" + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/shared-ini-file-loader": "npm:^4.0.5" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/89eeba91cec2f083519dc82c3ecace3ff338326964ff67a647b542f762bf629c0f25d9cce5d978e207c64629e2456ba1c099688bcd9058346ae8f3024e6e47a8 + languageName: node + linkType: hard + "@aws-sdk/credential-provider-node@npm:3.825.0": version: 3.825.0 resolution: "@aws-sdk/credential-provider-node@npm:3.825.0" @@ -1118,6 +1287,26 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-node@npm:3.888.0": + version: 3.888.0 + resolution: "@aws-sdk/credential-provider-node@npm:3.888.0" + dependencies: + "@aws-sdk/credential-provider-env": "npm:3.888.0" + "@aws-sdk/credential-provider-http": "npm:3.888.0" + "@aws-sdk/credential-provider-ini": "npm:3.888.0" + "@aws-sdk/credential-provider-process": "npm:3.888.0" + "@aws-sdk/credential-provider-sso": "npm:3.888.0" + "@aws-sdk/credential-provider-web-identity": "npm:3.888.0" + "@aws-sdk/types": "npm:3.887.0" + "@smithy/credential-provider-imds": "npm:^4.0.7" + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/shared-ini-file-loader": "npm:^4.0.5" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/a920c4c21bfc084db9ea02b32220a784f59d50cb1126ca9197c3c1b44c13cbe3638c0fa74fe384f06965c4c4e0b63eed2c2add371b55c44807a9cd5a3edda981 + languageName: node + linkType: hard + "@aws-sdk/credential-provider-process@npm:3.825.0": version: 3.825.0 resolution: "@aws-sdk/credential-provider-process@npm:3.825.0" @@ -1132,6 +1321,20 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-process@npm:3.888.0": + version: 3.888.0 + resolution: "@aws-sdk/credential-provider-process@npm:3.888.0" + dependencies: + "@aws-sdk/core": "npm:3.888.0" + "@aws-sdk/types": "npm:3.887.0" + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/shared-ini-file-loader": "npm:^4.0.5" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/117d6e2cb50bb064a3cf7b0f051d554981d7a4db514828d31ed9a44f5c4a0b83eed639a7bacaf64fdbe2e7bca705c71c590fa52e0d31ff5546a4745caaa0906c + languageName: node + linkType: hard + "@aws-sdk/credential-provider-sso@npm:3.825.0": version: 3.825.0 resolution: "@aws-sdk/credential-provider-sso@npm:3.825.0" @@ -1148,6 +1351,22 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-sso@npm:3.888.0": + version: 3.888.0 + resolution: "@aws-sdk/credential-provider-sso@npm:3.888.0" + dependencies: + "@aws-sdk/client-sso": "npm:3.888.0" + "@aws-sdk/core": "npm:3.888.0" + "@aws-sdk/token-providers": "npm:3.888.0" + "@aws-sdk/types": "npm:3.887.0" + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/shared-ini-file-loader": "npm:^4.0.5" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/2c5df695b2d4a7e2561e7fdba2ff456941e5fdb2bbf788d2c59f31fdc6ddab5f897ccc9e469c16be0688cd9edc3db0a6b010036a0d3d824c495c668a4238b312 + languageName: node + linkType: hard + "@aws-sdk/credential-provider-web-identity@npm:3.825.0": version: 3.825.0 resolution: "@aws-sdk/credential-provider-web-identity@npm:3.825.0" @@ -1162,6 +1381,20 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-web-identity@npm:3.888.0": + version: 3.888.0 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.888.0" + dependencies: + "@aws-sdk/core": "npm:3.888.0" + "@aws-sdk/nested-clients": "npm:3.888.0" + "@aws-sdk/types": "npm:3.887.0" + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/1508729df48d0426b661652ed0e333eacb3ec9732d64a92e95a325e30cdc82cdf56cc03713ea090c2648078e79188077865ba7093f00e8354f7b6b15ee96cf4e + languageName: node + linkType: hard + "@aws-sdk/credential-providers@npm:3.825.0": version: 3.825.0 resolution: "@aws-sdk/credential-providers@npm:3.825.0" @@ -1249,6 +1482,18 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/middleware-host-header@npm:3.887.0": + version: 3.887.0 + resolution: "@aws-sdk/middleware-host-header@npm:3.887.0" + dependencies: + "@aws-sdk/types": "npm:3.887.0" + "@smithy/protocol-http": "npm:^5.2.1" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/2fcd88f9e4bd06a37f7e6d53cd86d8e031decedaf036e4d76f044b034808042b6713af88c86d4d9d981c41ac6c52c6d3d420644283a56b58d0243b59b465a17a + languageName: node + linkType: hard + "@aws-sdk/middleware-location-constraint@npm:3.821.0": version: 3.821.0 resolution: "@aws-sdk/middleware-location-constraint@npm:3.821.0" @@ -1271,6 +1516,17 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/middleware-logger@npm:3.887.0": + version: 3.887.0 + resolution: "@aws-sdk/middleware-logger@npm:3.887.0" + dependencies: + "@aws-sdk/types": "npm:3.887.0" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/025e37a0945fe3c34ac49c34d420b5b66d3258d425039fcec150a0180e6f5cce744017256dfdbb773d860aaaf6f541de90e623bc0cd84ef3a99c0313d7d941b9 + languageName: node + linkType: hard + "@aws-sdk/middleware-recursion-detection@npm:3.821.0": version: 3.821.0 resolution: "@aws-sdk/middleware-recursion-detection@npm:3.821.0" @@ -1283,6 +1539,19 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/middleware-recursion-detection@npm:3.887.0": + version: 3.887.0 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.887.0" + dependencies: + "@aws-sdk/types": "npm:3.887.0" + "@aws/lambda-invoke-store": "npm:^0.0.1" + "@smithy/protocol-http": "npm:^5.2.1" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/a10fdf1ed626f7bcd8b8cc91145723611be8d5fc8d9127d8be6fe90bb0d4164914e74bc5946baad48bfd09352ac0810204f7be69df450da9c71391fda853d191 + languageName: node + linkType: hard + "@aws-sdk/middleware-sdk-s3@npm:3.825.0": version: 3.825.0 resolution: "@aws-sdk/middleware-sdk-s3@npm:3.825.0" @@ -1305,6 +1574,28 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/middleware-sdk-s3@npm:3.888.0": + version: 3.888.0 + resolution: "@aws-sdk/middleware-sdk-s3@npm:3.888.0" + dependencies: + "@aws-sdk/core": "npm:3.888.0" + "@aws-sdk/types": "npm:3.887.0" + "@aws-sdk/util-arn-parser": "npm:3.873.0" + "@smithy/core": "npm:^3.11.0" + "@smithy/node-config-provider": "npm:^4.2.1" + "@smithy/protocol-http": "npm:^5.2.1" + "@smithy/signature-v4": "npm:^5.1.3" + "@smithy/smithy-client": "npm:^4.6.1" + "@smithy/types": "npm:^4.5.0" + "@smithy/util-config-provider": "npm:^4.0.0" + "@smithy/util-middleware": "npm:^4.1.1" + "@smithy/util-stream": "npm:^4.3.1" + "@smithy/util-utf8": "npm:^4.1.0" + tslib: "npm:^2.6.2" + checksum: 10c0/1758ede7e6f539593c57dd8056c2dd1d14a90f7f0ac6bbcce8169f48f48efa3606c7bec4a40e72c288ecb251fdf5283a500961136da90803eb0729b1378b8337 + languageName: node + linkType: hard + "@aws-sdk/middleware-ssec@npm:3.821.0": version: 3.821.0 resolution: "@aws-sdk/middleware-ssec@npm:3.821.0" @@ -1331,6 +1622,21 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/middleware-user-agent@npm:3.888.0": + version: 3.888.0 + resolution: "@aws-sdk/middleware-user-agent@npm:3.888.0" + dependencies: + "@aws-sdk/core": "npm:3.888.0" + "@aws-sdk/types": "npm:3.887.0" + "@aws-sdk/util-endpoints": "npm:3.887.0" + "@smithy/core": "npm:^3.11.0" + "@smithy/protocol-http": "npm:^5.2.1" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/b11b4ab11cfaeb3a788de2a55c780bac0786684da371c662ace33641fabd04397f4bd88dd6958fcff8eab60422733c47781e90e79a7882f7f5a0761340ee093a + languageName: node + linkType: hard + "@aws-sdk/nested-clients@npm:3.825.0": version: 3.825.0 resolution: "@aws-sdk/nested-clients@npm:3.825.0" @@ -1377,6 +1683,52 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/nested-clients@npm:3.888.0": + version: 3.888.0 + resolution: "@aws-sdk/nested-clients@npm:3.888.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:3.888.0" + "@aws-sdk/middleware-host-header": "npm:3.887.0" + "@aws-sdk/middleware-logger": "npm:3.887.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.887.0" + "@aws-sdk/middleware-user-agent": "npm:3.888.0" + "@aws-sdk/region-config-resolver": "npm:3.887.0" + "@aws-sdk/types": "npm:3.887.0" + "@aws-sdk/util-endpoints": "npm:3.887.0" + "@aws-sdk/util-user-agent-browser": "npm:3.887.0" + "@aws-sdk/util-user-agent-node": "npm:3.888.0" + "@smithy/config-resolver": "npm:^4.2.1" + "@smithy/core": "npm:^3.11.0" + "@smithy/fetch-http-handler": "npm:^5.2.1" + "@smithy/hash-node": "npm:^4.1.1" + "@smithy/invalid-dependency": "npm:^4.1.1" + "@smithy/middleware-content-length": "npm:^4.1.1" + "@smithy/middleware-endpoint": "npm:^4.2.1" + "@smithy/middleware-retry": "npm:^4.2.1" + "@smithy/middleware-serde": "npm:^4.1.1" + "@smithy/middleware-stack": "npm:^4.1.1" + "@smithy/node-config-provider": "npm:^4.2.1" + "@smithy/node-http-handler": "npm:^4.2.1" + "@smithy/protocol-http": "npm:^5.2.1" + "@smithy/smithy-client": "npm:^4.6.1" + "@smithy/types": "npm:^4.5.0" + "@smithy/url-parser": "npm:^4.1.1" + "@smithy/util-base64": "npm:^4.1.0" + "@smithy/util-body-length-browser": "npm:^4.1.0" + "@smithy/util-body-length-node": "npm:^4.1.0" + "@smithy/util-defaults-mode-browser": "npm:^4.1.1" + "@smithy/util-defaults-mode-node": "npm:^4.1.1" + "@smithy/util-endpoints": "npm:^3.1.1" + "@smithy/util-middleware": "npm:^4.1.1" + "@smithy/util-retry": "npm:^4.1.1" + "@smithy/util-utf8": "npm:^4.1.0" + tslib: "npm:^2.6.2" + checksum: 10c0/c7ccdaccbcf9daa73b98776913655a0a453b5dfb68c5797e4e0f244dc04ea8777d80e74b92459844fa1bedb7bde33616b9893d36b6c6140e9e4c1108a1a7126f + languageName: node + linkType: hard + "@aws-sdk/region-config-resolver@npm:3.821.0": version: 3.821.0 resolution: "@aws-sdk/region-config-resolver@npm:3.821.0" @@ -1391,6 +1743,20 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/region-config-resolver@npm:3.887.0": + version: 3.887.0 + resolution: "@aws-sdk/region-config-resolver@npm:3.887.0" + dependencies: + "@aws-sdk/types": "npm:3.887.0" + "@smithy/node-config-provider": "npm:^4.2.1" + "@smithy/types": "npm:^4.5.0" + "@smithy/util-config-provider": "npm:^4.0.0" + "@smithy/util-middleware": "npm:^4.1.1" + tslib: "npm:^2.6.2" + checksum: 10c0/4dbd6b7e312b8930578b3d50e6104bd91e93d11e6ecc6921e5e5176e5eb320751c575e7e3f0a8916ce57a4367c3073ebc1886013c1767fa2eae46fc36e37ccb6 + languageName: node + linkType: hard + "@aws-sdk/signature-v4-multi-region@npm:3.825.0": version: 3.825.0 resolution: "@aws-sdk/signature-v4-multi-region@npm:3.825.0" @@ -1405,6 +1771,20 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/signature-v4-multi-region@npm:3.888.0": + version: 3.888.0 + resolution: "@aws-sdk/signature-v4-multi-region@npm:3.888.0" + dependencies: + "@aws-sdk/middleware-sdk-s3": "npm:3.888.0" + "@aws-sdk/types": "npm:3.887.0" + "@smithy/protocol-http": "npm:^5.2.1" + "@smithy/signature-v4": "npm:^5.1.3" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/67ca1fa741d3900cb9d6c8672e1ad1d7bfdb46aea195302badf04cfd4ff4d6cfd7d55b7ac65c1086b5d5d4fe4ecfefb6f7bc33fc0b882c631d3adf577d273eb9 + languageName: node + linkType: hard + "@aws-sdk/token-providers@npm:3.825.0": version: 3.825.0 resolution: "@aws-sdk/token-providers@npm:3.825.0" @@ -1420,6 +1800,21 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/token-providers@npm:3.888.0": + version: 3.888.0 + resolution: "@aws-sdk/token-providers@npm:3.888.0" + dependencies: + "@aws-sdk/core": "npm:3.888.0" + "@aws-sdk/nested-clients": "npm:3.888.0" + "@aws-sdk/types": "npm:3.887.0" + "@smithy/property-provider": "npm:^4.0.5" + "@smithy/shared-ini-file-loader": "npm:^4.0.5" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/3f303400d2fb5a2e015f375a08f4b0295565dea51ec7be7a6a10ca3264e00727781936e12025093349d0d49f0c9caa4da70407827c5c4dc635501ddd69841d5e + languageName: node + linkType: hard + "@aws-sdk/types@npm:3.821.0, @aws-sdk/types@npm:^3.222.0": version: 3.821.0 resolution: "@aws-sdk/types@npm:3.821.0" @@ -1430,6 +1825,16 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/types@npm:3.887.0": + version: 3.887.0 + resolution: "@aws-sdk/types@npm:3.887.0" + dependencies: + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/862ad368a8692cf75b0e44e96b2fe96c0e145f3c40bbb4a6fe07de1f5935722f0ecdc983fdf09789527a2555392a3cac08372d9a7cdec5f15849508083104413 + languageName: node + linkType: hard + "@aws-sdk/util-arn-parser@npm:3.804.0": version: 3.804.0 resolution: "@aws-sdk/util-arn-parser@npm:3.804.0" @@ -1439,6 +1844,15 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/util-arn-parser@npm:3.873.0": + version: 3.873.0 + resolution: "@aws-sdk/util-arn-parser@npm:3.873.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/d99aa771464ca63d0cc737fad34ac40be2b1fecbfa0eeb3aea0b59be6a5e628c24be2847490e8786ed782b818b9248a7b0391d988f2914b3a415f551cb5d1b93 + languageName: node + linkType: hard + "@aws-sdk/util-endpoints@npm:3.821.0": version: 3.821.0 resolution: "@aws-sdk/util-endpoints@npm:3.821.0" @@ -1451,6 +1865,19 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/util-endpoints@npm:3.887.0": + version: 3.887.0 + resolution: "@aws-sdk/util-endpoints@npm:3.887.0" + dependencies: + "@aws-sdk/types": "npm:3.887.0" + "@smithy/types": "npm:^4.5.0" + "@smithy/url-parser": "npm:^4.1.1" + "@smithy/util-endpoints": "npm:^3.1.1" + tslib: "npm:^2.6.2" + checksum: 10c0/679ff558bf480abf89e193fa0dcb92ffe96ad343ed8f6c0fca4aefc238f4f61e28aabdfff3a62b1d39c68a5d74ddb6c2b60eb1349e36c8cfec7fd2bb21abb8c2 + languageName: node + linkType: hard + "@aws-sdk/util-locate-window@npm:^3.0.0": version: 3.568.0 resolution: "@aws-sdk/util-locate-window@npm:3.568.0" @@ -1472,6 +1899,18 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/util-user-agent-browser@npm:3.887.0": + version: 3.887.0 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.887.0" + dependencies: + "@aws-sdk/types": "npm:3.887.0" + "@smithy/types": "npm:^4.5.0" + bowser: "npm:^2.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/f48410fbdb2f986e798072a7fb55ee0780fe90c57c35f8ad22bd9127914d911ea63b6d43b62ebb81edfb8b82ec5eed1f6bc93135bc55fad294f7e5b638d20f61 + languageName: node + linkType: hard + "@aws-sdk/util-user-agent-node@npm:3.825.0": version: 3.825.0 resolution: "@aws-sdk/util-user-agent-node@npm:3.825.0" @@ -1490,6 +1929,24 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/util-user-agent-node@npm:3.888.0": + version: 3.888.0 + resolution: "@aws-sdk/util-user-agent-node@npm:3.888.0" + dependencies: + "@aws-sdk/middleware-user-agent": "npm:3.888.0" + "@aws-sdk/types": "npm:3.887.0" + "@smithy/node-config-provider": "npm:^4.2.1" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + peerDependencies: + aws-crt: ">=1.0.0" + peerDependenciesMeta: + aws-crt: + optional: true + checksum: 10c0/bf33a98b75dbd8d4093eb191190df391cfa4dfff3a1ae6c48a3d51de68c144ba892b240b7c37c16d98deb60231e4e2c005f2df13a7e1df0c7f8df504aa07d4c5 + languageName: node + linkType: hard + "@aws-sdk/xml-builder@npm:3.821.0": version: 3.821.0 resolution: "@aws-sdk/xml-builder@npm:3.821.0" @@ -1500,6 +1957,23 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/xml-builder@npm:3.887.0": + version: 3.887.0 + resolution: "@aws-sdk/xml-builder@npm:3.887.0" + dependencies: + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/a8ec9b57934cba380ee72f7d5bab5305e61ffd12f8eed5957d062db0983de58a8a9f62f4979c036eccad4b74d8cf9267e8e6d21601f2dc85cc1db54a5eb17ce9 + languageName: node + linkType: hard + +"@aws/lambda-invoke-store@npm:^0.0.1": + version: 0.0.1 + resolution: "@aws/lambda-invoke-store@npm:0.0.1" + checksum: 10c0/0bbf3060014a462177fb743e132e9b106a6743ad9cd905df4bd26e9ca8bfe2cc90473b03a79938fa908934e45e43f366f57af56a697991abda71d9ac92f5018f + languageName: node + linkType: hard + "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.24.7, @babel/code-frame@npm:^7.27.1": version: 7.27.1 resolution: "@babel/code-frame@npm:7.27.1" @@ -17285,6 +17759,16 @@ __metadata: languageName: node linkType: hard +"@smithy/abort-controller@npm:^4.1.1": + version: 4.1.1 + resolution: "@smithy/abort-controller@npm:4.1.1" + dependencies: + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/f50ee8e76dab55df7af7247c5dac88209702b9e0a775a5d98472d67c607b6f624c3789ac75974c8b6fa452e1a4f9f72e5749dbea5b57f14d7ca137929e36f0ee + languageName: node + linkType: hard + "@smithy/chunked-blob-reader-native@npm:^4.0.0": version: 4.0.0 resolution: "@smithy/chunked-blob-reader-native@npm:4.0.0" @@ -17317,6 +17801,38 @@ __metadata: languageName: node linkType: hard +"@smithy/config-resolver@npm:^4.2.1": + version: 4.2.1 + resolution: "@smithy/config-resolver@npm:4.2.1" + dependencies: + "@smithy/node-config-provider": "npm:^4.2.1" + "@smithy/types": "npm:^4.5.0" + "@smithy/util-config-provider": "npm:^4.1.0" + "@smithy/util-middleware": "npm:^4.1.1" + tslib: "npm:^2.6.2" + checksum: 10c0/fc60c55bff658ab102e256b2e9e67d0a75f62ce75025152897ba9ec4f2a71a387cd79f99892e21dd91a1352bff5b7f35da98bc6da335c224e8395a2fe1c280f8 + languageName: node + linkType: hard + +"@smithy/core@npm:^3.11.0": + version: 3.11.0 + resolution: "@smithy/core@npm:3.11.0" + dependencies: + "@smithy/middleware-serde": "npm:^4.1.1" + "@smithy/protocol-http": "npm:^5.2.1" + "@smithy/types": "npm:^4.5.0" + "@smithy/util-base64": "npm:^4.1.0" + "@smithy/util-body-length-browser": "npm:^4.1.0" + "@smithy/util-middleware": "npm:^4.1.1" + "@smithy/util-stream": "npm:^4.3.1" + "@smithy/util-utf8": "npm:^4.1.0" + "@types/uuid": "npm:^9.0.1" + tslib: "npm:^2.6.2" + uuid: "npm:^9.0.1" + checksum: 10c0/290d088cc7a14b38c96943577d6bfde1b0c47588493c0b18dfacc98affb02a3d067f9b57d71a838bd79b46c3a7a10458f445eada37934bf308c1e21ae02b4b7d + languageName: node + linkType: hard + "@smithy/core@npm:^3.5.2, @smithy/core@npm:^3.5.3": version: 3.5.3 resolution: "@smithy/core@npm:3.5.3" @@ -17347,6 +17863,19 @@ __metadata: languageName: node linkType: hard +"@smithy/credential-provider-imds@npm:^4.0.7, @smithy/credential-provider-imds@npm:^4.1.1": + version: 4.1.1 + resolution: "@smithy/credential-provider-imds@npm:4.1.1" + dependencies: + "@smithy/node-config-provider": "npm:^4.2.1" + "@smithy/property-provider": "npm:^4.1.1" + "@smithy/types": "npm:^4.5.0" + "@smithy/url-parser": "npm:^4.1.1" + tslib: "npm:^2.6.2" + checksum: 10c0/23b97ccc84f69b7ff4f68085b69585922650f95c23e1d18c78df4786616b93b928bab6a696162dfe369c6d3c9e667451614e1f7442fc8cfcaeb8183c6516fd73 + languageName: node + linkType: hard + "@smithy/eventstream-codec@npm:^4.0.4": version: 4.0.4 resolution: "@smithy/eventstream-codec@npm:4.0.4" @@ -17415,6 +17944,19 @@ __metadata: languageName: node linkType: hard +"@smithy/fetch-http-handler@npm:^5.2.1": + version: 5.2.1 + resolution: "@smithy/fetch-http-handler@npm:5.2.1" + dependencies: + "@smithy/protocol-http": "npm:^5.2.1" + "@smithy/querystring-builder": "npm:^4.1.1" + "@smithy/types": "npm:^4.5.0" + "@smithy/util-base64": "npm:^4.1.0" + tslib: "npm:^2.6.2" + checksum: 10c0/c4a6a1a67f84361045bd10fef470ec6cda8691f549a455f734cfd3de05ccefc300973188e55578ae379b936f7e3f842971447386a3d8ec728f7df9c2f1c58fc2 + languageName: node + linkType: hard + "@smithy/hash-blob-browser@npm:^4.0.4": version: 4.0.4 resolution: "@smithy/hash-blob-browser@npm:4.0.4" @@ -17439,6 +17981,18 @@ __metadata: languageName: node linkType: hard +"@smithy/hash-node@npm:^4.1.1": + version: 4.1.1 + resolution: "@smithy/hash-node@npm:4.1.1" + dependencies: + "@smithy/types": "npm:^4.5.0" + "@smithy/util-buffer-from": "npm:^4.1.0" + "@smithy/util-utf8": "npm:^4.1.0" + tslib: "npm:^2.6.2" + checksum: 10c0/aedf905c5fba7c814a697d973ea49c76d529dc9a10675676984a811637623b4f41542d72e53ed0df0a30881ee7fbe77c74bd49bd272e4a034e9d80021b6022a7 + languageName: node + linkType: hard + "@smithy/hash-stream-node@npm:^4.0.4": version: 4.0.4 resolution: "@smithy/hash-stream-node@npm:4.0.4" @@ -17460,6 +18014,16 @@ __metadata: languageName: node linkType: hard +"@smithy/invalid-dependency@npm:^4.1.1": + version: 4.1.1 + resolution: "@smithy/invalid-dependency@npm:4.1.1" + dependencies: + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/5700333f00b6a31a97b792fa9a00fadd07b2eafaea01087a6ea212753dba2621a040dfb0d7dc5a1f75bb95cc28fba2e498cdaca43009b142610944c0fcd95a58 + languageName: node + linkType: hard + "@smithy/is-array-buffer@npm:^2.2.0": version: 2.2.0 resolution: "@smithy/is-array-buffer@npm:2.2.0" @@ -17478,6 +18042,15 @@ __metadata: languageName: node linkType: hard +"@smithy/is-array-buffer@npm:^4.1.0": + version: 4.1.0 + resolution: "@smithy/is-array-buffer@npm:4.1.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/399af810a9329c033d1816c492b17343d2ff956d32a358f327da6af0e4ad3c4640a1ef8dcd5f4d0f7d85ef19cf6909038f1a6539c938372dd33996d8f102bb9a + languageName: node + linkType: hard + "@smithy/md5-js@npm:^4.0.4": version: 4.0.4 resolution: "@smithy/md5-js@npm:4.0.4" @@ -17500,6 +18073,17 @@ __metadata: languageName: node linkType: hard +"@smithy/middleware-content-length@npm:^4.1.1": + version: 4.1.1 + resolution: "@smithy/middleware-content-length@npm:4.1.1" + dependencies: + "@smithy/protocol-http": "npm:^5.2.1" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/c841e9221f43303103076b3e2d0fb745b75f8caa0ec9cabb0be4fdb2c5a3fe4077391c083b6f8547ccdc58c44f267ee2423430e544bb95484d2b805e6008b8f3 + languageName: node + linkType: hard + "@smithy/middleware-endpoint@npm:^4.1.10, @smithy/middleware-endpoint@npm:^4.1.11": version: 4.1.11 resolution: "@smithy/middleware-endpoint@npm:4.1.11" @@ -17516,6 +18100,22 @@ __metadata: languageName: node linkType: hard +"@smithy/middleware-endpoint@npm:^4.2.1": + version: 4.2.1 + resolution: "@smithy/middleware-endpoint@npm:4.2.1" + dependencies: + "@smithy/core": "npm:^3.11.0" + "@smithy/middleware-serde": "npm:^4.1.1" + "@smithy/node-config-provider": "npm:^4.2.1" + "@smithy/shared-ini-file-loader": "npm:^4.1.1" + "@smithy/types": "npm:^4.5.0" + "@smithy/url-parser": "npm:^4.1.1" + "@smithy/util-middleware": "npm:^4.1.1" + tslib: "npm:^2.6.2" + checksum: 10c0/0f63a4f6d0bf14efbfba5dd051171447c7364f1e4aef6c3f6ea8f6a99dc09cc7e2008ad88540d8491f4a7f109d9cf2fd4e874c6cb83c7702e71527b3cf81bde7 + languageName: node + linkType: hard + "@smithy/middleware-retry@npm:^4.1.11": version: 4.1.12 resolution: "@smithy/middleware-retry@npm:4.1.12" @@ -17533,6 +18133,24 @@ __metadata: languageName: node linkType: hard +"@smithy/middleware-retry@npm:^4.2.1": + version: 4.2.1 + resolution: "@smithy/middleware-retry@npm:4.2.1" + dependencies: + "@smithy/node-config-provider": "npm:^4.2.1" + "@smithy/protocol-http": "npm:^5.2.1" + "@smithy/service-error-classification": "npm:^4.1.1" + "@smithy/smithy-client": "npm:^4.6.1" + "@smithy/types": "npm:^4.5.0" + "@smithy/util-middleware": "npm:^4.1.1" + "@smithy/util-retry": "npm:^4.1.1" + "@types/uuid": "npm:^9.0.1" + tslib: "npm:^2.6.2" + uuid: "npm:^9.0.1" + checksum: 10c0/fc79e81d53e7bf910400fe786f3741bd1876bc62d6d326a18a3c62a973733ff1c771d68b3838bfe6deb72b197fdb06b61eb4048d1bae0b0ad1ecdd9cdb41e998 + languageName: node + linkType: hard + "@smithy/middleware-serde@npm:^4.0.8": version: 4.0.8 resolution: "@smithy/middleware-serde@npm:4.0.8" @@ -17544,6 +18162,17 @@ __metadata: languageName: node linkType: hard +"@smithy/middleware-serde@npm:^4.1.1": + version: 4.1.1 + resolution: "@smithy/middleware-serde@npm:4.1.1" + dependencies: + "@smithy/protocol-http": "npm:^5.2.1" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/69c0cf035da2ccbdf2838f50a1fafb0f8e6fb286b820e0aa91be7bdc6dd102f51ce3b295e68cdf9e7441dfc3160a3d3cabac99d98a8f0a75675ecf0f1e09d439 + languageName: node + linkType: hard + "@smithy/middleware-stack@npm:^4.0.4": version: 4.0.4 resolution: "@smithy/middleware-stack@npm:4.0.4" @@ -17554,6 +18183,16 @@ __metadata: languageName: node linkType: hard +"@smithy/middleware-stack@npm:^4.1.1": + version: 4.1.1 + resolution: "@smithy/middleware-stack@npm:4.1.1" + dependencies: + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/8ee554c30e6802f6adcaf673e4d216cd8f56e13a9ef5d644ec94f0b553c3b62b451a8156fd49645cc1f5eedd09234a107edc42faff779416a4a43a215e370007 + languageName: node + linkType: hard + "@smithy/node-config-provider@npm:^4.1.3": version: 4.1.3 resolution: "@smithy/node-config-provider@npm:4.1.3" @@ -17566,6 +18205,18 @@ __metadata: languageName: node linkType: hard +"@smithy/node-config-provider@npm:^4.2.1": + version: 4.2.1 + resolution: "@smithy/node-config-provider@npm:4.2.1" + dependencies: + "@smithy/property-provider": "npm:^4.1.1" + "@smithy/shared-ini-file-loader": "npm:^4.1.1" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/ef648e075a36a0f543b9fdd51c2e250a2516934f6719331153a45f57d2fd5f367073a311b7fd5b03cdd19031282492a5be8b83df456dddf5186ff128f9ceae85 + languageName: node + linkType: hard + "@smithy/node-http-handler@npm:^4.0.6": version: 4.0.6 resolution: "@smithy/node-http-handler@npm:4.0.6" @@ -17579,6 +18230,19 @@ __metadata: languageName: node linkType: hard +"@smithy/node-http-handler@npm:^4.2.1": + version: 4.2.1 + resolution: "@smithy/node-http-handler@npm:4.2.1" + dependencies: + "@smithy/abort-controller": "npm:^4.1.1" + "@smithy/protocol-http": "npm:^5.2.1" + "@smithy/querystring-builder": "npm:^4.1.1" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/7536923c62b0bbbade8335b25368d02b4840cd381aba9dbdadb472fb501576d7b3b73121069356b022e9da3ec5d27711a00ec7786d31ba15089abdce582121cc + languageName: node + linkType: hard + "@smithy/property-provider@npm:^4.0.4": version: 4.0.4 resolution: "@smithy/property-provider@npm:4.0.4" @@ -17589,6 +18253,16 @@ __metadata: languageName: node linkType: hard +"@smithy/property-provider@npm:^4.0.5, @smithy/property-provider@npm:^4.1.1": + version: 4.1.1 + resolution: "@smithy/property-provider@npm:4.1.1" + dependencies: + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/5aa28b7e6cc23baf3605aa3be8a33ae4943635e698e0de773e8056f5ad06494f370f23cd3c4d083245d6fe411c25c38a76887d38a36d5daf075e36e6e6e3864f + languageName: node + linkType: hard + "@smithy/protocol-http@npm:^5.1.2": version: 5.1.2 resolution: "@smithy/protocol-http@npm:5.1.2" @@ -17599,6 +18273,16 @@ __metadata: languageName: node linkType: hard +"@smithy/protocol-http@npm:^5.2.1": + version: 5.2.1 + resolution: "@smithy/protocol-http@npm:5.2.1" + dependencies: + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/b27df0a94f8e0bab1e8310da82c3048e6d397a3b52f8413c4f19bb9c13d11afcdf7424293cb8d8d3e867b07ff8c5f3c8d0fbdd7d07a8328a39721eb202336d2b + languageName: node + linkType: hard + "@smithy/querystring-builder@npm:^4.0.4": version: 4.0.4 resolution: "@smithy/querystring-builder@npm:4.0.4" @@ -17610,6 +18294,17 @@ __metadata: languageName: node linkType: hard +"@smithy/querystring-builder@npm:^4.1.1": + version: 4.1.1 + resolution: "@smithy/querystring-builder@npm:4.1.1" + dependencies: + "@smithy/types": "npm:^4.5.0" + "@smithy/util-uri-escape": "npm:^4.1.0" + tslib: "npm:^2.6.2" + checksum: 10c0/15d41888eae29f57dbf9d2c8caa449d19ebb760b83958a0fe2cf4858948bb6e0466c176a207b868d8af7785e8f6688b87ada4e364ec6fd729ab6bffbd64b92d8 + languageName: node + linkType: hard + "@smithy/querystring-parser@npm:^4.0.4": version: 4.0.4 resolution: "@smithy/querystring-parser@npm:4.0.4" @@ -17620,6 +18315,16 @@ __metadata: languageName: node linkType: hard +"@smithy/querystring-parser@npm:^4.1.1": + version: 4.1.1 + resolution: "@smithy/querystring-parser@npm:4.1.1" + dependencies: + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/6bf8672aca07826af16625b41f20332fdfdc39861124e026ee929e4652f638edc7107d347a2fe7feb0c2e6f2c98d149d2d383cecaab46a48a990f36333e8f016 + languageName: node + linkType: hard + "@smithy/service-error-classification@npm:^4.0.5": version: 4.0.5 resolution: "@smithy/service-error-classification@npm:4.0.5" @@ -17629,6 +18334,15 @@ __metadata: languageName: node linkType: hard +"@smithy/service-error-classification@npm:^4.1.1": + version: 4.1.1 + resolution: "@smithy/service-error-classification@npm:4.1.1" + dependencies: + "@smithy/types": "npm:^4.5.0" + checksum: 10c0/946d3b7cc642d665a1717c69fdf7df4256a6fe03d3686be8fa9c514c6ff185eaee5a4ac5d0f45958087e8750a2fcba67f30e5567457889b54684e7dd00dfd400 + languageName: node + linkType: hard + "@smithy/shared-ini-file-loader@npm:^4.0.4": version: 4.0.4 resolution: "@smithy/shared-ini-file-loader@npm:4.0.4" @@ -17639,6 +18353,16 @@ __metadata: languageName: node linkType: hard +"@smithy/shared-ini-file-loader@npm:^4.0.5, @smithy/shared-ini-file-loader@npm:^4.1.1": + version: 4.1.1 + resolution: "@smithy/shared-ini-file-loader@npm:4.1.1" + dependencies: + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/1768c3f11519bd73797a63c062dd6ff26dd3cc1d7fc1ae5a5d92209fb8d3140a8799258a854ef0efbda27d19de619e608599c0d870539c251c504c3a56999a60 + languageName: node + linkType: hard + "@smithy/signature-v4@npm:^5.1.2": version: 5.1.2 resolution: "@smithy/signature-v4@npm:5.1.2" @@ -17655,6 +18379,22 @@ __metadata: languageName: node linkType: hard +"@smithy/signature-v4@npm:^5.1.3": + version: 5.2.1 + resolution: "@smithy/signature-v4@npm:5.2.1" + dependencies: + "@smithy/is-array-buffer": "npm:^4.1.0" + "@smithy/protocol-http": "npm:^5.2.1" + "@smithy/types": "npm:^4.5.0" + "@smithy/util-hex-encoding": "npm:^4.1.0" + "@smithy/util-middleware": "npm:^4.1.1" + "@smithy/util-uri-escape": "npm:^4.1.0" + "@smithy/util-utf8": "npm:^4.1.0" + tslib: "npm:^2.6.2" + checksum: 10c0/d00cb14155b89016493e90e19d3406f5362d7ec4205cd82a4fba47521f87d88b372e1ebfa34ceb739704f2f21d7a7bbf4da699773f71fab58028d515b932d014 + languageName: node + linkType: hard + "@smithy/smithy-client@npm:^4.4.2, @smithy/smithy-client@npm:^4.4.3": version: 4.4.3 resolution: "@smithy/smithy-client@npm:4.4.3" @@ -17670,6 +18410,21 @@ __metadata: languageName: node linkType: hard +"@smithy/smithy-client@npm:^4.6.1": + version: 4.6.1 + resolution: "@smithy/smithy-client@npm:4.6.1" + dependencies: + "@smithy/core": "npm:^3.11.0" + "@smithy/middleware-endpoint": "npm:^4.2.1" + "@smithy/middleware-stack": "npm:^4.1.1" + "@smithy/protocol-http": "npm:^5.2.1" + "@smithy/types": "npm:^4.5.0" + "@smithy/util-stream": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10c0/16c8aa6e44db638c7353e6e0068275cc30934650e8f67f1721e8adfcaad75dbe0eb4f2e9596e3a284f8f5528e1e26eab34f7ab9dab1398fae74d678b24446ef8 + languageName: node + linkType: hard + "@smithy/types@npm:^4.3.1": version: 4.3.1 resolution: "@smithy/types@npm:4.3.1" @@ -17679,6 +18434,15 @@ __metadata: languageName: node linkType: hard +"@smithy/types@npm:^4.5.0": + version: 4.5.0 + resolution: "@smithy/types@npm:4.5.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/7c765c9316893ab9e6575ba40e3d1569d43d7d1edd1110b505e190a4aa378a89e407b6f92de7bf0f22342ce05228ff0f1d37b14781e41c60c429fc22c8e5bae9 + languageName: node + linkType: hard + "@smithy/url-parser@npm:^4.0.4": version: 4.0.4 resolution: "@smithy/url-parser@npm:4.0.4" @@ -17690,6 +18454,17 @@ __metadata: languageName: node linkType: hard +"@smithy/url-parser@npm:^4.1.1": + version: 4.1.1 + resolution: "@smithy/url-parser@npm:4.1.1" + dependencies: + "@smithy/querystring-parser": "npm:^4.1.1" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/1f9e19d5d1e1a4874cf2f61df014715dc3685be385356758d3aed1a6b020b074af22961b12ae651faad74ed0460a102156471543031e74c726770820ede6f31c + languageName: node + linkType: hard + "@smithy/util-base64@npm:^4.0.0": version: 4.0.0 resolution: "@smithy/util-base64@npm:4.0.0" @@ -17701,6 +18476,17 @@ __metadata: languageName: node linkType: hard +"@smithy/util-base64@npm:^4.1.0": + version: 4.1.0 + resolution: "@smithy/util-base64@npm:4.1.0" + dependencies: + "@smithy/util-buffer-from": "npm:^4.1.0" + "@smithy/util-utf8": "npm:^4.1.0" + tslib: "npm:^2.6.2" + checksum: 10c0/e2275e4a09c245b8a0c1c6ead4418333d037f6cbc29a01881b56fb5676ad46839058bbdb3f9f357898c8000feccac9344ee66c9c36e17dd321bda84a93f2c36f + languageName: node + linkType: hard + "@smithy/util-body-length-browser@npm:^4.0.0": version: 4.0.0 resolution: "@smithy/util-body-length-browser@npm:4.0.0" @@ -17710,6 +18496,15 @@ __metadata: languageName: node linkType: hard +"@smithy/util-body-length-browser@npm:^4.1.0": + version: 4.1.0 + resolution: "@smithy/util-body-length-browser@npm:4.1.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/e86c39696dca4ce4b58e393fb85263e31ee046d88fdbd0bd1ee121f5101faca5fc945a7da17432aa39e86c178c80ac183568edb3b7df323f1134172dc36192c6 + languageName: node + linkType: hard + "@smithy/util-body-length-node@npm:^4.0.0": version: 4.0.0 resolution: "@smithy/util-body-length-node@npm:4.0.0" @@ -17719,6 +18514,15 @@ __metadata: languageName: node linkType: hard +"@smithy/util-body-length-node@npm:^4.1.0": + version: 4.1.0 + resolution: "@smithy/util-body-length-node@npm:4.1.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/d31fb7be66eb481f865d046b48c07221d25108b07c783f05eff7f165369d2259ca01de7c369f9de95e37e989b1344521bc6d4a6b38b42a7a46375a0c97f38a0b + languageName: node + linkType: hard + "@smithy/util-buffer-from@npm:^2.2.0": version: 2.2.0 resolution: "@smithy/util-buffer-from@npm:2.2.0" @@ -17739,6 +18543,16 @@ __metadata: languageName: node linkType: hard +"@smithy/util-buffer-from@npm:^4.1.0": + version: 4.1.0 + resolution: "@smithy/util-buffer-from@npm:4.1.0" + dependencies: + "@smithy/is-array-buffer": "npm:^4.1.0" + tslib: "npm:^2.6.2" + checksum: 10c0/f19457df277e7125ffbf106c26c70ffbc550956afceede4e2c2eb13a32f6f304f9e3b7a37f4c717df3c5ce97f8b759ee59ceed0e3f649f236bbaf2bfe8f266ef + languageName: node + linkType: hard + "@smithy/util-config-provider@npm:^4.0.0": version: 4.0.0 resolution: "@smithy/util-config-provider@npm:4.0.0" @@ -17748,6 +18562,15 @@ __metadata: languageName: node linkType: hard +"@smithy/util-config-provider@npm:^4.1.0": + version: 4.1.0 + resolution: "@smithy/util-config-provider@npm:4.1.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/099add392d9f029dec36d3646af4a63145a13ed8014af11f507bffbdb113fc2bb2bfd71ee157e385320f4c8de4bd48557c98f40878f93022187d3fc3082e6713 + languageName: node + linkType: hard + "@smithy/util-defaults-mode-browser@npm:^4.0.18": version: 4.0.19 resolution: "@smithy/util-defaults-mode-browser@npm:4.0.19" @@ -17761,6 +18584,19 @@ __metadata: languageName: node linkType: hard +"@smithy/util-defaults-mode-browser@npm:^4.1.1": + version: 4.1.1 + resolution: "@smithy/util-defaults-mode-browser@npm:4.1.1" + dependencies: + "@smithy/property-provider": "npm:^4.1.1" + "@smithy/smithy-client": "npm:^4.6.1" + "@smithy/types": "npm:^4.5.0" + bowser: "npm:^2.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/dd1e1e449ca44c50a7f6b0cfac51e40426f1034309921bcd6a591c9afdc09b5cb7d34685202a504cdded183297e4f455bb2404ebe012e912195ba32397ac7886 + languageName: node + linkType: hard + "@smithy/util-defaults-mode-node@npm:^4.0.18": version: 4.0.19 resolution: "@smithy/util-defaults-mode-node@npm:4.0.19" @@ -17776,6 +18612,21 @@ __metadata: languageName: node linkType: hard +"@smithy/util-defaults-mode-node@npm:^4.1.1": + version: 4.1.1 + resolution: "@smithy/util-defaults-mode-node@npm:4.1.1" + dependencies: + "@smithy/config-resolver": "npm:^4.2.1" + "@smithy/credential-provider-imds": "npm:^4.1.1" + "@smithy/node-config-provider": "npm:^4.2.1" + "@smithy/property-provider": "npm:^4.1.1" + "@smithy/smithy-client": "npm:^4.6.1" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/4296c76e2bf7af52a71bfa7811e4f0f070fd5e02da4b51ec35d23a2c3b32382a163d0c3d90a397cdcf4147fde1816e9b57081c9553b234b85159b0ddbed0d570 + languageName: node + linkType: hard + "@smithy/util-endpoints@npm:^3.0.6": version: 3.0.6 resolution: "@smithy/util-endpoints@npm:3.0.6" @@ -17787,6 +18638,17 @@ __metadata: languageName: node linkType: hard +"@smithy/util-endpoints@npm:^3.1.1": + version: 3.1.1 + resolution: "@smithy/util-endpoints@npm:3.1.1" + dependencies: + "@smithy/node-config-provider": "npm:^4.2.1" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/bb1bcd08c217dc6a7a55f18fa2277af3d43d4072894cbba8d0af8edb3942ef50a276f011f670f6236010ab65d34b148f67c114d598944de433fa3496439c77fa + languageName: node + linkType: hard + "@smithy/util-hex-encoding@npm:^4.0.0": version: 4.0.0 resolution: "@smithy/util-hex-encoding@npm:4.0.0" @@ -17796,6 +18658,15 @@ __metadata: languageName: node linkType: hard +"@smithy/util-hex-encoding@npm:^4.1.0": + version: 4.1.0 + resolution: "@smithy/util-hex-encoding@npm:4.1.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/eefaa537612afd13e497353a1bd55f3d6f977cdc52360f91fcb3b83b68d6cdd9b9fc16ab82561375b509ed8d5735c47b263c4e64e96471d1662d4c7a8c88449d + languageName: node + linkType: hard + "@smithy/util-middleware@npm:^4.0.4": version: 4.0.4 resolution: "@smithy/util-middleware@npm:4.0.4" @@ -17806,6 +18677,16 @@ __metadata: languageName: node linkType: hard +"@smithy/util-middleware@npm:^4.1.1": + version: 4.1.1 + resolution: "@smithy/util-middleware@npm:4.1.1" + dependencies: + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/47bee56b2fbf9fbe3c4be4e1daac247fea889848d43120c64895529bb92ef43b25cf07213792d1646622356a1572b91cc48b0976c39667a9020edfa5ec58d093 + languageName: node + linkType: hard + "@smithy/util-retry@npm:^4.0.5": version: 4.0.5 resolution: "@smithy/util-retry@npm:4.0.5" @@ -17817,6 +18698,17 @@ __metadata: languageName: node linkType: hard +"@smithy/util-retry@npm:^4.1.1": + version: 4.1.1 + resolution: "@smithy/util-retry@npm:4.1.1" + dependencies: + "@smithy/service-error-classification": "npm:^4.1.1" + "@smithy/types": "npm:^4.5.0" + tslib: "npm:^2.6.2" + checksum: 10c0/25f07dbf9be8798d2792b2ebfd68506408797815fc4ef75a6f526f52d3c6e6f7a53723f6c46b6a44855ed3cebee4da5a49a86c4e8b2e8b923e39aff965b00e7d + languageName: node + linkType: hard + "@smithy/util-stream@npm:^4.2.2": version: 4.2.2 resolution: "@smithy/util-stream@npm:4.2.2" @@ -17833,6 +18725,22 @@ __metadata: languageName: node linkType: hard +"@smithy/util-stream@npm:^4.3.1": + version: 4.3.1 + resolution: "@smithy/util-stream@npm:4.3.1" + dependencies: + "@smithy/fetch-http-handler": "npm:^5.2.1" + "@smithy/node-http-handler": "npm:^4.2.1" + "@smithy/types": "npm:^4.5.0" + "@smithy/util-base64": "npm:^4.1.0" + "@smithy/util-buffer-from": "npm:^4.1.0" + "@smithy/util-hex-encoding": "npm:^4.1.0" + "@smithy/util-utf8": "npm:^4.1.0" + tslib: "npm:^2.6.2" + checksum: 10c0/7fd8fde8b011fe3535799d9a60195fe8e1229c6976b76d3bf930dbb9d27204754acbf082816cdacaa00e77857ab9e4b673c331c6626aba7ef242cdb7e143b028 + languageName: node + linkType: hard + "@smithy/util-uri-escape@npm:^4.0.0": version: 4.0.0 resolution: "@smithy/util-uri-escape@npm:4.0.0" @@ -17842,6 +18750,15 @@ __metadata: languageName: node linkType: hard +"@smithy/util-uri-escape@npm:^4.1.0": + version: 4.1.0 + resolution: "@smithy/util-uri-escape@npm:4.1.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/3ff56036ce93226b05e68d34c1691e51cdd82ac5f2ba635701ba76a36a2b384ce945bfe2d9c4992f7b500387a6fe1de4d5d0825cd7c73fa10165678d443d3acc + languageName: node + linkType: hard + "@smithy/util-utf8@npm:^2.0.0": version: 2.3.0 resolution: "@smithy/util-utf8@npm:2.3.0" @@ -17862,6 +18779,16 @@ __metadata: languageName: node linkType: hard +"@smithy/util-utf8@npm:^4.1.0": + version: 4.1.0 + resolution: "@smithy/util-utf8@npm:4.1.0" + dependencies: + "@smithy/util-buffer-from": "npm:^4.1.0" + tslib: "npm:^2.6.2" + checksum: 10c0/4331c056b005647701609c42609c3bf0848fdaa01134d891327820c32cfcf7410d8bce1c15d534e5c75af79ea4527c3ca33bccfc104e19a94475fbfe125ecb86 + languageName: node + linkType: hard + "@smithy/util-waiter@npm:^4.0.5": version: 4.0.5 resolution: "@smithy/util-waiter@npm:4.0.5" @@ -32082,6 +33009,17 @@ __metadata: languageName: node linkType: hard +"fast-xml-parser@npm:5.2.5": + version: 5.2.5 + resolution: "fast-xml-parser@npm:5.2.5" + dependencies: + strnum: "npm:^2.1.0" + bin: + fxparser: src/cli/cli.js + checksum: 10c0/d1057d2e790c327ccfc42b872b91786a4912a152d44f9507bf053f800102dfb07ece3da0a86b33ff6a0caa5a5cad86da3326744f6ae5efb0c6c571d754fe48cd + languageName: node + linkType: hard + "fastq@npm:^1.6.0": version: 1.17.1 resolution: "fastq@npm:1.17.1" @@ -50030,6 +50968,13 @@ __metadata: languageName: node linkType: hard +"strnum@npm:^2.1.0": + version: 2.1.1 + resolution: "strnum@npm:2.1.1" + checksum: 10c0/1f9bd1f9b4c68333f25c2b1f498ea529189f060cd50aa59f1876139c994d817056de3ce57c12c970f80568d75df2289725e218bd9e3cdf73cd1a876c9c102733 + languageName: node + linkType: hard + "strtok3@npm:^6.2.4": version: 6.3.0 resolution: "strtok3@npm:6.3.0" @@ -51570,6 +52515,7 @@ __metadata: "@ai-sdk/xai": "npm:^2.0.19" "@aws-sdk/client-lambda": "npm:3.825.0" "@aws-sdk/client-s3": "npm:3.825.0" + "@aws-sdk/client-sesv2": "npm:^3.888.0" "@aws-sdk/client-sts": "npm:3.825.0" "@aws-sdk/credential-providers": "npm:3.825.0" "@babel/preset-env": "npm:7.26.9"