@robono/calling 0.4.0 → 0.5.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 CHANGED
@@ -1,4 +1,8 @@
1
- ## 0.4.0 — unreleased
1
+ ## 0.5.0 — release candidate
2
+
3
+ Add on-demand numeric connection diagnostics for direct and group calls without audio recording or raw network identifiers.
4
+
5
+ ## 0.4.0 — 2026-11-04
2
6
 
3
7
  Add the authenticated Messenger transport with server-held call controls and a separate hint-only events credential. Disabled calling creates no session or media resources.
4
8
 
package/README.md CHANGED
@@ -2,10 +2,22 @@
2
2
 
3
3
  Add voice and video calls within your app or between people connected across Robono Bridge networks, with optional group calls and screen sharing. The SDK manages WebRTC negotiation, direct connections, relay fallback, call state and microphone cleanup. Your app keeps its identity and platform notification integration. Use your own interface, or choose the separate optional mobile screen package. The calling engine never requires a Robono interface.
4
4
 
5
- ## Install
5
+ See [how calling connects](https://robono.com/guides/calling-architecture.html) for
6
+ hosted responsibilities, direct/relay paths, phone credentials and the supported
7
+ calling boundaries. Live-call recording/captions and large conferences are not
8
+ implied by voice-message transcription or messaging supergroups.
9
+
10
+ ## Start here
11
+
12
+ For calls in the supplied Messenger, follow the [hosted setup guide](https://robono.com/guides/hosted-messenger.html). Select voice and/or video in **Features & preview**, then run the Robono installer in your app. The supplied Messenger connects calling automatically. Native apps also need your Apple/Google notification credentials and a native rebuild, as described in the generated setup guide.
13
+
14
+ The reference below is for custom calling interfaces and advanced connections. You do not need to implement admission, signaling or call controllers yourself when using the supplied Messenger.
15
+
16
+ ## Advanced: install the calling engine
17
+
6
18
 
7
19
  ```sh
8
- npm install @robono/calling@0.1.2
20
+ npm install @robono/calling
9
21
  ```
10
22
 
11
23
  This is a versioned npm-compatible package. Commit your package lockfile to retain its integrity checksum. The release includes `SHA256SUMS` and the package includes an SPDX software bill of materials.
@@ -16,7 +28,7 @@ This is a versioned npm-compatible package. Commit your package lockfile to reta
16
28
 
17
29
  ## Connect your backend
18
30
 
19
- Open Calling in your Robono developer workspace, enable calling for the integration, and copy its admission and service URLs. Your server authenticates the user, then posts the following to that URL using a network API key with `calling:connect` scope:
31
+ Select calling in **Features & preview**. For a custom connection, copy the integration’s admission and service URLs from its calling setup. Your server authenticates the user, then posts the following to that URL using a network API key with `calling:connect` scope:
20
32
 
21
33
  ```json
22
34
  {"operation":"admit","kind":"network","protocolVersion":1,"externalUserId":"YOUR_AUTHENTICATED_USER_ID"}
@@ -164,3 +176,7 @@ Use `@robono/calling@0.1.3` with `@robono/calling-react-native@0.2.2` when using
164
176
  `createHttpTransport` supplies the event subscription automatically. If wrapping it, preserve `request.subscribe = transport.subscribe`. A custom request without `subscribe`, or `{events:false}`, retains the compatibility polling interval (`pollMs`, default 1000 ms).
165
177
 
166
178
  The 0.1.2 engine had polling and retry backoff but did not include this event transport. No native permissions or rebuild are added by this JavaScript patch to an app already using the native calling package.
179
+
180
+ ## Connection details
181
+
182
+ Direct and group clients expose `getDiagnostics()` for local connection measurements without addresses, media or credentials. The supplied call overlays include an optional Connection details panel; custom interfaces can use the same measurements. [Guide, fields and customization](https://robono.com/guides/call-diagnostics.html). Missing measurements remain unknown, and the first sample cannot calculate interval rates.
package/SBOM.spdx.json CHANGED
@@ -2,43 +2,454 @@
2
2
  "spdxVersion": "SPDX-2.3",
3
3
  "dataLicense": "CC0-1.0",
4
4
  "SPDXID": "SPDXRef-DOCUMENT",
5
- "name": "@robono/calling@0.4.0",
6
- "documentNamespace": "http://spdx.org/spdxdocs/@robono%2fcalling-0.4.0-***",
5
+ "name": "@robono/calling@0.5.0",
6
+ "documentNamespace": "https://robono.com/sbom/calling/0.5.0/538ac95caa705ca94a9cea02897f2d1441d7d77e",
7
7
  "creationInfo": {
8
- "created": "2026-11-04T11:34:49.725Z",
8
+ "created": "2026-11-05T10:33:05.150Z",
9
9
  "creators": [
10
- "Tool: npm/cli-11.6.2"
10
+ "Organization: Robono (support@robono.com)"
11
11
  ]
12
12
  },
13
- "documentDescribes": [
14
- "SPDXRef-Package-robono.calling-0.4.0"
15
- ],
16
13
  "packages": [
17
14
  {
18
15
  "name": "@robono/calling",
19
- "SPDXID": "SPDXRef-Package-robono.calling-0.4.0",
20
- "versionInfo": "0.4.0",
21
- "packageFileName": "",
22
- "description": "WebRTC voice, video, group calling and screen sharing within apps and across Robono Bridge networks.",
23
- "primaryPackagePurpose": "LIBRARY",
16
+ "SPDXID": "SPDXRef-Package",
17
+ "versionInfo": "0.5.0",
24
18
  "downloadLocation": "NOASSERTION",
25
- "filesAnalyzed": false,
26
- "homepage": "https://robono.com/docs#calling",
27
- "licenseDeclared": "SEE LICENSE IN LICENSE",
28
- "externalRefs": [
19
+ "filesAnalyzed": true,
20
+ "packageVerificationCode": {
21
+ "packageVerificationCodeValue": "538ac95caa705ca94a9cea02897f2d1441d7d77e"
22
+ },
23
+ "licenseConcluded": "LicenseRef-RobonoSDK",
24
+ "licenseDeclared": "LicenseRef-RobonoSDK",
25
+ "licenseInfoFromFiles": [
26
+ "LicenseRef-RobonoSDK"
27
+ ],
28
+ "copyrightText": "Copyright (c) 2026 Add to Loop LLC.",
29
+ "comment": "Host-supplied peers (not bundled): {}"
30
+ }
31
+ ],
32
+ "files": [
33
+ {
34
+ "fileName": "./CHANGELOG.md",
35
+ "SPDXID": "SPDXRef-File-0",
36
+ "checksums": [
37
+ {
38
+ "algorithm": "SHA256",
39
+ "checksumValue": "cc846301b9f9c1137f5ab4804ca72246f1e2762cb58cd3e50cf8cad87fa04322"
40
+ },
41
+ {
42
+ "algorithm": "SHA1",
43
+ "checksumValue": "a95ae3a0a653ad842b0d11c0f42fae1704e56012"
44
+ }
45
+ ],
46
+ "licenseConcluded": "LicenseRef-RobonoSDK",
47
+ "licenseInfoInFiles": [
48
+ "LicenseRef-RobonoSDK"
49
+ ],
50
+ "copyrightText": "Copyright (c) 2026 Add to Loop LLC."
51
+ },
52
+ {
53
+ "fileName": "./LICENSE",
54
+ "SPDXID": "SPDXRef-File-1",
55
+ "checksums": [
56
+ {
57
+ "algorithm": "SHA256",
58
+ "checksumValue": "6ddf078a10d1882d9d6d7dd9144b3b8def38a3e6915020a886f6e9f8b6c69dc0"
59
+ },
60
+ {
61
+ "algorithm": "SHA1",
62
+ "checksumValue": "5c880adec96a9096bd582480bc5de9aec50a0c1d"
63
+ }
64
+ ],
65
+ "licenseConcluded": "LicenseRef-RobonoSDK",
66
+ "licenseInfoInFiles": [
67
+ "LicenseRef-RobonoSDK"
68
+ ],
69
+ "copyrightText": "Copyright (c) 2026 Add to Loop LLC."
70
+ },
71
+ {
72
+ "fileName": "./README.md",
73
+ "SPDXID": "SPDXRef-File-2",
74
+ "checksums": [
75
+ {
76
+ "algorithm": "SHA256",
77
+ "checksumValue": "a7f7fc393294454b889657b3f4213060512e1c16706a8db658b4953470c5b0f4"
78
+ },
79
+ {
80
+ "algorithm": "SHA1",
81
+ "checksumValue": "20b37f37d14cd2fc2d0f82aa938da68675a21c77"
82
+ }
83
+ ],
84
+ "licenseConcluded": "LicenseRef-RobonoSDK",
85
+ "licenseInfoInFiles": [
86
+ "LicenseRef-RobonoSDK"
87
+ ],
88
+ "copyrightText": "Copyright (c) 2026 Add to Loop LLC."
89
+ },
90
+ {
91
+ "fileName": "./dist/diagnostics.js",
92
+ "SPDXID": "SPDXRef-File-3",
93
+ "checksums": [
94
+ {
95
+ "algorithm": "SHA256",
96
+ "checksumValue": "c6641dade8d7bf02f2d2086171ffc57551ed4f356f342873ff66ca9c2f85d043"
97
+ },
98
+ {
99
+ "algorithm": "SHA1",
100
+ "checksumValue": "1a3d45455171b4ad6a7189f51bb95d7e4a22a152"
101
+ }
102
+ ],
103
+ "licenseConcluded": "LicenseRef-RobonoSDK",
104
+ "licenseInfoInFiles": [
105
+ "LicenseRef-RobonoSDK"
106
+ ],
107
+ "copyrightText": "Copyright (c) 2026 Add to Loop LLC."
108
+ },
109
+ {
110
+ "fileName": "./dist/events.js",
111
+ "SPDXID": "SPDXRef-File-4",
112
+ "checksums": [
113
+ {
114
+ "algorithm": "SHA256",
115
+ "checksumValue": "e3ef0cf938886194ecf6bea82e87f932775d2674800fa47880883d5a716d1053"
116
+ },
117
+ {
118
+ "algorithm": "SHA1",
119
+ "checksumValue": "838931c25b514c9941dac1602c9e93b21fe02d9b"
120
+ }
121
+ ],
122
+ "licenseConcluded": "LicenseRef-RobonoSDK",
123
+ "licenseInfoInFiles": [
124
+ "LicenseRef-RobonoSDK"
125
+ ],
126
+ "copyrightText": "Copyright (c) 2026 Add to Loop LLC."
127
+ },
128
+ {
129
+ "fileName": "./dist/groups.d.ts",
130
+ "SPDXID": "SPDXRef-File-5",
131
+ "checksums": [
132
+ {
133
+ "algorithm": "SHA256",
134
+ "checksumValue": "3bdda72976a7714d76e5f856b4cbdff3823af06f624cc78de31e10fc19c8803e"
135
+ },
136
+ {
137
+ "algorithm": "SHA1",
138
+ "checksumValue": "213076aa45a39797239b2808d986db45ec5c92c2"
139
+ }
140
+ ],
141
+ "licenseConcluded": "LicenseRef-RobonoSDK",
142
+ "licenseInfoInFiles": [
143
+ "LicenseRef-RobonoSDK"
144
+ ],
145
+ "copyrightText": "Copyright (c) 2026 Add to Loop LLC."
146
+ },
147
+ {
148
+ "fileName": "./dist/groups.js",
149
+ "SPDXID": "SPDXRef-File-6",
150
+ "checksums": [
151
+ {
152
+ "algorithm": "SHA256",
153
+ "checksumValue": "671d204c07477f2827b7bcd2a90ac7cc8ec56884c7d4f872a8dd62264a484393"
154
+ },
155
+ {
156
+ "algorithm": "SHA1",
157
+ "checksumValue": "9bbd521150d53e9060f75c02445e869ba0cd4985"
158
+ }
159
+ ],
160
+ "licenseConcluded": "LicenseRef-RobonoSDK",
161
+ "licenseInfoInFiles": [
162
+ "LicenseRef-RobonoSDK"
163
+ ],
164
+ "copyrightText": "Copyright (c) 2026 Add to Loop LLC."
165
+ },
166
+ {
167
+ "fileName": "./dist/history.d.ts",
168
+ "SPDXID": "SPDXRef-File-7",
169
+ "checksums": [
170
+ {
171
+ "algorithm": "SHA256",
172
+ "checksumValue": "e1aaf45fb0401a677cf08d84f361a18dc9bc1566d7b478df04bd3b0a1307930c"
173
+ },
174
+ {
175
+ "algorithm": "SHA1",
176
+ "checksumValue": "46f79f635d81baae406203c3736c6492e56f7b39"
177
+ }
178
+ ],
179
+ "licenseConcluded": "LicenseRef-RobonoSDK",
180
+ "licenseInfoInFiles": [
181
+ "LicenseRef-RobonoSDK"
182
+ ],
183
+ "copyrightText": "Copyright (c) 2026 Add to Loop LLC."
184
+ },
185
+ {
186
+ "fileName": "./dist/history.js",
187
+ "SPDXID": "SPDXRef-File-8",
188
+ "checksums": [
189
+ {
190
+ "algorithm": "SHA256",
191
+ "checksumValue": "64c6d038ee554356817d7eb4f9213c51154c82399f0bc4bf2c81fa4ef7bc2f2f"
192
+ },
193
+ {
194
+ "algorithm": "SHA1",
195
+ "checksumValue": "466141b4a3cd3c2d87525b669cc46d1edf6d6f7a"
196
+ }
197
+ ],
198
+ "licenseConcluded": "LicenseRef-RobonoSDK",
199
+ "licenseInfoInFiles": [
200
+ "LicenseRef-RobonoSDK"
201
+ ],
202
+ "copyrightText": "Copyright (c) 2026 Add to Loop LLC."
203
+ },
204
+ {
205
+ "fileName": "./dist/index.d.ts",
206
+ "SPDXID": "SPDXRef-File-9",
207
+ "checksums": [
208
+ {
209
+ "algorithm": "SHA256",
210
+ "checksumValue": "ba60c30ff9e5d84f563ab61b8157f303f34796dd767ce9909478d25aa151da1c"
211
+ },
212
+ {
213
+ "algorithm": "SHA1",
214
+ "checksumValue": "4b0341465a58e9a7ceff6c3e4b55132bc9ebbfe4"
215
+ }
216
+ ],
217
+ "licenseConcluded": "LicenseRef-RobonoSDK",
218
+ "licenseInfoInFiles": [
219
+ "LicenseRef-RobonoSDK"
220
+ ],
221
+ "copyrightText": "Copyright (c) 2026 Add to Loop LLC."
222
+ },
223
+ {
224
+ "fileName": "./dist/index.js",
225
+ "SPDXID": "SPDXRef-File-10",
226
+ "checksums": [
227
+ {
228
+ "algorithm": "SHA256",
229
+ "checksumValue": "b1e1c9c9796a2228e95e1c645293be7afa40b3fb574d343461b717097d924bb0"
230
+ },
29
231
  {
30
- "referenceCategory": "PACKAGE-MANAGER",
31
- "referenceType": "purl",
32
- "referenceLocator": "pkg:npm/%40robono/calling@0.4.0"
232
+ "algorithm": "SHA1",
233
+ "checksumValue": "44de0281a1c5c504a936ce9b1f384fd43fd45a5c"
33
234
  }
34
- ]
235
+ ],
236
+ "licenseConcluded": "LicenseRef-RobonoSDK",
237
+ "licenseInfoInFiles": [
238
+ "LicenseRef-RobonoSDK"
239
+ ],
240
+ "copyrightText": "Copyright (c) 2026 Add to Loop LLC."
241
+ },
242
+ {
243
+ "fileName": "./dist/media-profile.js",
244
+ "SPDXID": "SPDXRef-File-11",
245
+ "checksums": [
246
+ {
247
+ "algorithm": "SHA256",
248
+ "checksumValue": "38fdcaad6dc23146ea2b08901eca111ac545c5b98cc877eedbe5dd79c8b93d78"
249
+ },
250
+ {
251
+ "algorithm": "SHA1",
252
+ "checksumValue": "eca76fd9eee52a1ca3e020bd59918adb5e687e1c"
253
+ }
254
+ ],
255
+ "licenseConcluded": "LicenseRef-RobonoSDK",
256
+ "licenseInfoInFiles": [
257
+ "LicenseRef-RobonoSDK"
258
+ ],
259
+ "copyrightText": "Copyright (c) 2026 Add to Loop LLC."
260
+ },
261
+ {
262
+ "fileName": "./dist/messenger.d.ts",
263
+ "SPDXID": "SPDXRef-File-12",
264
+ "checksums": [
265
+ {
266
+ "algorithm": "SHA256",
267
+ "checksumValue": "9f064da69a7bb17ad52372e8dd5bfc21715f5d67dbe9809b95689ba0baf4eb5b"
268
+ },
269
+ {
270
+ "algorithm": "SHA1",
271
+ "checksumValue": "a0bfb1f1eb88cafd0a99fdc2683afc6739518195"
272
+ }
273
+ ],
274
+ "licenseConcluded": "LicenseRef-RobonoSDK",
275
+ "licenseInfoInFiles": [
276
+ "LicenseRef-RobonoSDK"
277
+ ],
278
+ "copyrightText": "Copyright (c) 2026 Add to Loop LLC."
279
+ },
280
+ {
281
+ "fileName": "./dist/messenger.js",
282
+ "SPDXID": "SPDXRef-File-13",
283
+ "checksums": [
284
+ {
285
+ "algorithm": "SHA256",
286
+ "checksumValue": "d9d03e8dbba2c855f4e8621c0e60ed934c9752672a94b74e92458723a762ecf9"
287
+ },
288
+ {
289
+ "algorithm": "SHA1",
290
+ "checksumValue": "6a119ac2f56c443326cf46965e5a0fd2274c8971"
291
+ }
292
+ ],
293
+ "licenseConcluded": "LicenseRef-RobonoSDK",
294
+ "licenseInfoInFiles": [
295
+ "LicenseRef-RobonoSDK"
296
+ ],
297
+ "copyrightText": "Copyright (c) 2026 Add to Loop LLC."
298
+ },
299
+ {
300
+ "fileName": "./dist/sharing.js",
301
+ "SPDXID": "SPDXRef-File-14",
302
+ "checksums": [
303
+ {
304
+ "algorithm": "SHA256",
305
+ "checksumValue": "d34414dce592392edb025827e93f9d0606ddd98ff6391c762dab9112950d971b"
306
+ },
307
+ {
308
+ "algorithm": "SHA1",
309
+ "checksumValue": "fdefadaa2eb44b9fe723315f5a66bd6007433c24"
310
+ }
311
+ ],
312
+ "licenseConcluded": "LicenseRef-RobonoSDK",
313
+ "licenseInfoInFiles": [
314
+ "LicenseRef-RobonoSDK"
315
+ ],
316
+ "copyrightText": "Copyright (c) 2026 Add to Loop LLC."
317
+ },
318
+ {
319
+ "fileName": "./dist/telemetry.js",
320
+ "SPDXID": "SPDXRef-File-15",
321
+ "checksums": [
322
+ {
323
+ "algorithm": "SHA256",
324
+ "checksumValue": "21ae9d66687036dee701bfe5a167263ed13a0f71f1a7d5d7c0cc85c21114e76e"
325
+ },
326
+ {
327
+ "algorithm": "SHA1",
328
+ "checksumValue": "15d77216bc701fb437f12df48d0f636645252400"
329
+ }
330
+ ],
331
+ "licenseConcluded": "LicenseRef-RobonoSDK",
332
+ "licenseInfoInFiles": [
333
+ "LicenseRef-RobonoSDK"
334
+ ],
335
+ "copyrightText": "Copyright (c) 2026 Add to Loop LLC."
336
+ },
337
+ {
338
+ "fileName": "./package.json",
339
+ "SPDXID": "SPDXRef-File-16",
340
+ "checksums": [
341
+ {
342
+ "algorithm": "SHA256",
343
+ "checksumValue": "a2d6530e9896ab1edb81d42afae746a902f3f9f8fb249adee9a5e78dc2fd67bb"
344
+ },
345
+ {
346
+ "algorithm": "SHA1",
347
+ "checksumValue": "538d1e05ed1ab40adddf1c541de855c5c4cb8493"
348
+ }
349
+ ],
350
+ "licenseConcluded": "LicenseRef-RobonoSDK",
351
+ "licenseInfoInFiles": [
352
+ "LicenseRef-RobonoSDK"
353
+ ],
354
+ "copyrightText": "Copyright (c) 2026 Add to Loop LLC."
35
355
  }
36
356
  ],
37
357
  "relationships": [
38
358
  {
39
359
  "spdxElementId": "SPDXRef-DOCUMENT",
40
- "relatedSpdxElement": "SPDXRef-Package-robono.calling-0.4.0",
41
- "relationshipType": "DESCRIBES"
360
+ "relationshipType": "DESCRIBES",
361
+ "relatedSpdxElement": "SPDXRef-Package"
362
+ },
363
+ {
364
+ "spdxElementId": "SPDXRef-Package",
365
+ "relationshipType": "CONTAINS",
366
+ "relatedSpdxElement": "SPDXRef-File-0"
367
+ },
368
+ {
369
+ "spdxElementId": "SPDXRef-Package",
370
+ "relationshipType": "CONTAINS",
371
+ "relatedSpdxElement": "SPDXRef-File-1"
372
+ },
373
+ {
374
+ "spdxElementId": "SPDXRef-Package",
375
+ "relationshipType": "CONTAINS",
376
+ "relatedSpdxElement": "SPDXRef-File-2"
377
+ },
378
+ {
379
+ "spdxElementId": "SPDXRef-Package",
380
+ "relationshipType": "CONTAINS",
381
+ "relatedSpdxElement": "SPDXRef-File-3"
382
+ },
383
+ {
384
+ "spdxElementId": "SPDXRef-Package",
385
+ "relationshipType": "CONTAINS",
386
+ "relatedSpdxElement": "SPDXRef-File-4"
387
+ },
388
+ {
389
+ "spdxElementId": "SPDXRef-Package",
390
+ "relationshipType": "CONTAINS",
391
+ "relatedSpdxElement": "SPDXRef-File-5"
392
+ },
393
+ {
394
+ "spdxElementId": "SPDXRef-Package",
395
+ "relationshipType": "CONTAINS",
396
+ "relatedSpdxElement": "SPDXRef-File-6"
397
+ },
398
+ {
399
+ "spdxElementId": "SPDXRef-Package",
400
+ "relationshipType": "CONTAINS",
401
+ "relatedSpdxElement": "SPDXRef-File-7"
402
+ },
403
+ {
404
+ "spdxElementId": "SPDXRef-Package",
405
+ "relationshipType": "CONTAINS",
406
+ "relatedSpdxElement": "SPDXRef-File-8"
407
+ },
408
+ {
409
+ "spdxElementId": "SPDXRef-Package",
410
+ "relationshipType": "CONTAINS",
411
+ "relatedSpdxElement": "SPDXRef-File-9"
412
+ },
413
+ {
414
+ "spdxElementId": "SPDXRef-Package",
415
+ "relationshipType": "CONTAINS",
416
+ "relatedSpdxElement": "SPDXRef-File-10"
417
+ },
418
+ {
419
+ "spdxElementId": "SPDXRef-Package",
420
+ "relationshipType": "CONTAINS",
421
+ "relatedSpdxElement": "SPDXRef-File-11"
422
+ },
423
+ {
424
+ "spdxElementId": "SPDXRef-Package",
425
+ "relationshipType": "CONTAINS",
426
+ "relatedSpdxElement": "SPDXRef-File-12"
427
+ },
428
+ {
429
+ "spdxElementId": "SPDXRef-Package",
430
+ "relationshipType": "CONTAINS",
431
+ "relatedSpdxElement": "SPDXRef-File-13"
432
+ },
433
+ {
434
+ "spdxElementId": "SPDXRef-Package",
435
+ "relationshipType": "CONTAINS",
436
+ "relatedSpdxElement": "SPDXRef-File-14"
437
+ },
438
+ {
439
+ "spdxElementId": "SPDXRef-Package",
440
+ "relationshipType": "CONTAINS",
441
+ "relatedSpdxElement": "SPDXRef-File-15"
442
+ },
443
+ {
444
+ "spdxElementId": "SPDXRef-Package",
445
+ "relationshipType": "CONTAINS",
446
+ "relatedSpdxElement": "SPDXRef-File-16"
447
+ }
448
+ ],
449
+ "hasExtractedLicensingInfos": [
450
+ {
451
+ "licenseId": "LicenseRef-RobonoSDK",
452
+ "extractedText": "ROBONO SDK LICENSE AGREEMENT\nVersion 1.0, July 20, 2026\n\nCopyright (c) 2026 Add to Loop LLC. All rights reserved.\n\nThis Robono SDK License Agreement (the \"Agreement\") is between Add to Loop LLC,\noperating the Robono service (\"Robono\"), and the person or entity that downloads,\ninstalls, copies, or uses the Software (\"You\"). By downloading, installing,\ncopying, or using the Software, You accept this Agreement. If You do not accept\nit, do not use the Software.\n\n1. Definitions\n\n\"Software\" means this Robono software development kit, its documentation, and\nany updates Robono provides under this Agreement.\n\n\"Robono Service\" means the messaging interoperability, routing, policy,\nconversion, delivery, and related services operated or expressly authorized by\nRobono.\n\n\"Application\" means a product or service owned or controlled by You that is\nauthorized to connect to the Robono Service.\n\n2. Limited License\n\nSubject to this Agreement, Robono grants You a limited, non-exclusive,\nnon-transferable, non-sublicensable, revocable license to:\n\n(a) install, copy, and use the Software internally to develop, test, and\noperate an Application that communicates with the Robono Service; and\n\n(b) reproduce and distribute the portions of the Software that are necessarily\nincluded in Your Application, solely in compiled, minified, bundled, or object\ncode form and solely as needed for that Application to communicate with the\nRobono Service.\n\nNo right is granted to distribute the Software as a standalone product or SDK.\n\n3. Restrictions\n\nYou may not, and may not permit another person to:\n\n(a) sell, rent, lease, sublicense, publish, or redistribute the Software as a\nstandalone product, library, SDK, or service;\n\n(b) use the Software or a modified or derivative version of it to develop,\nprovide, or support a product or service that competes with the Robono Service;\n\n(c) use the Software to bypass Robono, avoid applicable usage controls or fees,\nor access the Robono Service without authorization;\n\n(d) remove or alter copyright, trademark, attribution, or proprietary notices;\n\n(e) reverse engineer, decompile, or disassemble the Software except to the\nlimited extent applicable law expressly prohibits this restriction; or\n\n(f) use the Software in violation of applicable law, the rights of another\nperson, or the terms governing access to the Robono Service.\n\n4. Ownership and Reserved Rights\n\nThe Software is licensed, not sold. Robono and its licensors retain all right,\ntitle, and interest in the Software and all related intellectual property.\nExcept for the limited rights expressly granted in Section 2, all rights are\nreserved.\n\nNo patent license is granted except the minimum license, if any, necessarily\nrequired to exercise the rights expressly granted in Section 2, and that\nlimited patent license applies only while the Software is used with an\nauthorized Robono Service.\n\n5. Service Access\n\nThis Agreement does not itself grant an account, API credentials, service\navailability, support, or a right to access the Robono Service. Service access\nmay be governed by additional terms, plans, limits, and policies. Robono may\nsuspend or terminate access to protect users, systems, legal compliance, or the\nintegrity of the Robono Service.\n\n6. Updates and Support\n\nRobono is not required to provide maintenance, updates, support, or continued\navailability of any version of the Software unless separately agreed in\nwriting.\n\n7. Term and Termination\n\nThis Agreement continues until terminated. Your rights terminate automatically\nif You breach this Agreement. On termination, You must stop using the Software\nand delete copies under Your control, except for copies necessarily contained\nin previously distributed Applications. Sections 3, 4, 8, 9, 10, and 11\nsurvive termination.\n\n8. Disclaimer of Warranties\n\nTO THE MAXIMUM EXTENT PERMITTED BY LAW, THE SOFTWARE IS PROVIDED \"AS IS\" AND\n\"AS AVAILABLE,\" WITHOUT WARRANTIES OF ANY KIND, EXPRESS, IMPLIED, OR\nSTATUTORY, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR\nPURPOSE, TITLE, AND NON-INFRINGEMENT.\n\n9. Limitation of Liability\n\nTO THE MAXIMUM EXTENT PERMITTED BY LAW, ROBONO WILL NOT BE LIABLE FOR INDIRECT,\nINCIDENTAL, SPECIAL, CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES, OR FOR LOSS\nOF DATA, REVENUE, PROFITS, GOODWILL, OR BUSINESS OPPORTUNITY, ARISING FROM OR\nRELATING TO THE SOFTWARE. ROBONO'S TOTAL LIABILITY RELATING TO THE SOFTWARE\nWILL NOT EXCEED THE GREATER OF ONE HUNDRED U.S. DOLLARS (US $100) OR THE AMOUNT\nYOU PAID ROBONO FOR THE SOFTWARE DURING THE TWELVE MONTHS BEFORE THE EVENT\nGIVING RISE TO THE CLAIM. THESE LIMITATIONS APPLY TO THE MAXIMUM EXTENT\nPERMITTED BY LAW.\n\n10. Governing Law\n\nThis Agreement is governed by the laws of the State of Florida, excluding its\nconflict-of-laws rules. The state and federal courts located in Palm Beach\nCounty, Florida have exclusive jurisdiction, and each party consents to their\njurisdiction and venue.\n\n11. General\n\nYou may not assign this Agreement without Robono's prior written consent.\nRobono may assign this Agreement in connection with a reorganization, transfer\nof intellectual property, financing, merger, acquisition, or sale of all or\nsubstantially all of the relevant assets. If a provision is unenforceable, it\nwill be enforced to the maximum extent permitted and the remaining provisions\nwill remain effective. A waiver must be in writing. This Agreement is the\nentire agreement concerning the Software unless You and Robono sign a separate\nwritten agreement that expressly replaces it.\n\nQuestions about this license may be submitted at https://robono.com/contact.\n"
42
453
  }
43
454
  ]
44
455
  }
@@ -0,0 +1,67 @@
1
+ // Local, on-demand measurements. Never return addresses, SDP, track IDs or media.
2
+ const number=(value,max=Number.MAX_SAFE_INTEGER)=>typeof value==='number'&&Number.isFinite(value)&&value>=0&&value<=max?value:null;
3
+ const states=new Set(['new','connecting','connected','disconnected','failed','closed']);
4
+ export function summarizeCallDiagnostics(report,previous=new Map(),connectionState='new'){
5
+ const rows=Array.isArray(report)?report:[...report.values()],byId=new Map(rows.map(row=>[row.id,row]));
6
+ const transport=rows.find(row=>row.type==='transport'&&row.selectedCandidatePairId);
7
+ const pair=rows.find(row=>row.type==='candidate-pair'&&(transport?row.id===transport.selectedCandidatePairId:row.nominated&&row.state==='succeeded'));
8
+ const counters=new Map();const media=[];
9
+ for(const kind of ['audio','video']){
10
+ const values={kind,sendKbps:null,receiveKbps:null,receiveLossPercent:null,jitterMs:null,width:null,height:null,framesPerSecond:null};
11
+ let received=0,lost=0,lossKnown=false;
12
+ for(const row of rows){
13
+ if(!['inbound-rtp','outbound-rtp'].includes(row.type)||(row.kind??row.mediaType)!==kind)continue;
14
+ const prior=previous.get(row.id),timestamp=number(row.timestamp),bytes=number(row.type==='inbound-rtp'?row.bytesReceived:row.bytesSent);
15
+ const packets=number(row.packetsReceived),missing=number(row.packetsLost);
16
+ counters.set(row.id,{timestamp,bytes,packets,missing});
17
+ const elapsed=timestamp!==null&&prior?.timestamp!==null?timestamp-prior?.timestamp:NaN;
18
+ if(elapsed>=250&&elapsed<=30000&&bytes!==null&&prior?.bytes!=null&&bytes>=prior.bytes){
19
+ const key=row.type==='inbound-rtp'?'receiveKbps':'sendKbps';values[key]=(values[key]??0)+(bytes-prior.bytes)*8/elapsed;
20
+ }
21
+ if(row.type==='inbound-rtp'){
22
+ const jitter=number(row.jitter,60);if(jitter!==null)values.jitterMs=Math.max(values.jitterMs??0,jitter*1000);
23
+ if(elapsed>=250&&elapsed<=30000&&packets!==null&&missing!==null&&prior?.packets!=null&&prior?.missing!=null&&packets>=prior.packets&&missing>=prior.missing){received+=packets-prior.packets;lost+=missing-prior.missing;lossKnown=true;}
24
+ if(kind==='video'){
25
+ const width=number(row.frameWidth,32768),height=number(row.frameHeight,32768),fps=number(row.framesPerSecond,1000);
26
+ if(width!==null&&height!==null&&(width*height>(values.width??0)*(values.height??0))){values.width=width;values.height=height;values.framesPerSecond=fps;}
27
+ }
28
+ }
29
+ }
30
+ if(lossKnown&&received+lost>0)values.receiveLossPercent=100*lost/(received+lost);
31
+ if(rows.some(row=>['inbound-rtp','outbound-rtp'].includes(row.type)&&(row.kind??row.mediaType)===kind))media.push(values);
32
+ }
33
+ const rtt=number(pair?.currentRoundTripTime,120),roundTripMs=rtt===null?null:rtt*1000;
34
+ const candidates=[byId.get(pair?.localCandidateId),byId.get(pair?.remoteCandidateId)];
35
+ const route=pair?(candidates.some(row=>row?.candidateType==='relay')?'relay':candidates.every(row=>['host','srflx','prflx'].includes(row?.candidateType))?'direct':'unknown'):'unknown';
36
+ const protocol=['udp','tcp'].includes(candidates[0]?.protocol)?candidates[0].protocol:null;
37
+ const state=states.has(connectionState)?connectionState:'new';let quality='unknown';
38
+ if(['failed','disconnected'].includes(state))quality='poor';
39
+ else if(state==='connected'){
40
+ const metrics=[...(roundTripMs===null?[]:[[roundTripMs,300,600]]),...media.flatMap(row=>[...(row.jitterMs===null?[]:[[row.jitterMs,30,60]]),...(row.receiveLossPercent===null?[]:[[row.receiveLossPercent,2,5]])])];
41
+ if(metrics.length)quality=metrics.some(([v,,poor])=>v>poor)?'poor':metrics.some(([v,fair])=>v>fair)?'fair':'good';
42
+ }
43
+ return {sample:{connectionState:state,quality,roundTripMs,route,protocol,media},counters};
44
+ }
45
+ /** Coalesces callers, bounds stalled getStats, and discards results from ended calls. */
46
+ export function createCallDiagnostics(resolve){
47
+ const previous=new WeakMap();let pending;
48
+ return function getDiagnostics(){
49
+ if(pending)return pending;
50
+ const current=resolve(),callId=current.callId;
51
+ if(!callId)return Promise.resolve({callId:null,sampledAt:Date.now(),peers:[]});
52
+ pending=(async()=>{
53
+ const peers=await Promise.all(current.peers.slice(0,7).map(async({pc,participant})=>{
54
+ const unavailable={participant,connectionState:states.has(pc?.connectionState)?pc.connectionState:'new',quality:'unknown',roundTripMs:null,route:'unknown',protocol:null,media:[]};
55
+ if(typeof pc?.getStats!=='function')return unavailable;
56
+ let timer;
57
+ try{
58
+ const report=await Promise.race([Promise.resolve().then(()=>pc.getStats()),new Promise((_,reject)=>{timer=setTimeout(()=>reject(Error('stats_timeout')),1000);})]);
59
+ const before=previous.get(pc),value=summarizeCallDiagnostics(report,before?.callId===callId?before.counters:undefined,pc.connectionState);
60
+ previous.set(pc,{callId,counters:value.counters});return {participant,...value.sample};
61
+ }catch{return unavailable;}finally{clearTimeout(timer);}
62
+ }));
63
+ if(resolve().callId!==callId)return {callId:null,sampledAt:Date.now(),peers:[]};
64
+ return {callId,sampledAt:Date.now(),peers};
65
+ })().finally(()=>{pending=undefined;});return pending;
66
+ };
67
+ }
package/dist/groups.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import type {Request,EventStatus} from './index.js';
1
+ import type {CallDiagnostics,Request,EventStatus} from './index.js';
2
2
  export interface GroupCall {screenSharingAvailable?:boolean;sharing?:boolean[];id:string;connectionId:string;group:true;kind:'voice'|'video';version:number;state:'ringing'|'connecting'|'connected'|'ended';reason?:string;createdAt:number;connectedAt:number|null;endedAt:number|null;ringRemainingMs:number;holdExpiresAt:number|null;self:number;controlled:boolean;memberState:'invited'|'joined'|'left'|'declined';localHeld:boolean;members:Array<{id:number;appId:string;userId:string;state:string;held:boolean;connectedAt:number|null}>;peers:Array<{id:string;participant:number;offerer:boolean;remoteDescription:RTCSessionDescriptionInit|null;remoteCandidates:RTCIceCandidateInit[];connected:boolean}>}
3
3
  export interface GroupSnapshot {listeningStatus?:EventStatus;screenSharing?:boolean;screenSharePending?:boolean;displayStream?:MediaStream|null;call:GroupCall|null;muted:boolean;cameraEnabled:boolean;localHeld:boolean;localStream:MediaStream|null;remoteStreams:Record<number,MediaStream>;error:string|null;preparing:boolean;connectedAt:number|null}
4
4
  export class RobonoGroupCallingClient {
5
5
  constructor(options:{videoQuality?:'480p'|'720p';request:Request;onSessionExpired?:(error:Error)=>void;onChange?:(state:GroupSnapshot)=>void;getDisplayMedia?:(context?:{isCurrent:()=>boolean})=>Promise<MediaStream>;getUserMedia?:(context:{kind:'voice'|'video';videoQuality?:'480p'|'720p';isCurrent:()=>boolean})=>Promise<MediaStream>;createPeerConnection?:(config:RTCConfiguration)=>RTCPeerConnection;createRemoteStream?:(track:MediaStreamTrack)=>MediaStream;createId?:()=>string;forceRelay?:boolean;pollMs?:number});
6
+ getDiagnostics():Promise<CallDiagnostics>;
6
7
  availability(connectionId:string,kind?:'voice'|'video'):Promise<{available:boolean;maxParticipants:number}>;startScreenSharing():Promise<void>;stopScreenSharing():Promise<void>;state:GroupSnapshot;closed:boolean;listen():Promise<void>;refresh():Promise<boolean>;call(connectionId:string,options?:{kind?:'voice'|'video'}):Promise<void>;answer():Promise<void>;leave():Promise<void>;hold():Promise<void>;resume(prepareAudio?:()=>Promise<void>):Promise<void>;mute(value?:boolean):void;setCamera(value:boolean):void;setForeground(value:boolean):void;setVideoForeground(value:boolean):void;dispose():Promise<void>;
7
8
  }
package/dist/groups.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import {createTransportTelemetry} from './telemetry.js';
2
+ import {createCallDiagnostics} from './diagnostics.js';
2
3
  import {mediaProfile,videoConstraints,limitSender} from './media-profile.js';
3
4
  import {createScreenSharing} from './sharing.js';
4
5
  /** Optional multiparty engine. It shares admission/transport with direct calls, not their UI. */
@@ -15,6 +16,7 @@ export class RobonoGroupCallingClient {
15
16
  this.request.subscribe=request.subscribe;
16
17
  this.videoQuality=mediaProfile(videoQuality).quality;
17
18
  this.reportTransport=createTransportTelemetry(request);this.peers=new Map();this.pendingRTC=new Set();this.state={call:null,muted:false,cameraEnabled:true,localHeld:false,localStream:null,remoteStreams:{},error:null,preparing:false,connectedAt:null};
19
+ this.getDiagnostics=createCallDiagnostics(()=>({callId:!this.closed&&this.state.call?.controlled&&this.state.call.state!=='ended'?this.state.call.id:null,peers:[...this.peers.values()].map(p=>({pc:p.pc,participant:p.participant}))}));
18
20
  this.sharing=createScreenSharing({limitVideo:sender=>limitSender(sender,this.ice?.videoQuality==='480p'?'480p':this.videoQuality),request:input=>this.request('groups/sharing',input),call:()=>this.state.call,peers:()=>[...this.peers.values()].map(p=>p.pc),stream:()=>this.stream,capture:getDisplayMedia,emit:()=>this.emit(),fail:e=>{this.state.error=e.message;this.emit();}});
19
21
  this.closed=false;this.epoch=0;this.foreground=true;this.failures=0;this.notBefore=0;this.lastSuccess=Date.now();this.holdRevision=0;
20
22
  }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,7 @@
1
+ export interface CallMediaDiagnostics {kind:'audio'|'video';sendKbps:number|null;receiveKbps:number|null;receiveLossPercent:number|null;jitterMs:number|null;width:number|null;height:number|null;framesPerSecond:number|null}
2
+ export interface CallPeerDiagnostics {participant:number|null;connectionState:'new'|'connecting'|'connected'|'disconnected'|'failed'|'closed';quality:'unknown'|'good'|'fair'|'poor';roundTripMs:number|null;route:'unknown'|'direct'|'relay';protocol:'udp'|'tcp'|null;media:CallMediaDiagnostics[]}
3
+ /** Local measurements only; contains no addresses, media, SDP or credentials. */
4
+ export interface CallDiagnostics {callId:string|null;sampledAt:number;peers:CallPeerDiagnostics[]}
1
5
  export interface CallHistoryEntry { id: string; sequence: number; connectionId: string; kind?: 'voice' | 'video'; role: 'caller' | 'callee'; reason: string; createdAt: number; answeredAt: number | null; connectedAt: number | null; endedAt: number; durationMs: number }
2
6
  export interface CallHistoryPage { entries: CallHistoryEntry[]; nextCursor: number; hasMore: boolean }
3
7
  export type CallState = 'ringing' | 'connecting' | 'connected' | 'ended';
@@ -10,6 +14,7 @@ export interface Request { (operation: string, payload?: Record<string, unknown>
10
14
  export function createHttpTransport(options: { baseUrl: string; token: string; fetcher?: typeof fetch; events?: boolean; WebSocketImpl?: typeof WebSocket }): Request;
11
15
  export class RobonoCallingClient {
12
16
  constructor(options: { videoQuality?: '480p' | '720p'; request: Request; onSessionExpired?: (error: Error) => void; onChange?: (state: Snapshot) => void; onLocalStream?: (stream: MediaStream | null) => void; onRemoteStream?: (stream: MediaStream | null) => void; getDisplayMedia?:(context?:{isCurrent:()=>boolean})=>Promise<MediaStream>;getUserMedia?: (context?: { kind?: 'voice' | 'video'; videoQuality?: '480p'|'720p'; isCurrent: () => boolean }) => Promise<MediaStream>; createPeerConnection?: (config: RTCConfiguration) => RTCPeerConnection; pollMs?: number; forceRelay?: boolean; createId?: () => string; createRemoteStream?: (track: MediaStreamTrack) => MediaStream });
17
+ getDiagnostics():Promise<CallDiagnostics>;
13
18
  state: Snapshot;
14
19
  readonly closed: boolean;
15
20
  listen(): Promise<void>;
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import {subscribeCallingEvents} from './events.js';
2
2
  import {createTransportTelemetry} from './telemetry.js';
3
+ import {createCallDiagnostics} from './diagnostics.js';
3
4
  import {mediaProfile,videoConstraints,limitSender} from './media-profile.js';
4
5
  import {createScreenSharing} from './sharing.js';
5
6
  /** Voice and video calling client. Media and identity adapters are supplied by the host. */
@@ -63,6 +64,7 @@ export class RobonoCallingClient {
63
64
  this.pollFailures = 0; this.pollNotBefore = 0; this.foreground = true;
64
65
  this.sharing=createScreenSharing({limitVideo:sender=>limitSender(sender,this.activeVideoQuality??this.videoQuality),request:input=>this.retry('sharing',input),call:()=>this.state.call,peers:()=>this.pc?[this.pc]:[],stream:()=>this.stream,capture:getDisplayMedia,emit:()=>this.emit(),fail:e=>{this.state.error=e.message;this.emit();}});
65
66
  this.reportTransport=createTransportTelemetry(request);
67
+ this.getDiagnostics=createCallDiagnostics(()=>({callId:!this.closed&&this.state.call?.controlled&&this.state.call.state!=='ended'?this.state.call.id:null,peers:this.pc?[{pc:this.pc,participant:null}]:[]}));
66
68
  this.localHeld = false; this.holdRevision = 0; this.holdDeadline = 0;
67
69
  }
68
70
  rtc(promise, phase, timeoutMs = 8000) {
package/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "provenance": true
18
18
  },
19
19
  "name": "@robono/calling",
20
- "version": "0.4.0",
20
+ "version": "0.5.0",
21
21
  "description": "WebRTC voice, video, group calling and screen sharing within apps and across Robono Bridge networks.",
22
22
  "homepage": "https://robono.com/docs#calling",
23
23
  "repository": {
@@ -57,7 +57,7 @@
57
57
  },
58
58
  "scripts": {
59
59
  "build": "node build.mjs",
60
- "test": "npm run build && node --test test/client.test.mjs test/package.test.mjs test/group-client.test.mjs test/sharing.test.mjs test/telemetry.test.mjs test/events.test.mjs test/messenger.test.mjs",
60
+ "test": "npm run build && node --test test/client.test.mjs test/package.test.mjs test/group-client.test.mjs test/sharing.test.mjs test/telemetry.test.mjs test/events.test.mjs test/messenger.test.mjs test/diagnostics.test.mjs",
61
61
  "prepack": "npm run build && node ../../scripts/generate-package-sbom.mjs",
62
62
  "prepublishOnly": "npm test"
63
63
  }