diff --git a/packages/twenty-server/src/app.module.ts b/packages/twenty-server/src/app.module.ts index 94f49fa7ea6..d06fa2c5a56 100644 --- a/packages/twenty-server/src/app.module.ts +++ b/packages/twenty-server/src/app.module.ts @@ -4,7 +4,6 @@ import { Module, RequestMethod, } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; import { GraphQLModule } from '@nestjs/graphql'; import { ServeStaticModule } from '@nestjs/serve-static'; @@ -48,10 +47,6 @@ const MIGRATED_REST_METHODS = [ @Module({ imports: [ SentryModule.forRoot(), - ConfigModule.forRoot({ - isGlobal: true, - envFilePath: process.env.NODE_ENV === 'test' ? '.env.test' : '.env', - }), GraphQLModule.forRootAsync({ driver: YogaDriver, imports: [GraphQLConfigModule, MetricsModule, DataloaderModule], diff --git a/packages/twenty-server/src/engine/core-modules/applicationVariable/__tests__/application-variable.service.spec.ts b/packages/twenty-server/src/engine/core-modules/applicationVariable/__tests__/application-variable.service.spec.ts new file mode 100644 index 00000000000..4a370707a4c --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/applicationVariable/__tests__/application-variable.service.spec.ts @@ -0,0 +1,329 @@ +import { Test, type TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; + +import { type Repository } from 'typeorm'; + +import { ApplicationVariableEntity } from 'src/engine/core-modules/applicationVariable/application-variable.entity'; +import { + ApplicationVariableEntityException, + ApplicationVariableEntityExceptionCode, +} from 'src/engine/core-modules/applicationVariable/application-variable.exception'; +import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service'; +import { SECRET_APPLICATION_VARIABLE_MASK } from 'src/engine/core-modules/applicationVariable/constants/secret-application-variable-mask.constant'; +import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; + +describe('ApplicationVariableEntityService', () => { + let service: ApplicationVariableEntityService; + let repository: jest.Mocked>; + let secretEncryptionService: jest.Mocked; + let workspaceCacheService: jest.Mocked; + + const mockWorkspaceId = 'workspace-123'; + const mockApplicationId = 'app-456'; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ApplicationVariableEntityService, + { + provide: getRepositoryToken(ApplicationVariableEntity), + useValue: { + findOne: jest.fn(), + update: jest.fn(), + save: jest.fn(), + delete: jest.fn(), + }, + }, + { + provide: SecretEncryptionService, + useValue: { + encrypt: jest.fn((value: string) => `encrypted_${value}`), + decrypt: jest.fn((value: string) => + value.replace('encrypted_', ''), + ), + decryptAndMask: jest.fn( + ({ + value: _value, + mask: _mask, + }: { + value: string; + mask: string; + }) => '********', + ), + }, + }, + { + provide: WorkspaceCacheService, + useValue: { + invalidateAndRecompute: jest.fn(), + }, + }, + ], + }).compile(); + + service = module.get( + ApplicationVariableEntityService, + ); + repository = module.get(getRepositoryToken(ApplicationVariableEntity)); + secretEncryptionService = module.get(SecretEncryptionService); + workspaceCacheService = module.get(WorkspaceCacheService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('update', () => { + it('should encrypt value when variable is secret', async () => { + const existingVariable = { + id: '1', + key: 'API_KEY', + value: 'old-encrypted-value', + isSecret: true, + applicationId: mockApplicationId, + } as ApplicationVariableEntity; + + repository.findOne.mockResolvedValue(existingVariable); + repository.update.mockResolvedValue({ affected: 1 } as any); + + await service.update({ + key: 'API_KEY', + plainTextValue: 'new-secret-value', + applicationId: mockApplicationId, + workspaceId: mockWorkspaceId, + }); + + expect(secretEncryptionService.encrypt).toHaveBeenCalledWith( + 'new-secret-value', + ); + expect(repository.update).toHaveBeenCalledWith( + { key: 'API_KEY', applicationId: mockApplicationId }, + { value: 'encrypted_new-secret-value' }, + ); + expect(workspaceCacheService.invalidateAndRecompute).toHaveBeenCalledWith( + mockWorkspaceId, + ['applicationVariableMaps'], + ); + }); + + it('should not encrypt value when variable is not secret', async () => { + const existingVariable = { + id: '1', + key: 'PUBLIC_URL', + value: 'https://old-url.com', + isSecret: false, + applicationId: mockApplicationId, + } as ApplicationVariableEntity; + + repository.findOne.mockResolvedValue(existingVariable); + repository.update.mockResolvedValue({ affected: 1 } as any); + + await service.update({ + key: 'PUBLIC_URL', + plainTextValue: 'https://new-url.com', + applicationId: mockApplicationId, + workspaceId: mockWorkspaceId, + }); + + expect(secretEncryptionService.encrypt).not.toHaveBeenCalled(); + expect(repository.update).toHaveBeenCalledWith( + { key: 'PUBLIC_URL', applicationId: mockApplicationId }, + { value: 'https://new-url.com' }, + ); + }); + + it('should throw exception when variable not found', async () => { + repository.findOne.mockResolvedValue(null); + + await expect( + service.update({ + key: 'NON_EXISTENT', + plainTextValue: 'some-value', + applicationId: mockApplicationId, + workspaceId: mockWorkspaceId, + }), + ).rejects.toThrow(ApplicationVariableEntityException); + + await expect( + service.update({ + key: 'NON_EXISTENT', + plainTextValue: 'some-value', + applicationId: mockApplicationId, + workspaceId: mockWorkspaceId, + }), + ).rejects.toMatchObject({ + code: ApplicationVariableEntityExceptionCode.APPLICATION_VARIABLE_NOT_FOUND, + }); + }); + }); + + describe('upsertManyApplicationVariableEntities', () => { + it('should encrypt secret values when creating new variables', async () => { + repository.findOne.mockResolvedValue(null); + repository.save.mockResolvedValue({} as any); + repository.delete.mockResolvedValue({ affected: 0 } as any); + + await service.upsertManyApplicationVariableEntities({ + applicationVariables: { + SECRET_KEY: { + universalIdentifier: 'secret-key-123', + value: 'my-secret', + description: 'A secret key', + isSecret: true, + }, + }, + applicationId: mockApplicationId, + workspaceId: mockWorkspaceId, + }); + + expect(secretEncryptionService.encrypt).toHaveBeenCalledWith('my-secret'); + expect(repository.save).toHaveBeenCalledWith({ + key: 'SECRET_KEY', + value: 'encrypted_my-secret', + description: 'A secret key', + isSecret: true, + applicationId: mockApplicationId, + }); + }); + + it('should not encrypt non-secret values when creating new variables', async () => { + repository.findOne.mockResolvedValue(null); + repository.save.mockResolvedValue({} as any); + repository.delete.mockResolvedValue({ affected: 0 } as any); + + await service.upsertManyApplicationVariableEntities({ + applicationVariables: { + PUBLIC_URL: { + universalIdentifier: 'public-url-123', + value: 'https://example.com', + description: 'Public URL', + isSecret: false, + }, + }, + applicationId: mockApplicationId, + workspaceId: mockWorkspaceId, + }); + + expect(secretEncryptionService.encrypt).not.toHaveBeenCalled(); + expect(repository.save).toHaveBeenCalledWith({ + key: 'PUBLIC_URL', + value: 'https://example.com', + description: 'Public URL', + isSecret: false, + applicationId: mockApplicationId, + }); + }); + + it('should handle undefined isSecret as false', async () => { + repository.findOne.mockResolvedValue(null); + repository.save.mockResolvedValue({} as any); + repository.delete.mockResolvedValue({ affected: 0 } as any); + + await service.upsertManyApplicationVariableEntities({ + applicationVariables: { + SOME_VAR: { + universalIdentifier: 'some-var-123', + value: 'some-value', + description: 'Some variable', + }, + }, + applicationId: mockApplicationId, + workspaceId: mockWorkspaceId, + }); + + expect(secretEncryptionService.encrypt).not.toHaveBeenCalled(); + expect(repository.save).toHaveBeenCalledWith( + expect.objectContaining({ + isSecret: false, + }), + ); + }); + + it('should update existing variables without changing values', async () => { + const existingVariable = { + id: '1', + key: 'EXISTING_VAR', + value: 'existing-encrypted-value', + isSecret: true, + applicationId: mockApplicationId, + } as ApplicationVariableEntity; + + repository.findOne.mockResolvedValue(existingVariable); + repository.update.mockResolvedValue({ affected: 1 } as any); + repository.delete.mockResolvedValue({ affected: 0 } as any); + + await service.upsertManyApplicationVariableEntities({ + applicationVariables: { + EXISTING_VAR: { + universalIdentifier: 'existing-var-123', + value: 'new-value', + description: 'Updated description', + isSecret: true, + }, + }, + applicationId: mockApplicationId, + workspaceId: mockWorkspaceId, + }); + + expect(repository.update).toHaveBeenCalledWith( + { key: 'EXISTING_VAR', applicationId: mockApplicationId }, + { + description: 'Updated description', + isSecret: true, + value: 'encrypted_new-value', + }, + ); + expect(repository.save).not.toHaveBeenCalled(); + }); + + it('should handle undefined applicationVariables', async () => { + await service.upsertManyApplicationVariableEntities({ + applicationVariables: undefined, + applicationId: mockApplicationId, + workspaceId: mockWorkspaceId, + }); + + expect(repository.findOne).not.toHaveBeenCalled(); + expect(repository.save).not.toHaveBeenCalled(); + expect(repository.update).not.toHaveBeenCalled(); + expect( + workspaceCacheService.invalidateAndRecompute, + ).not.toHaveBeenCalled(); + }); + }); + + describe('getDisplayValue', () => { + it('should return plain value for non-secret variables', () => { + const variable = { + id: '1', + key: 'PUBLIC_URL', + value: 'https://example.com', + isSecret: false, + applicationId: mockApplicationId, + } as ApplicationVariableEntity; + + const result = service.getDisplayValue(variable); + + expect(result).toBe('https://example.com'); + expect(secretEncryptionService.decryptAndMask).not.toHaveBeenCalled(); + }); + + it('should call decryptAndMask for secret variables', () => { + const variable = { + id: '1', + key: 'SECRET_KEY', + value: 'encrypted_value', + isSecret: true, + applicationId: mockApplicationId, + } as ApplicationVariableEntity; + + service.getDisplayValue(variable); + + expect(secretEncryptionService.decryptAndMask).toHaveBeenCalledWith({ + value: 'encrypted_value', + mask: SECRET_APPLICATION_VARIABLE_MASK, + }); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.module.ts b/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.module.ts index 979d050c28b..5c0248b4a13 100644 --- a/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.module.ts +++ b/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.module.ts @@ -7,6 +7,7 @@ import { ApplicationVariableEntity } from 'src/engine/core-modules/applicationVa import { ApplicationVariableEntityResolver } from 'src/engine/core-modules/applicationVariable/application-variable.resolver'; import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service'; import { WorkspaceApplicationVariableMapCacheService } from 'src/engine/core-modules/applicationVariable/services/workspace-application-variable-map-cache.service'; +import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @@ -16,6 +17,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache TypeOrmModule.forFeature([ApplicationVariableEntity]), PermissionsModule, WorkspaceCacheModule, + SecretEncryptionModule, ], providers: [ ApplicationVariableEntityService, diff --git a/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.resolver.ts b/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.resolver.ts index 84514b78720..62d051ec67f 100644 --- a/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.resolver.ts @@ -1,13 +1,21 @@ import { UseFilters, UseGuards } from '@nestjs/common'; -import { Args, Mutation, Resolver } from '@nestjs/graphql'; +import { + Args, + Mutation, + Parent, + ResolveField, + Resolver, +} from '@nestjs/graphql'; import { PermissionFlagType } from 'twenty-shared/constants'; import { ApplicationVariableEntityExceptionFilter } from 'src/engine/core-modules/applicationVariable/application-variable-exception-filter'; +import { ApplicationVariableEntity } from 'src/engine/core-modules/applicationVariable/application-variable.entity'; import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service'; +import { ApplicationVariableEntityDTO } from 'src/engine/core-modules/applicationVariable/dtos/application-variable.dto'; import { UpdateApplicationVariableEntityInput } from 'src/engine/core-modules/applicationVariable/dtos/update-application-variable.input'; -import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; @@ -15,13 +23,18 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; WorkspaceAuthGuard, SettingsPermissionGuard(PermissionFlagType.APPLICATIONS), ) -@Resolver() +@Resolver(() => ApplicationVariableEntityDTO) @UseFilters(ApplicationVariableEntityExceptionFilter) export class ApplicationVariableEntityResolver { constructor( private readonly applicationVariableService: ApplicationVariableEntityService, ) {} + @ResolveField(() => String) + value(@Parent() applicationVariable: ApplicationVariableEntity): string { + return this.applicationVariableService.getDisplayValue(applicationVariable); + } + @Mutation(() => Boolean) async updateOneApplicationVariable( @Args() { key, value, applicationId }: UpdateApplicationVariableEntityInput, @@ -29,7 +42,7 @@ export class ApplicationVariableEntityResolver { ) { await this.applicationVariableService.update({ key, - value, + plainTextValue: value, applicationId, workspaceId, }); diff --git a/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.service.ts b/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.service.ts index addec34de07..400d482bc55 100644 --- a/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.service.ts +++ b/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.service.ts @@ -1,11 +1,17 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { ApplicationVariables } from 'twenty-shared/application'; import { isDefined } from 'twenty-shared/utils'; import { In, Not, Repository } from 'typeorm'; -import { ApplicationVariables } from 'twenty-shared/application'; import { ApplicationVariableEntity } from 'src/engine/core-modules/applicationVariable/application-variable.entity'; +import { + ApplicationVariableEntityException, + ApplicationVariableEntityExceptionCode, +} from 'src/engine/core-modules/applicationVariable/application-variable.exception'; +import { SECRET_APPLICATION_VARIABLE_MASK } from 'src/engine/core-modules/applicationVariable/constants/secret-application-variable-mask.constant'; +import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; @Injectable() @@ -14,21 +20,58 @@ export class ApplicationVariableEntityService { @InjectRepository(ApplicationVariableEntity) private readonly applicationVariableRepository: Repository, private readonly workspaceCacheService: WorkspaceCacheService, + private readonly secretEncryptionService: SecretEncryptionService, ) {} + private encryptSecretValue(value: string, isSecret: boolean): string { + if (!isSecret) { + return value; + } + + return this.secretEncryptionService.encrypt(value); + } + + getDisplayValue(applicationVariable: ApplicationVariableEntity): string { + if (!applicationVariable.isSecret) { + return applicationVariable.value; + } + + return this.secretEncryptionService.decryptAndMask({ + value: applicationVariable.value, + mask: SECRET_APPLICATION_VARIABLE_MASK, + }); + } + async update({ key, - value, + plainTextValue, applicationId, workspaceId, - }: Pick & { + }: Pick & { applicationId: string; workspaceId: string; + plainTextValue: string; }) { + const existingVariable = await this.applicationVariableRepository.findOne({ + where: { key, applicationId }, + }); + + if (!isDefined(existingVariable)) { + throw new ApplicationVariableEntityException( + `Application variable with key ${key} not found`, + ApplicationVariableEntityExceptionCode.APPLICATION_VARIABLE_NOT_FOUND, + ); + } + + const encryptedValue = this.encryptSecretValue( + plainTextValue, + existingVariable.isSecret, + ); + await this.applicationVariableRepository.update( { key, applicationId }, { - value, + value: encryptedValue, }, ); @@ -53,6 +96,12 @@ export class ApplicationVariableEntityService { for (const [key, { value, description, isSecret }] of Object.entries( applicationVariables, )) { + const isSecretValue = isSecret ?? false; + const encryptedValue = this.encryptSecretValue( + value ?? '', + isSecretValue, + ); + if ( await this.applicationVariableRepository.findOne({ where: { @@ -67,16 +116,17 @@ export class ApplicationVariableEntityService { applicationId, }, { - description, - isSecret, + value: encryptedValue, + description: description ?? '', + isSecret: isSecretValue, }, ); } else { await this.applicationVariableRepository.save({ key, - value, - description, - isSecret, + value: encryptedValue, + description: description ?? '', + isSecret: isSecretValue, applicationId, }); } diff --git a/packages/twenty-server/src/engine/core-modules/applicationVariable/constants/secret-application-variable-mask.constant.ts b/packages/twenty-server/src/engine/core-modules/applicationVariable/constants/secret-application-variable-mask.constant.ts new file mode 100644 index 00000000000..402f8c269d0 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/applicationVariable/constants/secret-application-variable-mask.constant.ts @@ -0,0 +1 @@ +export const SECRET_APPLICATION_VARIABLE_MASK = '********'; 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 95aafd16cb4..281fec6063b 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 @@ -9,6 +9,7 @@ import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module'; import { AppTokenModule } from 'src/engine/core-modules/app-token/app-token.module'; import { ApplicationSyncModule } from 'src/engine/core-modules/application/application-sync.module'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; +import { EnvironmentModule } from 'src/engine/core-modules/environment/environment.module'; import { ApprovedAccessDomainModule } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { BillingWebhookModule } from 'src/engine/core-modules/billing-webhook/billing-webhook.module'; @@ -75,6 +76,7 @@ import { FileModule } from './file/file.module'; @Module({ imports: [ + EnvironmentModule, TwentyConfigModule.forRoot(), HealthModule, AuditModule, diff --git a/packages/twenty-server/src/engine/core-modules/environment/environment.module.ts b/packages/twenty-server/src/engine/core-modules/environment/environment.module.ts new file mode 100644 index 00000000000..74898d55876 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/environment/environment.module.ts @@ -0,0 +1,30 @@ +import { Global, Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; + +import { + ConfigVariables, + validate, +} from 'src/engine/core-modules/twenty-config/config-variables'; +import { CONFIG_VARIABLES_INSTANCE_TOKEN } from 'src/engine/core-modules/twenty-config/constants/config-variables-instance-tokens.constants'; +import { EnvironmentConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/environment-config.driver'; + +@Global() +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + expandVariables: true, + validate, + envFilePath: process.env.NODE_ENV === 'test' ? '.env.test' : '.env', + }), + ], + providers: [ + EnvironmentConfigDriver, + { + provide: CONFIG_VARIABLES_INSTANCE_TOKEN, + useValue: new ConfigVariables(), + }, + ], + exports: [EnvironmentConfigDriver], +}) +export class EnvironmentModule {} diff --git a/packages/twenty-server/src/engine/core-modules/secret-encryption/secret-encryption.module.ts b/packages/twenty-server/src/engine/core-modules/secret-encryption/secret-encryption.module.ts new file mode 100644 index 00000000000..a94982175be --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/secret-encryption/secret-encryption.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service'; + +@Module({ + providers: [SecretEncryptionService], + exports: [SecretEncryptionService], +}) +export class SecretEncryptionModule {} diff --git a/packages/twenty-server/src/engine/core-modules/secret-encryption/secret-encryption.service.spec.ts b/packages/twenty-server/src/engine/core-modules/secret-encryption/secret-encryption.service.spec.ts new file mode 100644 index 00000000000..581e11415e3 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/secret-encryption/secret-encryption.service.spec.ts @@ -0,0 +1,191 @@ +import { Test, type TestingModule } from '@nestjs/testing'; + +import { EnvironmentConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/environment-config.driver'; + +import { SecretEncryptionService } from './secret-encryption.service'; + +describe('SecretEncryptionService', () => { + let service: SecretEncryptionService; + + const mockAppSecret = 'mock-app-secret-for-testing-purposes-12345678'; + const testValue = 'my-secret-api-key-123'; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SecretEncryptionService, + { + provide: EnvironmentConfigDriver, + useValue: { + get: jest.fn().mockReturnValue(mockAppSecret), + }, + }, + ], + }).compile(); + + service = module.get(SecretEncryptionService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('encrypt and decrypt', () => { + it('should encrypt and decrypt a value correctly', () => { + const encrypted = service.encrypt(testValue); + const decrypted = service.decrypt(encrypted); + + expect(decrypted).toBe(testValue); + expect(encrypted).not.toBe(testValue); + }); + + it('should generate different encrypted values for the same input (due to random IV)', () => { + const encrypted1 = service.encrypt(testValue); + const encrypted2 = service.encrypt(testValue); + + expect(encrypted1).not.toBe(encrypted2); + + const decrypted1 = service.decrypt(encrypted1); + const decrypted2 = service.decrypt(encrypted2); + + expect(decrypted1).toBe(testValue); + expect(decrypted2).toBe(testValue); + }); + + it('should handle special characters in values', () => { + const specialValue = 'api-key_WITH@special#chars!123$%^&*()'; + + const encrypted = service.encrypt(specialValue); + const decrypted = service.decrypt(encrypted); + + expect(decrypted).toBe(specialValue); + }); + + it('should handle empty strings', () => { + const emptyValue = ''; + + const encrypted = service.encrypt(emptyValue); + const decrypted = service.decrypt(encrypted); + + expect(decrypted).toBe(emptyValue); + }); + + it('should handle long values', () => { + const longValue = 'a'.repeat(1000); + + const encrypted = service.encrypt(longValue); + const decrypted = service.decrypt(encrypted); + + expect(decrypted).toBe(longValue); + }); + + it('should handle unicode characters', () => { + const unicodeValue = 'secret-with-émojis-🔐-and-中文'; + + const encrypted = service.encrypt(unicodeValue); + const decrypted = service.decrypt(encrypted); + + expect(decrypted).toBe(unicodeValue); + }); + }); + + describe('encrypt edge cases', () => { + it('should return undefined values as-is', () => { + const result = service.encrypt(undefined as unknown as string); + + expect(result).toBeUndefined(); + }); + + it('should return null values as-is', () => { + const result = service.encrypt(null as unknown as string); + + expect(result).toBeNull(); + }); + }); + + describe('decrypt edge cases', () => { + it('should return undefined values as-is', () => { + const result = service.decrypt(undefined as unknown as string); + + expect(result).toBeUndefined(); + }); + + it('should return null values as-is', () => { + const result = service.decrypt(null as unknown as string); + + expect(result).toBeNull(); + }); + }); + + describe('decryptAndMask', () => { + const mask = '********'; + + it('should return undefined values as-is', () => { + const result = service.decryptAndMask({ + value: undefined as unknown as string, + mask, + }); + + expect(result).toBeUndefined(); + }); + + it('should return null values as-is', () => { + const result = service.decryptAndMask({ + value: null as unknown as string, + mask, + }); + + expect(result).toBeNull(); + }); + + it('should return only mask for short secrets (length <= 10)', () => { + const shortSecret = 'value'; + const encrypted = service.encrypt(shortSecret); + + const result = service.decryptAndMask({ value: encrypted, mask }); + + // "value" has 5 chars, floor(5/10) = 0, min(5, 0) = 0, slice(0,0) = "" + expect(result).toBe(mask); + }); + + it('should return partial value + mask for medium secrets (10 < length <= 50)', () => { + const mediumSecret = 'sk-abc123def456'; + const encrypted = service.encrypt(mediumSecret); + + const result = service.decryptAndMask({ value: encrypted, mask }); + + // "sk-abc123def456" has 15 chars, floor(15/10) = 1, min(5, 1) = 1 + expect(result).toBe(`s${mask}`); + }); + + it('should return partial value + mask for longer secrets', () => { + const longerSecret = 'sk-abcdefghij1234567890'; + const encrypted = service.encrypt(longerSecret); + + const result = service.decryptAndMask({ value: encrypted, mask }); + + // "sk-abcdefghij1234567890" has 23 chars, floor(23/10) = 2, min(5, 2) = 2 + expect(result).toBe(`sk${mask}`); + }); + + it('should cap visible chars at 5 for very long secrets', () => { + const veryLongSecret = 'a'.repeat(100); + const encrypted = service.encrypt(veryLongSecret); + + const result = service.decryptAndMask({ value: encrypted, mask }); + + // 100 chars, floor(100/10) = 10, min(5, 10) = 5 + expect(result).toBe(`aaaaa${mask}`); + }); + + it('should handle empty secret values', () => { + const emptySecret = ''; + const encrypted = service.encrypt(emptySecret); + + const result = service.decryptAndMask({ value: encrypted, mask }); + + // "" has 0 chars, floor(0/10) = 0, min(5, 0) = 0 + expect(result).toBe(mask); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/secret-encryption/secret-encryption.service.ts b/packages/twenty-server/src/engine/core-modules/secret-encryption/secret-encryption.service.ts new file mode 100644 index 00000000000..53587d94075 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/secret-encryption/secret-encryption.service.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@nestjs/common'; + +import { isDefined } from 'twenty-shared/utils'; + +import { + decryptText, + encryptText, +} from 'src/engine/core-modules/auth/auth.util'; +import { EnvironmentConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/environment-config.driver'; + +@Injectable() +export class SecretEncryptionService { + constructor( + private readonly environmentConfigDriver: EnvironmentConfigDriver, + ) {} + + private getAppSecret(): string { + return this.environmentConfigDriver.get('APP_SECRET'); + } + + public encrypt(value: string): string { + if (!isDefined(value)) { + return value; + } + + const appSecret = this.getAppSecret(); + + return encryptText(value, appSecret); + } + + public decrypt(value: string): string { + if (!isDefined(value)) { + return value; + } + + const appSecret = this.getAppSecret(); + + return decryptText(value, appSecret); + } + + public decryptAndMask({ + value, + mask, + }: { + value: string; + mask: string; + }): string { + if (!isDefined(value)) { + return value; + } + + const decryptedValue = this.decrypt(value); + + const visibleCharsCount = Math.min( + 5, + Math.floor(decryptedValue.length / 10), + ); + + return `${decryptedValue.slice(0, visibleCharsCount)}${mask}`; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/__tests__/build-env-var.spec.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/__tests__/build-env-var.spec.ts new file mode 100644 index 00000000000..22d96fe6ed2 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/__tests__/build-env-var.spec.ts @@ -0,0 +1,120 @@ +import { type FlatApplicationVariable } from 'src/engine/core-modules/applicationVariable/types/flat-application-variable.type'; +import { type SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service'; +import { buildEnvVar } from 'src/engine/core-modules/serverless/drivers/utils/build-env-var'; + +describe('buildEnvVar', () => { + const mockSecretEncryptionService = { + encrypt: jest.fn((value: string) => `encrypted_${value}`), + decrypt: jest.fn((value: string) => value.replace('encrypted_', '')), + } as unknown as SecretEncryptionService; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return empty object for empty array', () => { + const result = buildEnvVar([], mockSecretEncryptionService); + + expect(result).toEqual({}); + }); + + it('should handle mixed secret and non-secret variables', () => { + const flatVariables: FlatApplicationVariable[] = [ + { + id: '1', + key: 'PUBLIC_URL', + value: 'https://example.com', + description: 'Public URL', + isSecret: false, + applicationId: 'app-1', + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', + }, + { + id: '2', + key: 'API_SECRET', + value: 'encrypted_secret-123', + description: 'API secret', + isSecret: true, + applicationId: 'app-1', + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', + }, + { + id: '3', + key: 'DEBUG', + value: 'true', + description: 'Debug flag', + isSecret: false, + applicationId: 'app-1', + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', + }, + ]; + + const result = buildEnvVar(flatVariables, mockSecretEncryptionService); + + expect(result).toEqual({ + PUBLIC_URL: 'https://example.com', + API_SECRET: 'secret-123', + DEBUG: 'true', + }); + expect(mockSecretEncryptionService.decrypt).toHaveBeenCalledTimes(1); + expect(mockSecretEncryptionService.decrypt).toHaveBeenCalledWith( + 'encrypted_secret-123', + ); + }); + + it('should handle null or undefined values', () => { + const flatVariables: FlatApplicationVariable[] = [ + { + id: '1', + key: 'NULL_VALUE', + value: null as unknown as string, + description: '', + isSecret: false, + applicationId: 'app-1', + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', + }, + { + id: '2', + key: 'UNDEFINED_VALUE', + value: undefined as unknown as string, + description: '', + isSecret: false, + applicationId: 'app-1', + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', + }, + ]; + + const result = buildEnvVar(flatVariables, mockSecretEncryptionService); + + expect(result).toEqual({ + NULL_VALUE: '', + UNDEFINED_VALUE: '', + }); + }); + + it('should convert non-string values to strings', () => { + const flatVariables: FlatApplicationVariable[] = [ + { + id: '1', + key: 'NUMBER_VALUE', + value: 123 as unknown as string, + description: '', + isSecret: false, + applicationId: 'app-1', + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', + }, + ]; + + const result = buildEnvVar(flatVariables, mockSecretEncryptionService); + + expect(result).toEqual({ + NUMBER_VALUE: '123', + }); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/build-env-var.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/build-env-var.ts index f9fb7c3443b..0b72e9eac71 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/build-env-var.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/build-env-var.ts @@ -1,11 +1,17 @@ import { type FlatApplicationVariable } from 'src/engine/core-modules/applicationVariable/types/flat-application-variable.type'; +import { type SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service'; export const buildEnvVar = ( flatApplicationVariables: FlatApplicationVariable[], + secretEncryptionService: SecretEncryptionService, ): Record => { return flatApplicationVariables.reduce>( (acc, flatApplicationVariable) => { - acc[flatApplicationVariable.key] = flatApplicationVariable.value; + const value = String(flatApplicationVariable.value ?? ''); + + acc[flatApplicationVariable.key] = flatApplicationVariable.isSecret + ? secretEncryptionService.decrypt(value) + : value; return acc; }, diff --git a/packages/twenty-server/src/engine/core-modules/twenty-config/drivers/database-config.module.ts b/packages/twenty-server/src/engine/core-modules/twenty-config/drivers/database-config.module.ts index 4ae46395404..bf9395d246c 100644 --- a/packages/twenty-server/src/engine/core-modules/twenty-config/drivers/database-config.module.ts +++ b/packages/twenty-server/src/engine/core-modules/twenty-config/drivers/database-config.module.ts @@ -3,12 +3,12 @@ import { ScheduleModule } from '@nestjs/schedule'; import { TypeOrmModule } from '@nestjs/typeorm'; import { KeyValuePairEntity } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity'; +import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module'; import { ConfigCacheService } from 'src/engine/core-modules/twenty-config/cache/config-cache.service'; import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables'; import { CONFIG_VARIABLES_INSTANCE_TOKEN } from 'src/engine/core-modules/twenty-config/constants/config-variables-instance-tokens.constants'; import { ConfigValueConverterService } from 'src/engine/core-modules/twenty-config/conversion/config-value-converter.service'; import { DatabaseConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/database-config.driver'; -import { EnvironmentConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/environment-config.driver'; import { ConfigStorageService } from 'src/engine/core-modules/twenty-config/storage/config-storage.service'; @Module({}) @@ -19,13 +19,13 @@ export class DatabaseConfigModule { imports: [ TypeOrmModule.forFeature([KeyValuePairEntity]), ScheduleModule.forRoot(), + SecretEncryptionModule, ], providers: [ DatabaseConfigDriver, ConfigCacheService, ConfigStorageService, ConfigValueConverterService, - EnvironmentConfigDriver, { provide: CONFIG_VARIABLES_INSTANCE_TOKEN, useValue: new ConfigVariables(), diff --git a/packages/twenty-server/src/engine/core-modules/twenty-config/storage/__tests__/config-storage.service.spec.ts b/packages/twenty-server/src/engine/core-modules/twenty-config/storage/__tests__/config-storage.service.spec.ts index 34d48b02178..c7ad68688fb 100644 --- a/packages/twenty-server/src/engine/core-modules/twenty-config/storage/__tests__/config-storage.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/twenty-config/storage/__tests__/config-storage.service.spec.ts @@ -3,11 +3,11 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { type DeleteResult, IsNull, type Repository } from 'typeorm'; -import * as authUtils from 'src/engine/core-modules/auth/auth.util'; import { KeyValuePairEntity, KeyValuePairType, } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity'; +import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service'; import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables'; import { ConfigValueConverterService } from 'src/engine/core-modules/twenty-config/conversion/config-value-converter.service'; import { EnvironmentConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/environment-config.driver'; @@ -23,15 +23,15 @@ import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspac import { TypedReflect } from 'src/utils/typed-reflect'; jest.mock('src/engine/core-modules/auth/auth.util', () => ({ - encryptText: jest.fn((text) => `encrypted:${text}`), - decryptText: jest.fn((text) => text.replace('encrypted:', '')), + encryptText: jest.fn((text) => `${text}`), + decryptText: jest.fn((text) => text.replace('', '')), })); describe('ConfigStorageService', () => { let service: ConfigStorageService; let keyValuePairRepository: Repository; let configValueConverter: ConfigValueConverterService; - let environmentConfigDriver: EnvironmentConfigDriver; + let secretEncryptionService: SecretEncryptionService; const createMockKeyValuePair = ( key: string, @@ -79,6 +79,13 @@ describe('ConfigStorageService', () => { delete: jest.fn(), }, }, + { + provide: SecretEncryptionService, + useValue: { + decrypt: jest.fn((value) => value), + encrypt: jest.fn((value) => value), + }, + }, ], }).compile(); @@ -89,8 +96,8 @@ describe('ConfigStorageService', () => { configValueConverter = module.get( ConfigValueConverterService, ); - environmentConfigDriver = module.get( - EnvironmentConfigDriver, + secretEncryptionService = module.get( + SecretEncryptionService, ); jest.clearAllMocks(); @@ -167,7 +174,7 @@ describe('ConfigStorageService', () => { it('should decrypt sensitive string values', async () => { const key = 'SENSITIVE_CONFIG' as keyof ConfigVariables; const originalValue = 'sensitive-value'; - const encryptedValue = 'encrypted:sensitive-value'; + const encryptedValue = 'sensitive-value'; const mockRecord = createMockKeyValuePair(key as string, encryptedValue); @@ -191,10 +198,8 @@ describe('ConfigStorageService', () => { const result = await service.get(key); expect(result).toBe(originalValue); - expect(environmentConfigDriver.get).toHaveBeenCalledWith('APP_SECRET'); - expect(authUtils.decryptText).toHaveBeenCalledWith( + expect(secretEncryptionService.decrypt).toHaveBeenCalledWith( encryptedValue, - 'test-secret', ); }); @@ -227,43 +232,6 @@ describe('ConfigStorageService', () => { expect(result).toBe(convertedValue); }); - it('should handle decryption failure in get() by returning original value', async () => { - const key = 'SENSITIVE_CONFIG' as keyof ConfigVariables; - const encryptedValue = 'encrypted:sensitive-value'; - - const mockRecord = createMockKeyValuePair(key as string, encryptedValue); - - jest - .spyOn(keyValuePairRepository, 'findOne') - .mockResolvedValue(mockRecord); - - jest.spyOn(TypedReflect, 'getMetadata').mockReturnValue({ - [key]: { - isSensitive: true, - type: ConfigVariableType.STRING, - group: ConfigVariablesGroup.SERVER_CONFIG, - description: 'Test sensitive config', - }, - }); - - ( - configValueConverter.convertDbValueToAppValue as jest.Mock - ).mockReturnValue(encryptedValue); - - // Mock decryption to throw an error - (authUtils.decryptText as jest.Mock).mockImplementationOnce(() => { - throw new Error('Decryption failed'); - }); - - const result = await service.get(key); - - expect(result).toBe(encryptedValue); - expect(authUtils.decryptText).toHaveBeenCalledWith( - encryptedValue, - 'test-secret', - ); - }); - it('should handle findOne errors', async () => { const key = 'AUTH_PASSWORD_ENABLED' as keyof ConfigVariables; const error = new Error('Database error'); @@ -405,7 +373,7 @@ describe('ConfigStorageService', () => { const key = 'SENSITIVE_CONFIG' as keyof ConfigVariables; const value = 'sensitive-value'; const convertedValue = 'sensitive-value'; - const encryptedValue = 'encrypted:sensitive-value'; + const encryptedValue = 'sensitive-value'; jest.spyOn(keyValuePairRepository, 'findOne').mockResolvedValue(null); @@ -431,48 +399,10 @@ describe('ConfigStorageService', () => { workspaceId: null, type: KeyValuePairType.CONFIG_VARIABLE, }); - expect(environmentConfigDriver.get).toHaveBeenCalledWith('APP_SECRET'); - expect(authUtils.encryptText).toHaveBeenCalledWith( + expect(secretEncryptionService.encrypt).toHaveBeenCalledWith( convertedValue, - 'test-secret', ); }); - - it('should handle encryption failure in set() by using unconverted value', async () => { - const key = 'SENSITIVE_CONFIG' as keyof ConfigVariables; - const value = 'sensitive-value'; - const convertedValue = 'converted-value'; - - jest.spyOn(keyValuePairRepository, 'findOne').mockResolvedValue(null); - - jest.spyOn(TypedReflect, 'getMetadata').mockReturnValue({ - [key]: { - isSensitive: true, - type: ConfigVariableType.STRING, - group: ConfigVariablesGroup.SERVER_CONFIG, - description: 'Test sensitive config', - }, - }); - - ( - configValueConverter.convertAppValueToDbValue as jest.Mock - ).mockReturnValue(convertedValue); - - // Mock encryption to throw an error - (authUtils.encryptText as jest.Mock).mockImplementationOnce(() => { - throw new Error('Encryption failed'); - }); - - await service.set(key, value); - - expect(keyValuePairRepository.insert).toHaveBeenCalledWith({ - key: key as string, - value: convertedValue, // Should fall back to unconverted value - userId: null, - workspaceId: null, - type: KeyValuePairType.CONFIG_VARIABLE, - }); - }); }); describe('delete', () => { @@ -608,7 +538,7 @@ describe('ConfigStorageService', () => { it('should decrypt sensitive string values in loadAll', async () => { const configVars: KeyValuePairEntity[] = [ - createMockKeyValuePair('SENSITIVE_CONFIG', 'encrypted:sensitive-value'), + createMockKeyValuePair('SENSITIVE_CONFIG', 'sensitive-value'), createMockKeyValuePair('NORMAL_CONFIG', 'normal-value'), ]; @@ -640,10 +570,8 @@ describe('ConfigStorageService', () => { expect(result.get('NORMAL_CONFIG' as keyof ConfigVariables)).toBe( 'normal-value', ); - expect(environmentConfigDriver.get).toHaveBeenCalledWith('APP_SECRET'); - expect(authUtils.decryptText).toHaveBeenCalledWith( - 'encrypted:sensitive-value', - 'test-secret', + expect(secretEncryptionService.decrypt).toHaveBeenCalledWith( + 'sensitive-value', ); }); }); diff --git a/packages/twenty-server/src/engine/core-modules/twenty-config/storage/config-storage.service.ts b/packages/twenty-server/src/engine/core-modules/twenty-config/storage/config-storage.service.ts index 1b04b798ba3..01094e41ac0 100644 --- a/packages/twenty-server/src/engine/core-modules/twenty-config/storage/config-storage.service.ts +++ b/packages/twenty-server/src/engine/core-modules/twenty-config/storage/config-storage.service.ts @@ -4,16 +4,12 @@ import { InjectRepository } from '@nestjs/typeorm'; import { type FindOptionsWhere, IsNull, Repository } from 'typeorm'; import { - decryptText, - encryptText, -} from 'src/engine/core-modules/auth/auth.util'; -import { - KeyValuePairType, KeyValuePairEntity, + KeyValuePairType, } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity'; +import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service'; import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables'; import { ConfigValueConverterService } from 'src/engine/core-modules/twenty-config/conversion/config-value-converter.service'; -import { EnvironmentConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/environment-config.driver'; import { ConfigVariableType } from 'src/engine/core-modules/twenty-config/enums/config-variable-type.enum'; import { ConfigVariableException, @@ -31,7 +27,7 @@ export class ConfigStorageService implements ConfigStorageInterface { @InjectRepository(KeyValuePairEntity) private readonly keyValuePairRepository: Repository, private readonly configValueConverter: ConfigValueConverterService, - private readonly environmentConfigDriver: EnvironmentConfigDriver, + private readonly secretEncryptionService: SecretEncryptionService, ) {} private getConfigVariableWhereClause( @@ -45,10 +41,6 @@ export class ConfigStorageService implements ConfigStorageInterface { }; } - private getAppSecret(): string { - return this.environmentConfigDriver.get('APP_SECRET'); - } - private getConfigMetadata(key: T) { return TypedReflect.getMetadata('config-variables', ConfigVariables)?.[ key as string @@ -77,21 +69,9 @@ export class ConfigStorageService implements ConfigStorageInterface { return convertedValue; } - const appSecret = this.getAppSecret(); - - try { - return isDecrypt - ? decryptText(convertedValue, appSecret) - : encryptText(convertedValue, appSecret); - } catch (error) { - this.logger.debug( - `${isDecrypt ? 'Decryption' : 'Encryption'} failed for key ${ - key as string - }: ${error.message}. Using original value.`, - ); - - return convertedValue; - } + return isDecrypt + ? this.secretEncryptionService.decrypt(convertedValue) + : this.secretEncryptionService.encrypt(convertedValue); } catch (error) { throw new ConfigVariableException( `Failed to convert value for key ${key as string}: ${error.message}`, diff --git a/packages/twenty-server/src/engine/core-modules/twenty-config/twenty-config.module.ts b/packages/twenty-server/src/engine/core-modules/twenty-config/twenty-config.module.ts index f594285a7ca..ae2399c77b2 100644 --- a/packages/twenty-server/src/engine/core-modules/twenty-config/twenty-config.module.ts +++ b/packages/twenty-server/src/engine/core-modules/twenty-config/twenty-config.module.ts @@ -1,13 +1,8 @@ import { type DynamicModule, Global, Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { - ConfigVariables, - validate, -} from 'src/engine/core-modules/twenty-config/config-variables'; +import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables'; import { CONFIG_VARIABLES_INSTANCE_TOKEN } from 'src/engine/core-modules/twenty-config/constants/config-variables-instance-tokens.constants'; import { DatabaseConfigModule } from 'src/engine/core-modules/twenty-config/drivers/database-config.module'; -import { EnvironmentConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/environment-config.driver'; import { ConfigurableModuleClass } from 'src/engine/core-modules/twenty-config/twenty-config.module-definition'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; @@ -18,22 +13,15 @@ export class TwentyConfigModule extends ConfigurableModuleClass { const isConfigVariablesInDbEnabled = process.env.IS_CONFIG_VARIABLES_IN_DB_ENABLED !== 'false'; - const imports = [ - ConfigModule.forRoot({ - isGlobal: true, - expandVariables: true, - validate, - envFilePath: process.env.NODE_ENV === 'test' ? '.env.test' : '.env', - }), - ...(isConfigVariablesInDbEnabled ? [DatabaseConfigModule.forRoot()] : []), - ]; + const imports = isConfigVariablesInDbEnabled + ? [DatabaseConfigModule.forRoot()] + : []; return { module: TwentyConfigModule, imports, providers: [ TwentyConfigService, - EnvironmentConfigDriver, { provide: CONFIG_VARIABLES_INSTANCE_TOKEN, useValue: new ConfigVariables(), diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts index 912a992bcef..63a95e1cc83 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts @@ -10,6 +10,7 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature- import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; +import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module'; import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module'; import { CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity'; import { DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity'; @@ -51,6 +52,7 @@ import { FunctionBuildModule } from 'src/engine/metadata-modules/function-build/ SubscriptionsModule, WorkspaceCacheModule, TokenModule, + SecretEncryptionModule, ], providers: [ ServerlessFunctionService, diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts index 301358dd153..6b2d24df885 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts @@ -18,6 +18,7 @@ import { AuditService } from 'src/engine/core-modules/audit/services/audit.servi import { SERVERLESS_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/serverless-function/serverless-function-executed'; import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; +import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service'; import { buildEnvVar } from 'src/engine/core-modules/serverless/drivers/utils/build-env-var'; import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service'; import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/get-serverless-folder-or-throw.utils'; @@ -70,6 +71,7 @@ export class ServerlessFunctionService { private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, private readonly applicationService: ApplicationService, private readonly workspaceCacheService: WorkspaceCacheService, + private readonly secretEncryptionService: SecretEncryptionService, ) {} async hasServerlessFunctionPublishedVersion( @@ -205,7 +207,7 @@ export class ServerlessFunctionService { [DEFAULT_API_KEY_NAME]: applicationAccessToken.token, } : {}), - ...buildEnvVar(flatApplicationVariables), + ...buildEnvVar(flatApplicationVariables, this.secretEncryptionService), }; // We keep that check to build functions