feat: AI Gateway credentials endpoint instance url (backport to release-candidate/2.17.x) (#28527)

Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com>
This commit is contained in:
n8n-assistant[bot] 2026-04-15 12:46:58 +00:00 committed by GitHub
parent 2d50843216
commit 2ec433263b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 24 additions and 3 deletions

View File

@ -10,6 +10,9 @@ import type { License } from '@/license';
import { AiGatewayService } from '@/services/ai-gateway.service';
import type { Project, User, UserRepository } from '@n8n/db';
import type { OwnershipService } from '@/services/ownership.service';
import type { UrlService } from '@/services/url.service';
const INSTANCE_BASE_URL = 'https://my-n8n.example.com';
const BASE_URL = 'http://gateway.test';
const INSTANCE_ID = 'test-instance-id';
@ -34,6 +37,9 @@ function makeService({
isAiGatewayLicensed = true,
ownershipService = mock<OwnershipService>(),
userRepository = mock<UserRepository>({ findOneBy: jest.fn().mockResolvedValue(null) }),
urlService = mock<UrlService>({
getInstanceBaseUrl: jest.fn().mockReturnValue(INSTANCE_BASE_URL),
}),
} = {}) {
const globalConfig = {
aiAssistant: { baseUrl: baseUrl ?? undefined },
@ -53,6 +59,7 @@ function makeService({
instanceSettings,
ownershipService,
userRepository,
urlService,
);
}
@ -189,11 +196,21 @@ describe('AiGatewayService', () => {
'x-n8n-version': N8N_VERSION,
'x-instance-id': INSTANCE_ID,
},
body: JSON.stringify({ licenseCert: LICENSE_CERT }),
body: JSON.stringify({ licenseCert: LICENSE_CERT, instanceUrl: INSTANCE_BASE_URL }),
}),
);
});
it('includes instanceUrl in token body', async () => {
mockConfigThenToken(fetchMock);
const service = makeService();
await service.getSyntheticCredential({ credentialType: 'googlePalmApi', userId: USER_ID });
const body = JSON.parse(fetchMock.mock.calls[1][1].body as string);
expect(body.instanceUrl).toBe(INSTANCE_BASE_URL);
});
it('includes userEmail and userName in token body when user exists', async () => {
const userRepository = mock<UserRepository>({
findOneBy: jest
@ -215,6 +232,7 @@ describe('AiGatewayService', () => {
licenseCert: LICENSE_CERT,
userEmail: 'alice@example.com',
userName: 'Alice Smith',
instanceUrl: INSTANCE_BASE_URL,
}),
}),
);
@ -245,7 +263,7 @@ describe('AiGatewayService', () => {
await service.getSyntheticCredential({ credentialType: 'googlePalmApi', userId: USER_ID });
const body = JSON.parse(fetchMock.mock.calls[1][1].body as string);
expect(body).toEqual({ licenseCert: LICENSE_CERT });
expect(body).toEqual({ licenseCert: LICENSE_CERT, instanceUrl: INSTANCE_BASE_URL });
});
it('caches config and token — second call makes no additional fetches', async () => {
@ -416,7 +434,7 @@ describe('AiGatewayService', () => {
`${BASE_URL}/v1/gateway/credentials`,
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ licenseCert: LICENSE_CERT }),
body: JSON.stringify({ licenseCert: LICENSE_CERT, instanceUrl: INSTANCE_BASE_URL }),
}),
);
expect(fetchMock).toHaveBeenNthCalledWith(2, `${BASE_URL}/v1/gateway/wallet`, {

View File

@ -12,6 +12,7 @@ import { N8N_VERSION, AI_ASSISTANT_SDK_VERSION } from '@/constants';
import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
import { License } from '@/license';
import { OwnershipService } from '@/services/ownership.service';
import { UrlService } from '@/services/url.service';
interface GatewayTokenResponse {
token: string;
@ -44,6 +45,7 @@ export class AiGatewayService {
private readonly instanceSettings: InstanceSettings,
private readonly ownershipService: OwnershipService,
private readonly userRepository: UserRepository,
private readonly urlService: UrlService,
) {}
/**
@ -273,6 +275,7 @@ export class AiGatewayService {
...(user && {
userName: [user.firstName, user.lastName].filter(Boolean).join(' ') || undefined,
}),
instanceUrl: this.urlService.getInstanceBaseUrl(),
}),
});