@crvouga/mockingbird-service-daily 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +159 -0
- package/dist/chunk-5XQDEARP.js +3340 -0
- package/dist/chunk-5XQDEARP.js.map +7 -0
- package/dist/chunk-COEOIBLF.js +421 -0
- package/dist/chunk-COEOIBLF.js.map +7 -0
- package/dist/cli.js +19 -0
- package/dist/cli.js.map +7 -0
- package/dist/index.d.ts +1072 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +7 -0
- package/dist/server.d.ts +1346 -0
- package/dist/server.js +12 -0
- package/dist/server.js.map +7 -0
- package/package.json +90 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# @crvouga/mockingbird-service-daily
|
|
2
|
+
|
|
3
|
+
Stateful mock of the **Daily.co** REST API for test suites: rooms (create, get, update,
|
|
4
|
+
delete, presence, eject), meeting tokens (mint and validate), verification of the HS256 meeting
|
|
5
|
+
tokens our backend signs itself, and the end-of-call webhooks (`transcription.stopped`,
|
|
6
|
+
`recording.ready-to-download`) with the transcript written to the stack's S3. Every EMR
|
|
7
|
+
booking creates a room and a token; today a broken Daily integration is silent because booking
|
|
8
|
+
swallows the error. Against the mock it is observable and assertable.
|
|
9
|
+
|
|
10
|
+
- Operation coverage: [SUPPORT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/daily/SUPPORT.md)
|
|
11
|
+
- The contract (`openapi.yaml`) is trimmed from Daily's documented REST API to the calls our
|
|
12
|
+
backend and EMR make, with the fields they send.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -D @crvouga/mockingbird-service-daily
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
ESM only. Node >= 22 or Bun >= 1.2. No native dependencies. Serve it with
|
|
21
|
+
`npx mockingbird-daily serve`, `createServer` from `./server` (Node), or `createRuntime` with
|
|
22
|
+
any Fetch server.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
Point the apps at the mock (the G-D1 seams):
|
|
27
|
+
|
|
28
|
+
| App | Env | Value |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| backend | `DAILY_API_BASE_URL` | `http://127.0.0.1:8800/v1` (needs the http-loopback exception) |
|
|
31
|
+
| backend | `DAILY_API_KEY`, `DAILY_API_DOMAIN_ID` | any key (or `--api-key`), and the same value as `--domain-id` |
|
|
32
|
+
| EMR backend | `DailyService.baseUrl` | `http://127.0.0.1:8800/v1` (hardcoded today) |
|
|
33
|
+
| EMR backend | `DEFAULT_DAILY_BASE_URL` | the same value as `--room-url-base` |
|
|
34
|
+
| EMR backend | `DAILY_WEBHOOK_SECRET` | the same value as `--webhook-secret` |
|
|
35
|
+
| member-app | `EXPO_PUBLIC_DAILY_BASE_URL` | the same value as `--room-url-base` |
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx mockingbird-daily serve --port 8800 \
|
|
39
|
+
--room-url-base https://geviti-mock.daily.test/ \
|
|
40
|
+
--domain-id "$DAILY_API_DOMAIN_ID" \
|
|
41
|
+
--webhook-url http://127.0.0.1:4000/v1/webhooks/daily \
|
|
42
|
+
--webhook-secret "$DAILY_WEBHOOK_SECRET" \
|
|
43
|
+
--s3-endpoint http://127.0.0.1:4569 --s3-bucket "$S3_BUCKET_NAME"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { createRuntime } from "@crvouga/mockingbird-service-daily"
|
|
48
|
+
|
|
49
|
+
const daily = createRuntime({
|
|
50
|
+
settings: { roomUrlBase: "https://geviti-mock.daily.test/" },
|
|
51
|
+
webhooks: { url: "http://127.0.0.1:4000/v1/webhooks/daily", secret: "whsec-daily" },
|
|
52
|
+
transcripts: { endpoint: "http://127.0.0.1:4569", bucket: "emr-transcripts" },
|
|
53
|
+
})
|
|
54
|
+
const admin = (path: string, body: unknown) =>
|
|
55
|
+
daily.fetch(
|
|
56
|
+
new Request(`http://daily.test/__admin${path}`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { "content-type": "application/json" },
|
|
59
|
+
body: JSON.stringify(body),
|
|
60
|
+
}),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// …the EMR books an appointment: POST /v1/rooms + POST /v1/meeting-tokens…
|
|
64
|
+
|
|
65
|
+
// End the call: writes <room>/<session>.json to S3, then fires transcription.stopped.
|
|
66
|
+
await admin("/rooms/<room name>/session", {
|
|
67
|
+
participants: [{ userId: "prac-1" }, { userId: "pat-1" }],
|
|
68
|
+
durationSec: 1200,
|
|
69
|
+
transcript: [{ s: "prac-1", t: "How are you feeling?", ts: 0.5, te: 2.1 }],
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Routes
|
|
74
|
+
|
|
75
|
+
| Route | Behaviour |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| `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. |
|
|
78
|
+
| `GET /v1/rooms/:name` | The room, or 404 `{error: "not-found", info: "room <name> not found"}` (the EMR branches on `message.includes('404')`). |
|
|
79
|
+
| `POST /v1/rooms/:name` | Merge `properties` (and `privacy`) into the room; 404 when missing. |
|
|
80
|
+
| `DELETE /v1/rooms/:name` | `{deleted: true, name}`; 404 when missing (the EMR tolerates it). |
|
|
81
|
+
| `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`. |
|
|
82
|
+
| `POST /v1/rooms/:name/eject` | `{user_ids?, ids?}` → `{ejectedIds}` (participant session ids), removing them from presence. |
|
|
83
|
+
| `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`. |
|
|
84
|
+
| `GET /v1/meeting-tokens/:token` | Verifies a token (minted by the mock **or self-signed by our backend**) with the caller's API key and its `nbf`/`exp` on the mock clock (`?ignoreNbf=true` skips nbf); answers its properties under full names, else 400. |
|
|
85
|
+
|
|
86
|
+
**Auth.** `Authorization: Bearer <DAILY_API_KEY>`; any key unless `apiKeys` is set. Missing →
|
|
87
|
+
401 `{error: "authentication-error"}`.
|
|
88
|
+
|
|
89
|
+
**Milliseconds.** Some of our callers pass `nbf`/`exp` in milliseconds (e.g. the EMR's
|
|
90
|
+
`generatePatientToken` passes `Date.parse(end)`). The mock accepts them, interprets values
|
|
91
|
+
≥ 1e11 as ms in time checks, records each in `GET /__admin/warnings`, and tags the journal entry
|
|
92
|
+
(`ids.warning: "exp in milliseconds"`).
|
|
93
|
+
|
|
94
|
+
### Webhooks
|
|
95
|
+
|
|
96
|
+
Admin sessions post Daily-shaped events: `{version: "1.0.0", type, event, id, event_ts,
|
|
97
|
+
payload}` (our receiver reads `event`; Daily documents `type`; both are sent).
|
|
98
|
+
|
|
99
|
+
- `transcription.stopped` — `payload: {room_name, session_id, duration, s3_key, instance_id}`.
|
|
100
|
+
- `recording.ready-to-download` — `payload: {type: "cloud", recording_id, room_name, session_id,
|
|
101
|
+
start_ts, status: "finished", max_participants, duration, s3_key}` (our EMR just acks it).
|
|
102
|
+
|
|
103
|
+
**Signature — our scheme, not Daily's.** `x-webhook-signature` = **hex**
|
|
104
|
+
HMAC-SHA256(`DAILY_WEBHOOK_SECRET`, raw body), which is what our EMR verifies (only when the
|
|
105
|
+
secret is set). Daily's documented scheme is different — base64 HMAC-SHA256 over
|
|
106
|
+
`"<X-Webhook-Timestamp>.<body>"` with the base64-decoded secret — so a real Daily webhook would
|
|
107
|
+
fail our check; the mock signs the way our code checks. `x-webhook-timestamp` is sent too.
|
|
108
|
+
Retries: immediately, 5 s, 5 min, 30 min, 2 h; `GET /__admin/webhooks`, `…/events`,
|
|
109
|
+
`…/replay`, `…/flush` as usual.
|
|
110
|
+
|
|
111
|
+
### Admin (beyond the standard contract)
|
|
112
|
+
|
|
113
|
+
| Route | Effect |
|
|
114
|
+
| --- | --- |
|
|
115
|
+
| `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. |
|
|
116
|
+
| `PUT /__admin/rooms/:name/presence` | `{participants: [{userId, userName?, joinedAt?}]}` — who `GET …/presence` reports. |
|
|
117
|
+
| `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 mock clock (owners may enter before the room's `nbf`). |
|
|
118
|
+
| `GET /__admin/rooms`, `GET /__admin/rooms/:name` | Rooms with config, presence and session metadata. |
|
|
119
|
+
| `GET /__admin/warnings` | Tolerated oddities (millisecond timestamps, tokens for rooms that do not exist). |
|
|
120
|
+
| `PUT /__admin/settings` | `{apiKeys?, domainId?, roomUrlBase?}` for the calling namespace (`GET` masks keys). |
|
|
121
|
+
|
|
122
|
+
Time rules run on the mock clock (`POST /__admin/clock`): token/room `nbf` and `exp`, and
|
|
123
|
+
therefore the backend's 13 h room-creation delay and 30-min guardrail window as seen by Daily.
|
|
124
|
+
|
|
125
|
+
Fault presets (`POST /__admin/faults {"preset": "<name>", "count"?: n}`): `room_not_found`
|
|
126
|
+
(404 on `/v1/rooms/:name…`), `unauthorized` (401), `rate_limited` (429), `server_error` (500),
|
|
127
|
+
`webhook_duplicate`, `webhook_reorder`, `webhook_drop`.
|
|
128
|
+
|
|
129
|
+
### Namespaces
|
|
130
|
+
|
|
131
|
+
`x-mockingbird-namespace`, a `/ns/<name>` prefix on the base URL, or by API key:
|
|
132
|
+
`PUT /__admin/credentials {"credentials": {"<DAILY_API_KEY>": "<namespace>"}}`.
|
|
133
|
+
|
|
134
|
+
### Deliberately not modelled
|
|
135
|
+
|
|
136
|
+
- The media plane: SFU, WebRTC, knocking/admission, recording and transcription themselves.
|
|
137
|
+
daily-js loads Daily's CDN bundle; member-app UI tests should inject a fake `DailyCallLike`
|
|
138
|
+
through `useCallProviderLogic(createCallObject)`.
|
|
139
|
+
- The room page at `https://<domain>.daily.co/<room>?t=` (the URL is produced, not served).
|
|
140
|
+
- Daily's own webhook signature scheme and webhook registration API (see above).
|
|
141
|
+
- Room listing, recordings/transcripts REST APIs, dial-out, streaming.
|
|
142
|
+
|
|
143
|
+
## API
|
|
144
|
+
|
|
145
|
+
| Export | Kind | Description |
|
|
146
|
+
| --- | --- | --- |
|
|
147
|
+
| `DailyAPI` | class | The in-process mock: `fetch(request)`, `reset()`, `inspectToken(token, keys?)`, `setPresence(name, participants)`, `endSession(name, input)`, `rooms()`. Options: `sqlite`, `now`, `namespace`, `settings`, `onWebhook`, `transcripts`. |
|
|
148
|
+
| `createRuntime` | function | The mock with the full service contract. Options: `webhooks: {url, secret, retryDelaysMs?, fetch?}`, `transcripts: {endpoint, bucket, region?, accessKeyId?, secretAccessKey?, keyPattern?, fetch?}`, `settings`, `clock`, `seed`, `adminKey`, `onLog`. |
|
|
149
|
+
| `DAILY_PRESETS` | object | Every named fault preset. |
|
|
150
|
+
| `DAILY_NAMESPACE` | string | The service name, `"daily"`. |
|
|
151
|
+
| `WEBHOOK_PATH` | string | `"/v1/webhooks/daily"`, our EMR receiver's route. |
|
|
152
|
+
| `dailyWebhookSigner` | function | The `x-webhook-signature` signer (hex HMAC-SHA256 of the raw body). |
|
|
153
|
+
| `signToken`, `decodeToken`, `verifySignature` | functions | HS256 meeting-token helpers. |
|
|
154
|
+
| `claimsToProperties`, `propertiesToClaims`, `CLAIM_NAMES` | values | Daily's abbreviated claim names ↔ full property names. |
|
|
155
|
+
| `synthesizeTranscript` | function | The default transcript for a session with none given. |
|
|
156
|
+
| `document`, `operationIds`, `supportedOperationIds` | values | The vendored OpenAPI contract and its operation ids. |
|
|
157
|
+
| `createServer`, `serveTarget`, `DEFAULT_PORT` (`./server`) | Node | Serve over `node:http`; the `serve` CLI target; port 8800. |
|
|
158
|
+
|
|
159
|
+
Part of [mockingbird](https://github.com/crvouga/mockingbird).
|