Files
Sauci/apps/supabase/functions/sync-subscription/index.ts
T
jjbruntonandClaude Opus 4.5 6f1be7a4e1 Fix supabase-js Deno compatibility by pinning to v2.45.0
The latest supabase-js@2.92.0 has a Deno compatibility bug with
createRequire that causes all edge functions to fail with:
"The argument 'filename' must be a file URL object..."

Pin all edge functions to @2.45.0 with ?target=deno parameter
to ensure Deno-compatible builds from esm.sh.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 21:55:35 +00:00

130 lines
5.1 KiB
TypeScript

import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.0?target=deno";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};
// RevenueCat API key (secret key, not public)
const REVENUECAT_API_KEY = Deno.env.get("REVENUECAT_API_KEY");
const ENTITLEMENT_ID = Deno.env.get("REVENUECAT_ENTITLEMENT_ID") || "Sauci Pro";
Deno.serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
try {
// Get user from JWT
const authHeader = req.headers.get("Authorization");
if (!authHeader) {
return new Response(
JSON.stringify({ error: "Missing authorization" }),
{ status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
// Verify the JWT and get user
const token = authHeader.replace("Bearer ", "");
const { data: { user }, error: authError } = await supabase.auth.getUser(token);
if (authError || !user) {
return new Response(
JSON.stringify({ error: "Invalid token" }),
{ status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
console.log(`Checking subscription for user: ${user.id}`);
console.log(`Using entitlement ID: ${ENTITLEMENT_ID}`);
console.log(`API key present: ${!!REVENUECAT_API_KEY}`);
console.log(`API key length: ${REVENUECAT_API_KEY?.length || 0}`);
if (!REVENUECAT_API_KEY) {
console.error("REVENUECAT_API_KEY is not set!");
return new Response(
JSON.stringify({ error: "Server configuration error: missing API key" }),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
// Fetch subscription status from RevenueCat API (server-side verification)
const rcResponse = await fetch(
`https://api.revenuecat.com/v1/subscribers/${user.id}`,
{
headers: {
"Authorization": `Bearer ${REVENUECAT_API_KEY}`,
"Content-Type": "application/json",
},
}
);
console.log(`RevenueCat API response status: ${rcResponse.status}`);
if (!rcResponse.ok) {
const errorText = await rcResponse.text();
console.error(`RevenueCat API error (${rcResponse.status}):`, errorText);
// Return the actual error details for debugging
return new Response(
JSON.stringify({
error: "Failed to verify subscription",
status: rcResponse.status,
details: errorText
}),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
const rcData = await rcResponse.json();
console.log("RevenueCat subscriber data:", JSON.stringify(rcData.subscriber?.entitlements || {}));
const entitlements = rcData.subscriber?.entitlements || {};
const entitlement = entitlements[ENTITLEMENT_ID];
console.log(`Found entitlement "${ENTITLEMENT_ID}":`, entitlement ? "yes" : "no");
if (entitlement) {
console.log(`Expires: ${entitlement.expires_date}`);
}
// Check if entitlement is active
// Note: expires_date can be null for lifetime subscriptions
const isActive = entitlement && (
entitlement.expires_date === null ||
new Date(entitlement.expires_date) > new Date()
);
console.log(`User ${user.id} subscription status: ${isActive ? "active" : "inactive"}`);
// Update the database
const { error: updateError } = await supabase
.from("profiles")
.update({ is_premium: isActive })
.eq("id", user.id);
if (updateError) {
console.error("Database update error:", updateError);
return new Response(
JSON.stringify({ error: "Failed to update status" }),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
return new Response(
JSON.stringify({ success: true, is_premium: isActive }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
} catch (error) {
console.error("Sync error:", error);
return new Response(
JSON.stringify({ error: "Internal server error" }),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
});