@crvouga/mockingbird-service-daily

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

Stateful emulator of the Daily.co REST API: rooms, presence, eject, meeting tokens (minted and self-signed HS256, verified and decodable), and transcription/recording webhooks with transcripts written to S3.

原始使用文档
文档目录8 节
  1. Install
  2. Usage
  3. Routes
  4. Webhooks
  5. Admin (beyond the standard contract)
  6. Namespaces
  7. Deliberately not modelled
  8. API

@crvouga/mockingbird-service-daily

Local emulators. Real API contracts. Part of Mockingbird.

Stateful emulator of the Daily.co REST API for test suites: rooms (create, get, update, delete, presence, eject), meeting tokens (mint and validate), verification of the HS256 meeting tokens our backend signs itself, and the end-of-call webhooks (transcription.stopped, recording.ready-to-download) with the transcript written to the stack's S3. Every EMR booking creates a room and a token; today a broken Daily integration is silent because booking swallows the error. Against the emulator it is observable and assertable.

  • Operation coverage: SUPPORT.md
  • The contract (openapi.yaml) is trimmed from Daily's documented REST API to the calls our backend and EMR make, with the fields they send.

Install

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

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

Usage

Point the apps at the emulator (the G-D1 seams):

AppEnvValue
backendDAILY_API_BASE_URLhttp://127.0.0.1:8800/v1 (needs the http-loopback exception)
backendDAILY_API_KEY, DAILY_API_DOMAIN_IDany key (or --api-key), and the same value as --domain-id
EMR backendDailyService.baseUrlhttp://127.0.0.1:8800/v1 (hardcoded today)
EMR backendDEFAULT_DAILY_BASE_URLthe same value as --room-url-base
EMR backendDAILY_WEBHOOK_SECRETthe same value as --webhook-secret
member-appEXPO_PUBLIC_DAILY_BASE_URLthe same value as --room-url-base
npx mockingbird-daily serve --port 8800 \
  --room-url-base https://acme-mock.daily.test/ \
  --domain-id "$DAILY_API_DOMAIN_ID" \
  --webhook-url http://127.0.0.1:4000/v1/webhooks/daily \
  --webhook-secret "$DAILY_WEBHOOK_SECRET" \
  --s3-endpoint http://127.0.0.1:4569 --s3-bucket "$S3_BUCKET_NAME"
import { createServer } from "@crvouga/mockingbird-service-daily/server"

// Test setup: the emulator, its webhook receiver and the S3 bucket transcripts land in.
const server = await createServer({
  settings: { roomUrlBase: "https://acme-mock.daily.test/" },
  webhooks: { url: "http://127.0.0.1:4000/v1/webhooks/daily", secret: "whsec-daily" },
  transcripts: { endpoint: "http://127.0.0.1:4569", bucket: "emr-transcripts" },
})

// Application code: DAILY_API_BASE_URL is `${server.url}/v1`. The EMR books an appointment.
const baseUrl = `${server.url}/v1`
const headers = { authorization: "Bearer daily-api-key", "content-type": "application/json" }
const created = await fetch(`${baseUrl}/rooms`, {
  method: "POST",
  headers,
  body: JSON.stringify({ privacy: "private" }),
})
const room = (await created.json()) as { name: string; url: string }
const minted = await fetch(`${baseUrl}/meeting-tokens`, {
  method: "POST",
  headers,
  body: JSON.stringify({ properties: { room_name: room.name, user_id: "prac-1", is_owner: true } }),
})
const { token } = (await minted.json()) as { token: string }

// Test control: end the call. Writes <room>/<session>.json to S3, then fires transcription.stopped.
await fetch(`${server.url}/__admin/rooms/${room.name}/session`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({
    participants: [{ userId: "prac-1" }, { userId: "pat-1" }],
    durationSec: 1200,
    transcript: [{ s: "prac-1", t: "How are you feeling?", ts: 0.5, te: 2.1 }],
  }),
})
await server.close()

Routes

RouteBehaviour
GET /v1/rooms{total_count, data} in newest-first creation order; limit (default 100), ending_before and starting_after use room IDs. Shares state with room creation, updates and deletion.
POST /v1/rooms{name?, privacy?, properties?}; unknown properties, bad types and a duplicate name are 400 {error: "invalid-request-error", info}. No name → a random 20-character one. Answers {id, name, api_created, privacy, url: <roomUrlBase><name>, created_at, config} with config echoing the properties. Accepts both the backend's strict body and the EMR's generateRoomConfig body.
GET /v1/rooms/:nameThe room, or 404 {error: "not-found", info: "room <name> not found"} (the EMR branches on message.includes('404')).
POST /v1/rooms/:nameMerge properties (and privacy) into the room; 404 when missing.
DELETE /v1/rooms/:name{deleted: true, name}; 404 when missing (the EMR tolerates it).
GET /v1/rooms/:name/presence{total_count, data: [{room, id, userId, userName, joinTime, duration}]} — exactly the backend's strict schema. Participants come from PUT /__admin/rooms/:name/presence.
POST /v1/rooms/:name/eject{user_ids?, ids?} → {ejectedIds} (participant session ids), removing them from presence.
POST /v1/meeting-tokens{properties} → {token}: an HS256 JWT signed with the caller's API key, claims under Daily's abbreviations (r, d, o, u, ud, nbf, exp, ejt, eje, er, erui, sr, ast, p, …) plus iat.
GET /v1/meeting-tokens/:tokenVerifies a token (minted by the emulator or self-signed by our backend) with the caller's API key and its nbf/exp on the emulator clock (?ignoreNbf=true skips nbf); answers its properties under full names, else 400.

Auth. Authorization: Bearer <DAILY_API_KEY>; any key unless apiKeys is set. Missing → 401 {error: "authentication-error"}.

Milliseconds. Some of our callers pass nbf/exp in milliseconds (e.g. the EMR's generatePatientToken passes Date.parse(end)). The emulator accepts them, interprets values ≥ 1e11 as ms in time checks, records each in GET /__admin/warnings, and tags the journal entry (ids.warning: "exp in milliseconds").

Webhooks

Admin sessions post Daily-shaped events: {version: "1.0.0", type, event, id, event_ts, payload} (our receiver reads event; Daily documents type; both are sent).

  • transcription.stopped — payload: {room_name, session_id, duration, s3_key, instance_id}.
  • recording.ready-to-download — payload: {type: "cloud", recording_id, room_name, session_id, start_ts, status: "finished", max_participants, duration, s3_key} (our EMR just acks it).

Signature — our scheme, not Daily's. x-webhook-signature = hex HMAC-SHA256(DAILY_WEBHOOK_SECRET, raw body), which is what our EMR verifies (only when the secret is set). Daily's documented scheme is different — base64 HMAC-SHA256 over "<X-Webhook-Timestamp>.<body>" with the base64-decoded secret — so a real Daily webhook would fail our check; the emulator signs the way our code checks. x-webhook-timestamp is sent too. Retries: immediately, 5 s, 5 min, 30 min, 2 h; GET /__admin/webhooks, …/events, …/replay, …/flush as usual.

Admin (beyond the standard contract)

RouteEffect
POST /__admin/rooms/:name/session{participants: [{userId, userName?}], durationSec, transcript?: [{s, t, ts, te}], sessionId?, recording?} writes the transcript JSON to S3 at {roomName}/{sessionId}.json (SigV4 PutObject to the --s3-endpoint / transcripts target; a synthetic transcript alternating between participants when none is given), clears presence, then emits transcription.stopped (and recording.ready-to-download unless recording: false). Answers {sessionId, s3Key, transcript: "s3://…" | null, events}; 502 when S3 refuses. The transcript text is never kept.
PUT /__admin/rooms/:name/presence{participants: [{userId, userName?, joinedAt?}]} — who GET …/presence reports.
POST /__admin/tokens/decode{token, apiKey?} → {decodable, header, claims, properties, signatureValid, room, joinable, problems, warnings}: signature (against apiKey or apiKeys), the backend's strict claim set, ud ≤ 36, domain id, ms timestamps, and the token and room nbf/exp windows on the emulator clock (owners may enter before the room's nbf).
GET /__admin/rooms, GET /__admin/rooms/:nameRooms with config, presence and session metadata.
GET /__admin/warningsTolerated oddities (millisecond timestamps, tokens for rooms that do not exist).
PUT /__admin/settings{apiKeys?, domainId?, roomUrlBase?} for the calling namespace (GET masks keys).

Time rules run on the emulator clock (POST /__admin/clock): token/room nbf and exp, and therefore the backend's 13 h room-creation delay and 30-min guardrail window as seen by Daily.

Fault presets (POST /__admin/faults {"preset": "<name>", "count"?: n}): room_not_found (404 on /v1/rooms/:name…), unauthorized (401), rate_limited (429), server_error (500), webhook_duplicate, webhook_reorder, webhook_drop.

Namespaces

x-mockingbird-namespace, a /__admin/ns/<name> prefix on the base URL, or by API key: PUT /__admin/credentials {"credentials": {"<DAILY_API_KEY>": "<namespace>"}}.

Deliberately not modelled

  • The media plane: SFU, WebRTC, knocking/admission, recording and transcription themselves. daily-js loads Daily's CDN bundle; member-app UI tests should inject a fake DailyCallLike through useCallProviderLogic(createCallObject).
  • The room page at https://<domain>.daily.co/<room>?t= (the URL is produced, not served).
  • Daily's own webhook signature scheme and webhook registration API (see above).
  • Recordings/transcripts REST APIs, dial-out, streaming.

API

ExportKindDescription
DailyAPIclassThe in-process emulator: fetch(request), reset(), inspectToken(token, keys?), setPresence(name, participants), endSession(name, input), rooms(). Options: sqlite, now, namespace, settings, onWebhook, transcripts.
createRuntimefunctionThe emulator with the full service contract. Options: webhooks: {url, secret, retryDelaysMs?, fetch?}, transcripts: {endpoint, bucket, region?, accessKeyId?, secretAccessKey?, keyPattern?, fetch?}, settings, clock, seed, adminKey, onLog.
DAILY_PRESETSobjectEvery named fault preset.
DAILY_NAMESPACEstringThe service name, "daily".
WEBHOOK_PATHstring"/v1/webhooks/daily", our EMR receiver's route.
dailyWebhookSignerfunctionThe x-webhook-signature signer (hex HMAC-SHA256 of the raw body).
signToken, decodeToken, verifySignaturefunctionsHS256 meeting-token helpers.
claimsToProperties, propertiesToClaims, CLAIM_NAMESvaluesDaily's abbreviated claim names ↔ full property names.
synthesizeTranscriptfunctionThe default transcript for a session with none given.
document, operationIds, supportedOperationIdsvaluesThe vendored OpenAPI contract and its operation ids.
createServer, serveTarget, DEFAULT_PORT (./server)NodeServe over node:http; the serve CLI target; port 8800.

Part of mockingbird.

版本与发布记录

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

版本发布日期距今
3.11.0latest2026/11/11今天
3.10.92026/11/038 天前
3.10.82026/11/038 天前
3.10.72026/11/038 天前
3.10.62026/11/038 天前
3.10.52026/11/038 天前

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

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

Stateful emulator of the Daily.co REST API: rooms, presence, eject, meeting tokens (minted and self-signed HS256, verified and decodable), and transcription/recording webhooks with transcripts written to S3.

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

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

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

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

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

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

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

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

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

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

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

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