fix(gateway/auth): Implement proper auth/init flow with automatic background userId generation

- Fix getUserId() to use placeholder that triggers auth/init if the auth/init endpoint is down for whatever reason
- Add silent auth/init attempt in AI services
- Improve hosted mode error handling
- Remove fake userId/email generation from init.js
This commit is contained in:
Eyal Toledano
2025-05-31 19:47:18 -04:00
parent 769275b3bc
commit 9b87dd23de
11 changed files with 4699 additions and 4558 deletions

View File

@@ -278,6 +278,7 @@ async function _attemptProviderCallWithRetries(
* @param {string} commandName - Command name for tracking
* @param {string} outputType - Output type (cli, mcp)
* @param {string} projectRoot - Project root path
* @param {string} initialRole - The initial client role
* @returns {Promise<object>} AI response with usage data
*/
async function _callGatewayAI(
@@ -288,36 +289,62 @@ async function _callGatewayAI(
userId,
commandName,
outputType,
projectRoot
projectRoot,
initialRole
) {
const gatewayUrl =
process.env.TASKMASTER_GATEWAY_URL || "http://localhost:4444";
const endpoint = `${gatewayUrl}/api/v1/ai/${serviceType}`;
// Hard-code service-level constants
const gatewayUrl = "http://localhost:4444"; // or your production URL
const serviceApiKey = "339a81c9-5b9c-4d60-92d8-cba2ee2a8cc3"; // Hardcoded service key -- if you change this, the Hosted Gateway will not work
// Get API key from env
const apiKey = resolveEnvVariable("TASKMASTER_API_KEY", null, projectRoot);
if (!apiKey) {
throw new Error("TASKMASTER_API_KEY not found for hosted mode");
// Get user auth info for headers
const userMgmt = require("./user-management.js");
const userToken = await userMgmt.getUserToken(projectRoot);
const userEmail = await userMgmt.getUserEmail(projectRoot);
if (!userToken) {
throw new Error(
"User token not found. Run 'task-master init' to register with gateway."
);
}
// need to make sure the user is authenticated and has a valid paid user token + enough credits for this call
const endpoint = `${gatewayUrl}/api/v1/ai/${serviceType}`;
// Extract messages from callParams and convert to gateway format
const systemPrompt =
callParams.messages?.find((m) => m.role === "system")?.content || "";
const prompt =
callParams.messages?.find((m) => m.role === "user")?.content || "";
const requestBody = {
provider: providerName,
model: modelId,
serviceType,
userId,
role: initialRole,
messages: callParams.messages,
modelId,
commandName,
outputType,
...callParams,
roleParams: {
maxTokens: callParams.maxTokens,
temperature: callParams.temperature,
},
...(serviceType === "generateObject" && {
schema: callParams.schema,
objectName: callParams.objectName,
}),
};
const headers = {
"Content-Type": "application/json",
"X-TaskMaster-API-Key": serviceApiKey, // Service-level auth (hardcoded)
Authorization: `Bearer ${userToken}`, // User-level auth
};
// Add user email header if available
if (userEmail) {
headers["X-User-Email"] = userEmail;
}
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
headers,
body: JSON.stringify(requestBody),
});
@@ -383,14 +410,45 @@ async function _unifiedServiceRunner(serviceType, params) {
const effectiveProjectRoot = projectRoot || findProjectRoot();
const userId = getUserId(effectiveProjectRoot);
// Check if user is in hosted mode
// If userId is the placeholder, try to initialize user silently
if (userId === "1234567890") {
try {
// Dynamic import to avoid circular dependency
const userMgmt = await import("./user-management.js");
const initResult = await userMgmt.initializeUser(effectiveProjectRoot);
if (initResult.success) {
// Update the config with the new userId
const { writeConfig, getConfig } = await import("./config-manager.js");
const config = getConfig(effectiveProjectRoot);
config.account.userId = initResult.userId;
writeConfig(config, effectiveProjectRoot);
log("info", "User successfully authenticated with gateway");
} else {
log("warn", `Silent auth/init failed: ${initResult.error}`);
}
} catch (error) {
log("warn", `Silent auth/init attempt failed: ${error.message}`);
}
}
// Add hosted mode check here
const hostedMode = isHostedMode(effectiveProjectRoot);
if (hostedMode) {
// For hosted mode, route through gateway
// Route through gateway - use your existing implementation
log("info", "Routing AI call through TaskMaster gateway (hosted mode)");
try {
// Check if we have a valid userId (not placeholder)
const finalUserId = getUserId(effectiveProjectRoot); // Re-check after potential auth
if (finalUserId === "1234567890" || !finalUserId) {
throw new Error(
"Hosted mode requires user authentication. Please run 'task-master init' to register with the gateway, or switch to BYOK mode if the gateway service is unavailable."
);
}
// Get the role configuration for provider/model selection
let providerName, modelId;
if (initialRole === "main") {
@@ -442,10 +500,11 @@ async function _unifiedServiceRunner(serviceType, params) {
callParams,
providerName,
modelId,
userId,
finalUserId,
commandName,
outputType,
effectiveProjectRoot
effectiveProjectRoot,
initialRole
);
// For hosted mode, we don't need to submit telemetry separately
@@ -455,7 +514,7 @@ async function _unifiedServiceRunner(serviceType, params) {
// Convert gateway account info to telemetry format for UI display
telemetryData = {
timestamp: new Date().toISOString(),
userId,
userId: finalUserId,
commandName,
modelUsed: modelId,
providerName,

File diff suppressed because it is too large Load Diff

View File

@@ -65,7 +65,7 @@ const defaultConfig = {
},
},
account: {
userId: null,
userId: "1234567890", // Placeholder that triggers auth/init
userEmail: "",
mode: "byok",
telemetryEnabled: false,
@@ -710,9 +710,9 @@ function isConfigFilePresent(explicitRoot = null) {
/**
* Gets the user ID from the configuration.
* Sets a default value if none exists and saves the config.
* Returns a placeholder that triggers auth/init if no real userId exists.
* @param {string|null} explicitRoot - Optional explicit path to the project root.
* @returns {string} The user ID (never null).
* @returns {string|null} The user ID or placeholder, or null if auth unavailable.
*/
function getUserId(explicitRoot = null) {
const config = getConfig(explicitRoot);
@@ -722,25 +722,14 @@ function getUserId(explicitRoot = null) {
config.account = { ...defaultConfig.account };
}
// If userId exists, return it
if (config.account.userId) {
// If userId exists and is not the placeholder, return it
if (config.account.userId && config.account.userId !== "1234567890") {
return config.account.userId;
}
// Set default userId if none exists
const defaultUserId = "1234567890";
config.account.userId = defaultUserId;
// Save the updated config
const success = writeConfig(config, explicitRoot);
if (!success) {
log(
"warn",
"Failed to write updated configuration with new userId. Please let the developers know."
);
}
return defaultUserId;
// If userId is null or the placeholder, return the placeholder
// This signals to other code that auth/init needs to be attempted
return "1234567890";
}
/**

File diff suppressed because it is too large Load Diff

View File

@@ -68,7 +68,7 @@ async function registerUserWithGateway(email = null, explicitRoot = null) {
/**
* Updates the user configuration with gateway registration results
* @param {string} userId - User ID from gateway
* @param {string} token - API token from gateway
* @param {string} token - User authentication token from gateway (stored in .env)
* @param {string} mode - User mode ('byok' or 'hosted')
* @param {string|null} explicitRoot - Optional explicit project root path
* @returns {boolean} Success status
@@ -86,7 +86,7 @@ function updateUserConfig(userId, token, mode, explicitRoot = null) {
config.account.userId = userId;
config.account.mode = mode; // 'byok' or 'hosted'
// Write API token to .env file (not config)
// Write user authentication token to .env file (not config)
if (token) {
writeApiKeyToEnv(token, explicitRoot);
}
@@ -107,8 +107,9 @@ function updateUserConfig(userId, token, mode, explicitRoot = null) {
}
/**
* Writes the API token to the .env file
* @param {string} token - API token to write
* Writes the user authentication token to the .env file
* This token is used as Bearer auth for gateway API calls
* @param {string} token - Authentication token to write
* @param {string|null} explicitRoot - Optional explicit project root path
*/
function writeApiKeyToEnv(token, explicitRoot = null) {
@@ -155,9 +156,9 @@ function writeApiKeyToEnv(token, explicitRoot = null) {
// Write updated content
fs.writeFileSync(envPath, envContent);
log("info", "API key written to .env file");
log("info", "User authentication token written to .env file");
} catch (error) {
log("error", `Failed to write API key to .env: ${error.message}`);
log("error", `Failed to write user token to .env: ${error.message}`);
}
}
@@ -303,6 +304,60 @@ async function initializeUser(explicitRoot = null) {
}
}
/**
* Gets the current user authentication token from .env file
* This is the Bearer token used for gateway API calls
* @param {string|null} explicitRoot - Optional explicit project root path
* @returns {string|null} User authentication token or null if not found
*/
function getUserToken(explicitRoot = null) {
try {
// Determine project root
let rootPath = explicitRoot;
if (!rootPath) {
rootPath = findProjectRoot();
if (!rootPath) {
log("error", "Could not determine project root for .env file");
return null;
}
}
const envPath = path.join(rootPath, ".env");
if (!fs.existsSync(envPath)) {
return null;
}
const envContent = fs.readFileSync(envPath, "utf8");
const lines = envContent.split("\n");
for (const line of lines) {
if (line.startsWith("TASKMASTER_API_KEY=")) {
return line.substring("TASKMASTER_API_KEY=".length).trim();
}
}
return null;
} catch (error) {
log("error", `Error getting user token from .env: ${error.message}`);
return null;
}
}
/**
* Gets the current user email from configuration
* @param {string|null} explicitRoot - Optional explicit project root path
* @returns {string|null} User email or null if not found
*/
function getUserEmail(explicitRoot = null) {
try {
const config = getConfig(explicitRoot);
return config?.global?.email || null;
} catch (error) {
log("error", `Error getting user email: ${error.message}`);
return null;
}
}
export {
registerUserWithGateway,
updateUserConfig,
@@ -312,4 +367,6 @@ export {
isByokMode,
setupUser,
initializeUser,
getUserToken,
getUserEmail,
};