Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions packages/backend/src/common/db-rest.util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { resolveInternalDbRestUrl } from './db-rest.util';

describe('resolveInternalDbRestUrl', () => {
const PUBLIC = 'https://v6.db.transport.rest';
const cloud = { DEPLOYMENT_MODE: 'cloud', DB_REST_INTERNAL_URL: 'http://db-rest:3000' };

it('swaps the public db-rest host for the internal one in cloud', () => {
expect(resolveInternalDbRestUrl(PUBLIC, cloud as any)).toBe('http://db-rest:3000');
});

it('preserves any path and strips a trailing slash on the internal URL', () => {
expect(
resolveInternalDbRestUrl(`${PUBLIC}/locations`, {
DEPLOYMENT_MODE: 'cloud',
DB_REST_INTERNAL_URL: 'http://db-rest:3000/',
} as any),
).toBe('http://db-rest:3000/locations');
});

it('leaves the URL untouched when not in cloud (self-host)', () => {
expect(
resolveInternalDbRestUrl(PUBLIC, { DB_REST_INTERNAL_URL: 'http://db-rest:3000' } as any),
).toBe(PUBLIC);
});

it('leaves the URL untouched when the internal URL is not configured', () => {
expect(resolveInternalDbRestUrl(PUBLIC, { DEPLOYMENT_MODE: 'cloud' } as any)).toBe(PUBLIC);
});

it('does not touch non-db-rest base URLs', () => {
expect(resolveInternalDbRestUrl('https://api.example.com', cloud as any)).toBe(
'https://api.example.com',
);
});
});
24 changes: 24 additions & 0 deletions packages/backend/src/common/db-rest.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Cloud-only: route the public db-rest base URL to our internal self-hosted
* instance. The shipped Deutsche Bahn connector stores the public base URL
* (`v6.db.transport.rest`) so self-hosters use it as-is; in cloud we swap the
* host to the internal db-rest (`DB_REST_INTERNAL_URL`) at request time. Pure
* host swap — same db-rest schema both sides, so paths/params/responses are
* unchanged. Returns the URL untouched on self-host (env unset / not cloud).
*
* Used by both the tool-execution path (DynamicMcpTools) and the connector
* "Test connection" / health-check path so they exercise the SAME endpoint.
*/
const PUBLIC_DB_REST = 'https://v6.db.transport.rest';

export function resolveInternalDbRestUrl(
baseUrl: string,
env: NodeJS.ProcessEnv = process.env,
): string {
const internal = env.DB_REST_INTERNAL_URL;
const isCloud = (env.DEPLOYMENT_MODE || '') === 'cloud';
if (internal && isCloud && baseUrl.startsWith(PUBLIC_DB_REST)) {

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization High

'
https://v6.db.transport.rest
' may be followed by an arbitrary host name.
return internal.replace(/\/$/, '') + baseUrl.slice(PUBLIC_DB_REST.length);
}
return baseUrl;
}
6 changes: 5 additions & 1 deletion packages/backend/src/connectors/connectors.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DatabaseEngine } from './engines/database.engine';
import { McpClientEngine } from './engines/mcp-client.engine';
import { encrypt, decrypt } from '../common/crypto/encryption.util';
import { getRequiredSecret } from '../common/secrets.util';
import { resolveInternalDbRestUrl } from '../common/db-rest.util';
import { resolveAdapterIcon } from './connector-icon.util';

@Injectable()
Expand Down Expand Up @@ -198,7 +199,10 @@ export class ConnectorsService {
const path = connector.healthcheckPath || '/';
await this.restEngine.execute(
{
baseUrl: connector.baseUrl,
// Apply the same cloud db-rest host swap as tool execution, so
// "Test connection" exercises the real (internal) endpoint
// instead of the public base URL stored on the connector.
baseUrl: resolveInternalDbRestUrl(connector.baseUrl),
authType: connector.authType,
authConfig,
headers: connector.headers as Record<string, string>,
Expand Down
59 changes: 0 additions & 59 deletions packages/backend/src/mcp-server/dynamic-mcp-tools.spec.ts

This file was deleted.

17 changes: 5 additions & 12 deletions packages/backend/src/mcp-server/dynamic-mcp-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { LicenseGuardService } from '../license/license-guard.service';
import { DeploymentService } from '../common/deployment.service';
import { PrismaService } from '../common/prisma.service';
import { interpolateConnectorConfig } from '../common/env-interpolation.util';
import { resolveInternalDbRestUrl } from '../common/db-rest.util';
import type { RegisteredTool } from './tool-registry';

/**
Expand Down Expand Up @@ -85,20 +86,12 @@ export class DynamicMcpTools {
}

/**
* Cloud-only: route the public db-rest base URL to our internal self-hosted
* instance. The shipped connector stays identical for everyone — self-hosted
* installs (env unset, or not cloud) keep talking to the public API, while
* Cloud transparently swaps the host to the internal db-rest for reliability
* and to avoid the public instance's rate limits / 503s. Pure host swap:
* same db-rest schema on both sides, so paths/params/responses are unchanged.
* Cloud-only db-rest host swap — see resolveInternalDbRestUrl. Kept as a thin
* method so the host swap stays consistent with the connector "Test
* connection" path, which uses the same shared util.
*/
private resolveInternalBaseUrl(baseUrl: string): string {
const PUBLIC_DB_REST = 'https://v6.db.transport.rest';
const internal = process.env.DB_REST_INTERNAL_URL;
if (internal && this.deployment.isCloud() && baseUrl.startsWith(PUBLIC_DB_REST)) {
return internal.replace(/\/$/, '') + baseUrl.slice(PUBLIC_DB_REST.length);
}
return baseUrl;
return resolveInternalDbRestUrl(baseUrl);
}

/**
Expand Down
Loading