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

@@ -22,10 +22,16 @@ import chalk from "chalk";
import figlet from "figlet";
import boxen from "boxen";
import gradient from "gradient-string";
import inquirer from "inquirer";
import open from "open";
import express from "express";
import { isSilentMode } from "./modules/utils.js";
import { convertAllCursorRulesToRooRules } from "./modules/rule-transformer.js";
import { execSync } from "child_process";
import { registerUserWithGateway } from "./modules/telemetry-submission.js";
import {
initializeUser,
registerUserWithGateway,
} from "./modules/user-management.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -379,46 +385,51 @@ async function initializeProject(options = {}) {
};
}
// STEP 1: Create/find userId first (MCP/non-interactive mode)
let userId = null;
let gatewayRegistration = null;
// NON-INTERACTIVE MODE - Use proper auth/init flow
let userSetupResult;
try {
// Try to get existing userId from config if it exists
// Check if existing config has userId
const existingConfigPath = path.join(process.cwd(), ".taskmasterconfig");
let existingUserId = null;
if (fs.existsSync(existingConfigPath)) {
const existingConfig = JSON.parse(
fs.readFileSync(existingConfigPath, "utf8")
);
userId = existingConfig.account?.userId;
const existingUserEmail = existingConfig.account?.userEmail;
existingUserId = existingConfig.account?.userId;
}
// Pass existing data to gateway for validation/lookup
gatewayRegistration = await registerUserWithGateway(
existingUserEmail || tempEmail,
userId
);
if (gatewayRegistration.success) {
userId = gatewayRegistration.userId;
} else {
// Generate fallback userId if gateway unavailable
userId = `tm_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
if (existingUserId) {
// Validate existing userId through auth/init
userSetupResult = await registerUserWithGateway(null, process.cwd());
if (!userSetupResult.success) {
throw new Error(
`Failed to validate existing user: ${userSetupResult.error}`
);
}
} else {
// Create new user through auth/init
userSetupResult = await initializeUser(process.cwd());
if (!userSetupResult.success) {
throw new Error(
`Failed to initialize user: ${userSetupResult.error}`
);
}
}
} catch (error) {
// Generate fallback userId on any error
userId = `tm_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
log("error", `User initialization failed: ${error.message}`);
throw error; // Don't fall back to random userId!
}
// For non-interactive mode, default to BYOK mode with proper userId
// Create project structure with properly authenticated userId
createProjectStructure(
addAliases,
dryRun,
gatewayRegistration,
"byok",
userSetupResult, // Pass the full auth result
"byok", // or determine from result
null,
userId
userSetupResult.userId || null
);
} else {
// Interactive logic - NEW FLOW STARTS HERE
@@ -444,127 +455,124 @@ async function initializeProject(options = {}) {
)
);
// Generate or retrieve userId from gateway
let userId = null;
let gatewayRegistration = null;
// INTERACTIVE MODE - Also use proper auth/init flow
// STEP 1: Proper user setup
let userSetupResult;
try {
// Try to get existing userId from config if it exists
// Same logic as non-interactive mode
const existingConfigPath = path.join(
process.cwd(),
".taskmasterconfig"
);
let existingUserId = null;
if (fs.existsSync(existingConfigPath)) {
const existingConfig = JSON.parse(
fs.readFileSync(existingConfigPath, "utf8")
);
userId = existingConfig.account?.userId;
const existingUserEmail = existingConfig.account?.userEmail;
existingUserId = existingConfig.account?.userId;
}
// Pass existing data to gateway for validation/lookup
gatewayRegistration = await registerUserWithGateway(
existingUserEmail || tempEmail,
userId
);
if (gatewayRegistration.success) {
userId = gatewayRegistration.userId;
} else {
// Generate fallback userId if gateway unavailable
userId = `tm_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
if (existingUserId) {
userSetupResult = await registerUserWithGateway(null, process.cwd());
if (!userSetupResult.success) {
throw new Error(
`Failed to validate existing user: ${userSetupResult.error}`
);
}
} else {
userSetupResult = await initializeUser(process.cwd());
if (!userSetupResult.success) {
throw new Error(
`Failed to initialize user: ${userSetupResult.error}`
);
}
}
} catch (error) {
// Generate fallback userId on any error
userId = `tm_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
log("error", `User initialization failed: ${error.message}`);
// Don't fall back to random userId - exit or prompt user
throw error;
}
// STEP 2: Choose AI access method (MAIN DECISION)
// STEP 2: Choose AI access method using inquirer
const modeResponse = await inquirer.prompt([
{
type: "list",
name: "accessMode",
message: "Choose Your AI Access Method:",
choices: [
{
name: "🔑 BYOK - Bring Your Own API Keys (You manage API keys & billing)",
value: "byok",
},
{
name: "🎯 Hosted API Gateway - All models, no keys needed (Recommended)",
value: "hosted",
},
],
default: "hosted",
},
]);
const selectedMode = modeResponse.accessMode;
console.log(
boxen(
chalk.white.bold("Choose Your AI Access Method") +
"\n\n" +
chalk.cyan.bold("(1) BYOK - Bring Your Own API Keys") +
"\n" +
chalk.white(
" → You manage API keys & billing with AI providers"
) +
"\n" +
chalk.white(" → Pay provider directly based on token usage") +
"\n" +
chalk.white(
" → Requires setup with each provider individually"
) +
"\n\n" +
chalk.green.bold("(2) Hosted API Gateway") +
" " +
chalk.yellow.bold("(Recommended)") +
"\n" +
chalk.white(" → Use any model, zero API keys needed") +
"\n" +
chalk.white(" → Flat, credit-based pricing with no surprises") +
"\n" +
chalk.white(" → Support the development of Taskmaster"),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderStyle: "round",
borderColor: "cyan",
title: "🎯 AI Access Setup",
titleAlignment: "center",
}
)
);
let choice;
while (true) {
choice = await promptQuestion(
rl,
chalk.cyan.bold("Your choice (1 or 2): ")
);
if (choice === "1" || choice.toLowerCase() === "byok") {
console.log(
boxen(
chalk.blue.bold("🔑 BYOK Mode Selected") +
selectedMode === "byok"
? chalk.blue.bold("🔑 BYOK Mode Selected") +
"\n\n" +
chalk.white("You'll manage your own API keys and billing.") +
"\n" +
chalk.white("After setup, add your API keys to ") +
chalk.cyan(".env") +
chalk.white(" file."),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderStyle: "round",
borderColor: "blue",
}
)
);
return "byok";
} else if (choice === "2" || choice.toLowerCase() === "hosted") {
console.log(
boxen(
chalk.green.bold("🎯 Hosted API Gateway Selected") +
chalk.white(" file.")
: chalk.green.bold("🎯 Hosted API Gateway Selected") +
"\n\n" +
chalk.white(
"All AI models available instantly - no API keys needed!"
) +
"\n" +
chalk.dim("Let's set up your subscription plan..."),
{
padding: 0.5,
margin: { top: 0.5, bottom: 0.5 },
borderStyle: "round",
borderColor: "green",
}
)
);
return "hosted";
} else {
console.log(chalk.red("Please enter 1 or 2"));
}
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderStyle: "round",
borderColor: selectedMode === "byok" ? "blue" : "green",
}
)
);
// STEP 3: If hosted mode, handle subscription plan with Stripe simulation
let selectedPlan = null;
if (selectedMode === "hosted") {
selectedPlan = await handleHostedSubscription();
}
// STEP 4: Continue with aliases (this fixes the hanging issue)
const aliasResponse = await inquirer.prompt([
{
type: "confirm",
name: "addAliases",
message: "Add shell aliases (tm, taskmaster) for easier access?",
default: true,
},
]);
const addAliases = aliasResponse.addAliases;
const dryRun = options.dryRun || false;
// STEP 5: Show overview and continue with project creation
rl.close();
createProjectStructure(
addAliases,
dryRun,
userSetupResult,
selectedMode,
selectedPlan,
userSetupResult.userId
);
} catch (error) {
rl.close();
log("error", `Error during initialization process: ${error.message}`);
@@ -1137,251 +1145,132 @@ function setupMCPConfiguration(targetDir) {
log("info", "MCP server will use the installed task-master-ai package");
}
// Function to let user choose between BYOK and Hosted API Gateway
async function selectAccessMode() {
// Function to handle hosted subscription with browser pattern
async function handleHostedSubscription() {
const planResponse = await inquirer.prompt([
{
type: "list",
name: "plan",
message: "Select Your Monthly AI Credit Pack:",
choices: [
{
name: "50 credits - $5/mo [$0.10 per credit] - Perfect for personal projects",
value: { name: "Starter", credits: 50, price: "$5/mo", value: 1 },
},
{
name: "120 credits - $10/mo [$0.083 per credit] - Popular choice",
value: { name: "Popular", credits: 120, price: "$10/mo", value: 2 },
},
{
name: "250 credits - $20/mo [$0.08 per credit] - Great value",
value: { name: "Pro", credits: 250, price: "$20/mo", value: 3 },
},
{
name: "550 credits - $40/mo [$0.073 per credit] - Best value",
value: {
name: "Enterprise",
credits: 550,
price: "$40/mo",
value: 4,
},
},
],
default: 1, // Popular plan
},
]);
const selectedPlan = planResponse.plan;
console.log(
boxen(
chalk.cyan.bold("🚀 Choose Your AI Access Method") +
chalk.green.bold(`✅ Selected: ${selectedPlan.name} Plan`) +
"\n\n" +
chalk.white("TaskMaster supports two ways to access AI models:") +
chalk.white(
`${selectedPlan.credits} credits/month for ${selectedPlan.price}`
) +
"\n\n" +
chalk.yellow.bold("(1) BYOK - Bring Your Own API Keys") +
chalk.yellow("🔄 Opening browser for Stripe checkout...") +
"\n" +
chalk.white(" ✓ Use your existing provider accounts") +
"\n" +
chalk.white(" ✓ Pay providers directly") +
"\n" +
chalk.white(" ✓ Full control over billing & usage") +
"\n" +
chalk.dim(" → Best for: Teams with existing AI accounts") +
"\n\n" +
chalk.green.bold("(2) Hosted API Gateway") +
chalk.yellow(" (Recommended)") +
"\n" +
chalk.white(" ✓ No API keys required") +
"\n" +
chalk.white(" ✓ Access all supported models instantly") +
"\n" +
chalk.white(" ✓ Simple credit-based billing") +
"\n" +
chalk.white(" ✓ Better rates through volume pricing") +
"\n" +
chalk.dim(" → Best for: Getting started quickly"),
chalk.dim("Complete your subscription setup in the browser."),
{
padding: 1,
margin: { top: 1, bottom: 1 },
borderStyle: "round",
borderColor: "cyan",
title: "🎯 AI Access Configuration",
titleAlignment: "center",
}
)
);
let choice;
while (true) {
choice = await promptQuestion(
rl,
chalk.cyan("Your choice") +
chalk.gray(" (1 for BYOK, 2 for Hosted)") +
": "
);
if (choice === "1" || choice.toLowerCase() === "byok") {
console.log(
boxen(
chalk.blue.bold("🔑 BYOK Mode Selected") +
"\n\n" +
chalk.white("You'll configure your own AI provider API keys.") +
"\n" +
chalk.dim("The setup will guide you through model configuration."),
{
padding: 0.5,
margin: { top: 0.5, bottom: 0.5 },
borderStyle: "round",
borderColor: "blue",
}
)
);
return "byok";
} else if (choice === "2" || choice.toLowerCase() === "hosted") {
console.log(
boxen(
chalk.green.bold("🎯 Hosted API Gateway Selected") +
"\n\n" +
chalk.white(
"All AI models available instantly - no API keys needed!"
) +
"\n" +
chalk.dim("Let's set up your subscription plan..."),
{
padding: 0.5,
margin: { top: 0.5, bottom: 0.5 },
borderStyle: "round",
borderColor: "green",
}
)
);
return "hosted";
} else {
console.log(chalk.red("Please enter 1 or 2"));
}
}
}
// Function to let user select a subscription plan
async function selectSubscriptionPlan() {
console.log(
boxen(
chalk.cyan.bold("💳 Select Your Monthly AI Credit Pack") +
"\n\n" +
chalk.white("Choose the plan that fits your usage:") +
"\n\n" +
chalk.white("(1) ") +
chalk.yellow.bold("50 credits") +
chalk.white(" - ") +
chalk.green("$5/mo") +
chalk.gray(" [$0.10 per credit]") +
"\n" +
chalk.dim(" → Perfect for: Personal projects, light usage") +
"\n\n" +
chalk.white("(2) ") +
chalk.yellow.bold("120 credits") +
chalk.white(" - ") +
chalk.green("$10/mo") +
chalk.gray(" [$0.083 per credit]") +
chalk.cyan.bold(" ← Popular") +
"\n" +
chalk.dim(" → Perfect for: Active development, small teams") +
"\n\n" +
chalk.white("(3) ") +
chalk.yellow.bold("250 credits") +
chalk.white(" - ") +
chalk.green("$20/mo") +
chalk.gray(" [$0.08 per credit]") +
chalk.blue.bold(" ← Great Value") +
"\n" +
chalk.dim(" → Perfect for: Professional development, medium teams") +
"\n\n" +
chalk.white("(4) ") +
chalk.yellow.bold("550 credits") +
chalk.white(" - ") +
chalk.green("$40/mo") +
chalk.gray(" [$0.073 per credit]") +
chalk.magenta.bold(" ← Best Value") +
"\n" +
chalk.dim(" → Perfect for: Heavy usage, large teams, enterprises") +
"\n\n" +
chalk.blue("💡 ") +
chalk.white("Credits roll over month-to-month. Cancel anytime."),
{
padding: 1,
margin: { top: 1, bottom: 1 },
margin: { top: 0.5, bottom: 0.5 },
borderStyle: "round",
borderColor: "green",
title: "💳 Subscription Plans",
titleAlignment: "center",
}
)
);
const plans = [
{
name: "Starter",
credits: 50,
price: "$5/mo",
perCredit: "$0.10",
value: 1,
},
{
name: "Popular",
credits: 120,
price: "$10/mo",
perCredit: "$0.083",
value: 2,
},
{
name: "Pro",
credits: 250,
price: "$20/mo",
perCredit: "$0.08",
value: 3,
},
{
name: "Enterprise",
credits: 550,
price: "$40/mo",
perCredit: "$0.073",
value: 4,
},
];
// Stripe simulation with browser opening pattern (like Shopify CLI)
await simulateStripeCheckout(selectedPlan);
let choice;
while (true) {
choice = await promptQuestion(
rl,
chalk.cyan("Your choice") + chalk.gray(" (1-4)") + ": "
);
const planIndex = parseInt(choice) - 1;
if (planIndex >= 0 && planIndex < plans.length) {
const selectedPlan = plans[planIndex];
console.log(
boxen(
chalk.green.bold(`✅ Selected: ${selectedPlan.name} Plan`) +
"\n\n" +
chalk.white(
`${selectedPlan.credits} credits/month for ${selectedPlan.price}`
) +
"\n" +
chalk.gray(`(${selectedPlan.perCredit} per credit)`) +
"\n\n" +
chalk.yellow("🔄 Opening Stripe checkout...") +
"\n" +
chalk.dim("Complete your subscription setup in the browser."),
{
padding: 1,
margin: { top: 0.5, bottom: 0.5 },
borderStyle: "round",
borderColor: "green",
}
)
);
// TODO: Integrate with actual Stripe checkout
// For now, simulate the process
console.log(chalk.yellow("\n⏳ Simulating Stripe checkout process..."));
console.log(chalk.green("✅ Subscription setup complete! (Simulated)"));
return selectedPlan;
} else {
console.log(chalk.red("Please enter a number from 1 to 4"));
}
}
return selectedPlan;
}
// Function to create or retrieve user ID
async function getOrCreateUserId() {
// Try to find existing userId first
const existingConfig = path.join(process.cwd(), ".taskmasterconfig");
if (fs.existsSync(existingConfig)) {
try {
const config = JSON.parse(fs.readFileSync(existingConfig, "utf8"));
if (config.userId) {
log("info", `Using existing user ID: ${config.userId}`);
return config.userId;
}
} catch (error) {
log("warn", "Could not read existing config, creating new user ID");
}
// Stripe checkout simulation with browser pattern
async function simulateStripeCheckout(plan) {
console.log(chalk.yellow("\n⏳ Starting Stripe checkout process..."));
// Start a simple HTTP server to handle the callback
const app = express();
let server;
let checkoutComplete = false;
// For demo/testing, we'll use a simple success simulation
const checkoutUrl = `https://example-stripe-simulation.com/checkout?plan=${plan.value}&return_url=http://localhost:3333/success`;
app.get("/success", (req, res) => {
checkoutComplete = true;
res.send(`
<html>
<body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
<h1 style="color: #28a745;">✅ Subscription Complete!</h1>
<p>Your ${plan.name} plan (${plan.credits} credits/month) is now active.</p>
<p style="color: #666; margin-top: 30px;">You can close this window and return to your terminal.</p>
</body>
</html>
`);
setTimeout(() => {
server.close();
}, 1000);
});
// Start the callback server
server = app.listen(3333, () => {
console.log(chalk.blue("📡 Started local callback server on port 3333"));
});
// Prompt user before opening browser
await inquirer.prompt([
{
type: "input",
name: "ready",
message: chalk.cyan(
"Press Enter to open your browser for Stripe checkout..."
),
},
]);
// Open the browser (for demo, we'll simulate immediate success)
console.log(chalk.blue("🌐 Opening browser..."));
// For demo purposes, simulate immediate success instead of opening real browser
// In real implementation: await open(checkoutUrl);
console.log(chalk.gray(`Demo URL: ${checkoutUrl}`));
// Simulate the checkout completion after 2 seconds
setTimeout(() => {
console.log(chalk.green("✅ Subscription setup complete! (Simulated)"));
checkoutComplete = true;
server.close();
}, 2000);
// Wait for checkout completion
while (!checkoutComplete) {
await new Promise((resolve) => setTimeout(resolve, 500));
}
// Generate new user ID
const { v4: uuidv4 } = require("uuid");
const newUserId = uuidv4();
log("info", `Generated new user ID: ${newUserId}`);
return newUserId;
console.log(chalk.green("🎉 Payment successful! Continuing setup..."));
}
// Ensure necessary functions are exported

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,
};