UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

260 lines 9.36 kB
// src/lib/auth/providers/supabase.ts import { BaseAuthProvider } from "./BaseAuthProvider.js"; import { AuthError } from "../errors.js"; import { logger } from "../../utils/logger.js"; import { createProxyFetch } from "../../proxy/proxyFetch.js"; import * as jose from "jose"; /** * Supabase Authentication Provider * * Supports Supabase JWT validation and user management. * Can validate tokens locally with JWT secret or via Supabase API. * * Features: * - Local JWT validation with JWT secret * - API-based token validation * - User profile fetching (requires service role key) * - Role extraction from app_metadata * * @example * ```typescript * const supabase = new SupabaseAuthProvider({ * type: "supabase", * url: "https://your-project.supabase.co", * anonKey: "your-anon-key", * jwtSecret: "your-jwt-secret" // Optional for local validation * }); * * const result = await supabase.authenticateToken(accessToken); * if (result.valid) { * console.log("Authenticated user:", result.user); * } * ``` */ export class SupabaseAuthProvider extends BaseAuthProvider { type = "supabase"; supabaseUrl; anonKey; serviceRoleKey; jwtSecret; constructor(config) { super(config); if (!config.url) { throw AuthError.create("CONFIGURATION_ERROR", "Supabase URL is required", { details: { missingFields: ["url"] } }); } if (!config.anonKey) { throw AuthError.create("CONFIGURATION_ERROR", "Supabase anon key is required", { details: { missingFields: ["anonKey"] } }); } this.supabaseUrl = config.url.replace(/\/$/, ""); // Remove trailing slash this.anonKey = config.anonKey; this.serviceRoleKey = config.serviceRoleKey; this.jwtSecret = config.jwtSecret; } /** * Validate Supabase JWT */ async authenticateToken(token, _context) { try { // If JWT secret is provided, verify locally if (this.jwtSecret) { const secret = new TextEncoder().encode(this.jwtSecret); const { payload } = await jose.jwtVerify(token, secret); // Reject tokens without a sub claim (anon/service_role JWTs) if (!payload.sub) { return { valid: false, error: "Token missing sub claim: cannot authenticate without a user identity", }; } // Only accept tokens with "authenticated" role const role = payload.role; if (role && role !== "authenticated") { return { valid: false, error: `Invalid token role: ${role}. Only "authenticated" role is accepted`, }; } const user = this.payloadToUser(payload); return { valid: true, payload: payload, user, expiresAt: payload.exp ? new Date(payload.exp * 1000) : undefined, tokenType: "jwt", }; } // Otherwise, validate via Supabase API const proxyFetch = createProxyFetch(); const response = await proxyFetch(`${this.supabaseUrl}/auth/v1/user`, { headers: { Authorization: `Bearer ${token}`, apikey: this.anonKey, }, }); if (!response.ok) { return { valid: false, error: `Token validation failed: HTTP ${response.status}`, }; } const userData = (await response.json()); const user = this.supabaseUserToAuthUser(userData); return { valid: true, payload: userData, user, tokenType: "jwt", }; } catch (error) { return { valid: false, error: error instanceof Error ? error.message : String(error), }; } } /** * Convert JWT payload to AuthUser */ payloadToUser(payload) { const appMetadata = payload.app_metadata; const userMetadata = payload.user_metadata; // Use payload.role (Supabase standard claim) for the roles array const role = payload.role; return { id: payload.sub, email: payload.email, name: userMetadata?.full_name || userMetadata?.name, picture: userMetadata?.avatar_url, emailVerified: payload.email_confirmed || false, roles: role ? [role] : appMetadata?.roles || [], permissions: appMetadata?.permissions || [], metadata: userMetadata, }; } /** * Convert Supabase user to AuthUser */ supabaseUserToAuthUser(userData) { const appMetadata = userData.app_metadata; const userMetadata = userData.user_metadata; return { id: userData.id, email: userData.email, name: userMetadata?.full_name || userMetadata?.name, picture: userMetadata?.avatar_url, emailVerified: !!userData.email_confirmed_at, roles: appMetadata?.roles || [], permissions: appMetadata?.permissions || [], createdAt: userData.created_at ? new Date(userData.created_at) : undefined, lastLoginAt: userData.last_sign_in_at ? new Date(userData.last_sign_in_at) : undefined, metadata: userMetadata, }; } /** * Get user by ID via Supabase Admin API * Requires service role key */ async getUser(userId) { if (!this.serviceRoleKey) { logger.warn("Service role key required for user lookup"); return null; } try { const proxyFetch = createProxyFetch(); const response = await proxyFetch(`${this.supabaseUrl}/auth/v1/admin/users/${userId}`, { headers: { Authorization: `Bearer ${this.serviceRoleKey}`, apikey: this.anonKey, }, }); if (!response.ok) { if (response.status === 404) { return null; } throw AuthError.create("PROVIDER_ERROR", `Supabase API returned ${response.status}`, { details: { statusCode: response.status } }); } const userData = (await response.json()); return this.supabaseUserToAuthUser(userData); } catch (error) { logger.error("Failed to fetch Supabase user:", error); if (error && typeof error === "object" && "code" in error && typeof error.code === "string") { throw error; } return null; } } /** * Get user by email via Supabase Admin API * Requires service role key */ async getUserByEmail(email) { if (!this.serviceRoleKey) { logger.warn("Service role key required for user lookup by email"); return null; } try { const proxyFetch = createProxyFetch(); const response = await proxyFetch(`${this.supabaseUrl}/auth/v1/admin/users?email=${encodeURIComponent(email)}`, { headers: { Authorization: `Bearer ${this.serviceRoleKey}`, apikey: this.anonKey, }, }); if (!response.ok) { throw AuthError.create("PROVIDER_ERROR", `Supabase API returned ${response.status}`, { details: { statusCode: response.status } }); } const result = (await response.json()); const users = result.users || []; if (users.length === 0) { return null; } return this.supabaseUserToAuthUser(users[0]); } catch (error) { logger.error("Failed to fetch Supabase user by email:", error); if (error && typeof error === "object" && "code" in error && typeof error.code === "string") { throw error; } return null; } } /** * Health check */ async healthCheck() { try { const proxyFetch = createProxyFetch(); const response = await proxyFetch(`${this.supabaseUrl}/auth/v1/health`, { headers: { apikey: this.anonKey, }, }); return { healthy: response.ok, providerConnected: response.ok, sessionStorageHealthy: true, }; } catch (error) { return { healthy: false, providerConnected: false, sessionStorageHealthy: true, error: error instanceof Error ? error.message : String(error), }; } } } //# sourceMappingURL=supabase.js.map