@strevokit/meeting-sdk 0.6.0 → 0.8.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 +28 -0
- package/README.md +45 -13
- package/dist/index.d.ts +20 -2
- package/dist/index.js +1210 -92
- package/dist/index.js.map +4 -4
- package/docs/api-reference.md +19 -1
- package/docs/backend-api.md +370 -0
- package/docs/examples/backend-integration.md +103 -0
- package/docs/examples/vanilla.html +1 -1
- package/package.json +1 -1
package/docs/api-reference.md
CHANGED
|
@@ -30,6 +30,8 @@ Throws synchronously:
|
|
|
30
30
|
| `realtimeUrl` | `string` | yes | Realtime endpoint base URL, `wss://host[:port]`. `ws://` is accepted only for `localhost` and `127.0.0.1`. No path, query, fragment, or credentials. |
|
|
31
31
|
| `joinTimeoutMs` | `number` | no | How long `join()` waits for admission before rejecting with `join_timeout`. Integer, 1000–60000. Default 15000. |
|
|
32
32
|
| `logger` | `MeetingLogger` | no | Diagnostics sink. Receives short messages and allow-listed fields only. |
|
|
33
|
+
| `getJoinToken` | `() => Promise<string>` | no | Called when automatic recovery needs a fresh join token and the in-memory token cannot be used. Resolve with a token your backend issued for the same participant; reject to end recovery. Kept in memory only and never logged. |
|
|
34
|
+
| `reconnectWindowMs` | `number` | no | Continuous unsuccessful recovery budget in milliseconds. Default `120000`; values outside `1000`–`600000` are clamped. |
|
|
33
35
|
|
|
34
36
|
### `MeetingLogger`
|
|
35
37
|
|
|
@@ -129,6 +131,12 @@ microphone) to `enableCamera()` or `unmute()`. Without one, the browser's defaul
|
|
|
129
131
|
| Muted, with another device kept open | `unmute({ deviceId })` | The chosen microphone is captured and announced; the previous one is released. |
|
|
130
132
|
| Unmuted with another device | `unmute({ deviceId })` | Switches in place, as for the camera. |
|
|
131
133
|
|
|
134
|
+
If the camera or an unmuted microphone ends on its own, and another device of that kind is listed, capture continues
|
|
135
|
+
on that device. `local_media_changed` fires with the kind still on and the new stream. The change is not announced as
|
|
136
|
+
stopped or started. When no other device can be opened, the kind turns off and is announced. A muted microphone that
|
|
137
|
+
ends is not replaced and is not unmuted. Calls made while the engine is `reconnecting` reject with `not_joined` and
|
|
138
|
+
do not capture.
|
|
139
|
+
|
|
132
140
|
If the chosen device is missing, the call rejects with `device_not_found`; if another app is using it, with
|
|
133
141
|
`device_unavailable`. Either way, the device already in use keeps running. A pending call is shared only by calls for
|
|
134
142
|
the same state **and** the same device. Device IDs are never logged. Read the device in use from the track:
|
|
@@ -137,7 +145,14 @@ the same state **and** the same device. Device IDs are never logged. Read the de
|
|
|
137
145
|
## States: `ConnectionState`
|
|
138
146
|
|
|
139
147
|
```ts
|
|
140
|
-
type ConnectionState =
|
|
148
|
+
type ConnectionState =
|
|
149
|
+
| "idle"
|
|
150
|
+
| "connecting"
|
|
151
|
+
| "joining"
|
|
152
|
+
| "joined"
|
|
153
|
+
| "reconnecting"
|
|
154
|
+
| "leaving"
|
|
155
|
+
| "closed";
|
|
141
156
|
```
|
|
142
157
|
|
|
143
158
|
| State | Meaning |
|
|
@@ -146,6 +161,7 @@ type ConnectionState = "idle" | "connecting" | "joining" | "joined" | "leaving"
|
|
|
146
161
|
| `connecting` | Connecting and presenting the join token. |
|
|
147
162
|
| `joining` | The token was accepted; waiting for the participant list. |
|
|
148
163
|
| `joined` | In the meeting. Media methods are available. |
|
|
164
|
+
| `reconnecting` | Previously joined; automatic recovery of the meeting-service connection is in progress. Success returns to `joined` (same self identity; `joined` is not re-emitted). |
|
|
149
165
|
| `leaving` | `leave()` was called; waiting for the platform to confirm. |
|
|
150
166
|
| `closed` | Finished. The engine cannot be reused; create a new one with a new token. |
|
|
151
167
|
|
|
@@ -272,6 +288,8 @@ it; otherwise `null`.
|
|
|
272
288
|
| `interrupted` | The connection dropped after working; it may recover on its own. |
|
|
273
289
|
| `failed` | The connection could not be set up or did not recover. An `error` event with `media_connection_failed` accompanies it. Rejoining starts a new attempt. |
|
|
274
290
|
|
|
291
|
+
While a peer link is alive, the SDK may periodically report aggregated media-health samples (RTT, jitter, loss, bitrates, and path/connectivity indicators) to the realtime service for operator diagnostics. Sampling failures never tear down media.
|
|
292
|
+
|
|
275
293
|
### `ParticipantLeftReason`
|
|
276
294
|
|
|
277
295
|
| Reason | Meaning |
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# Backend API for meeting clients
|
|
2
|
+
|
|
3
|
+
Your backend prepares each meeting with the Strevokit HTTP API, then mints a short-lived **join token** for the
|
|
4
|
+
browser. The meeting SDK only ever receives that token — never your API credential.
|
|
5
|
+
|
|
6
|
+
Base URL in the examples: `https://api.example.com` (replace with your Strevokit API host).
|
|
7
|
+
|
|
8
|
+
## Auth
|
|
9
|
+
|
|
10
|
+
Every route below requires:
|
|
11
|
+
|
|
12
|
+
```http
|
|
13
|
+
Authorization: Bearer <STREVOKIT_API_CREDENTIAL>
|
|
14
|
+
Content-Type: application/json
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Keep `STREVOKIT_API_CREDENTIAL` on the server only. Never put it in browser code, `VITE_*` / `NEXT_PUBLIC_*`
|
|
18
|
+
variables, or mobile apps.
|
|
19
|
+
|
|
20
|
+
Meeting and participant identifiers in paths and JSON are opaque `public_id` values (26-character Crockford ULIDs).
|
|
21
|
+
Responses never include numeric internal database ids.
|
|
22
|
+
|
|
23
|
+
## Route index
|
|
24
|
+
|
|
25
|
+
| Method | Path | Purpose |
|
|
26
|
+
|--------|------|---------|
|
|
27
|
+
| `POST` | `/v1/embed-origins` | Allow-list a browser origin that may join |
|
|
28
|
+
| `GET` | `/v1/embed-origins` | List allow-listed origins |
|
|
29
|
+
| `DELETE` | `/v1/embed-origins/{public_id}` | Remove an allow-listed origin |
|
|
30
|
+
| `POST` | `/v1/meetings` | Create a meeting |
|
|
31
|
+
| `GET` | `/v1/meetings/{public_id}` | Fetch a meeting |
|
|
32
|
+
| `POST` | `/v1/meetings/{public_id}/participants` | Authorize (register) a participant |
|
|
33
|
+
| `POST` | `/v1/meetings/{public_id}/join-token` | Mint a join token for one participant |
|
|
34
|
+
| `POST` | `/v1/meetings/{public_id}/end` | End the meeting for everyone |
|
|
35
|
+
|
|
36
|
+
## Integration sequence
|
|
37
|
+
|
|
38
|
+
1. **Allow-list your app origin** (`POST /v1/embed-origins`) once per environment (for example `https://app.example.com`).
|
|
39
|
+
Joins from other origins fail with `origin_not_allowed` at the realtime handshake.
|
|
40
|
+
2. **Create a meeting** (`POST /v1/meetings`) when your product starts a session. Store `data.public_id`.
|
|
41
|
+
3. **Authorize each person** (`POST /v1/meetings/{public_id}/participants`) as `host` or `participant`. Store each
|
|
42
|
+
`data.public_id` (the participant public id).
|
|
43
|
+
4. **Mint a join token** (`POST /v1/meetings/{public_id}/join-token`) when that user opens the meeting page. Return
|
|
44
|
+
only `data.token` to the browser.
|
|
45
|
+
5. **Join in the browser** with `createMeetingEngine({ token, realtimeUrl })` (see the [README](../README.md)).
|
|
46
|
+
|
|
47
|
+
Optional later: `GET /v1/meetings/{public_id}` to read status; `POST /v1/meetings/{public_id}/end` to end from your
|
|
48
|
+
backend.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## POST /v1/embed-origins
|
|
53
|
+
|
|
54
|
+
Register one browser origin that may open meetings.
|
|
55
|
+
|
|
56
|
+
```http
|
|
57
|
+
POST /v1/embed-origins
|
|
58
|
+
Authorization: Bearer <STREVOKIT_API_CREDENTIAL>
|
|
59
|
+
Content-Type: application/json
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{ "origin": "https://app.example.com" }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`origin` must be a full web origin (`http` or `https`, host, optional non-default port). No path, query, fragment, or
|
|
67
|
+
wildcards.
|
|
68
|
+
|
|
69
|
+
**Success:** `201` when newly inserted, `200` when the same origin already exists.
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"data": {
|
|
74
|
+
"public_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
|
|
75
|
+
"origin": "https://app.example.com",
|
|
76
|
+
"created_at": "2026-09-24T12:00:00.000000Z"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Also available: `GET /v1/embed-origins` (list) and `DELETE /v1/embed-origins/{public_id}` (`204` on success). An empty
|
|
82
|
+
allow-list means joins fail closed until you add at least one origin.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## POST /v1/meetings
|
|
87
|
+
|
|
88
|
+
Create a meeting.
|
|
89
|
+
|
|
90
|
+
```http
|
|
91
|
+
POST /v1/meetings
|
|
92
|
+
Authorization: Bearer <STREVOKIT_API_CREDENTIAL>
|
|
93
|
+
Content-Type: application/json
|
|
94
|
+
Idempotency-Key: <optional opaque string>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"type": "one_to_one",
|
|
100
|
+
"max_participants": 2,
|
|
101
|
+
"recording_requested": false,
|
|
102
|
+
"external_reference": "session_9876",
|
|
103
|
+
"empty_timeout_seconds": 300
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
| Field | Required | Notes |
|
|
108
|
+
|-------|----------|--------|
|
|
109
|
+
| `type` | yes | `one_to_one` or `one_to_many` |
|
|
110
|
+
| `max_participants` | yes | `2` for `one_to_one`; `3`–`50` for `one_to_many` |
|
|
111
|
+
| `recording_requested` | no | Default `false`. Alias `recording` is accepted |
|
|
112
|
+
| `scheduled_start` | no | ISO-8601 with timezone; stored as `scheduled_at` |
|
|
113
|
+
| `metadata` | no | Flat JSON object; keep it small |
|
|
114
|
+
| `external_reference` | no | Your own id, 1–255 characters when present; echoed unchanged |
|
|
115
|
+
| `empty_timeout_seconds` | no | `60`–`86400`; default `300`. Idle empty meetings end after this many seconds |
|
|
116
|
+
|
|
117
|
+
Do not send `tenant_id`, numeric `id`, or a client-chosen media mode — the platform selects delivery.
|
|
118
|
+
|
|
119
|
+
Optional `Idempotency-Key` (≤255 characters): same key + same body replays the first response; same key + different
|
|
120
|
+
body returns `409` `idempotency_conflict`.
|
|
121
|
+
|
|
122
|
+
**Success:** `201 Created` (idempotent replay may return the original status).
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"data": {
|
|
127
|
+
"public_id": "01JB2Q8ABCDEF01234567890AB",
|
|
128
|
+
"external_reference": "session_9876",
|
|
129
|
+
"type": "one_to_one",
|
|
130
|
+
"max_participants": 2,
|
|
131
|
+
"recording_requested": false,
|
|
132
|
+
"recording_allowed": false,
|
|
133
|
+
"status": "created",
|
|
134
|
+
"scheduled_at": null,
|
|
135
|
+
"started_at": null,
|
|
136
|
+
"ended_at": null,
|
|
137
|
+
"end_reason": null,
|
|
138
|
+
"empty_timeout_seconds": 300,
|
|
139
|
+
"metadata": null,
|
|
140
|
+
"created_at": "2026-09-22T12:00:00+00:00",
|
|
141
|
+
"updated_at": "2026-09-22T12:00:00+00:00"
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The response may also include platform-selected fields (for example how media is delivered). Store `public_id` for
|
|
147
|
+
later calls.
|
|
148
|
+
|
|
149
|
+
| Case | HTTP | `error.code` |
|
|
150
|
+
|------|------|--------------|
|
|
151
|
+
| Missing or invalid credential | 401 | `unauthorized` |
|
|
152
|
+
| Bad or missing fields | 422 | `validation_failed` |
|
|
153
|
+
| Idempotency key reused with a different body | 409 | `idempotency_conflict` |
|
|
154
|
+
| Rate limited | 429 | `rate_limited` (+ `Retry-After`) |
|
|
155
|
+
| Suspended tenant | 403 | `forbidden` |
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## GET /v1/meetings/{public_id}
|
|
160
|
+
|
|
161
|
+
Fetch the meeting resource for your tenant (same shape as create). Missing or other-tenant ids return `404`
|
|
162
|
+
`meeting_not_found`.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## POST /v1/meetings/{public_id}/participants
|
|
167
|
+
|
|
168
|
+
Authorize (register) a participant on a meeting. Call once per person who may join.
|
|
169
|
+
|
|
170
|
+
```http
|
|
171
|
+
POST /v1/meetings/{public_id}/participants
|
|
172
|
+
Authorization: Bearer <STREVOKIT_API_CREDENTIAL>
|
|
173
|
+
Content-Type: application/json
|
|
174
|
+
Idempotency-Key: <optional opaque string>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"external_user_id": "user_42",
|
|
180
|
+
"display_name": "Alex",
|
|
181
|
+
"role": "host"
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
| Field | Required | Notes |
|
|
186
|
+
|-------|----------|--------|
|
|
187
|
+
| `external_user_id` | yes | Your user id within this meeting (1–255 chars; not whitespace-only) |
|
|
188
|
+
| `display_name` | yes | Shown name (1–255 chars; not whitespace-only) |
|
|
189
|
+
| `role` | yes | `host` or `participant` |
|
|
190
|
+
|
|
191
|
+
**Upsert:** the same `external_user_id` on the same meeting returns the same participant `public_id`, updates name and
|
|
192
|
+
role, and does not consume an extra capacity slot. A **new** identity is rejected with `422`
|
|
193
|
+
`participant_limit_exceeded` when the meeting already has `max_participants` distinct people.
|
|
194
|
+
|
|
195
|
+
**Display names:** Pass the name users should see in the roster as `display_name` when you authorize. The join-token
|
|
196
|
+
route and the browser (including this SDK) do not accept a name. If no usable name is available at join, the UI shows
|
|
197
|
+
**Guest**. To rename someone, re-authorize with the same `external_user_id` and a new `display_name`, then mint a new
|
|
198
|
+
join token and have them join again.
|
|
199
|
+
|
|
200
|
+
Optional `Idempotency-Key` behaves like create (same key + different body → `409` `idempotency_conflict`).
|
|
201
|
+
|
|
202
|
+
**Success:** `201` on first insert, `200` on upsert of an existing identity (idempotent replay returns the stored
|
|
203
|
+
status and body).
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"data": {
|
|
208
|
+
"public_id": "01JQ4ABCDEF01234567890ABCD",
|
|
209
|
+
"external_user_id": "user_42",
|
|
210
|
+
"display_name": "Alex",
|
|
211
|
+
"role": "host",
|
|
212
|
+
"status": "invited"
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
| Case | HTTP | `error.code` |
|
|
218
|
+
|------|------|--------------|
|
|
219
|
+
| Missing or invalid credential | 401 | `unauthorized` |
|
|
220
|
+
| Bad or missing fields | 422 | `validation_failed` |
|
|
221
|
+
| Meeting missing or other tenant | 404 | `meeting_not_found` |
|
|
222
|
+
| Meeting already `ended` | 409 | `meeting_ended` |
|
|
223
|
+
| Meeting `cancelled` / `failed` | 403 | `forbidden` |
|
|
224
|
+
| New identity over capacity | 422 | `participant_limit_exceeded` |
|
|
225
|
+
| Idempotency conflict | 409 | `idempotency_conflict` |
|
|
226
|
+
| Rate limited | 429 | `rate_limited` (+ `Retry-After`) |
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## POST /v1/meetings/{public_id}/join-token
|
|
231
|
+
|
|
232
|
+
Mint a short-lived join token for one authorized participant. Mint a **new** token for every join (including rejoin
|
|
233
|
+
after leave). Do **not** send `Idempotency-Key` — each successful call issues a new token.
|
|
234
|
+
|
|
235
|
+
```http
|
|
236
|
+
POST /v1/meetings/{public_id}/join-token
|
|
237
|
+
Authorization: Bearer <STREVOKIT_API_CREDENTIAL>
|
|
238
|
+
Content-Type: application/json
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{ "participant_public_id": "01JQ4ABCDEF01234567890ABCD" }
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
| Field | Required | Notes |
|
|
246
|
+
|-------|----------|--------|
|
|
247
|
+
| `participant_public_id` | yes | From authorize; must belong to this meeting |
|
|
248
|
+
|
|
249
|
+
Do not send `role`, `display_name`, `tenant_id`, or numeric `id`. Role and display name come from the authorized
|
|
250
|
+
participant row.
|
|
251
|
+
|
|
252
|
+
**Success:** `201 Created`
|
|
253
|
+
|
|
254
|
+
```json
|
|
255
|
+
{
|
|
256
|
+
"data": {
|
|
257
|
+
"token": "<compact JWT>",
|
|
258
|
+
"expires_at": "2026-09-24T15:05:00Z",
|
|
259
|
+
"expires_in": 300
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Default TTL is 300 seconds. Treat `token` like a secret: do not log it, put it in URLs, or cache it (`Cache-Control:
|
|
265
|
+
no-store` on your app response). Return only the token string to the browser.
|
|
266
|
+
|
|
267
|
+
| Case | HTTP | `error.code` |
|
|
268
|
+
|------|------|--------------|
|
|
269
|
+
| Missing or invalid credential | 401 | `unauthorized` |
|
|
270
|
+
| Bad or missing `participant_public_id` | 422 | `validation_failed` |
|
|
271
|
+
| Meeting missing or other tenant | 404 | `meeting_not_found` |
|
|
272
|
+
| Participant missing or not on this meeting | 404 | `participant_not_found` |
|
|
273
|
+
| Meeting already `ended` | 409 | `meeting_ended` |
|
|
274
|
+
| Meeting `cancelled` / `failed` | 403 | `forbidden` |
|
|
275
|
+
| Join-token rate limited | 429 | `rate_limited` (+ `Retry-After`) |
|
|
276
|
+
|
|
277
|
+
Realtime handshake failures (after you hand the token to the SDK) include `invalid_join_token`, `join_token_expired`,
|
|
278
|
+
and `origin_not_allowed`.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## POST /v1/meetings/{public_id}/end
|
|
283
|
+
|
|
284
|
+
End the meeting for everyone from your backend. Optional `Idempotency-Key`. Success returns the updated meeting
|
|
285
|
+
resource with `status` `ended` and `end_reason` `tenant_api`.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Errors
|
|
290
|
+
|
|
291
|
+
Error bodies use a stable `error.code` string (and a message). On `429`, honor `Retry-After`. Prefer mapping known
|
|
292
|
+
codes in your backend rather than parsing message text.
|
|
293
|
+
|
|
294
|
+
## End-to-end example (Node.js)
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
const STREVOKIT_API = "https://api.example.com";
|
|
298
|
+
const API_CREDENTIAL = process.env.STREVOKIT_API_CREDENTIAL!;
|
|
299
|
+
|
|
300
|
+
async function strevokit(path: string, init: RequestInit = {}) {
|
|
301
|
+
const response = await fetch(`${STREVOKIT_API}${path}`, {
|
|
302
|
+
...init,
|
|
303
|
+
headers: {
|
|
304
|
+
Authorization: `Bearer ${API_CREDENTIAL}`,
|
|
305
|
+
"Content-Type": "application/json",
|
|
306
|
+
...(init.headers ?? {}),
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
if (!response.ok) throw new Error(`Strevokit ${path} → HTTP ${response.status}`);
|
|
310
|
+
if (response.status === 204) return null;
|
|
311
|
+
return response.json();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Once per environment:
|
|
315
|
+
await strevokit("/v1/embed-origins", {
|
|
316
|
+
method: "POST",
|
|
317
|
+
body: JSON.stringify({ origin: "https://app.example.com" }),
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// When your product starts a session:
|
|
321
|
+
const { data: meeting } = await strevokit("/v1/meetings", {
|
|
322
|
+
method: "POST",
|
|
323
|
+
headers: { "Idempotency-Key": crypto.randomUUID() },
|
|
324
|
+
body: JSON.stringify({
|
|
325
|
+
type: "one_to_one",
|
|
326
|
+
max_participants: 2,
|
|
327
|
+
external_reference: "booking_123",
|
|
328
|
+
}),
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const { data: host } = await strevokit(`/v1/meetings/${meeting.public_id}/participants`, {
|
|
332
|
+
method: "POST",
|
|
333
|
+
body: JSON.stringify({
|
|
334
|
+
external_user_id: "user_host",
|
|
335
|
+
display_name: "Alex",
|
|
336
|
+
role: "host",
|
|
337
|
+
}),
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const { data: guest } = await strevokit(`/v1/meetings/${meeting.public_id}/participants`, {
|
|
341
|
+
method: "POST",
|
|
342
|
+
body: JSON.stringify({
|
|
343
|
+
external_user_id: "user_guest",
|
|
344
|
+
display_name: "Sam",
|
|
345
|
+
role: "participant",
|
|
346
|
+
}),
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// When that signed-in user opens the meeting page:
|
|
350
|
+
const { data: issued } = await strevokit(`/v1/meetings/${meeting.public_id}/join-token`, {
|
|
351
|
+
method: "POST",
|
|
352
|
+
body: JSON.stringify({ participant_public_id: host.public_id }),
|
|
353
|
+
});
|
|
354
|
+
// Return issued.token to the browser only.
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Hand the token to the SDK
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
import { createMeetingEngine } from "@strevokit/meeting-sdk";
|
|
361
|
+
|
|
362
|
+
const engine = createMeetingEngine({
|
|
363
|
+
token, // from your backend
|
|
364
|
+
realtimeUrl: "wss://realtime.example.com",
|
|
365
|
+
});
|
|
366
|
+
await engine.join();
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
A longer Express-style proxy that creates, authorizes, and mints is in
|
|
370
|
+
[examples/backend-integration.md](./examples/backend-integration.md).
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Backend integration (create, authorize, join token)
|
|
2
|
+
|
|
3
|
+
The meeting SDK needs a join token for each participant. Tokens are short-lived and single-use. Only your backend can
|
|
4
|
+
mint them, because minting needs your Strevokit API credential.
|
|
5
|
+
|
|
6
|
+
Before minting, your backend usually **creates** the meeting and **authorizes** participants. The full HTTP reference
|
|
7
|
+
(methods, paths, bodies, errors) is in [Backend API](../backend-api.md).
|
|
8
|
+
|
|
9
|
+
**Keep the API credential on your server.** Never ship it in browser code, environment variables exposed to the
|
|
10
|
+
browser (such as `VITE_*` or `NEXT_PUBLIC_*`), or mobile apps. The browser only ever receives the join token.
|
|
11
|
+
|
|
12
|
+
## Flow
|
|
13
|
+
|
|
14
|
+
1. (Once) Allow-list the origin that hosts your meeting page (`POST /v1/embed-origins`).
|
|
15
|
+
2. When your product starts a session, create a meeting (`POST /v1/meetings`) and authorize each person
|
|
16
|
+
(`POST /v1/meetings/{public_id}/participants`).
|
|
17
|
+
3. The signed-in user opens a meeting page in your app.
|
|
18
|
+
4. The page asks your backend for a join token.
|
|
19
|
+
5. Your backend checks that this user may join, then calls `POST /v1/meetings/{public_id}/join-token`.
|
|
20
|
+
6. Your backend returns only the token to the page, which passes it to `createMeetingEngine({ token, realtimeUrl })`.
|
|
21
|
+
|
|
22
|
+
## Example (Node.js with Express)
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import express from "express";
|
|
26
|
+
|
|
27
|
+
const app = express();
|
|
28
|
+
app.use(express.json());
|
|
29
|
+
|
|
30
|
+
const STREVOKIT_API = "https://api.example.com"; // your Strevokit API base URL
|
|
31
|
+
const API_CREDENTIAL = process.env.STREVOKIT_API_CREDENTIAL; // server-side secret, never sent to browsers
|
|
32
|
+
|
|
33
|
+
const strevokitHeaders = {
|
|
34
|
+
Authorization: `Bearer ${API_CREDENTIAL}`,
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Once per environment (run from a setup script or admin path):
|
|
39
|
+
// await fetch(`${STREVOKIT_API}/v1/embed-origins`, {
|
|
40
|
+
// method: "POST",
|
|
41
|
+
// headers: strevokitHeaders,
|
|
42
|
+
// body: JSON.stringify({ origin: "https://app.example.com" }),
|
|
43
|
+
// });
|
|
44
|
+
|
|
45
|
+
app.post("/api/meetings", requireSignedInUser, async (req, res) => {
|
|
46
|
+
const response = await fetch(`${STREVOKIT_API}/v1/meetings`, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: { ...strevokitHeaders, "Idempotency-Key": crypto.randomUUID() },
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
type: "one_to_one",
|
|
51
|
+
max_participants: 2,
|
|
52
|
+
external_reference: req.body.bookingId,
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) return res.sendStatus(502);
|
|
56
|
+
const { data: meeting } = await response.json();
|
|
57
|
+
|
|
58
|
+
const hostResponse = await fetch(`${STREVOKIT_API}/v1/meetings/${meeting.public_id}/participants`, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: strevokitHeaders,
|
|
61
|
+
body: JSON.stringify({
|
|
62
|
+
external_user_id: String(req.user.id),
|
|
63
|
+
display_name: req.user.displayName,
|
|
64
|
+
role: "host",
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
if (!hostResponse.ok) return res.sendStatus(502);
|
|
68
|
+
const { data: host } = await hostResponse.json();
|
|
69
|
+
|
|
70
|
+
// Persist meeting.public_id and host.public_id in your own database, keyed by your booking / room.
|
|
71
|
+
res.status(201).json({ meetingPublicId: meeting.public_id, participantPublicId: host.public_id });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
app.post("/api/meetings/:meetingId/join-token", requireSignedInUser, async (req, res) => {
|
|
75
|
+
// Your own authorization: may this user join this meeting, and as which Strevokit participant?
|
|
76
|
+
const participantPublicId = await participantFor(req.user, req.params.meetingId);
|
|
77
|
+
if (!participantPublicId) return res.sendStatus(403);
|
|
78
|
+
|
|
79
|
+
const response = await fetch(
|
|
80
|
+
`${STREVOKIT_API}/v1/meetings/${encodeURIComponent(req.params.meetingId)}/join-token`,
|
|
81
|
+
{
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: strevokitHeaders,
|
|
84
|
+
body: JSON.stringify({ participant_public_id: participantPublicId }),
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
if (!response.ok) return res.sendStatus(502);
|
|
88
|
+
|
|
89
|
+
const { data } = await response.json(); // { token, expires_at, expires_in }
|
|
90
|
+
res.set("Cache-Control", "no-store").json({ token: data.token });
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`requireSignedInUser` and `participantFor` stand for your own session and authorization code.
|
|
95
|
+
|
|
96
|
+
## Notes
|
|
97
|
+
|
|
98
|
+
- Create and authorize before the first join-token call; join-token needs an existing participant `public_id`.
|
|
99
|
+
- Pass a real `display_name` on authorize; join-token cannot set or change it. If the UI shows **Guest**, authorize
|
|
100
|
+
used a missing or placeholder name, or that step was skipped.
|
|
101
|
+
- Mint a new token for every join, including rejoining after `leave()` / `dispose()`.
|
|
102
|
+
- Send the join-token response with `Cache-Control: no-store` so the token isn't cached.
|
|
103
|
+
- Don't log the token or put it in a URL. The SDK keeps it in memory only.
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// With a bundler, import from "@strevokit/meeting-sdk" instead.
|
|
21
21
|
import { createMeetingEngine, MeetingError } from "./node_modules/@strevokit/meeting-sdk/dist/index.js";
|
|
22
22
|
|
|
23
|
-
// Ask your backend for a join token
|
|
23
|
+
// Ask your backend for a join token (see backend-integration.md and ../backend-api.md).
|
|
24
24
|
const { token } = await fetch("/my-backend/join-token", { method: "POST" }).then((r) => r.json());
|
|
25
25
|
|
|
26
26
|
const engine = createMeetingEngine({ token, realtimeUrl: "wss://realtime.example.com" });
|