mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-19 07:36:52 +00:00
feat(core): Support multiple base URLs in n8n Connect synthetic credentials (no-changelog) (#32495)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,6 +6,10 @@ const aiGatewayProviderConfigEntryShape = {
|
||||
gatewayPath: z.string(),
|
||||
urlField: z.string(),
|
||||
apiKeyField: z.string(),
|
||||
// Maps a credential URL field to the gateway path it should be rewritten to.
|
||||
// Lets a single credential fan out to multiple gateway providers. When present,
|
||||
// it is authoritative over `urlField`/`gatewayPath`.
|
||||
routing: z.record(z.string()).optional(),
|
||||
};
|
||||
|
||||
export class AiGatewayProviderConfigEntry extends Z.class(aiGatewayProviderConfigEntryShape) {}
|
||||
|
||||
@@ -441,6 +441,80 @@ describe('AiGatewayService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('fans a single credential out to multiple gateway URLs when routing is present', async () => {
|
||||
const routingConfig = {
|
||||
...MOCK_GATEWAY_CONFIG,
|
||||
credentialTypes: ['browserbaseApi'],
|
||||
providerConfig: {
|
||||
browserbaseApi: {
|
||||
gatewayPath: '/v1/gateway/browserbase',
|
||||
urlField: 'baseUrl',
|
||||
apiKeyField: 'browserbaseApiKey',
|
||||
routing: {
|
||||
baseUrl: '/v1/gateway/browserbase',
|
||||
stagehandBaseUrl: '/v1/gateway/browserbaseStagehand',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
fetchMock
|
||||
.mockResolvedValueOnce({ ok: true, json: jest.fn().mockResolvedValue(routingConfig) })
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({ token: 'mock-jwt-token', expiresIn: 3600 }),
|
||||
});
|
||||
const service = makeService();
|
||||
|
||||
const result = await service.getSyntheticCredential({
|
||||
credentialType: 'browserbaseApi',
|
||||
userId: USER_ID,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
browserbaseApiKey: 'mock-jwt-token',
|
||||
baseUrl: `${BASE_URL}/v1/gateway/browserbase`,
|
||||
stagehandBaseUrl: `${BASE_URL}/v1/gateway/browserbaseStagehand`,
|
||||
});
|
||||
});
|
||||
|
||||
it('embeds exec context in every routed gateway URL', async () => {
|
||||
const routingConfig = {
|
||||
...MOCK_GATEWAY_CONFIG,
|
||||
credentialTypes: ['browserbaseApi'],
|
||||
providerConfig: {
|
||||
browserbaseApi: {
|
||||
gatewayPath: '/v1/gateway/browserbase',
|
||||
urlField: 'baseUrl',
|
||||
apiKeyField: 'browserbaseApiKey',
|
||||
routing: {
|
||||
baseUrl: '/v1/gateway/browserbase',
|
||||
stagehandBaseUrl: '/v1/gateway/browserbaseStagehand',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
fetchMock
|
||||
.mockResolvedValueOnce({ ok: true, json: jest.fn().mockResolvedValue(routingConfig) })
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({ token: 'mock-jwt-token', expiresIn: 3600 }),
|
||||
});
|
||||
const service = makeService();
|
||||
|
||||
const result = await service.getSyntheticCredential({
|
||||
credentialType: 'browserbaseApi',
|
||||
userId: USER_ID,
|
||||
executionId: '29021',
|
||||
workflowId: 'R9JFXwkUCL1jZBuw',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
browserbaseApiKey: 'mock-jwt-token',
|
||||
baseUrl: `${BASE_URL}/v1/gateway/exec/29021/R9JFXwkUCL1jZBuw/browserbase`,
|
||||
stagehandBaseUrl: `${BASE_URL}/v1/gateway/exec/29021/R9JFXwkUCL1jZBuw/browserbaseStagehand`,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses standard gateway URL when gatewayPath does not start with the gateway prefix', async () => {
|
||||
const customConfig = {
|
||||
...MOCK_GATEWAY_CONFIG,
|
||||
|
||||
@@ -121,14 +121,37 @@ export class AiGatewayService {
|
||||
throw new UserError('Failed to obtain a valid AI Gateway token.');
|
||||
}
|
||||
|
||||
const gatewayUrl = this.buildGatewayUrl(baseUrl, providerConfig.gatewayPath, {
|
||||
executionId,
|
||||
workflowId,
|
||||
});
|
||||
const urlFields = this.buildUrlFields(baseUrl, providerConfig, { executionId, workflowId });
|
||||
|
||||
return {
|
||||
[providerConfig.apiKeyField]: jwt,
|
||||
[providerConfig.urlField]: gatewayUrl,
|
||||
...urlFields,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the credential URL field(s) pointing at the gateway.
|
||||
*
|
||||
* When `routing` is present, each `<credential field> → <gateway path>` entry is
|
||||
* rewritten to its gateway URL, letting a single credential fan out to multiple
|
||||
* gateway providers. Otherwise falls back to the single `urlField`/`gatewayPath`.
|
||||
*/
|
||||
private buildUrlFields(
|
||||
baseUrl: string,
|
||||
providerConfig: { gatewayPath: string; urlField: string; routing?: Record<string, string> },
|
||||
context: { executionId?: string; workflowId?: string },
|
||||
): Record<string, string> {
|
||||
const routing = providerConfig.routing;
|
||||
if (routing && Object.keys(routing).length > 0) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(routing).map(([urlField, gatewayPath]) => [
|
||||
urlField,
|
||||
this.buildGatewayUrl(baseUrl, gatewayPath, context),
|
||||
]),
|
||||
);
|
||||
}
|
||||
return {
|
||||
[providerConfig.urlField]: this.buildGatewayUrl(baseUrl, providerConfig.gatewayPath, context),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user