Initial commit

This commit is contained in:
2026-04-17 23:26:01 +00:00
commit 2ea4ca5d52
409 changed files with 63459 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
interface ServiceAccount {
client_email: string
private_key: string
project_id: string
}
interface CachedToken {
token: string
expiresAt: number
}
let serviceAccount: ServiceAccount | null = null
let privateKey: CryptoKey | null = null
let cachedToken: CachedToken | null = null
export function getServiceAccount(): ServiceAccount {
if (serviceAccount) return serviceAccount
const credentialsJson = Deno.env.get('GOOGLE_CREDENTIALS_JSON')
if (!credentialsJson?.trim()) {
throw new Error('GOOGLE_CREDENTIALS_JSON environment variable is required')
}
serviceAccount = JSON.parse(credentialsJson)
return serviceAccount!
}
async function getPrivateKey(): Promise<CryptoKey> {
if (privateKey) return privateKey
const pem = getServiceAccount().private_key
const pemContents = pem
.replace(/-----BEGIN PRIVATE KEY-----/, '')
.replace(/-----END PRIVATE KEY-----/, '')
.replace(/\s/g, '')
const der = Uint8Array.from(atob(pemContents), c => c.charCodeAt(0))
privateKey = await crypto.subtle.importKey(
'pkcs8',
der.buffer,
{ name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
false,
['sign']
)
return privateKey
}
function b64url(data: ArrayBuffer | string): string {
const str = typeof data === 'string'
? btoa(data)
: btoa(String.fromCharCode(...new Uint8Array(data)))
return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}
export async function getAccessToken(): Promise<string> {
const now = Date.now()
if (cachedToken && cachedToken.expiresAt > now + 60_000) {
return cachedToken.token
}
const sa = getServiceAccount()
const iat = Math.floor(now / 1000)
const exp = iat + 3600
const header = b64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }))
const payload = b64url(JSON.stringify({
iss: sa.client_email,
scope: 'https://www.googleapis.com/auth/firebase.messaging',
aud: 'https://oauth2.googleapis.com/token',
exp,
iat
}))
const signingInput = `${header}.${payload}`
const key = await getPrivateKey()
const signature = await crypto.subtle.sign(
'RSASSA-PKCS1-v1_5',
key,
new TextEncoder().encode(signingInput)
)
const jwt = `${signingInput}.${b64url(signature)}`
const res = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: jwt
})
})
if (!res.ok) {
throw new Error(`Failed to get FCM access token: ${await res.text()}`)
}
const { access_token, expires_in } = await res.json()
cachedToken = { token: access_token, expiresAt: now + expires_in * 1000 }
return access_token
}