feat(API-11): Route API calls through AgilitonAPI gateway
Add gateway-first pattern: when AGILITON_API_KEY is set, route all external API calls through the gateway with X-API-Key auth. Falls back to direct API access when gateway is unavailable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
6
.env
6
.env
@@ -28,7 +28,11 @@ POSTGRES_PORT=6432
|
|||||||
LLM_API_URL=https://api.agiliton.cloud/llm
|
LLM_API_URL=https://api.agiliton.cloud/llm
|
||||||
LLM_API_KEY=sk-c02d41a118ce8330c428100afaa816c8
|
LLM_API_KEY=sk-c02d41a118ce8330c428100afaa816c8
|
||||||
|
|
||||||
# Jira Cloud (CF-762 migration)
|
# AgilitonAPI Gateway (API-11: centralized API access)
|
||||||
|
AGILITON_API_KEY=gw_92399e154f02730ebadec65ddbde9426c9378ec77093d1c9
|
||||||
|
AGILITON_API_URL=https://api.agiliton.cloud
|
||||||
|
|
||||||
|
# Jira Cloud (fallback if gateway unavailable)
|
||||||
JIRA_URL=https://agiliton.atlassian.net
|
JIRA_URL=https://agiliton.atlassian.net
|
||||||
JIRA_USERNAME=christian.gick@agiliton.eu
|
JIRA_USERNAME=christian.gick@agiliton.eu
|
||||||
JIRA_API_TOKEN=ATATT3xFfGF0tpaJTS4nJklW587McubEw-1SYbLWqfovkxI5320NdbFc-3fgHlw0HGTLOikgV082m9N-SIsYVZveGXa553_1LAyOevV6Qples93xF4hIExWGAvwvXPy_4pW2tH5FNusN5ieMca5_-YUP0i69SIN0RLIMQjfqDmQyhZXbkIvrm-I=A8A2A1FC
|
JIRA_API_TOKEN=ATATT3xFfGF0tpaJTS4nJklW587McubEw-1SYbLWqfovkxI5320NdbFc-3fgHlw0HGTLOikgV082m9N-SIsYVZveGXa553_1LAyOevV6Qples93xF4hIExWGAvwvXPy_4pW2tH5FNusN5ieMca5_-YUP0i69SIN0RLIMQjfqDmQyhZXbkIvrm-I=A8A2A1FC
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Jira Cloud REST API client for session-mcp.
|
* Jira Cloud REST API client — routes through AgilitonAPI gateway.
|
||||||
* Creates/closes CF issues for sessions and posts session output as comments.
|
* Falls back to direct Jira access if AGILITON_API_KEY is not set.
|
||||||
*
|
*
|
||||||
* Uses JIRA_URL, JIRA_USERNAME, JIRA_API_TOKEN env vars.
|
* Gateway: AGILITON_API_KEY + AGILITON_API_URL
|
||||||
|
* Direct: JIRA_URL, JIRA_USERNAME, JIRA_API_TOKEN
|
||||||
*/
|
*/
|
||||||
|
|
||||||
interface JiraIssue {
|
interface JiraIssue {
|
||||||
@@ -16,35 +17,48 @@ interface JiraTransition {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getConfig = () => ({
|
// Gateway config
|
||||||
url: process.env.JIRA_URL || 'https://agiliton.atlassian.net',
|
const GATEWAY_URL = (process.env.AGILITON_API_URL || 'https://api.agiliton.cloud').replace(/\/$/, '');
|
||||||
username: process.env.JIRA_USERNAME || '',
|
const GATEWAY_KEY = process.env.AGILITON_API_KEY || '';
|
||||||
token: process.env.JIRA_API_TOKEN || '',
|
|
||||||
});
|
|
||||||
|
|
||||||
function getAuthHeader(): string {
|
// Direct config (fallback)
|
||||||
const { username, token } = getConfig();
|
const JIRA_URL = process.env.JIRA_URL || 'https://agiliton.atlassian.net';
|
||||||
return `Basic ${Buffer.from(`${username}:${token}`).toString('base64')}`;
|
const JIRA_USERNAME = process.env.JIRA_USERNAME || '';
|
||||||
|
const JIRA_API_TOKEN = process.env.JIRA_API_TOKEN || '';
|
||||||
|
|
||||||
|
function useGateway(): boolean {
|
||||||
|
return !!GATEWAY_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isConfigured(): boolean {
|
function isConfigured(): boolean {
|
||||||
const { username, token } = getConfig();
|
if (useGateway()) return true;
|
||||||
return !!(username && token);
|
return !!(JIRA_USERNAME && JIRA_API_TOKEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function jiraFetch(path: string, options: RequestInit = {}): Promise<Response> {
|
async function jiraFetch(path: string, options: RequestInit = {}): Promise<Response> {
|
||||||
const { url } = getConfig();
|
let url: string;
|
||||||
const fullUrl = `${url}/rest/api/3${path}`;
|
let headers: Record<string, string>;
|
||||||
|
|
||||||
return fetch(fullUrl, {
|
if (useGateway()) {
|
||||||
...options,
|
url = `${GATEWAY_URL}/jira-cloud${path}`;
|
||||||
headers: {
|
headers = {
|
||||||
'Authorization': getAuthHeader(),
|
'X-API-Key': GATEWAY_KEY,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
...options.headers,
|
...options.headers as Record<string, string>,
|
||||||
},
|
};
|
||||||
});
|
} else {
|
||||||
|
url = `${JIRA_URL}/rest/api/3${path}`;
|
||||||
|
const auth = Buffer.from(`${JIRA_USERNAME}:${JIRA_API_TOKEN}`).toString('base64');
|
||||||
|
headers = {
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
...options.headers as Record<string, string>,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(url, { ...options, headers });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -165,7 +179,6 @@ export async function transitionToDone(issueKey: string): Promise<boolean> {
|
|||||||
if (!isConfigured()) return false;
|
if (!isConfigured()) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get available transitions
|
|
||||||
const transResponse = await jiraFetch(`/issue/${issueKey}/transitions`);
|
const transResponse = await jiraFetch(`/issue/${issueKey}/transitions`);
|
||||||
if (!transResponse.ok) {
|
if (!transResponse.ok) {
|
||||||
console.error(`session-mcp: Jira get transitions failed (${transResponse.status})`);
|
console.error(`session-mcp: Jira get transitions failed (${transResponse.status})`);
|
||||||
@@ -182,7 +195,6 @@ export async function transitionToDone(issueKey: string): Promise<boolean> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute transition
|
|
||||||
const response = await jiraFetch(`/issue/${issueKey}/transitions`, {
|
const response = await jiraFetch(`/issue/${issueKey}/transitions`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
Reference in New Issue
Block a user