import { corsHeaders } from '../_shared/cors.ts' import { createServiceRoleClient } from '../_shared/quota.ts' import { AdMobSsvError, getAllowedRewardedAdUnitIds, rememberVerifiedAdMobTransaction, verifyAdMobCallback, } from '../_shared/admob-ssv.ts' function jsonResponse(body: Record, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }) } Deno.serve(async (req: Request) => { if (req.method !== 'GET') return jsonResponse({ error: 'method_not_allowed' }, 405) try { const callback = await verifyAdMobCallback(req.url) const userId = callback.params.get('user_id') ?? '' const transactionId = callback.params.get('transaction_id') ?? '' const adNetwork = callback.params.get('ad_network') ?? '' const adUnitId = callback.params.get('ad_unit') ?? '' const rewardAmount = Number(callback.params.get('reward_amount')) const rewardItem = callback.params.get('reward_item') ?? '' if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(userId)) { throw new AdMobSsvError('invalid_user_id', 400) } if (!/^[0-9a-f]{16,128}$/i.test(transactionId)) { throw new AdMobSsvError('invalid_transaction_id', 400) } if (!/^\d+$/.test(adNetwork)) throw new AdMobSsvError('invalid_ad_network', 400) if (!getAllowedRewardedAdUnitIds().has(adUnitId)) { throw new AdMobSsvError('invalid_ad_unit', 400) } if (rewardAmount !== 50 || rewardItem !== 'cloud_ai_tokens') { throw new AdMobSsvError('invalid_reward', 400) } const serviceClient = createServiceRoleClient() const { error } = await serviceClient.rpc('grant_verified_ad_reward', { p_user_id: userId, p_network: `admob:${adNetwork}`, p_placement: 'rewarded_video_quota', p_ad_unit_id: adUnitId, p_transaction_id: transactionId, p_reward_tokens: rewardAmount, }) if (error) throw new Error('reward_persistence_failed') rememberVerifiedAdMobTransaction(transactionId) // AdMob's SSV contract expects HTTP 200 OK. Never expose balances or // ledger data from this unauthenticated server-to-server endpoint. return new Response(null, { status: 200 }) } catch (error) { const code = error instanceof AdMobSsvError ? error.code : 'reward_verification_failed' console.warn('[admob-ssv] rejected callback:', code) // Authentication/validation failures acknowledge receipt without granting // a reward. Transient key-fetch or persistence failures return 5xx so // AdMob can retry instead of permanently losing a legitimate reward. if (!(error instanceof AdMobSsvError) || error.status >= 500) { return jsonResponse({ error: code }, 503) } return jsonResponse({ error: code }, 200) } })