@geckou/firebase-server
v0.4.0
Published
Server-side Firebase helpers (Auth middleware / FCM push) for Cloud Functions and other Node servers
Readme
@geckou/firebase-server
サーバーサイド(Cloud Functions 等)向けの Firebase ヘルパー。 Firebase Auth の ID トークンを検証する Express 互換ミドルウェアと、FCM によるプッシュ通知送信を提供する。
firebase-admin / express は依存に持たない。インスタンスは利用側から注入し、
型は使用するメソッドだけを構造的に要求する(TokenVerifierLike / MessagingLike)。
peer の型を公開 API に使うとメジャーバージョン間の型定義の差が利用側の型エラーになるため
(@geckou/billing の StripeClientLike と同じ扱い)。getAuth() / getMessaging() の
戻り値はそのまま渡せる。
インストール
yarn add @geckou/firebase-serverfirebase-admin は利用側のプロジェクトが持っている前提(Cloud Functions なら必ずある)。
認証ミドルウェア
import { createRequireAuth } from '@geckou/firebase-server'
import { getAuth } from 'firebase-admin/auth'
// ゲッターで渡すと Auth の解決がリクエスト時まで遅延されるため、
// initializeApp() より先にミドルウェアを定義しても安全
export const requireAuth = createRequireAuth(getAuth)
// 失効(revokeRefreshTokens)済みのトークンも弾きたい場合。
// 検証のたびに Firebase Auth へ問い合わせるので、レイテンシと呼び出し回数が増える。
// 既定(false)だと、失効させても ID トークンの有効期限(最大 1 時間)は通り続ける
export const requireFreshAuth = createRequireAuth(getAuth, {
checkRevoked: true,
})import type { AuthenticatedRequest } from '@geckou/firebase-server'
import type { Request } from 'express'
app.get('/me', requireAuth, (req, res) => {
// 検証に成功すると req.uid に uid、req.token に検証済みトークン全体が入る
const { uid, token } = req as Request & AuthenticatedRequest
res.json({ uid, plan: token.plan })
})req.token は verifyIdToken の戻り値そのもの。@geckou/billing の syncClaims が書く
subscriptionActive / plan や role などのカスタムクレームは全てここに載るので、
ハンドラ側で再度 verifyIdToken を呼んだり Firestore を読んだりする必要はない。
型は { uid: string } & Record<string, unknown>(DecodedTokenLike)なので、
uid 以外は利用側で絞り込むこと。
TokenVerifierLike が要求する戻り値は { uid: string } のまま(最小契約)。
DecodedTokenLike を要求すると、uid を持つ interface / class を返す独自 verifier が
インデックスシグネチャ不足で代入できなくなるため。
Authorization: Bearer <ID トークン> を検証し、トークンが無い・不正な場合は
401 { "error": "Unauthorized" } を返してハンドラへ進まない。
⚠️ next() は try の外で呼ぶ実装になっている(中で呼ぶと後続ハンドラの
同期例外まで 401 に化けるため)。その代わり、後続ハンドラが投げた例外は
このミドルウェアの Promise の reject として外へ出る。Express 4 は async
ミドルウェアの reject を捕まえないので、express-async-errors を読み込むか
Express 5 を使うこと。捕まえないままだと unhandledRejection になり、
クライアントへレスポンスが返らない。
import 'express-async-errors' // Express 4 のみ必要プッシュ通知(FCM)
import {
sendPushNotification,
sendPushNotificationBatch,
} from '@geckou/firebase-server'
import { getMessaging } from 'firebase-admin/messaging'
// 単一デバイス
await sendPushNotification(getMessaging(), fcmToken, {
title: '新着メッセージ',
body: '○○さんからメッセージが届きました',
data: { screen: 'chat' },
})
// 複数デバイス(トークンが空配列なら送信せず 0 件で返る)
const { successCount, failureCount, invalidTokens, errors } =
await sendPushNotificationBatch(getMessaging(), fcmTokens, {
title: 'お知らせ',
body: '本文',
})
// アンインストール済み端末などのトークンは恒久的に失敗する。
// 保存先から消さないと溜まり続け、送信のたびに失敗数が増える
await removeFcmTokens(invalidTokens)FCM は 1 リクエストにつき 500 件までのため、sendPushNotificationBatch は
500 件ずつに分割して送り、件数を合算して返す(呼び出し側で分ける必要はない)。
分割したチャンクの送信そのものが失敗しても throw せず、そのチャンクの件数を
failureCount に計上し、{ error, tokens } を errors に入れて返す。途中で throw すると、
それ以前のチャンクで判明した invalidTokens を呼び出し側が掃除できず、
次回も同じ失敗を繰り返すため。再試行するときは errors[].tokens だけを送り直す
(全件を送り直すと、成功済みのトークンに通知が二重に届く)。
モバイル側の受信(権限リクエスト・トークン取得)はこのパッケージの対象外。
テンプレートの apps/mobile/src/lib/push-notifications.ts(expo-notifications)が担当する。
