import { AdMobSsvError, getAllowedRewardedAdUnitIds, parseAdMobCallbackUrl, rememberVerifiedAdMobTransaction, verifyAdMobCallback, } from './admob-ssv.ts' function assert(condition: boolean, message: string): asserts condition { if (!condition) throw new Error(message) } function base64(bytes: Uint8Array): string { let binary = '' for (const byte of bytes) binary += String.fromCharCode(byte) return btoa(binary) } function base64Url(bytes: Uint8Array): string { return base64(bytes).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '') } function derInteger(raw: Uint8Array): Uint8Array { let firstNonZero = 0 while (firstNonZero < raw.length - 1 && raw[firstNonZero] === 0) firstNonZero += 1 const trimmed = raw.slice(firstNonZero) const needsLeadingZero = (trimmed[0] & 0x80) !== 0 const value = needsLeadingZero ? Uint8Array.from([0, ...trimmed]) : trimmed return Uint8Array.from([0x02, value.length, ...value]) } function rawEcdsaToDer(raw: Uint8Array): Uint8Array { assert(raw.length === 64, 'test signer must return a P-256 raw signature') const r = derInteger(raw.slice(0, 32)) const s = derInteger(raw.slice(32)) return Uint8Array.from([0x30, r.length + s.length, ...r, ...s]) } Deno.test('AdMob callback verifies exact raw query content and rejects tampering', async () => { const keyPair = await crypto.subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'], ) const signedContent = [ 'ad_network=5450213213286189855', 'ad_unit=ca-app-pub-1234567890123456%2F1234567890', 'reward_amount=50', 'reward_item=cloud_ai_tokens', `timestamp=${Date.now()}`, 'transaction_id=18fa792de1bca816048293fc71035638', 'user_id=11111111-2222-4333-8444-555555555555', ].join('&') const rawSignature = new Uint8Array(await crypto.subtle.sign( { name: 'ECDSA', hash: 'SHA-256' }, keyPair.privateKey, new TextEncoder().encode(signedContent), )) const signature = base64Url(rawEcdsaToDer(rawSignature)) const callbackUrl = `https://example.test/admob?${signedContent}&signature=${signature}&key_id=123` const publicKey = new Uint8Array(await crypto.subtle.exportKey('spki', keyPair.publicKey)) const keyResponse = JSON.stringify({ keys: [{ keyId: 123, base64: base64(publicKey) }], }) const fakeFetch: typeof fetch = (_input, _init) => Promise.resolve(new Response(keyResponse, { status: 200, headers: { 'Content-Type': 'application/json' }, })) const verified = await verifyAdMobCallback(callbackUrl, fakeFetch) assert(verified.params.get('reward_amount') === '50', 'verified reward amount must parse') assert(verified.params.get('ad_unit')?.includes('/') === true, 'percent encoding must decode after verification') let tamperError: unknown try { await verifyAdMobCallback(callbackUrl.replace('reward_amount=50', 'reward_amount=51'), fakeFetch) } catch (error) { tamperError = error } assert(tamperError instanceof AdMobSsvError, 'tampered callback must fail') assert(tamperError.code === 'invalid_signature', 'tampering must be reported as invalid signature') }) Deno.test('AdMob callback requires an epoch-millisecond timestamp within the freshness window', async () => { const keyPair = await crypto.subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'], ) const publicKey = new Uint8Array(await crypto.subtle.exportKey('spki', keyPair.publicKey)) const fakeFetch: typeof fetch = () => Promise.resolve(new Response(JSON.stringify({ keys: [{ keyId: 456, base64: base64(publicKey) }], }), { status: 200, headers: { 'Content-Type': 'application/json' } })) async function signedUrl(timestamp: string, transactionId: string): Promise { const signedContent = [ 'ad_network=5450213213286189855', 'ad_unit=1234567890', 'reward_amount=50', 'reward_item=cloud_ai_tokens', `timestamp=${timestamp}`, `transaction_id=${transactionId}`, 'user_id=11111111-2222-4333-8444-555555555555', ].join('&') const rawSignature = new Uint8Array(await crypto.subtle.sign( { name: 'ECDSA', hash: 'SHA-256' }, keyPair.privateKey, new TextEncoder().encode(signedContent), )) const signature = base64Url(rawEcdsaToDer(rawSignature)) return `https://example.test/admob?${signedContent}&signature=${signature}&key_id=456` } await verifyAdMobCallback( await signedUrl(String(Date.now()), '18fa792de1bca816048293fc71035639'), fakeFetch, ) for (const [timestamp, transactionId] of [ [String(Math.floor(Date.now() / 1000)), '18fa792de1bca816048293fc71035640'], [String(Date.now() - 61 * 60 * 1000), '18fa792de1bca816048293fc71035641'], [String(Date.now() + 61 * 60 * 1000), '18fa792de1bca816048293fc71035642'], ] as const) { let error: unknown try { await verifyAdMobCallback(await signedUrl(timestamp, transactionId), fakeFetch) } catch (candidate) { error = candidate } assert(error instanceof AdMobSsvError, 'invalid timestamp must fail closed') assert(error.code === 'stale_timestamp', 'invalid timestamp must report stale_timestamp') } }) Deno.test('AdMob callback requires signature and key id to be the final ordered parameters', () => { for (const url of [ 'https://example.test/admob?reward_amount=50', 'https://example.test/admob?reward_amount=50&signature=abc', 'https://example.test/admob?reward_amount=50&signature=abc&key_id=123&extra=1', ]) { let error: unknown try { parseAdMobCallbackUrl(url) } catch (candidate) { error = candidate } assert(error instanceof AdMobSsvError, 'invalid parameter order must fail closed') } }) Deno.test('AdMob replay memory is committed only after durable reward persistence', async () => { const keyPair = await crypto.subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'], ) const transactionId = '18fa792de1bca816048293fc71035643' const signedContent = [ 'ad_network=5450213213286189855', 'ad_unit=1234567890', 'reward_amount=50', 'reward_item=cloud_ai_tokens', `timestamp=${Date.now()}`, `transaction_id=${transactionId}`, 'user_id=11111111-2222-4333-8444-555555555555', ].join('&') const rawSignature = new Uint8Array(await crypto.subtle.sign( { name: 'ECDSA', hash: 'SHA-256' }, keyPair.privateKey, new TextEncoder().encode(signedContent), )) const signature = base64Url(rawEcdsaToDer(rawSignature)) const callbackUrl = `https://example.test/admob?${signedContent}&signature=${signature}&key_id=789` const publicKey = new Uint8Array(await crypto.subtle.exportKey('spki', keyPair.publicKey)) const fakeFetch: typeof fetch = () => Promise.resolve(new Response(JSON.stringify({ keys: [{ keyId: 789, base64: base64(publicKey) }], }), { status: 200, headers: { 'Content-Type': 'application/json' } })) await verifyAdMobCallback(callbackUrl, fakeFetch) await verifyAdMobCallback(callbackUrl, fakeFetch) rememberVerifiedAdMobTransaction(transactionId) let error: unknown try { await verifyAdMobCallback(callbackUrl, fakeFetch) } catch (candidate) { error = candidate } assert(error instanceof AdMobSsvError, 'committed transaction must reject replay') assert(error.code === 'replay_detected', 'committed transaction must report replay_detected') }) Deno.test('AdMob key cache obeys max-age=0 and rejects a removed signing key', async () => { const oldPair = await crypto.subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'], ) const replacementPair = await crypto.subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'], ) const oldPublicKey = new Uint8Array(await crypto.subtle.exportKey('spki', oldPair.publicKey)) const replacementPublicKey = new Uint8Array( await crypto.subtle.exportKey('spki', replacementPair.publicKey), ) let oldKeyIsAuthoritative = true let fetchCount = 0 const fakeFetch: typeof fetch = () => { fetchCount += 1 const record = oldKeyIsAuthoritative ? { keyId: 901, base64: base64(oldPublicKey) } : { keyId: 902, base64: base64(replacementPublicKey) } return Promise.resolve(new Response(JSON.stringify({ keys: [record] }), { status: 200, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=0', Age: '0', }, })) } async function oldKeyCallback(transactionId: string): Promise { const signedContent = [ 'ad_network=5450213213286189855', 'ad_unit=1234567890', 'reward_amount=50', 'reward_item=cloud_ai_tokens', `timestamp=${Date.now()}`, `transaction_id=${transactionId}`, 'user_id=11111111-2222-4333-8444-555555555555', ].join('&') const rawSignature = new Uint8Array(await crypto.subtle.sign( { name: 'ECDSA', hash: 'SHA-256' }, oldPair.privateKey, new TextEncoder().encode(signedContent), )) const signature = base64Url(rawEcdsaToDer(rawSignature)) return `https://example.test/admob?${signedContent}&signature=${signature}&key_id=901` } await verifyAdMobCallback( await oldKeyCallback('18fa792de1bca816048293fc71035644'), fakeFetch, ) oldKeyIsAuthoritative = false let error: unknown try { await verifyAdMobCallback( await oldKeyCallback('18fa792de1bca816048293fc71035645'), fakeFetch, ) } catch (candidate) { error = candidate } assert(fetchCount === 2, 'max-age=0 must force one authoritative fetch per verification') assert(error instanceof AdMobSsvError, 'removed signing key must fail closed') assert(error.code === 'unknown_key_id', 'removed signing key must report unknown_key_id') assert(error.status === 503, 'key propagation miss must remain retryable') }) Deno.test('AdMob callback allowlist normalizes full mobile unit ids to numeric SSV ids', () => { Deno.env.set('ADMOB_REWARDED_AD_UNIT_ANDROID', 'ca-app-pub-1234567890123456/9876543210') Deno.env.set('SUPABASE_URL', 'https://project.supabase.co') try { const allowed = getAllowedRewardedAdUnitIds() assert(allowed.has('9876543210'), 'numeric callback ad_unit must be allowlisted') assert(allowed.has('ca-app-pub-1234567890123456/9876543210'), 'configured full id must remain accepted') } finally { Deno.env.delete('ADMOB_REWARDED_AD_UNIT_ANDROID') Deno.env.delete('SUPABASE_URL') } })