feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
173
server/supabase/functions/_shared/google-pubsub.ts
Normal file
173
server/supabase/functions/_shared/google-pubsub.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
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<void> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue