@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
JavaScript
// 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