feat(release): prepare 1.1.0 candidate

This commit is contained in:
Yun Chan 2026-08-29 18:33:45 +09:00
parent 5a34f66981
commit 5205dcdfa9
736 changed files with 115667 additions and 12203 deletions

View 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,
}
}