Initial commit
This commit is contained in:
142
mcp-server/auth/apiClient.js
Normal file
142
mcp-server/auth/apiClient.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import axios from "axios";
|
||||
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { sessionApiClients, getSessionCredentials, setCredentials, findRoleByToken } from "./credentials.js";
|
||||
import { assertSafeCmsTarget } from "../utils/cmsTargetSafety.js";
|
||||
const DEFAULT_ROLE = 'developer';
|
||||
|
||||
/**
|
||||
* Check if session is configured with valid credentials
|
||||
*/
|
||||
export const ensureConfigured = async (sessionId) => {
|
||||
const creds = await getSessionCredentials(sessionId);
|
||||
if (!creds.token || !creds.web_url || !creds.api_web_url) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidRequest,
|
||||
"Acai site not configured for safe local execution. Use the project MCP config/select_project flow so ACAI_API_WEB_URL points to the local environment."
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
assertSafeCmsTarget(creds, "apiClient");
|
||||
} catch (error) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidRequest,
|
||||
error.message
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Rebuild API client for a session
|
||||
*/
|
||||
export const rebuildApiClient = async (sessionId) => {
|
||||
const creds = await getSessionCredentials(sessionId);
|
||||
if (!creds.token || !creds.web_url || !creds.api_web_url) {
|
||||
return null;
|
||||
}
|
||||
assertSafeCmsTarget(creds, "apiClient");
|
||||
const client = axios.create({
|
||||
baseURL: creds.api_web_url,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Acai-Token": creds.token,
|
||||
...(creds.forge_host ? { Host: creds.forge_host } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
// Request interceptor: always send latest token
|
||||
client.interceptors.request.use((config) => {
|
||||
if (creds.token) {
|
||||
config.headers["X-Acai-Token"] = creds.token;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
sessionApiClients.set(sessionId, client);
|
||||
return client;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get or create API client for a session
|
||||
* @param {string} sessionId - The session ID
|
||||
*/
|
||||
export const getApiClient = async (sessionId) => {
|
||||
const creds = await getSessionCredentials(sessionId);
|
||||
|
||||
console.error(`[API Client] getApiClient called for session ${sessionId}`);
|
||||
console.error(`[API Client] Current creds: token=${!!creds.token}, web_url=${creds.web_url}`);
|
||||
|
||||
await ensureConfigured(sessionId);
|
||||
let client = sessionApiClients.get(sessionId);
|
||||
if (!client) {
|
||||
console.error(`[API Client] No cached client, rebuilding...`);
|
||||
client = await rebuildApiClient(sessionId);
|
||||
}
|
||||
if (!client) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidRequest,
|
||||
"Unable to create API client. Verify credentials and try again."
|
||||
);
|
||||
}
|
||||
console.error(`[API Client] Returning client for session ${sessionId}`);
|
||||
return client;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set credentials and rebuild API client
|
||||
* @param {Object} credentials - The credentials object
|
||||
* @param {string} sessionId - The session ID
|
||||
* @param {string} mcpSessionId - Optional MCP-Session-Id for persistence across SSE reconnections
|
||||
*/
|
||||
export const setCredentialsAndRebuild = async (credentials, sessionId, mcpSessionId = null) => {
|
||||
await setCredentials(credentials, sessionId, mcpSessionId);
|
||||
await rebuildApiClient(sessionId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper for authenticated handlers
|
||||
* Supports both session-based auth and inline credentials (stateless mode).
|
||||
*
|
||||
* If args contains acaiToken + acaiWebsite, these are used directly,
|
||||
* allowing Claude to send credentials with each request.
|
||||
*/
|
||||
export const withAuth = (handler) => {
|
||||
return async (args, extra) => {
|
||||
const sessionId = extra?.sessionId || "_default";
|
||||
|
||||
console.error(`[withAuth] Called with sessionId: ${sessionId}`);
|
||||
|
||||
// Check for inline credentials (stateless mode)
|
||||
const inlineCredentials = {
|
||||
acaiToken: args.acaiToken,
|
||||
acaiWebsite: args.acaiWebsite,
|
||||
acaiTokenHash: args.acaiTokenHash
|
||||
};
|
||||
|
||||
const hasInlineCredentials = inlineCredentials.acaiToken && inlineCredentials.acaiWebsite;
|
||||
|
||||
if (hasInlineCredentials) {
|
||||
// Lookup role by token before storing credentials
|
||||
const role = findRoleByToken(inlineCredentials.acaiToken) || DEFAULT_ROLE;
|
||||
console.error(`[withAuth] Using INLINE credentials: website=${inlineCredentials.acaiWebsite}, role=${role}`);
|
||||
|
||||
// Temporarily store inline credentials in session for this request
|
||||
await setCredentials({
|
||||
token: inlineCredentials.acaiToken,
|
||||
website: inlineCredentials.acaiWebsite,
|
||||
web_url: `https://${inlineCredentials.acaiWebsite}`,
|
||||
api_web_url: null,
|
||||
forge_host: null,
|
||||
tokenHash: inlineCredentials.acaiTokenHash || null,
|
||||
profileName: 'inline',
|
||||
role: role
|
||||
}, sessionId);
|
||||
}
|
||||
|
||||
console.error(`[withAuth] Getting API client for session ${sessionId}...`);
|
||||
await getApiClient(sessionId);
|
||||
console.error(`[withAuth] API client ready, calling handler...`);
|
||||
|
||||
return handler(args, { ...extra, sessionId, inlineCredentials: hasInlineCredentials ? inlineCredentials : null });
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user