feat: integrate Supabase authenticated users

- Updated init.js, ai-services-unified.js, user-management.js, telemetry-submission.js, and .taskmasterconfig to support Supabase authentication flow and authenticated gateway calls
This commit is contained in:
Eyal Toledano
2025-06-04 18:53:28 -04:00
parent 58aa0992f6
commit 685365270d
6 changed files with 169 additions and 48 deletions

View File

@@ -30,8 +30,8 @@
"ollamaBaseUrl": "http://localhost:11434/api" "ollamaBaseUrl": "http://localhost:11434/api"
}, },
"account": { "account": {
"userId": "ee196145-8f01-41b9-a9ce-85faae397254", "userId": "1234567890",
"email": "eyal@testing.com", "email": "",
"mode": "hosted", "mode": "hosted",
"telemetryEnabled": true "telemetryEnabled": true
} }

View File

@@ -31,6 +31,8 @@ import { execSync } from "child_process";
import { import {
initializeUser, initializeUser,
registerUserWithGateway, registerUserWithGateway,
initializeBYOKUser,
initializeHostedUser,
} from "./modules/user-management.js"; } from "./modules/user-management.js";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);

View File

@@ -284,6 +284,10 @@ async function _attemptProviderCallWithRetries(
* @param {string} initialRole - The initial client role * @param {string} initialRole - The initial client role
* @returns {Promise<object>} AI response with usage data * @returns {Promise<object>} AI response with usage data
*/ */
/**
* Calls the TaskMaster gateway for AI processing (hosted mode only).
* BYOK users don't use this function - they make direct API calls.
*/
async function _callGatewayAI( async function _callGatewayAI(
serviceType, serviceType,
callParams, callParams,
@@ -301,9 +305,17 @@ async function _callGatewayAI(
// Get user auth info for headers // Get user auth info for headers
const userMgmt = await import("./user-management.js"); const userMgmt = await import("./user-management.js");
const config = getConfig(projectRoot);
const mode = config.account?.mode || "byok";
// Both BYOK and hosted users have the same user token
// BYOK users just don't use it for AI calls (they use their own API keys)
const userToken = await userMgmt.getUserToken(projectRoot); const userToken = await userMgmt.getUserToken(projectRoot);
const userEmail = await userMgmt.getUserEmail(projectRoot); const userEmail = await userMgmt.getUserEmail(projectRoot);
// Note: BYOK users will have both token and email, but won't use this function
// since they make direct API calls with their own keys
if (!userToken) { if (!userToken) {
throw new Error( throw new Error(
"User token not found. Run 'task-master init' to register with gateway." "User token not found. Run 'task-master init' to register with gateway."

View File

@@ -163,7 +163,7 @@ export async function submitTelemetryData(telemetryData) {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Service-ID": telemetryConfig.serviceId, // Hardcoded service ID "x-taskmaster-service-id": telemetryConfig.serviceId, // Hardcoded service ID
Authorization: `Bearer ${telemetryConfig.apiKey}`, // User's Bearer token Authorization: `Bearer ${telemetryConfig.apiKey}`, // User's Bearer token
"X-User-Email": telemetryConfig.email, // User's email from config "X-User-Email": telemetryConfig.email, // User's email from config
}, },

View File

@@ -293,65 +293,170 @@ async function setupUser(email = null, mode = "hosted", explicitRoot = null) {
* @returns {Promise<{success: boolean, userId: string, error?: string}>} * @returns {Promise<{success: boolean, userId: string, error?: string}>}
*/ */
async function initializeUser(explicitRoot = null) { async function initializeUser(explicitRoot = null) {
const config = getConfig(explicitRoot);
const mode = config.account?.mode || "byok";
if (mode === "byok") {
return await initializeBYOKUser(explicitRoot);
} else {
return await initializeHostedUser(explicitRoot);
}
}
async function initializeBYOKUser(projectRoot) {
try { try {
// Try to register with gateway without email const gatewayUrl =
const result = await registerUserWithGateway(null, explicitRoot); process.env.TASKMASTER_GATEWAY_URL || "http://localhost:4444";
// If gateway call succeeded, use the returned values // Check if we already have an anonymous user ID stored
if (result.success) { let config = getConfig(projectRoot);
// Update config with userId, token, and preserve existing mode (or default) const existingAnonymousUserId = config?.account?.userId;
const existingMode = getUserMode(explicitRoot);
const modeToUse = existingMode !== "unknown" ? existingMode : "byok";
const configResult = updateUserConfig( // Prepare headers for the request
result.userId, const headers = {
result.token, "Content-Type": "application/json",
modeToUse, "X-TaskMaster-Service-ID": "98fb3198-2dfc-42d1-af53-07b99e4f3bde",
null, };
explicitRoot
// If we have an existing anonymous user ID, try to reuse it
if (existingAnonymousUserId && existingAnonymousUserId !== "1234567890") {
headers["X-Anonymous-User-ID"] = existingAnonymousUserId;
log(
"info",
`Attempting to reuse existing anonymous user: ${existingAnonymousUserId}`
); );
}
if (!configResult) { // Call gateway /auth/anonymous to create or reuse a user account
return { // BYOK users still get an account for potential future hosted mode switch
success: false, const response = await fetch(`${gatewayUrl}/auth/anonymous`, {
userId: result.userId, method: "POST",
error: "Failed to update user configuration", headers,
}; body: JSON.stringify({}),
});
if (response.ok) {
const result = await response.json();
// Log whether user was reused or newly created
if (result.isReused) {
log(
"info",
`Successfully reused existing anonymous user: ${result.anonymousUserId}`
);
} else {
log("info", `Created new anonymous user: ${result.anonymousUserId}`);
} }
// Store the user token (same as hosted users)
// BYOK users won't use this for AI calls, but will have it for potential mode switch
if (result.session && result.session.access_token) {
writeApiKeyToEnv(result.session.access_token, projectRoot);
}
// Update config with BYOK user info, ensuring we store the anonymous user ID
if (!config.account) {
config.account = {};
}
config.account.userId = result.anonymousUserId || result.user.id;
config.account.mode = "byok";
config.account.userEmail =
result.user.email ||
`anon-${result.anonymousUserId || result.user.id}@taskmaster.temp`;
config.account.telemetryEnabled = false; // BYOK users don't send telemetry by default
writeConfig(config, projectRoot);
return { return {
success: true, success: true,
userId: result.userId, userId: result.anonymousUserId || result.user.id,
message: result.isNewUser token: result.session?.access_token || null,
? "New user registered with gateway" mode: "byok",
: "Existing user found in gateway", isAnonymous: true,
isReused: result.isReused || false,
}; };
} } else {
const errorText = await response.text();
// 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 { return {
success: true, success: false,
userId: existingUserId, error: `Gateway not available: ${response.status} ${errorText}`,
message: "Gateway unavailable, using existing user credentials",
}; };
} }
// No existing credentials and gateway failed
return {
success: false,
userId: "",
error: result.error,
};
} catch (error) { } catch (error) {
return { return {
success: false, success: false,
userId: "", error: `Network error: ${error.message}`,
error: `Initialization failed: ${error.message}`, };
}
}
async function initializeHostedUser(projectRoot) {
try {
// For hosted users, we need proper authentication
// This would typically involve OAuth flow or registration
const gatewayUrl =
process.env.TASKMASTER_GATEWAY_URL || "http://localhost:4444";
// Check if we already have stored credentials
const existingToken = getUserToken(projectRoot);
const existingUserId = getUserId(projectRoot);
if (existingToken && existingUserId && existingUserId !== "1234567890") {
// Try to validate existing credentials
try {
const response = await fetch(`${gatewayUrl}/auth/validate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${existingToken}`,
"X-TaskMaster-Service-ID": "98fb3198-2dfc-42d1-af53-07b99e4f3bde",
},
});
if (response.ok) {
return {
success: true,
userId: existingUserId,
token: existingToken,
mode: "hosted",
isExisting: true,
};
}
} catch (error) {
// Fall through to re-authentication
}
}
// If no valid credentials, use the existing registration flow
const registrationResult = await registerUserWithGateway(null, projectRoot);
if (registrationResult.success) {
// Update config for hosted mode
updateUserConfig(
registrationResult.userId,
registrationResult.token,
"hosted",
null,
projectRoot
);
return {
success: true,
userId: registrationResult.userId,
token: registrationResult.token,
mode: "hosted",
isNewUser: registrationResult.isNewUser,
};
} else {
return {
success: false,
error: `Hosted mode setup failed: ${registrationResult.error}`,
};
}
} catch (error) {
return {
success: false,
error: `Hosted user initialization failed: ${error.message}`,
}; };
} }
} }
@@ -419,6 +524,8 @@ export {
isByokMode, isByokMode,
setupUser, setupUser,
initializeUser, initializeUser,
initializeBYOKUser,
initializeHostedUser,
getUserToken, getUserToken,
getUserEmail, getUserEmail,
}; };

View File

@@ -95,7 +95,7 @@ describe("Telemetry Submission Service", () => {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Service-ID": "98fb3198-2dfc-42d1-af53-07b99e4f3bde", "x-taskmaster-service-id": "98fb3198-2dfc-42d1-af53-07b99e4f3bde",
Authorization: "Bearer test-api-key", Authorization: "Bearer test-api-key",
"X-User-Email": "test@example.com", "X-User-Email": "test@example.com",
}, },