donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
190 lines • 8.68 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SupabaseClient = void 0;
const jose_1 = require("jose");
const Logger_1 = require("../utils/Logger");
const JsonUtils_1 = require("../utils/JsonUtils");
/**
* A utility class that, given a Supabase service role key, can generate a custom JWT to impersonate
* a user.
*/
class SupabaseTokenImpersonator {
/**
* Constructs a new SupabaseTokenImpersonator.
*
* @param supabasePublishableApiKey - This is a non-secret API key to be sent as the `apikey`
* header field when fetching user data.
* @param supabaseJwtSecretKey - The Supabase JWT secret key (from your project's API settings).
* This key should be handled with utmost care.
* @param supabaseProjectUrl - The Supabase project's base URL, for example:
* "https://<your-project>.supabase.co"
*/
constructor(supabasePublishableApiKey, supabaseJwtSecretKey, supabaseProjectUrl) {
this.supabasePublishableApiKey = supabasePublishableApiKey;
this.supabaseJwtSecretKey = new TextEncoder().encode(supabaseJwtSecretKey);
this.authApiUrl = `${supabaseProjectUrl}/auth/v1/user`;
}
/**
* Creates a custom JWT that impersonates the user identified by the provided access token.
*
* 1. It verifies the user's token by making a request to /auth/v1/user.
* 2. If valid, Supabase returns the user's profile; we extract the user ID from there.
* 3. We then return a new custom JWT signed with the service role key.
*
* @param userAccessToken - The user's existing JWT access token.
* @returns A custom JWT signed with the service role key, containing the user's ID as the subject.
* @throws Error If the provided userAccessToken is invalid/expired or if there are any issues
* during the process.
*/
async createCustomUserToken(userAccessToken) {
// Validate the user's token by calling Supabase.
const userId = await this.fetchUserIdFromSupabase(userAccessToken);
if (!userId) {
throw new Error('Unable to fetch a valid user ID from the provided token.');
}
const now = Math.floor(Date.now() / 1000);
// NOTE: This expiry is coupled with the expiry defined in the
// `AccessTokenManager::getValidAccessToken` method.
const exp = now + 3600; // 1 hour expiration
const jwt = new jose_1.SignJWT({ role: 'authenticated' })
.setProtectedHeader({ alg: 'HS256' })
.setSubject(userId)
.setIssuedAt(now)
.setExpirationTime(exp)
.setAudience('authenticated')
.setIssuer('https://www.donobu.com');
return jwt.sign(this.supabaseJwtSecretKey);
}
/**
* Uses the user token in a Bearer header to call Supabase's /auth/v1/user endpoint. If
* successful, returns the user's ID from the JSON response.
*
* @param userAccessToken - The user's JWT to validate.
* @returns The user's ID if successful.
* @throws Error If the user token is invalid/expired or if there are any issues during the process.
*/
async fetchUserIdFromSupabase(userAccessToken) {
try {
const response = await fetch(this.authApiUrl, {
headers: {
apikey: this.supabasePublishableApiKey,
Authorization: `Bearer ${userAccessToken}`,
},
});
if (!response.ok) {
throw new Error(`User token is invalid or expired. HTTP status: ${response.status}`);
}
const userData = await response.json();
if (!userData?.id) {
throw new Error("Failed to retrieve 'id' from user details");
}
return userData.id;
}
catch (error) {
Logger_1.appLogger.error('Error fetching user ID from Supabase:', error);
throw error;
}
}
}
class SupabaseClient {
constructor(tokenManager, supabaseBaseUrl, supabasePublishableApiKey) {
this.tokenManager = tokenManager;
this.supabaseBaseUrl = supabaseBaseUrl;
this.supabasePublishableApiKey = supabasePublishableApiKey;
}
static createClient(userAccessToken, supabaseJwtSecretKey) {
return this.createClientWithConfig(userAccessToken, this.DEFAULT_SUPABASE_URL, this.DEFAULT_SUPABASE_PUBLISHABLE_API_KEY, supabaseJwtSecretKey);
}
static createClientWithConfig(userAccessToken, supabaseBaseUrl, supabasePublishableApiKey, supabaseJwtSecretKey) {
const tokenManager = new AccessTokenManager(userAccessToken, supabaseBaseUrl, supabasePublishableApiKey, supabaseJwtSecretKey);
return new SupabaseClient(tokenManager, supabaseBaseUrl, supabasePublishableApiKey);
}
/**
* Makes a request with automatic token refresh handling.
*/
async makeRequest(path, method, customHeaders, body) {
const makeRequestWithToken = async (token) => {
const headers = new Headers({
'Content-Type': 'application/json',
Prefer: 'resolution=merge-duplicates',
apikey: this.supabasePublishableApiKey,
Authorization: `Bearer ${token}`,
});
// Add custom headers if provided.
if (customHeaders) {
Object.entries(customHeaders).forEach(([key, value]) => {
headers.append(key, value);
});
}
return fetch(`${this.supabaseBaseUrl}${path}`, {
method,
headers,
body,
});
};
// Initial request.
let token = await this.tokenManager.getValidAccessToken();
let response = await makeRequestWithToken(token);
// Handle 401 with retry.
if (response.status === 401) {
// Get fresh token and retry once.
token = await this.tokenManager.getValidAccessToken();
response = await makeRequestWithToken(token);
}
return response;
}
async executeGet(path, headers) {
try {
// Modify makeRequest to accept custom headers
const response = await this.makeRequest(path, 'GET', headers);
if (response.status === 404) {
return null;
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.text();
}
catch (error) {
Logger_1.appLogger.error(`GET request failed: ${JSON.stringify(JsonUtils_1.JsonUtils.objectToJson(error))}`);
throw error;
}
}
async executeMethod(path, method, body) {
try {
const response = await this.makeRequest(path, method, undefined, body);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.text();
}
catch (error) {
Logger_1.appLogger.error(`Request failed: ${error}`);
throw error;
}
}
}
exports.SupabaseClient = SupabaseClient;
SupabaseClient.DEFAULT_SUPABASE_URL = 'https://pxxliqamgggjpeltoeus.supabase.co';
SupabaseClient.DEFAULT_SUPABASE_PUBLISHABLE_API_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InB4eGxpcWFtZ2dnanBlbHRvZXVzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MjUyOTA1MjMsImV4cCI6MjA0MDg2NjUyM30.iQrTLwdzVO1zN7mawZ7hSUnZP-GrVU_vrJR4iACML3A';
class AccessTokenManager {
constructor(originalUserAccessToken, supabaseBaseUrl, supabasePublishableApiKey, supabaseJwtSecretKey) {
this.originalUserAccessToken = originalUserAccessToken;
this.supabaseBaseUrl = supabaseBaseUrl;
this.supabasePublishableApiKey = supabasePublishableApiKey;
this.supabaseJwtSecretKey = supabaseJwtSecretKey;
this.accessToken = null;
this.accessTokenExpiresAt = new Date(0);
}
async getValidAccessToken() {
if (this.accessToken && new Date() < this.accessTokenExpiresAt) {
return this.accessToken;
}
const tokenImpersonator = new SupabaseTokenImpersonator(this.supabasePublishableApiKey, this.supabaseJwtSecretKey, this.supabaseBaseUrl);
this.accessTokenExpiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour from now
const tmp = await tokenImpersonator.createCustomUserToken(this.originalUserAccessToken);
this.accessToken = tmp;
return tmp;
}
}
//# sourceMappingURL=SupabaseClient.js.map