const { onCall, HttpsError } = require("firebase-functions/v2/https"); const { onMessagePublished } = require("firebase-functions/v2/pubsub"); const admin = require("firebase-admin"); const { AccessToken } = require("livekit-server-sdk"); const { CloudBillingClient } = require("@google-cloud/billing"); admin.initializeApp(); // ── Hard billing cutoff ─────────────────────────────────────────────────────── exports.stopBilling = onMessagePublished("billing-cutoff", async (event) => { const data = JSON.parse( Buffer.from(event.data.message.data, "base64").toString() ); if (data.costAmount >= data.budgetAmount) { const projectId = process.env.GCLOUD_PROJECT; const billingClient = new CloudBillingClient(); const [projectInfo] = await billingClient.getProjectBillingInfo({ name: `projects/${projectId}`, }); if (!projectInfo.billingEnabled) { console.log("Billing already disabled."); return; } await billingClient.updateProjectBillingInfo({ name: `projects/${projectId}`, projectBillingInfo: { billingAccountName: "" }, }); console.log(`Billing DISABLED for ${projectId} — budget exceeded.`); } }); // ── Livekit token ───────────────────────────────────────────────────────────── exports.getLivekitToken = onCall(async (request) => { if (!request.auth) { throw new HttpsError("unauthenticated", "Must be signed in."); } const { roomName, participantName } = request.data; if (!roomName || !participantName) { throw new HttpsError("invalid-argument", "roomName and participantName required."); } const apiKey = process.env.LIVEKIT_API_KEY; const apiSecret = process.env.LIVEKIT_API_SECRET; const url = process.env.LIVEKIT_URL; if (!apiKey || !apiSecret) { throw new HttpsError("internal", "Livekit credentials not configured."); } const at = new AccessToken(apiKey, apiSecret, { identity: request.auth.uid, name: participantName, ttl: "4h", }); at.addGrant({ roomJoin: true, room: roomName, canPublish: true, canSubscribe: true, canPublishData: true, canUpdateOwnMetadata: true, }); const token = await at.toJwt(); return { token, url }; });