export interface GooglePlaySubscriptionRtdn { kind: 'subscription' messageId: string packageName: string eventTimeMillis: number purchaseToken: string notificationType: number } export interface GooglePlayTestRtdn { kind: 'test' messageId: string packageName: string eventTimeMillis: number } export type GooglePlayRtdn = GooglePlaySubscriptionRtdn | GooglePlayTestRtdn interface PubSubEnvelope { message?: { data?: unknown messageId?: unknown message_id?: unknown } } interface RtdnPayload { packageName?: unknown eventTimeMillis?: unknown subscriptionNotification?: { notificationType?: unknown purchaseToken?: unknown } testNotification?: { version?: unknown } } export interface GoogleOidcClaims { aud?: unknown email?: unknown email_verified?: unknown exp?: unknown iss?: unknown } export class GooglePubSubError extends Error { constructor( public readonly code: string, public readonly status: number, ) { super(code) this.name = 'GooglePubSubError' } } function decodeBase64Utf8(value: string): string { try { const binary = atob(value.replace(/-/g, '+').replace(/_/g, '/')) const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0)) return new TextDecoder().decode(bytes) } catch { throw new GooglePubSubError('invalid_pubsub_data', 400) } } export function validateGoogleOidcClaims( claims: GoogleOidcClaims, expectedAudience: string, expectedEmail: string, nowSeconds = Math.floor(Date.now() / 1000), ): void { const audiences = Array.isArray(claims.aud) ? claims.aud : [claims.aud] const expiry = typeof claims.exp === 'string' ? Number(claims.exp) : claims.exp const verified = claims.email_verified === true || claims.email_verified === 'true' if ( !audiences.includes(expectedAudience) || claims.email !== expectedEmail || !verified || (claims.iss !== 'https://accounts.google.com' && claims.iss !== 'accounts.google.com') || typeof expiry !== 'number' || !Number.isFinite(expiry) || expiry <= nowSeconds ) { throw new GooglePubSubError('invalid_pubsub_identity', 401) } } export async function verifyGooglePubSubIdentity( req: Request, fetchImpl: typeof fetch = fetch, ): Promise { const expectedAudience = Deno.env.get('GOOGLE_PLAY_PUBSUB_AUDIENCE') ?? '' const expectedEmail = Deno.env.get('GOOGLE_PLAY_PUBSUB_SERVICE_ACCOUNT') ?? '' if (!expectedAudience || !expectedEmail) { throw new GooglePubSubError('google_pubsub_not_configured', 503) } const authorization = req.headers.get('Authorization') ?? '' const match = /^Bearer\s+([^\s]+)$/i.exec(authorization) if (!match) throw new GooglePubSubError('missing_pubsub_identity', 401) let response: Response try { response = await fetchImpl( `https://oauth2.googleapis.com/tokeninfo?id_token=${encodeURIComponent(match[1])}`, { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(10_000), }, ) } catch { throw new GooglePubSubError('google_pubsub_identity_unavailable', 503) } if (!response.ok) throw new GooglePubSubError('invalid_pubsub_identity', 401) const claims = await response.json() as GoogleOidcClaims validateGoogleOidcClaims(claims, expectedAudience, expectedEmail) } export function parseGooglePlayRtdn(envelope: PubSubEnvelope): GooglePlayRtdn { const messageId = envelope.message?.messageId ?? envelope.message?.message_id const encodedData = envelope.message?.data if (typeof messageId !== 'string' || !messageId || messageId.length > 256) { throw new GooglePubSubError('invalid_pubsub_message_id', 400) } if (typeof encodedData !== 'string' || encodedData.length > 32_768) { throw new GooglePubSubError('invalid_pubsub_data', 400) } let payload: RtdnPayload try { payload = JSON.parse(decodeBase64Utf8(encodedData)) as RtdnPayload } catch (error) { if (error instanceof GooglePubSubError) throw error throw new GooglePubSubError('invalid_pubsub_data', 400) } const eventTimeMillis = Number(payload.eventTimeMillis) if ( payload.packageName === 'com.d3ro.voice' && payload.testNotification && Number.isSafeInteger(eventTimeMillis) ) { return { kind: 'test', messageId, packageName: payload.packageName, eventTimeMillis, } } const notification = payload.subscriptionNotification if ( payload.packageName !== 'com.d3ro.voice' || !notification || typeof notification.purchaseToken !== 'string' || notification.purchaseToken.length < 8 || notification.purchaseToken.length > 4096 || typeof notification.notificationType !== 'number' || !Number.isSafeInteger(notification.notificationType) || !Number.isSafeInteger(eventTimeMillis) ) { throw new GooglePubSubError('invalid_google_play_notification', 400) } return { kind: 'subscription', messageId, packageName: payload.packageName, eventTimeMillis, purchaseToken: notification.purchaseToken, notificationType: notification.notificationType, } }