Centralize outbound HTTP requests through SecureHttpClientService (#17779)

## Summary

- Migrates all direct `axios` and `@nestjs/axios` `HttpService` usages
across the server to go through `SecureHttpClientService`, which
conditionally applies SSRF protection based on the
`OUTBOUND_HTTP_SAFE_MODE_ENABLED` config flag
- `SecureHttpClientService.getHttpClient()` now accepts optional
`AxiosRequestConfig` (e.g., `baseURL`) so callers can configure their
client while still getting protection
- Adds `getInternalHttpClient()` for trusted same-server requests (e.g.,
REST-to-GraphQL proxy, code-interpreter downloading internal files)
- Renames `getSecureAdapter` to `getSecureAxiosAdapter` for clarity
- Captcha drivers now receive a pre-configured `AxiosInstance` from the
module factory instead of creating their own

## Migrated services

| Service | Previous | Risk level |
|---------|----------|-----------|
| `file-upload.service` | `HttpService` | High (user-provided image
URLs) |
| `code-interpreter-tool` | `HttpService` + direct adapter | High
(user-provided file URLs) |
| `search-help-center-tool` | `axios.post()` | Low (hardcoded endpoints)
|
| `http-tool` | Already migrated | High (user-provided URLs) |
| `admin-panel.service` | `axios.get()` | Low (Docker Hub API) |
| `sign-in-up.service` | `HttpService` | Medium (logo URL validation) |
| `google-apis-scopes` | `HttpService` | Low (Google API) |
| `geo-map.service` | `HttpService` | Low (Google Maps API) |
| `telemetry.service` | `HttpService` | Low (telemetry endpoint) |
| `rest-api.service` | `HttpService` | Internal (uses
`getInternalHttpClient`) |
| `create-company.service` | `axios.create()` | Low (Twenty companies
API) |
| `google-recaptcha.driver` | `axios.create()` | Low (Google reCAPTCHA)
|
| `turnstile.driver` | `axios.create()` | Low (Cloudflare Turnstile) |

## Test plan

- [x] `npx nx typecheck twenty-server` passes
- [x] `npx nx lint:diff-with-main twenty-server` passes
- [x] Admin panel unit tests pass
- [x] Secure adapter unit tests pass

Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
This commit is contained in:
Félix Malfait
2026-02-08 19:52:35 +01:00
committed by GitHub
parent bc6268bb29
commit d7132b35d3
29 changed files with 187 additions and 134 deletions
@@ -1,4 +1,3 @@
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { CoreCommonApiModule } from 'src/engine/api/common/core-common-api.module';
@@ -22,6 +21,7 @@ import { restToCommonArgsHandlers } from 'src/engine/api/rest/core/rest-to-commo
import { RestApiCoreService } from 'src/engine/api/rest/core/services/rest-api-core.service';
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
import { ActorModule } from 'src/engine/core-modules/actor/actor.module';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
@@ -58,7 +58,6 @@ const restApiCoreResolvers = [
AuthModule,
ApiKeyModule,
UserRoleModule,
HttpModule,
TwentyORMModule,
RecordTransformerModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
@@ -72,6 +71,7 @@ const restApiCoreResolvers = [
providers: [
RestApiService,
RestApiCoreService,
SecureHttpClientService,
...restApiCoreResolvers,
...restToCommonArgsHandlers,
],
@@ -1,9 +1,9 @@
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { MetadataQueryBuilderModule } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.module';
import { RestApiMetadataService } from 'src/engine/api/rest/metadata/rest-api-metadata.service';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { RestApiCoreModule } from 'src/engine/api/rest/core/rest-api-core.module';
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
@@ -15,12 +15,11 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
MetadataQueryBuilderModule,
WorkspaceCacheStorageModule,
AuthModule,
HttpModule,
RestApiCoreModule,
PermissionsModule,
],
controllers: [RestApiMetadataController],
providers: [RestApiService, RestApiMetadataService],
providers: [RestApiService, RestApiMetadataService, SecureHttpClientService],
exports: [RestApiMetadataService, RestApiService],
})
export class RestApiModule {}
@@ -1,4 +1,3 @@
import { HttpService } from '@nestjs/axios';
import { Injectable } from '@nestjs/common';
import { type AxiosResponse } from 'axios';
@@ -6,6 +5,7 @@ import { type AxiosResponse } from 'axios';
import { type Query } from 'src/engine/api/rest/core/types/query.type';
import { RestApiException } from 'src/engine/api/rest/errors/RestApiException';
import { type RequestContext } from 'src/engine/api/rest/types/RequestContext';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
export enum GraphqlApiType {
CORE = 'core',
@@ -14,7 +14,9 @@ export enum GraphqlApiType {
@Injectable()
export class RestApiService {
constructor(private readonly httpService: HttpService) {}
constructor(
private readonly secureHttpClientService: SecureHttpClientService,
) {}
async call(
graphqlApiType: GraphqlApiType,
@@ -28,8 +30,11 @@ export class RestApiService {
: GraphqlApiType.METADATA
}`;
// Internal request to the server's own GraphQL endpoint
const httpClient = this.secureHttpClientService.getInternalHttpClient();
try {
response = await this.httpService.axiosRef.post(url, data, {
response = await httpClient.post(url, data, {
headers: {
'Content-Type': 'application/json',
Authorization: requestContext.headers.authorization,
@@ -1,13 +1,12 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import axios from 'axios';
import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
@@ -15,6 +14,8 @@ const UserFindOneMock = jest.fn();
const LoginTokenServiceGenerateLoginTokenMock = jest.fn();
const TwentyConfigServiceGetAllMock = jest.fn();
const TwentyConfigServiceGetVariableWithMetadataMock = jest.fn();
const mockHttpClientGet = jest.fn();
const mockGetHttpClient = jest.fn().mockReturnValue({ get: mockHttpClientGet });
jest.mock(
'src/engine/core-modules/twenty-config/constants/config-variables-group-metadata',
@@ -87,6 +88,12 @@ describe('AdminPanelService', () => {
provide: FileService,
useValue: {},
},
{
provide: SecureHttpClientService,
useValue: {
getHttpClient: mockGetHttpClient,
},
},
],
}).compile();
@@ -256,18 +263,16 @@ describe('AdminPanelService', () => {
describe('getVersionInfo', () => {
const mockEnvironmentGet = jest.fn();
const mockAxiosGet = jest.fn();
beforeEach(() => {
mockEnvironmentGet.mockReset();
mockAxiosGet.mockReset();
jest.spyOn(axios, 'get').mockImplementation(mockAxiosGet);
mockHttpClientGet.mockReset();
service['twentyConfigService'].get = mockEnvironmentGet;
});
it('should return current and latest version when everything works', async () => {
mockEnvironmentGet.mockReturnValue('1.0.0');
mockAxiosGet.mockResolvedValue({
mockHttpClientGet.mockResolvedValue({
data: {
results: [
{ name: '2.0.0' },
@@ -288,7 +293,7 @@ describe('AdminPanelService', () => {
it('should handle undefined APP_VERSION', async () => {
mockEnvironmentGet.mockReturnValue(undefined);
mockAxiosGet.mockResolvedValue({
mockHttpClientGet.mockResolvedValue({
data: {
results: [{ name: '2.0.0' }, { name: 'latest' }],
},
@@ -304,7 +309,7 @@ describe('AdminPanelService', () => {
it('should handle Docker Hub API error', async () => {
mockEnvironmentGet.mockReturnValue('1.0.0');
mockAxiosGet.mockRejectedValue(new Error('API Error'));
mockHttpClientGet.mockRejectedValue(new Error('API Error'));
const result = await service.getVersionInfo();
@@ -316,7 +321,7 @@ describe('AdminPanelService', () => {
it('should handle empty Docker Hub tags', async () => {
mockEnvironmentGet.mockReturnValue('1.0.0');
mockAxiosGet.mockResolvedValue({
mockHttpClientGet.mockResolvedValue({
data: {
results: [],
},
@@ -332,7 +337,7 @@ describe('AdminPanelService', () => {
it('should handle invalid semver tags', async () => {
mockEnvironmentGet.mockReturnValue('1.0.0');
mockAxiosGet.mockResolvedValue({
mockHttpClientGet.mockResolvedValue({
data: {
results: [
{ name: '2.0.0' },
@@ -7,6 +7,7 @@ import { AdminPanelQueueService } from 'src/engine/core-modules/admin-panel/admi
import { AdminPanelResolver } from 'src/engine/core-modules/admin-panel/admin-panel.resolver';
import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
@@ -38,6 +39,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
AdminPanelService,
AdminPanelHealthService,
AdminPanelQueueService,
SecureHttpClientService,
],
exports: [AdminPanelService],
})
@@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import axios from 'axios';
import semver from 'semver';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
@@ -21,6 +20,7 @@ import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspac
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { type FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { type ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
import { CONFIG_VARIABLES_GROUP_METADATA } from 'src/engine/core-modules/twenty-config/constants/config-variables-group-metadata';
import { type ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
@@ -34,6 +34,7 @@ export class AdminPanelService {
private readonly twentyConfigService: TwentyConfigService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
private readonly fileService: FileService,
private readonly secureHttpClientService: SecureHttpClientService,
@InjectRepository(UserEntity)
private readonly userRepository: Repository<UserEntity>,
) {}
@@ -185,7 +186,9 @@ export class AdminPanelService {
const currentVersion = this.twentyConfigService.get('APP_VERSION');
try {
const rawResponse = await axios.get<unknown>(
const httpClient = this.secureHttpClientService.getHttpClient();
const rawResponse = await httpClient.get<unknown>(
'https://hub.docker.com/v2/repositories/twentycrm/twenty/tags?page_size=100',
);
const response = z
@@ -1,5 +1,3 @@
/* eslint-disable no-restricted-imports */
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@@ -8,6 +6,7 @@ import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
import { AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
import { AppTokenService } from 'src/engine/core-modules/app-token/services/app-token.service';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { GoogleAPIsAuthController } from 'src/engine/core-modules/auth/controllers/google-apis-auth.controller';
@@ -45,6 +44,9 @@ import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module';
import { WorkspaceSSOIdentityProviderEntity } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { TwoFactorAuthenticationMethodEntity } from 'src/engine/core-modules/two-factor-authentication/entities/two-factor-authentication-method.entity';
import { TwoFactorAuthenticationModule } from 'src/engine/core-modules/two-factor-authentication/two-factor-authentication.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
@@ -63,10 +65,6 @@ import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/se
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
import { MessagingFolderSyncManagerModule } from 'src/modules/messaging/message-folder-manager/messaging-folder-sync-manager.module';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { TwoFactorAuthenticationMethodEntity } from '../two-factor-authentication/entities/two-factor-authentication-method.entity';
import { TwoFactorAuthenticationModule } from '../two-factor-authentication/two-factor-authentication.module';
import { AuthResolver } from './auth.resolver';
@@ -96,7 +94,6 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
TwoFactorAuthenticationMethodEntity,
ObjectMetadataEntity,
]),
HttpModule,
UserWorkspaceModule,
WorkspaceModule,
OnboardingModule,
@@ -151,6 +148,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
UpdateConnectedAccountOnReconnectService,
TransientTokenService,
AuthSsoService,
SecureHttpClientService,
],
exports: [
AccessTokenService,
@@ -1,4 +1,3 @@
import { HttpService } from '@nestjs/axios';
import { Injectable } from '@nestjs/common';
import {
@@ -7,6 +6,7 @@ import {
} from 'src/engine/core-modules/auth/auth.exception';
import { includesExpectedScopes } from 'src/engine/core-modules/auth/services/google-apis-scopes.service.util';
import { getGoogleApisOauthScopes } from 'src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
interface TokenInfoResponse {
scope: string;
@@ -24,13 +24,17 @@ interface TokenInfoResponse {
@Injectable()
export class GoogleAPIScopesService {
constructor(private httpService: HttpService) {}
constructor(
private readonly secureHttpClientService: SecureHttpClientService,
) {}
public async getScopesFromGoogleAccessTokenAndCheckIfExpectedScopesArePresent(
accessToken: string,
): Promise<{ scopes: string[]; isValid: boolean }> {
try {
const response = await this.httpService.axiosRef.get<TokenInfoResponse>(
const httpClient = this.secureHttpClientService.getHttpClient();
const response = await httpClient.get<TokenInfoResponse>(
`https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=${accessToken}`,
{ timeout: 600 },
);
@@ -1,4 +1,3 @@
import { HttpService } from '@nestjs/axios';
import { Injectable } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
@@ -31,6 +30,7 @@ import { SubdomainManagerService } from 'src/engine/core-modules/domain/subdomai
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
@@ -55,7 +55,7 @@ export class SignInUpService {
private readonly userWorkspaceService: UserWorkspaceService,
private readonly onboardingService: OnboardingService,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
private readonly httpService: HttpService,
private readonly secureHttpClientService: SecureHttpClientService,
private readonly twentyConfigService: TwentyConfigService,
private readonly subdomainManagerService: SubdomainManagerService,
private readonly userService: UserService,
@@ -471,10 +471,10 @@ export class SignInUpService {
const logoUrl = `${TWENTY_ICONS_BASE_URL}/${getDomainNameByEmail(email)}`;
const isLogoUrlValid = async () => {
try {
return (
(await this.httpService.axiosRef.get(logoUrl, { timeout: 600 }))
.status === 200
);
const httpClient = this.secureHttpClientService.getHttpClient();
const response = await httpClient.get(logoUrl, { timeout: 600 });
return response.status === 200;
} catch {
return false;
}
@@ -8,14 +8,18 @@ import {
CaptchaDriverType,
type CaptchaModuleAsyncOptions,
} from 'src/engine/core-modules/captcha/interfaces';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
@Global()
export class CaptchaModule {
static forRoot(options: CaptchaModuleAsyncOptions): DynamicModule {
const provider = {
provide: CAPTCHA_DRIVER,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
useFactory: async (...args: any[]) => {
useFactory: async (
secureHttpClientService: SecureHttpClientService,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...args: any[]
) => {
const config = await options.useFactory(...args);
if (!config) {
@@ -24,19 +28,30 @@ export class CaptchaModule {
switch (config.type) {
case CaptchaDriverType.GOOGLE_RECAPTCHA:
return new GoogleRecaptchaDriver(config.options);
return new GoogleRecaptchaDriver(
config.options,
secureHttpClientService.getHttpClient({
baseURL: 'https://www.google.com/recaptcha/api/siteverify',
}),
);
case CaptchaDriverType.TURNSTILE:
return new TurnstileDriver(config.options);
return new TurnstileDriver(
config.options,
secureHttpClientService.getHttpClient({
baseURL:
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
}),
);
default:
return;
}
},
inject: options.inject || [],
inject: [SecureHttpClientService, ...(options.inject || [])],
};
return {
module: CaptchaModule,
providers: [CaptchaService, provider],
providers: [CaptchaService, SecureHttpClientService, provider],
exports: [CaptchaService],
};
}
@@ -1,4 +1,4 @@
import axios, { type AxiosInstance } from 'axios';
import { type AxiosInstance } from 'axios';
import { type CaptchaDriver } from 'src/engine/core-modules/captcha/drivers/interfaces/captcha-driver.interface';
import { type CaptchaServerResponse } from 'src/engine/core-modules/captcha/drivers/interfaces/captcha-server-response';
@@ -12,12 +12,13 @@ export class GoogleRecaptchaDriver implements CaptchaDriver {
private readonly _siteKey: string;
private readonly secretKey: string;
private readonly httpService: AxiosInstance;
constructor(private _options: CaptchaDriverOptions) {
constructor(
private _options: CaptchaDriverOptions,
httpClient: AxiosInstance,
) {
this._siteKey = _options.siteKey;
this.secretKey = _options.secretKey;
this.httpService = axios.create({
baseURL: 'https://www.google.com/recaptcha/api/siteverify',
});
this.httpService = httpClient;
}
async validate(token: string): Promise<CaptchaValidateResult> {
@@ -1,4 +1,4 @@
import axios, { type AxiosInstance } from 'axios';
import { type AxiosInstance } from 'axios';
import { type CaptchaDriver } from 'src/engine/core-modules/captcha/drivers/interfaces/captcha-driver.interface';
import { type CaptchaServerResponse } from 'src/engine/core-modules/captcha/drivers/interfaces/captcha-server-response';
@@ -12,12 +12,13 @@ export class TurnstileDriver implements CaptchaDriver {
private readonly _siteKey: string;
private readonly secretKey: string;
private readonly httpService: AxiosInstance;
constructor(private _options: CaptchaDriverOptions) {
constructor(
private _options: CaptchaDriverOptions,
httpClient: AxiosInstance,
) {
this._siteKey = _options.siteKey;
this.secretKey = _options.secretKey;
this.httpService = axios.create({
baseURL: 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
});
this.httpService = httpClient;
}
async validate(token: string): Promise<CaptchaValidateResult> {
@@ -1,14 +1,14 @@
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { FileUploadResolver } from 'src/engine/core-modules/file/file-upload/resolvers/file-upload.resolver';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
@Module({
imports: [FileModule, HttpModule, PermissionsModule],
providers: [FileUploadService, FileUploadResolver],
imports: [FileModule, PermissionsModule],
providers: [FileUploadService, FileUploadResolver, SecureHttpClientService],
exports: [FileUploadService, FileUploadResolver],
})
export class FileUploadModule {}
@@ -1,4 +1,3 @@
import { HttpService } from '@nestjs/axios';
import { Injectable } from '@nestjs/common';
import FileType from 'file-type';
@@ -11,6 +10,7 @@ import { FileStorageService } from 'src/engine/core-modules/file-storage/file-st
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { buildFileInfo } from 'src/engine/core-modules/file/utils/build-file-info.utils';
import { sanitizeFile } from 'src/engine/core-modules/file/utils/sanitize-file.utils';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { getCropSize, getImageBufferFromUrl } from 'src/utils/image';
export type SignedFile = { path: string; token: string };
@@ -26,7 +26,7 @@ export class FileUploadService {
constructor(
private readonly fileStorage: FileStorageService,
private readonly fileService: FileService,
private readonly httpService: HttpService,
private readonly secureHttpClientService: SecureHttpClientService,
) {}
private async _uploadFile({
@@ -95,10 +95,9 @@ export class FileUploadService {
fileFolder: FileFolder;
workspaceId: string;
}) {
const buffer = await getImageBufferFromUrl(
imageUrl,
this.httpService.axiosRef,
);
const httpClient = this.secureHttpClientService.getHttpClient();
const buffer = await getImageBufferFromUrl(imageUrl, httpClient);
const type = await FileType.fromBuffer(buffer);
@@ -1,4 +1,3 @@
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@@ -10,6 +9,7 @@ import { FileWorkspaceFolderDeletionJob } from 'src/engine/core-modules/file/job
import { FileAttachmentListener } from 'src/engine/core-modules/file/listeners/file-attachment.listener';
import { FileWorkspaceMemberListener } from 'src/engine/core-modules/file/listeners/file-workspace-member.listener';
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
@@ -25,7 +25,6 @@ import { FileService } from './services/file.service';
imports: [
JwtModule,
TypeOrmModule.forFeature([FileEntity, WorkspaceEntity, ApplicationEntity]),
HttpModule,
PermissionsModule,
FileStorageModule,
FilesFieldModule,
@@ -40,6 +39,7 @@ import { FileService } from './services/file.service';
FileWorkspaceFolderDeletionJob,
FileDeletionJob,
FileUploadService,
SecureHttpClientService,
],
exports: [FileService, FileMetadataService],
controllers: [FileController],
@@ -1,14 +1,14 @@
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { GeoMapResolver } from 'src/engine/core-modules/geo-map/resolver/geo-map.resolver';
import { GeoMapService } from 'src/engine/core-modules/geo-map/services/geo-map.service';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
@Module({
imports: [HttpModule, WorkspaceCacheStorageModule, TokenModule],
providers: [GeoMapService, GeoMapResolver],
imports: [WorkspaceCacheStorageModule, TokenModule],
providers: [GeoMapService, GeoMapResolver, SecureHttpClientService],
exports: [],
})
export class GeoMapModule {}
@@ -1,4 +1,3 @@
import { HttpService } from '@nestjs/axios';
import { Injectable } from '@nestjs/common';
import { isNonEmptyString } from '@sniptt/guards';
@@ -12,6 +11,7 @@ import {
type AddressFields,
sanitizePlaceDetailsResults,
} from 'src/engine/core-modules/geo-map/utils/sanitize-place-details-results.util';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@Injectable()
@@ -19,7 +19,7 @@ export class GeoMapService {
private apiMapKey: string | undefined;
constructor(
private readonly twentyConfigService: TwentyConfigService,
private readonly httpService: HttpService,
private readonly secureHttpClientService: SecureHttpClientService,
) {
if (
!this.twentyConfigService.get(
@@ -50,7 +50,9 @@ export class GeoMapService {
if (isDefined(isFieldCity) && isFieldCity === true) {
url += `&types=(cities)`;
}
const result = await this.httpService.axiosRef.get(url);
const httpClient = this.secureHttpClientService.getHttpClient();
const result = await httpClient.get(url);
if (result.data.status === 'OK') {
return sanitizeAutocompleteResults(result.data.predictions);
@@ -63,7 +65,9 @@ export class GeoMapService {
placeId: string,
token: string,
): Promise<AddressFields | undefined> {
const result = await this.httpService.axiosRef.get(
const httpClient = this.secureHttpClientService.getHttpClient();
const result = await httpClient.get(
`https://maps.googleapis.com/maps/api/place/details/json?place_id=${placeId}&sessiontoken=${token}&fields=address_components%2Cgeometry&key=${this.apiMapKey}`,
);
@@ -1,15 +1,11 @@
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { TelemetryService } from './telemetry.service';
@Module({
providers: [TelemetryService],
imports: [
HttpModule.register({
baseURL: 'https://twenty-telemetry.com/api/v2',
}),
],
providers: [TelemetryService, SecureHttpClientService],
exports: [TelemetryService],
})
export class TelemetryModule {}
@@ -1,6 +1,6 @@
import { HttpService } from '@nestjs/axios';
import { Injectable } from '@nestjs/common';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
type CreateEventInput = {
@@ -12,7 +12,7 @@ type CreateEventInput = {
export class TelemetryService {
constructor(
private readonly twentyConfigService: TwentyConfigService,
private readonly httpService: HttpService,
private readonly secureHttpClientService: SecureHttpClientService,
) {}
async create(
@@ -36,7 +36,11 @@ export class TelemetryService {
};
try {
await this.httpService.axiosRef.post(`/selfHostingEvent`, data);
const httpClient = this.secureHttpClientService.getHttpClient({
baseURL: 'https://twenty-telemetry.com/api/v2',
});
await httpClient.post(`/selfHostingEvent`, data);
} catch {
return { success: false };
}
@@ -1,21 +1,28 @@
import { Injectable } from '@nestjs/common';
import axios, { type AxiosInstance } from 'axios';
import axios, { type AxiosInstance, type CreateAxiosDefaults } from 'axios';
import { getSecureAdapter } from 'src/engine/core-modules/tool/utils/get-secure-axios-adapter.util';
import { getSecureAxiosAdapter } from 'src/engine/core-modules/tool/utils/get-secure-axios-adapter.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@Injectable()
export class SecureHttpClientService {
constructor(private readonly twentyConfigService: TwentyConfigService) {}
getHttpClient(): AxiosInstance {
// Returns an SSRF-protected HTTP client for external requests
getHttpClient(config?: CreateAxiosDefaults): AxiosInstance {
const isSafeModeEnabled = this.twentyConfigService.get(
'OUTBOUND_HTTP_SAFE_MODE_ENABLED',
);
return isSafeModeEnabled
? axios.create({ adapter: getSecureAdapter() })
: axios.create();
? axios.create({ ...config, adapter: getSecureAxiosAdapter() })
: axios.create(config);
}
// Returns a plain HTTP client for requests to trusted internal URLs
// (e.g., the server's own API endpoints). Not SSRF-protected.
getInternalHttpClient(config?: CreateAxiosDefaults): AxiosInstance {
return axios.create(config);
}
}
@@ -1,4 +1,3 @@
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@@ -17,7 +16,6 @@ import { MessagingImportManagerModule } from 'src/modules/messaging/message-impo
MessagingImportManagerModule,
TypeOrmModule.forFeature([FileEntity]),
FileModule,
HttpModule,
JwtModule,
],
providers: [
@@ -1,4 +1,3 @@
import { HttpService } from '@nestjs/axios';
import { Injectable, Logger } from '@nestjs/common';
import path from 'path';
@@ -24,6 +23,7 @@ import { CodeInterpreterService } from 'src/engine/core-modules/code-interpreter
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { CodeInterpreterInputZodSchema } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.schema';
import { TWENTY_MCP_HELPER } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/twenty-mcp-helper.const';
import { type CodeInterpreterInput } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/types/code-interpreter-input.type';
@@ -33,7 +33,6 @@ import {
type Tool,
type ToolExecutionContext,
} from 'src/engine/core-modules/tool/types/tool.type';
import { getSecureAdapter } from 'src/engine/core-modules/tool/utils/get-secure-axios-adapter.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
@@ -50,7 +49,7 @@ export class CodeInterpreterTool implements Tool {
private readonly codeInterpreterService: CodeInterpreterService,
private readonly fileStorageService: FileStorageService,
private readonly fileService: FileService,
private readonly httpService: HttpService,
private readonly secureHttpClientService: SecureHttpClientService,
private readonly twentyConfigService: TwentyConfigService,
private readonly jwtWrapperService: JwtWrapperService,
) {}
@@ -274,15 +273,16 @@ export class CodeInterpreterTool implements Tool {
continue;
}
// Allow requests to the server's own URL (for internal file downloads)
// but block all other private/internal IPs to prevent SSRF attacks
// Internal file downloads (from the server itself) use a plain client;
// external URLs go through the SSRF-protected client
const isInternalFileUrl = file.url.startsWith(serverUrl);
const adapter = isInternalFileUrl ? undefined : getSecureAdapter();
const httpClient = isInternalFileUrl
? this.secureHttpClientService.getInternalHttpClient()
: this.secureHttpClientService.getHttpClient();
const response = await this.httpService.axiosRef.get(file.url, {
const response = await httpClient.get(file.url, {
responseType: 'arraybuffer',
timeout: 30_000,
adapter,
});
inputFiles.push({
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import axios, { type AxiosRequestConfig } from 'axios';
import { type AxiosRequestConfig, isAxiosError } from 'axios';
import { isDefined } from 'twenty-shared/utils';
import { parseDataFromContentType } from 'twenty-shared/workflow';
@@ -60,7 +60,7 @@ export class HttpTool implements Tool {
headers: response.headers as Record<string, string>,
};
} catch (error) {
if (axios.isAxiosError(error)) {
if (isAxiosError(error)) {
return {
success: false,
message: `HTTP ${method} request to ${url} failed`,
@@ -1,7 +1,8 @@
import { Injectable } from '@nestjs/common';
import axios from 'axios';
import { isAxiosError } from 'axios';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { SearchHelpCenterInputZodSchema } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool.schema';
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
@@ -17,7 +18,10 @@ export class SearchHelpCenterTool implements Tool {
'Search Twenty documentation and help center to find information about features, setup, usage, and troubleshooting.';
inputSchema = SearchHelpCenterInputZodSchema;
constructor(private readonly twentyConfigService: TwentyConfigService) {}
constructor(
private readonly twentyConfigService: TwentyConfigService,
private readonly secureHttpClientService: SecureHttpClientService,
) {}
async execute(
parameters: ToolInput,
@@ -41,7 +45,9 @@ export class SearchHelpCenterTool implements Tool {
...(useDirectApi && { Authorization: `Bearer ${MINTLIFY_API_KEY}` }),
};
const response = await axios.post(
const httpClient = this.secureHttpClientService.getHttpClient();
const response = await httpClient.post(
endpoint,
{ query, pageSize: 10 },
{ headers },
@@ -63,7 +69,7 @@ export class SearchHelpCenterTool implements Tool {
result: results,
};
} catch (error) {
const errorDetail = axios.isAxiosError(error)
const errorDetail = isAxiosError(error)
? error.response?.data?.message || error.message
: error instanceof Error
? error.message
@@ -4,9 +4,9 @@ import * as https from 'https';
import { AxiosHeaders, type InternalAxiosRequestConfig } from 'axios';
import { type SecureAdapterDependencies } from 'src/engine/core-modules/tool/utils/get-secure-axios-adapter.types';
import { getSecureAdapter } from 'src/engine/core-modules/tool/utils/get-secure-axios-adapter.util';
import { getSecureAxiosAdapter } from 'src/engine/core-modules/tool/utils/get-secure-axios-adapter.util';
describe('getSecureAdapter', () => {
describe('getSecureAxiosAdapter', () => {
let mockDnsLookup: jest.Mock;
let mockHttpAdapter: jest.Mock;
let dependencies: SecureAdapterDependencies;
@@ -22,14 +22,14 @@ describe('getSecureAdapter', () => {
describe('URL validation', () => {
it('should throw if URL is not provided', async () => {
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = { url: undefined } as InternalAxiosRequestConfig;
await expect(adapter(config)).rejects.toThrow('URL is required');
});
it('should throw for non-http/https protocols', async () => {
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'file:///etc/passwd',
} as InternalAxiosRequestConfig;
@@ -40,7 +40,7 @@ describe('getSecureAdapter', () => {
});
it('should throw for ftp protocol', async () => {
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'ftp://example.com/file',
} as InternalAxiosRequestConfig;
@@ -56,7 +56,7 @@ describe('getSecureAdapter', () => {
family: 4,
});
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'http://example.com',
headers: new AxiosHeaders(),
@@ -73,7 +73,7 @@ describe('getSecureAdapter', () => {
family: 4,
});
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'https://example.com',
headers: new AxiosHeaders(),
@@ -89,7 +89,7 @@ describe('getSecureAdapter', () => {
it('should block requests to 127.0.0.1', async () => {
mockDnsLookup.mockResolvedValue({ address: '127.0.0.1', family: 4 });
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'http://localhost',
headers: new AxiosHeaders(),
@@ -103,7 +103,7 @@ describe('getSecureAdapter', () => {
it('should block requests to 10.x.x.x range', async () => {
mockDnsLookup.mockResolvedValue({ address: '10.0.0.1', family: 4 });
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'http://internal.example.com',
headers: new AxiosHeaders(),
@@ -117,7 +117,7 @@ describe('getSecureAdapter', () => {
it('should block requests to 192.168.x.x range', async () => {
mockDnsLookup.mockResolvedValue({ address: '192.168.1.1', family: 4 });
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'http://router.local',
headers: new AxiosHeaders(),
@@ -131,7 +131,7 @@ describe('getSecureAdapter', () => {
it('should block requests to 172.16-31.x.x range', async () => {
mockDnsLookup.mockResolvedValue({ address: '172.16.0.1', family: 4 });
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'http://internal.corp',
headers: new AxiosHeaders(),
@@ -148,7 +148,7 @@ describe('getSecureAdapter', () => {
family: 4,
});
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'http://metadata.google.internal',
headers: new AxiosHeaders(),
@@ -167,7 +167,7 @@ describe('getSecureAdapter', () => {
family: 4,
});
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'https://example.com/api/data',
headers: new AxiosHeaders(),
@@ -188,7 +188,7 @@ describe('getSecureAdapter', () => {
family: 4,
});
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'https://example.com/api/data',
headers: new AxiosHeaders(),
@@ -206,7 +206,7 @@ describe('getSecureAdapter', () => {
family: 4,
});
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'http://example.com/api/data',
headers: new AxiosHeaders(),
@@ -224,7 +224,7 @@ describe('getSecureAdapter', () => {
family: 4,
});
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'https://example.com/api/data',
headers: new AxiosHeaders(),
@@ -261,7 +261,7 @@ describe('getSecureAdapter', () => {
family: 6,
});
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'https://example.com/api/data',
headers: new AxiosHeaders(),
@@ -295,7 +295,7 @@ describe('getSecureAdapter', () => {
family: 4,
});
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'https://example.com/api?foo=bar&baz=qux',
headers: new AxiosHeaders(),
@@ -316,7 +316,7 @@ describe('getSecureAdapter', () => {
family: 4,
});
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'https://example.com:8443/api',
headers: new AxiosHeaders(),
@@ -336,7 +336,7 @@ describe('getSecureAdapter', () => {
it('should allow requests to public IP addresses', async () => {
mockDnsLookup.mockResolvedValue({ address: '8.8.8.8', family: 4 });
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'https://dns.google',
headers: new AxiosHeaders(),
@@ -353,7 +353,7 @@ describe('getSecureAdapter', () => {
family: 4,
});
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'https://example.com',
headers: new AxiosHeaders(),
@@ -372,7 +372,7 @@ describe('getSecureAdapter', () => {
family: 4,
});
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'https://user:pass@example.com/api',
headers: new AxiosHeaders(),
@@ -393,7 +393,7 @@ describe('getSecureAdapter', () => {
family: 4,
});
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'https://example.com/page#section',
headers: new AxiosHeaders(),
@@ -410,7 +410,7 @@ describe('getSecureAdapter', () => {
family: 4,
});
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'https://example.com',
headers: undefined,
@@ -428,7 +428,7 @@ describe('getSecureAdapter', () => {
family: 4,
});
const adapter = getSecureAdapter(dependencies);
const adapter = getSecureAxiosAdapter(dependencies);
const config = {
url: 'https://example.com',
headers: { 'Content-Type': 'application/json' },
@@ -17,7 +17,7 @@ const defaultDependencies: SecureAdapterDependencies = {
httpAdapter: axios.getAdapter('http'),
};
export const getSecureAdapter = (
export const getSecureAxiosAdapter = (
dependencies: SecureAdapterDependencies = defaultDependencies,
): AxiosAdapter => {
const { dnsLookup, httpAdapter } = dependencies;
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
@@ -19,6 +20,7 @@ import { CreatePersonService } from 'src/modules/contact-creation-manager/servic
CreateCompanyService,
CreatePersonService,
CreateCompanyAndPersonService,
SecureHttpClientService,
],
exports: [CreateCompanyAndPersonService],
})
@@ -1,7 +1,6 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import axios from 'axios';
import {
ConnectedAccountProvider,
FieldActorSource,
@@ -9,14 +8,13 @@ import {
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import {
type CompanyToCreate,
CreateCompanyService,
} from 'src/modules/contact-creation-manager/services/create-company.service';
jest.mock('axios');
describe('CreateCompanyService', () => {
let service: CreateCompanyService;
let mockCompanyRepository: any;
@@ -109,11 +107,15 @@ describe('CreateCompanyService', () => {
get: jest.fn(),
};
(axios.create as jest.Mock).mockReturnValue(mockHttpService);
const module: TestingModule = await Test.createTestingModule({
providers: [
CreateCompanyService,
{
provide: SecureHttpClientService,
useValue: {
getHttpClient: jest.fn().mockReturnValue(mockHttpService),
},
},
{
provide: GlobalWorkspaceOrmManager,
useValue: {
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import axios, { type AxiosInstance } from 'axios';
import { type AxiosInstance } from 'axios';
import uniqBy from 'lodash.uniqby';
import { TWENTY_COMPANIES_BASE_URL } from 'twenty-shared/constants';
import {
@@ -13,6 +13,7 @@ import {
} from 'twenty-shared/utils';
import { type DeepPartial, ILike } from 'typeorm';
import { SecureHttpClientService } from 'src/engine/core-modules/tool/services/secure-http-client.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
@@ -37,8 +38,9 @@ export class CreateCompanyService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly secureHttpClientService: SecureHttpClientService,
) {
this.httpService = axios.create({
this.httpService = this.secureHttpClientService.getHttpClient({
baseURL: TWENTY_COMPANIES_BASE_URL,
});
}