- Fix Zod schema conversion, update headers, add premium telemetry display, improve user auth flow, and standardize email fields Functionally complete on this end, mostly polish around user experience and need to add in profile, upgrade/downgrade, etc. But the AI commands are working off the gateway.
425 lines
12 KiB
JavaScript
425 lines
12 KiB
JavaScript
import fs from "fs";
|
|
import path from "path";
|
|
import { log, findProjectRoot } from "./utils.js";
|
|
import { getConfig, writeConfig, getUserId } from "./config-manager.js";
|
|
|
|
/**
|
|
* Registers or finds a user via the gateway's /auth/init endpoint
|
|
* @param {string|null} email - Optional user's email address (only needed for billing)
|
|
* @param {string|null} explicitRoot - Optional explicit project root path
|
|
* @returns {Promise<{success: boolean, userId: string, token: string, isNewUser: boolean, error?: string}>}
|
|
*/
|
|
async function registerUserWithGateway(email = null, explicitRoot = null) {
|
|
try {
|
|
const gatewayUrl =
|
|
process.env.TASKMASTER_GATEWAY_URL || "http://localhost:4444";
|
|
|
|
// Check for existing userId and email to pass to gateway
|
|
const existingUserId = getUserId(explicitRoot);
|
|
const existingEmail = email || getUserEmail(explicitRoot);
|
|
|
|
// Build request body with existing values (gateway can handle userId for existing users)
|
|
const requestBody = {};
|
|
if (existingUserId && existingUserId !== "1234567890") {
|
|
requestBody.userId = existingUserId;
|
|
}
|
|
if (existingEmail) {
|
|
requestBody.email = existingEmail;
|
|
}
|
|
|
|
const response = await fetch(`${gatewayUrl}/auth/init`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(requestBody),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
return {
|
|
success: false,
|
|
userId: "",
|
|
token: "",
|
|
isNewUser: false,
|
|
error: `Gateway registration failed: ${response.status} ${errorText}`,
|
|
};
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success && result.data) {
|
|
return {
|
|
success: true,
|
|
userId: result.data.userId,
|
|
token: result.data.token,
|
|
isNewUser: result.data.isNewUser,
|
|
};
|
|
} else {
|
|
return {
|
|
success: false,
|
|
userId: "",
|
|
token: "",
|
|
isNewUser: false,
|
|
error: "Invalid response format from gateway",
|
|
};
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
userId: "",
|
|
token: "",
|
|
isNewUser: false,
|
|
error: `Network error: ${error.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the user configuration with gateway registration results
|
|
* @param {string} userId - User ID from gateway
|
|
* @param {string} token - User authentication token from gateway (stored in .env)
|
|
* @param {string} mode - User mode ('byok' or 'hosted')
|
|
* @param {string|null} email - Optional user email to save
|
|
* @param {string|null} explicitRoot - Optional explicit project root path
|
|
* @returns {boolean} Success status
|
|
*/
|
|
function updateUserConfig(
|
|
userId,
|
|
token,
|
|
mode,
|
|
email = null,
|
|
explicitRoot = null
|
|
) {
|
|
try {
|
|
const config = getConfig(explicitRoot);
|
|
|
|
// Ensure account section exists
|
|
if (!config.account) {
|
|
config.account = {};
|
|
}
|
|
|
|
// Ensure global section exists for email
|
|
if (!config.global) {
|
|
config.global = {};
|
|
}
|
|
|
|
// Update user configuration in account section
|
|
config.account.userId = userId;
|
|
config.account.mode = mode; // 'byok' or 'hosted'
|
|
|
|
// Save email if provided
|
|
if (email) {
|
|
config.account.email = email;
|
|
}
|
|
|
|
// Write user authentication token to .env file (not config)
|
|
if (token) {
|
|
writeApiKeyToEnv(token, explicitRoot);
|
|
}
|
|
|
|
// Save updated config
|
|
const success = writeConfig(config, explicitRoot);
|
|
if (success) {
|
|
const emailInfo = email ? `, email=${email}` : "";
|
|
log(
|
|
"info",
|
|
`User configuration updated: userId=${userId}, mode=${mode}${emailInfo}`
|
|
);
|
|
} else {
|
|
log("error", "Failed to write updated user configuration");
|
|
}
|
|
|
|
return success;
|
|
} catch (error) {
|
|
log("error", `Error updating user config: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
try {
|
|
// Determine project root
|
|
let rootPath = explicitRoot;
|
|
if (!rootPath) {
|
|
rootPath = findProjectRoot();
|
|
if (!rootPath) {
|
|
log("warn", "Could not determine project root for .env file");
|
|
return;
|
|
}
|
|
}
|
|
|
|
const envPath = path.join(rootPath, ".env");
|
|
let envContent = "";
|
|
|
|
// Read existing .env content if file exists
|
|
if (fs.existsSync(envPath)) {
|
|
envContent = fs.readFileSync(envPath, "utf8");
|
|
}
|
|
|
|
// Check if TASKMASTER_API_KEY already exists
|
|
const lines = envContent.split("\n");
|
|
let keyExists = false;
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
if (lines[i].startsWith("TASKMASTER_API_KEY=")) {
|
|
lines[i] = `TASKMASTER_API_KEY=${token}`;
|
|
keyExists = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Add key if it doesn't exist
|
|
if (!keyExists) {
|
|
if (envContent && !envContent.endsWith("\n")) {
|
|
envContent += "\n";
|
|
}
|
|
envContent += `TASKMASTER_API_KEY=${token}\n`;
|
|
} else {
|
|
envContent = lines.join("\n");
|
|
}
|
|
|
|
// Write updated content
|
|
fs.writeFileSync(envPath, envContent);
|
|
log("info", "User authentication token written to .env file");
|
|
} catch (error) {
|
|
log("error", `Failed to write user token to .env: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the current user mode from configuration
|
|
* @param {string|null} explicitRoot - Optional explicit project root path
|
|
* @returns {string} User mode ('byok', 'hosted', or 'unknown')
|
|
*/
|
|
function getUserMode(explicitRoot = null) {
|
|
try {
|
|
const config = getConfig(explicitRoot);
|
|
return config?.account?.mode || "unknown";
|
|
} catch (error) {
|
|
log("error", `Error getting user mode: ${error.message}`);
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if user is in hosted mode
|
|
* @param {string|null} explicitRoot - Optional explicit project root path
|
|
* @returns {boolean} True if user is in hosted mode
|
|
*/
|
|
function isHostedMode(explicitRoot = null) {
|
|
return getUserMode(explicitRoot) === "hosted";
|
|
}
|
|
|
|
/**
|
|
* Checks if user is in BYOK mode
|
|
* @param {string|null} explicitRoot - Optional explicit project root path
|
|
* @returns {boolean} True if user is in BYOK mode
|
|
*/
|
|
function isByokMode(explicitRoot = null) {
|
|
return getUserMode(explicitRoot) === "byok";
|
|
}
|
|
|
|
/**
|
|
* Complete user setup: register with gateway and configure TaskMaster
|
|
* @param {string|null} email - Optional user's email (only needed for billing)
|
|
* @param {string} mode - User's mode: 'byok' or 'hosted'
|
|
* @param {string|null} explicitRoot - Optional explicit project root path
|
|
* @returns {Promise<{success: boolean, userId: string, mode: string, error?: string}>}
|
|
*/
|
|
async function setupUser(email = null, mode = "hosted", explicitRoot = null) {
|
|
try {
|
|
// Step 1: Register with gateway (email optional)
|
|
const registrationResult = await registerUserWithGateway(
|
|
email,
|
|
explicitRoot
|
|
);
|
|
|
|
if (!registrationResult.success) {
|
|
return {
|
|
success: false,
|
|
userId: "",
|
|
mode: "",
|
|
error: registrationResult.error,
|
|
};
|
|
}
|
|
|
|
// Step 2: Update config with userId, mode, and email
|
|
const configResult = updateUserConfig(
|
|
registrationResult.userId,
|
|
registrationResult.token,
|
|
mode,
|
|
email,
|
|
explicitRoot
|
|
);
|
|
|
|
if (!configResult) {
|
|
return {
|
|
success: false,
|
|
userId: registrationResult.userId,
|
|
mode: "",
|
|
error: "Failed to update user configuration",
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
userId: registrationResult.userId,
|
|
mode: mode,
|
|
message: email
|
|
? `User setup complete with email ${email}`
|
|
: "User setup complete (email will be collected during billing setup)",
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
userId: "",
|
|
mode: "",
|
|
error: `Setup failed: ${error.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize TaskMaster user (typically called during init)
|
|
* Gets userId from gateway without requiring email upfront
|
|
* @param {string|null} explicitRoot - Optional explicit project root path
|
|
* @returns {Promise<{success: boolean, userId: string, error?: string}>}
|
|
*/
|
|
async function initializeUser(explicitRoot = null) {
|
|
try {
|
|
// Try to register with gateway without email
|
|
const result = await registerUserWithGateway(null, explicitRoot);
|
|
|
|
// If gateway call succeeded, use the returned values
|
|
if (result.success) {
|
|
// Update config with userId, token, and preserve existing mode (or default)
|
|
const existingMode = getUserMode(explicitRoot);
|
|
const modeToUse = existingMode !== "unknown" ? existingMode : "byok";
|
|
|
|
const configResult = updateUserConfig(
|
|
result.userId,
|
|
result.token,
|
|
modeToUse,
|
|
null,
|
|
explicitRoot
|
|
);
|
|
|
|
if (!configResult) {
|
|
return {
|
|
success: false,
|
|
userId: result.userId,
|
|
error: "Failed to update user configuration",
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
userId: result.userId,
|
|
message: result.isNewUser
|
|
? "New user registered with gateway"
|
|
: "Existing user found in gateway",
|
|
};
|
|
}
|
|
|
|
// Gateway call failed - check if we have existing credentials to use
|
|
const existingUserId = getUserId(explicitRoot);
|
|
const existingToken = getUserToken(explicitRoot);
|
|
|
|
if (existingUserId && existingUserId !== "1234567890" && existingToken) {
|
|
// We have existing credentials, use them (gateway unavailable scenario)
|
|
return {
|
|
success: true,
|
|
userId: existingUserId,
|
|
message: "Gateway unavailable, using existing user credentials",
|
|
};
|
|
}
|
|
|
|
// No existing credentials and gateway failed
|
|
return {
|
|
success: false,
|
|
userId: "",
|
|
error: result.error,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
userId: "",
|
|
error: `Initialization failed: ${error.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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?.account?.email || null;
|
|
} catch (error) {
|
|
log("error", `Error getting user email: ${error.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export {
|
|
registerUserWithGateway,
|
|
updateUserConfig,
|
|
writeApiKeyToEnv,
|
|
getUserMode,
|
|
isHostedMode,
|
|
isByokMode,
|
|
setupUser,
|
|
initializeUser,
|
|
getUserToken,
|
|
getUserEmail,
|
|
};
|