@crvouga/mockingbird-service-fcm

中文文档
2.11.0发布于 2026/11/11MITTypeScriptES Module构建来源已验证近 30 天下载 1.5万

Stateful emulator of the Firebase Cloud Messaging HTTP v1 send API used by firebase-admin: fixture projects and registration tokens, an outbox, and a device inbox.

原始使用文档
文档目录9 节
  1. Install
  2. Usage
  3. Routes
  4. Admin
  5. Presets
  6. Namespaces
  7. SDK error codes
  8. Deliberately not modelled
  9. API

@crvouga/mockingbird-service-fcm

Local emulators. Real API contracts. Part of Mockingbird.

Stateful emulator of the Firebase Cloud Messaging HTTP v1 send API (POST /v1/projects/{project_id}/messages:send), the call firebase-admin makes. Projects, access tokens, and registration tokens are fixtures. There is no FCM sandbox. This package is status: "wip".

  • Operation coverage: SUPPORT.md
  • The contract (openapi.yaml) is hand-authored from the FCM REST reference and firebase-admin 12.7.0 / 13.5.0.

Install

npm install -D @crvouga/mockingbird-service-fcm

ESM only. Node >= 22 or Bun >= 1.2. No native dependencies. Serve it with npx mockingbird-fcm serve, createServer from ./server (Node), or createRuntime with any Fetch server.

Usage

firebase-admin hardcodes https://fcm.googleapis.com. Point it at the emulator with createAdminTransport (an https.Agent that dials the emulator) and call enableLegacyHttpTransport() so multicast sends honor that agent. send is already HTTP/1.1. The fixture project is demo-project, the fixture device token is fixture-device-token, and any bearer is accepted until you turn on strict.

import { deleteApp, initializeApp } from "firebase-admin/app"
import { getMessaging } from "firebase-admin/messaging"
import { createServer } from "@crvouga/mockingbird-service-fcm/server"
import { createAdminTransport } from "@crvouga/mockingbird-service-fcm/admin"

// Test setup: the emulator, plus a credential and an agent that reach it.
const fcm = await createServer()
const transport = await createAdminTransport({ origin: fcm.url, token: "fixture-token" })

// The application's firebase-admin app. Only its credential and httpAgent change.
const app = initializeApp({
  projectId: "demo-project",
  credential: transport.credential,
  httpAgent: transport.agent,
})
const messaging = getMessaging(app)
messaging.enableLegacyHttpTransport()
const name = await messaging.send({
  token: "fixture-device-token",
  notification: { title: "Hello", body: "World" },
})
// name → "projects/demo-project/messages/<id>"

// Test control: every message the emulator accepted.
const outbox = await (await fetch(`${fcm.url}/__admin/outbox`)).json()
void outbox

await deleteApp(app)
await transport.close()
await fcm.close()

Routes

RouteBehaviour
POST /v1/projects/{project_id}/messages:sendBearer required. Body { message, validate_only? }. One registration token (topic and condition are rejected). Success is { name: "projects/{project_id}/messages/{id}" }. Errors are a Google RPC envelope. validate_only: true validates and returns a name without storing anything.
GET /__admin/healthLiveness, plus the x-mockingbird header on every response.

Admin

RouteBehaviour
GET /__admin/outboxAccepted messages, oldest first. Filters: token, platform, project, since (epoch ms), collapseKey, state. Each row has the normalized fields and the original wire message.
POST /__admin/tokensRegister { token, platform?, project?, state?, appId? }. tokens: [...] registers a batch.
POST /__admin/tokens/:token/expireMark expired. Optional { replacement }.
POST /__admin/tokens/:token/state{ state: active | expired | unregistered | sender_mismatch }.
GET /__admin/inbox/:tokenDevice inbox, in delivery order.
POST /__admin/inbox/:token{ action: "ack" } drops the oldest item. { action: "clear" } empties it.
POST /__admin/deliverDeliver accepted messages. Expired ones stay in the outbox as expired.
POST /__admin/messages/:id/deliverDeliver one accepted message.
POST /__admin/messages/:id/dropMark dropped and do not inbox it.
POST /__admin/messages/:id/duplicateCopy the inbox entry again.
GET /__admin/settings, PUT /__admin/settingsstrict, automaticDelivery (default true), collapse (default false), credentials (bearer → { project, expired? }), clockOffsetMs.
POST /__admin/time{ advanceMs } moves this namespace's offset only.
POST /__admin/scripts{ token?, errorCode, count, httpStatus? }. The next count sends to that token (or every token when token is omitted) return that Google RPC error and store nothing. Then the token's own state applies.
GET /__admin/scripts, DELETE /__admin/scriptsList or clear scripts. ?token= clears one.

Logical message time is clockOffsetMs plus the process-wide runtime clock. POST /__admin/clock still moves every namespace. Records, faults, outboxes, inboxes, and message ids are per namespace.

Presets

POST /__admin/faults {"preset": "<name>"}. Each one targets SendMessage:

PresetEffect
invalid_auth401 UNAUTHENTICATED, nothing stored
permission_denied403 PERMISSION_DENIED, nothing stored
invalid_argument400 INVALID_ARGUMENT, nothing stored
unregistered404 NOT_FOUND + FcmError UNREGISTERED, nothing stored
sender_mismatch403 + FcmError SENDER_ID_MISMATCH, nothing stored
quota_exceeded429 RESOURCE_EXHAUSTED, Retry-After, google.rpc.RetryInfo, FcmError QUOTA_EXCEEDED
unavailable503 + FcmError UNAVAILABLE
internal500 + FcmError INTERNAL
slowwaits 15s (the Firebase Admin send timeout)
dropconnection drop before accept; outbox stays empty
accepted_then_network_dropoutbox record is stored, then the connection drops

Namespaces

x-mockingbird-namespace, a /__admin/ns/<name>/ prefix, or PUT /__admin/credentials (bearer → namespace). A bearer listed in settings.credentials authorizes only that project. strict: true rejects bearers that are not listed. Webhooks: none. The device inbox is a test control, not a callback.

SDK error codes

firebase-admin maps error.details[].errorCode when @type is type.googleapis.com/google.firebase.fcm.v1.FcmError, otherwise error.status. Two mappings differ from a naive reading of the error-code names, and the emulator follows firebase-admin 12.7.0 and 13.5.0:

  • A JSON 401 with status UNAUTHENTICATED and no FcmError details becomes messaging/third-party-auth-error. messaging/authentication-error is what the SDK uses for a non-JSON 401.
  • FcmError DEADLINE_EXCEEDED is not in the messaging map, so the SDK reports messaging/unknown-error. UNAVAILABLE is messaging/server-unavailable.

INVALID_ARGUMENT, SENDER_ID_MISMATCH, QUOTA_EXCEEDED, UNAVAILABLE, and INTERNAL map to messaging/invalid-argument, messaging/mismatched-credential, messaging/message-rate-exceeded, messaging/server-unavailable, and messaging/internal-error. Status PERMISSION_DENIED (a bearer for another project) is also messaging/mismatched-credential.

Deliberately not modelled

  • Firebase Auth, Firestore, Realtime Database, Hosting, Remote Config, Analytics, and client installation APIs.
  • Topic and condition sends, and topic subscription management.
  • APNs and Android transport, real devices, app callbacks, notification rendering, badges, and sounds.
  • Production quotas and Google's unpublished retry schedule. The Admin SDK's own retry (503, ECONNRESET, ETIMEDOUT) is left to the SDK.

API

ExportKindDescription
FcmAPIclassIn-process emulator: fetch(request), reset(), logicalNow(), outbox(query), inbox(token), registerToken, setTokenState, ackInbox, deliverPending, dropMessage, duplicateMessage, putScript. Options: sqlite, now, namespace, settings.
createRuntimefunctionEmulator plus /__admin/health, /__admin/*, namespaces, clock, presets, and the journal. Options: sqlite, clock, seed, adminKey, onLog, settings.
FCM_PRESETSobjectNamed fault presets.
FCM_NAMESPACEstringService name, "fcm".
FCM_FIXTURE_PROJECTstringSeeded project id, "demo-project".
FCM_FIXTURE_TOKENstringSeeded Android registration token, "fixture-device-token".
FCM_FIXTURE_BEARERstringBearer the playground sends, "fixture-token". Accepted whenever strict is off.
fcmCredentialfunction(accessToken?) → { getAccessToken } for initializeApp({ credential }).
document, operationIds, supportedOperationIdsvaluesThe OpenAPI contract and its operation ids.
createServer, serveTarget, DEFAULT_PORT (./server)NodeServe over node:http. CLI flag --strict. Port 8826.
createAdminTransport (./admin)Node{ origin, token? } → { credential, agent, port, close }. agent is an https.Agent aimed at the emulator.

Part of mockingbird.

版本与发布记录

@crvouga/mockingbird-service-fcm 共发布 28 个版本,首次发布于 2026/10/01;过去 12 个月发布了 28 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
2.11.0latest2026/11/11今天
2.10.92026/11/038 天前
2.10.82026/11/038 天前
2.10.72026/11/038 天前
2.10.62026/11/038 天前
2.10.52026/11/038 天前

关于 @crvouga/mockingbird-service-fcm 的常见问题

@crvouga/mockingbird-service-fcm 是什么?

Stateful emulator of the Firebase Cloud Messaging HTTP v1 send API used by firebase-admin: fixture projects and registration tokens, an outbox, and a device inbox.

如何安装 @crvouga/mockingbird-service-fcm?

在项目目录运行 npm install @crvouga/mockingbird-service-fcm,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 mockingbird-fcm,可以用 npx mockingbird-fcm 直接运行。

@crvouga/mockingbird-service-fcm 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@crvouga/mockingbird-service-fcm 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@crvouga/mockingbird-service-fcm 可以商用吗?

@crvouga/mockingbird-service-fcm 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@crvouga/mockingbird-service-fcm 的下载量有多少?

根据 npm 官方下载统计,@crvouga/mockingbird-service-fcm 在 2026/10/11 – 2026/11/09 这 30 天内被下载 15,280 次,日均约 509 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@crvouga/mockingbird-service-fcm 还在维护吗?

最新版本 2.11.0 发布于 2026/11/11(今天);至今共发布 28 个版本,其中过去 12 个月发布了 28 个;首次发布于 2026/10/01。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。