@polymorfa/browser 0.1.0-dev.20260922094209
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/LICENSE +21 -0
- package/README.md +248 -0
- package/dist/calls/api.d.ts +22 -0
- package/dist/calls/api.d.ts.map +1 -0
- package/dist/calls/api.js +104 -0
- package/dist/calls/api.js.map +1 -0
- package/dist/calls/backend.d.ts +85 -0
- package/dist/calls/backend.d.ts.map +1 -0
- package/dist/calls/backend.js +141 -0
- package/dist/calls/backend.js.map +1 -0
- package/dist/calls/client.d.ts +56 -0
- package/dist/calls/client.d.ts.map +1 -0
- package/dist/calls/client.js +260 -0
- package/dist/calls/client.js.map +1 -0
- package/dist/calls/controller.d.ts +329 -0
- package/dist/calls/controller.d.ts.map +1 -0
- package/dist/calls/controller.js +1328 -0
- package/dist/calls/controller.js.map +1 -0
- package/dist/calls/diagnostics.d.ts +52 -0
- package/dist/calls/diagnostics.d.ts.map +1 -0
- package/dist/calls/diagnostics.js +188 -0
- package/dist/calls/diagnostics.js.map +1 -0
- package/dist/calls/index.d.ts +3 -0
- package/dist/calls/index.d.ts.map +1 -0
- package/dist/calls/index.js +5 -0
- package/dist/calls/index.js.map +1 -0
- package/dist/calls/internal.d.ts +8 -0
- package/dist/calls/internal.d.ts.map +1 -0
- package/dist/calls/internal.js +8 -0
- package/dist/calls/internal.js.map +1 -0
- package/dist/calls/media.d.ts +144 -0
- package/dist/calls/media.d.ts.map +1 -0
- package/dist/calls/media.js +512 -0
- package/dist/calls/media.js.map +1 -0
- package/dist/calls/signaling.d.ts +74 -0
- package/dist/calls/signaling.d.ts.map +1 -0
- package/dist/calls/signaling.js +169 -0
- package/dist/calls/signaling.js.map +1 -0
- package/dist/calls/socket.d.ts +95 -0
- package/dist/calls/socket.d.ts.map +1 -0
- package/dist/calls/socket.js +267 -0
- package/dist/calls/socket.js.map +1 -0
- package/dist/chat/composer.d.ts +59 -0
- package/dist/chat/composer.d.ts.map +1 -0
- package/dist/chat/composer.js +217 -0
- package/dist/chat/composer.js.map +1 -0
- package/dist/chat/conversation.d.ts +71 -0
- package/dist/chat/conversation.d.ts.map +1 -0
- package/dist/chat/conversation.js +204 -0
- package/dist/chat/conversation.js.map +1 -0
- package/dist/chat/index.d.ts +4 -0
- package/dist/chat/index.d.ts.map +1 -0
- package/dist/chat/index.js +4 -0
- package/dist/chat/index.js.map +1 -0
- package/dist/chat/voice-recorder.d.ts +75 -0
- package/dist/chat/voice-recorder.d.ts.map +1 -0
- package/dist/chat/voice-recorder.js +346 -0
- package/dist/chat/voice-recorder.js.map +1 -0
- package/dist/controller.d.ts +16 -0
- package/dist/controller.d.ts.map +1 -0
- package/dist/controller.js +50 -0
- package/dist/controller.js.map +1 -0
- package/dist/diagnostics.d.ts +27 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +2 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/dropin/client.d.ts +82 -0
- package/dist/dropin/client.d.ts.map +1 -0
- package/dist/dropin/client.js +314 -0
- package/dist/dropin/client.js.map +1 -0
- package/dist/dropin/connect.d.ts +23 -0
- package/dist/dropin/connect.d.ts.map +1 -0
- package/dist/dropin/connect.js +54 -0
- package/dist/dropin/connect.js.map +1 -0
- package/dist/dropin/inbox.d.ts +131 -0
- package/dist/dropin/inbox.d.ts.map +1 -0
- package/dist/dropin/inbox.js +406 -0
- package/dist/dropin/inbox.js.map +1 -0
- package/dist/dropin/index.d.ts +5 -0
- package/dist/dropin/index.d.ts.map +1 -0
- package/dist/dropin/index.js +5 -0
- package/dist/dropin/index.js.map +1 -0
- package/dist/dropin/permissions.d.ts +29 -0
- package/dist/dropin/permissions.d.ts.map +1 -0
- package/dist/dropin/permissions.js +53 -0
- package/dist/dropin/permissions.js.map +1 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +41 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.d.ts +12 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +13 -0
- package/dist/internal.js.map +1 -0
- package/dist/messaging/client.d.ts +54 -0
- package/dist/messaging/client.d.ts.map +1 -0
- package/dist/messaging/client.js +153 -0
- package/dist/messaging/client.js.map +1 -0
- package/dist/messaging/composer.d.ts +12 -0
- package/dist/messaging/composer.d.ts.map +1 -0
- package/dist/messaging/composer.js +36 -0
- package/dist/messaging/composer.js.map +1 -0
- package/dist/messaging/index.d.ts +4 -0
- package/dist/messaging/index.d.ts.map +1 -0
- package/dist/messaging/index.js +3 -0
- package/dist/messaging/index.js.map +1 -0
- package/dist/messaging/types.d.ts +209 -0
- package/dist/messaging/types.d.ts.map +1 -0
- package/dist/messaging/types.js +2 -0
- package/dist/messaging/types.js.map +1 -0
- package/dist/templates/controller.d.ts +45 -0
- package/dist/templates/controller.d.ts.map +1 -0
- package/dist/templates/controller.js +298 -0
- package/dist/templates/controller.js.map +1 -0
- package/dist/templates/index.d.ts +4 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +3 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/templates/transport.d.ts +7 -0
- package/dist/templates/transport.d.ts.map +1 -0
- package/dist/templates/transport.js +124 -0
- package/dist/templates/transport.js.map +1 -0
- package/dist/templates/types.d.ts +141 -0
- package/dist/templates/types.d.ts.map +1 -0
- package/dist/templates/types.js +2 -0
- package/dist/templates/types.js.map +1 -0
- package/dist/token.d.ts +37 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +152 -0
- package/dist/token.js.map +1 -0
- package/dist/transport.d.ts +53 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +328 -0
- package/dist/transport.js.map +1 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Polymorfa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# `@polymorfa/browser`
|
|
2
|
+
|
|
3
|
+
Framework-neutral browser transport and product controllers for Polymorfa.
|
|
4
|
+
|
|
5
|
+
## Drop-in client
|
|
6
|
+
|
|
7
|
+
`PolymorfaClient` (`createPolymorfaClient({ tokenEndpoint })`) fetches client
|
|
8
|
+
tokens from your `createPolymorfaHandler` route, refreshes them before expiry
|
|
9
|
+
with backoff, and reports the grant through `can(permission)`. It never sends
|
|
10
|
+
or chooses permissions. `InboxController` with `createHandlerInboxSource()`
|
|
11
|
+
drives an inbox from the handler's `history` and `events` routes, and
|
|
12
|
+
`connectWhatsApp(client)` asks the handler for a QuickLink and opens the
|
|
13
|
+
hosted URL. The React and Web Component drop-ins are built on these.
|
|
14
|
+
|
|
15
|
+
`@polymorfa/browser` depends on `@polymorfa/sdk` for the Calls client
|
|
16
|
+
(`@polymorfa/sdk/calls`). It imports nothing else from that package, so no
|
|
17
|
+
server code reaches the browser, and call errors such as `CallsDisabledError`
|
|
18
|
+
are the classes exported from `@polymorfa/sdk/calls`.
|
|
19
|
+
|
|
20
|
+
This package accepts only short-lived `pmfa_ct_` client tokens obtained from an application callback. It never accepts or stores server API keys.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import {
|
|
24
|
+
BrowserMessagingClient,
|
|
25
|
+
createClientTokenProvider,
|
|
26
|
+
} from "@polymorfa/browser";
|
|
27
|
+
|
|
28
|
+
const messaging = new BrowserMessagingClient({
|
|
29
|
+
session: "support",
|
|
30
|
+
getClientToken: createClientTokenProvider(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await messaging.messages.send({
|
|
34
|
+
conversation: { phoneNumber: "+15551234567" },
|
|
35
|
+
content: { text: "Hello" },
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Requests expose status, request ID, response headers, and attempt count. Safe reads retry transient failures; mutations retry only when supplied an idempotency key. Caller cancellation and timeouts use distinct exported error types.
|
|
40
|
+
|
|
41
|
+
Product controllers use immutable snapshots and `subscribe()`/`getSnapshot()` so Web Components and framework bindings share the same behavior.
|
|
42
|
+
|
|
43
|
+
`BrowserMessagingClient` binds one session and exposes only the exact
|
|
44
|
+
client-token allowlist: message actions, presence reads/subscriptions, contact
|
|
45
|
+
reads, and permitted widget pairing actions. Session start and status require a server
|
|
46
|
+
Platform credential; use QuickLink for onboarding. The browser widget start/status
|
|
47
|
+
helpers are removed in this breaking contract revision. Conversation history,
|
|
48
|
+
template management and media upload remain application-owned server adapters.
|
|
49
|
+
`createBrowserCalls` supplies client-token call lifecycle and controls.
|
|
50
|
+
|
|
51
|
+
`createBrowserComposerActions` connects a `MessageComposerController` to the
|
|
52
|
+
message resource for text and reply sends. Supply an upload adapter and a
|
|
53
|
+
`createMessage` mapping for attachments; client tokens cannot call the media
|
|
54
|
+
routes directly.
|
|
55
|
+
|
|
56
|
+
## Voice notes
|
|
57
|
+
|
|
58
|
+
`VoiceNoteRecorder` records from the microphone with `getUserMedia` and
|
|
59
|
+
`MediaRecorder`. Its snapshot has `status` (`idle`, `requesting`,
|
|
60
|
+
`recording`, `stopping`, or `error`), `elapsed` milliseconds, an input
|
|
61
|
+
`level` from 0 to 1, the container `mimeType`, and an `error` of
|
|
62
|
+
`permission-denied`, `unavailable`, or `failed`.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { VoiceNoteRecorder, localAttachmentFromFile } from "@polymorfa/browser";
|
|
66
|
+
|
|
67
|
+
if (VoiceNoteRecorder.isSupported()) {
|
|
68
|
+
const recorder = new VoiceNoteRecorder();
|
|
69
|
+
await recorder.start();
|
|
70
|
+
// later
|
|
71
|
+
const file = await recorder.stop(); // File or undefined
|
|
72
|
+
if (file) await composer.addAttachment(localAttachmentFromFile(file));
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`stop()` resolves with an `audio/webm`, `audio/ogg`, or `audio/mp4` file named
|
|
77
|
+
`voice-note-<time>.webm` (`.ogg` or `.m4a`); the time is ISO 8601 with `:` and `.`
|
|
78
|
+
replaced by `-`. `cancel()` discards the recording. The microphone tracks
|
|
79
|
+
stop when recording ends, when a pending permission request is cancelled,
|
|
80
|
+
and on `dispose()`.
|
|
81
|
+
|
|
82
|
+
## Calls
|
|
83
|
+
|
|
84
|
+
`createBrowserCalls` is the browser calling component: incoming calls,
|
|
85
|
+
placement, answer, join, leave and end, microphone, camera and device
|
|
86
|
+
control, and per-participant video. Your server mints a short-lived client
|
|
87
|
+
token with `POST /platform/client-tokens`, granting `voip_place`,
|
|
88
|
+
`voip_answer`, and `voip_signal` and the destination and concurrency rules you
|
|
89
|
+
need. That token is the only credential the component uses; it never appears
|
|
90
|
+
in a URL.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import {
|
|
94
|
+
createBrowserCalls,
|
|
95
|
+
createClientTokenProvider,
|
|
96
|
+
} from "@polymorfa/browser";
|
|
97
|
+
|
|
98
|
+
const calls = createBrowserCalls({
|
|
99
|
+
session: "support",
|
|
100
|
+
getClientToken: createClientTokenProvider(),
|
|
101
|
+
onError: (error) => {
|
|
102
|
+
// "unauthorized": the platform stopped accepting the token.
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
await calls.connect();
|
|
106
|
+
await calls.controller.place("+15550100");
|
|
107
|
+
// When the application releases the widget:
|
|
108
|
+
await calls.dispose();
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Pass `calls.controller` to React's `CallSurface` or the `pmfa-call` element.
|
|
112
|
+
`controller.call` exposes the shared `Call`, including its state, duration,
|
|
113
|
+
end reason, claim state and `addParticipant()` method. Direct placement
|
|
114
|
+
supports linked WhatsApp devices.
|
|
115
|
+
|
|
116
|
+
### Tokens
|
|
117
|
+
|
|
118
|
+
Return `{ value, audience: "browser", expiresAt }` from `getClientToken` so
|
|
119
|
+
the SDK can replace the token before it expires without interrupting calls.
|
|
120
|
+
When a token expires or is revoked, `onError` receives
|
|
121
|
+
`code: "unauthorized"` and the next reconnect asks `getClientToken` for a new
|
|
122
|
+
token. REST requests refresh a cached token before it expires; a provider
|
|
123
|
+
that returns an expired token fails with `expired_client_token`.
|
|
124
|
+
|
|
125
|
+
### Answering, joining, and leaving
|
|
126
|
+
|
|
127
|
+
Incoming calls ring until a participant answers or declines them.
|
|
128
|
+
`snapshot.invitations` lists every ringing call; several can ring at once and
|
|
129
|
+
the controller never declines one for you. The first invitation is displayed;
|
|
130
|
+
`controller.select(callId)` shows another while no call is active, and
|
|
131
|
+
`controller.dismiss(callId?)` hides one locally without declining it.
|
|
132
|
+
|
|
133
|
+
- `controller.answer({ exclusive, video, callId })` accepts the call and
|
|
134
|
+
connects microphone, camera and speaker. `exclusive` defaults to `false`, which leaves other
|
|
135
|
+
participants ringing so they can join. `exclusive: true` claims the call.
|
|
136
|
+
- `controller.join({ video, callId })` joins a call another participant
|
|
137
|
+
answered without a claim (`snapshot.canJoin`).
|
|
138
|
+
- `snapshot.claimedByOther` is `true` when another participant claimed the
|
|
139
|
+
call. `answer()` and `join()` then reject with `CallClaimedError`, and
|
|
140
|
+
`reject()` is refused: declining would end the claimer's call.
|
|
141
|
+
- While calling is turned off for the number, `place()`, `answer()` and
|
|
142
|
+
`join()` reject with `CallsDisabledError` from `@polymorfa/sdk/calls`.
|
|
143
|
+
- While an answer or join is in flight, `snapshot.answering` is `true` and
|
|
144
|
+
`answer()`, `join()`, `reject()` and `place()` are refused. `select()` and
|
|
145
|
+
`dismiss()` still work: the answered call is displayed once it is accepted,
|
|
146
|
+
even if you selected another invitation. If you dismissed it, the controller
|
|
147
|
+
leaves it (or ends it when the answer claimed it) instead of showing it.
|
|
148
|
+
Only that call ending cancels the answer; `end()` for another displayed
|
|
149
|
+
call is refused meanwhile.
|
|
150
|
+
- If the platform refuses an answer, the call stays displayed as `incoming`
|
|
151
|
+
with `snapshot.error` (`answer_failed` or `join_failed`), so it can be
|
|
152
|
+
retried or declined. If media fails after the call was answered, the
|
|
153
|
+
failed call is published as `error` (or `ended` for a terminal offer)
|
|
154
|
+
and the next waiting invitation is displayed. The failure stays in
|
|
155
|
+
`snapshot.error`; `error.callId` names the call it belongs to.
|
|
156
|
+
- `controller.reject()` declines a ringing call and ends it for everyone.
|
|
157
|
+
- `controller.leave()` closes this browser's connection; the call continues.
|
|
158
|
+
On a call this browser placed that has not connected, it ends the call
|
|
159
|
+
instead, so the callee stops ringing.
|
|
160
|
+
- `controller.end()` (and `hangup()`) ends the call for every participant.
|
|
161
|
+
- Disposing the controller leaves a joined call. It ends only a call this
|
|
162
|
+
browser placed that is still ringing.
|
|
163
|
+
|
|
164
|
+
After a call ends, the next waiting invitation is displayed.
|
|
165
|
+
|
|
166
|
+
### Media
|
|
167
|
+
|
|
168
|
+
- `controller.localStream` is your microphone and camera.
|
|
169
|
+
- `controller.remoteStream` is the merged call audio; play it through one
|
|
170
|
+
element.
|
|
171
|
+
- `controller.remoteVideos` holds one entry per remote participant who sends
|
|
172
|
+
video: a `MediaStream`, a stable `key`, a display `label`, and the
|
|
173
|
+
`participant` or the other connection's `connectionId` and
|
|
174
|
+
`connectionParticipant`. `snapshot.remoteVideos` carries the same entries
|
|
175
|
+
without streams, so a render can react to changes. Videos are separate
|
|
176
|
+
streams; nothing is composed.
|
|
177
|
+
- `controller.setMuted()`, `enableVideo()`, `switchDevice()`,
|
|
178
|
+
`setPreferredDevices()` and `refreshDevices()` control capture and playback.
|
|
179
|
+
- `snapshot.participants` lists the call's WhatsApp participants.
|
|
180
|
+
- A dropped connection shows `reconnecting` and recovers or ends as
|
|
181
|
+
`connection_failed` after a bounded window.
|
|
182
|
+
|
|
183
|
+
Disposal releases tracks and connections.
|
|
184
|
+
|
|
185
|
+
### Diagnostics
|
|
186
|
+
|
|
187
|
+
While a call's media is open, the controller sends call diagnostics for this
|
|
188
|
+
browser's connection to `POST /messaging/voip/calls/{id}/reports`, where
|
|
189
|
+
they appear with the call in the Console:
|
|
190
|
+
|
|
191
|
+
- Every 15 seconds, and once when the connection closes: round-trip time,
|
|
192
|
+
audio jitter, packets lost and received, the audio and video codecs, the
|
|
193
|
+
ICE candidate type (`relay` means a TURN relay), and how many times the
|
|
194
|
+
connection reconnected. Figures come from `RTCPeerConnection.getStats()`.
|
|
195
|
+
- When something fails: an error code (`media_permission_denied`,
|
|
196
|
+
`device_not_found`, `device_in_use`, `ice_failed`, `negotiation_failed`,
|
|
197
|
+
`media_timeout`, `reconnect_exhausted` or `unsupported_browser`).
|
|
198
|
+
|
|
199
|
+
Each report names the connection and `@polymorfa/browser` with its version.
|
|
200
|
+
Reports contain no phone numbers, names, device labels, IP addresses, audio or
|
|
201
|
+
video. They are best-effort: a failed report is not retried, a refused one
|
|
202
|
+
(other than rate limiting) stops reporting for the connection, and nothing
|
|
203
|
+
about reporting affects the call. The client token needs the `voip_signal`
|
|
204
|
+
action. Turn reporting off with `createBrowserCalls({ diagnostics: false })`.
|
|
205
|
+
|
|
206
|
+
If a reject or hangup request fails, the call stays active and its controls remain
|
|
207
|
+
available for retry. The UI shows a localized failure message and keeps existing
|
|
208
|
+
media connected until the call ends. `snapshot.error` clears when ending succeeds
|
|
209
|
+
or when an incoming call is successfully answered after a failed reject.
|
|
210
|
+
|
|
211
|
+
## Template builder
|
|
212
|
+
|
|
213
|
+
The builder uses the canonical `TemplateDefinition` contract: standard,
|
|
214
|
+
carousel, authentication, and limited-time-offer templates with structured
|
|
215
|
+
headers, buttons, variables, and previews. Saving and Meta submission are
|
|
216
|
+
separate controller actions.
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
import {
|
|
220
|
+
TemplateBuilderController,
|
|
221
|
+
createSameOriginTemplateBuilderTransport,
|
|
222
|
+
} from "@polymorfa/browser";
|
|
223
|
+
|
|
224
|
+
const templates = new TemplateBuilderController(
|
|
225
|
+
createSameOriginTemplateBuilderTransport({
|
|
226
|
+
path: "/api/polymorfa/templates",
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
templates.create({
|
|
231
|
+
name: "order_ready",
|
|
232
|
+
definition: {
|
|
233
|
+
version: 1,
|
|
234
|
+
kind: "standard",
|
|
235
|
+
category: "UTILITY",
|
|
236
|
+
language: "en_US",
|
|
237
|
+
body: "Hello {{name}}",
|
|
238
|
+
variables: [{ name: "name", type: "text", example: "Ada" }],
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await templates.save();
|
|
243
|
+
await templates.refreshPreview();
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The same-origin transport sends application actions with browser cookies. It
|
|
247
|
+
does not accept a server credential, project slug, or Cloud API session. The
|
|
248
|
+
application route resolves those values after authorizing the request.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type AcceptCallOptions, type AcceptCallResult, type CallReport, type CallsApi, type CallsToken, type CallsTokenRequest, type Participant, type PlaceCallRequest } from "@polymorfa/sdk/calls/internal";
|
|
2
|
+
import { BrowserTransport } from "../transport.js";
|
|
3
|
+
/**
|
|
4
|
+
* Client-token call controls. The token determines the session and the
|
|
5
|
+
* participant, so `session` and `participant` are never sent.
|
|
6
|
+
*/
|
|
7
|
+
export declare class BrowserCallsApi implements CallsApi {
|
|
8
|
+
#private;
|
|
9
|
+
constructor(transport: BrowserTransport);
|
|
10
|
+
token(request?: CallsTokenRequest): Promise<CallsToken>;
|
|
11
|
+
socketUrl(path: string): string;
|
|
12
|
+
place(input: PlaceCallRequest, signal?: AbortSignal): Promise<{
|
|
13
|
+
readonly callId: string;
|
|
14
|
+
}>;
|
|
15
|
+
accept(callId: string, options: AcceptCallOptions, signal?: AbortSignal): Promise<AcceptCallResult>;
|
|
16
|
+
reject(callId: string, signal?: AbortSignal): Promise<void>;
|
|
17
|
+
leave(callId: string, connectionId: string, signal?: AbortSignal): Promise<void>;
|
|
18
|
+
end(callId: string, signal?: AbortSignal): Promise<void>;
|
|
19
|
+
report(callId: string, report: CallReport, signal?: AbortSignal): Promise<void>;
|
|
20
|
+
addParticipant(callId: string, to: string, signal?: AbortSignal): Promise<Participant>;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/calls/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACtB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAGnD;;;GAGG;AACH,qBAAa,eAAgB,YAAW,QAAQ;;gBAIlC,SAAS,EAAE,gBAAgB;IAKvC,KAAK,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,UAAU,CAAC;IAI3D,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAMzB,KAAK,CACT,KAAK,EAAE,gBAAgB,EACvB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAyBjC,MAAM,CACV,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,iBAAiB,EAC1B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,gBAAgB,CAAC;IA4B5B,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3D,KAAK,CACH,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC;IAIhB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxD,MAAM,CACJ,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC;IAIV,cAAc,CAClB,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,EACV,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,WAAW,CAAC;CAexB"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { CallsApiError, isParticipant, } from "@polymorfa/sdk/calls/internal";
|
|
2
|
+
import { BrowserTransport } from "../transport.js";
|
|
3
|
+
import { CallsSignalingClient, claimedError } from "./signaling.js";
|
|
4
|
+
/**
|
|
5
|
+
* Client-token call controls. The token determines the session and the
|
|
6
|
+
* participant, so `session` and `participant` are never sent.
|
|
7
|
+
*/
|
|
8
|
+
export class BrowserCallsApi {
|
|
9
|
+
#transport;
|
|
10
|
+
#signaling;
|
|
11
|
+
constructor(transport) {
|
|
12
|
+
this.#transport = transport;
|
|
13
|
+
this.#signaling = new CallsSignalingClient(transport);
|
|
14
|
+
}
|
|
15
|
+
token(request = {}) {
|
|
16
|
+
return this.#signaling.token(request);
|
|
17
|
+
}
|
|
18
|
+
socketUrl(path) {
|
|
19
|
+
const url = this.#signaling.socketUrl(path);
|
|
20
|
+
if (!/^wss?:\/\//.test(url))
|
|
21
|
+
throw malformed("socket URL");
|
|
22
|
+
return url;
|
|
23
|
+
}
|
|
24
|
+
async place(input, signal) {
|
|
25
|
+
const response = await this.#transport
|
|
26
|
+
.request({
|
|
27
|
+
method: "POST",
|
|
28
|
+
path: "/messaging/voip/calls",
|
|
29
|
+
body: {
|
|
30
|
+
to: input.to,
|
|
31
|
+
video: input.video,
|
|
32
|
+
...(input.exclusive === undefined
|
|
33
|
+
? {}
|
|
34
|
+
: { exclusive: input.exclusive }),
|
|
35
|
+
},
|
|
36
|
+
idempotencyKey: input.idempotencyKey,
|
|
37
|
+
...(signal === undefined ? {} : { signal }),
|
|
38
|
+
})
|
|
39
|
+
.catch((cause) => {
|
|
40
|
+
throw claimedError(cause);
|
|
41
|
+
});
|
|
42
|
+
const callId = response.data?.data?.callId;
|
|
43
|
+
if (typeof callId !== "string" || !callId)
|
|
44
|
+
throw malformed("call id");
|
|
45
|
+
return { callId };
|
|
46
|
+
}
|
|
47
|
+
async accept(callId, options, signal) {
|
|
48
|
+
const response = await this.#transport
|
|
49
|
+
.request({
|
|
50
|
+
method: "POST",
|
|
51
|
+
path: `/messaging/voip/calls/${encodeURIComponent(callId)}/accept`,
|
|
52
|
+
body: {
|
|
53
|
+
exclusive: options.exclusive === true,
|
|
54
|
+
...(options.video === undefined ? {} : { video: options.video }),
|
|
55
|
+
},
|
|
56
|
+
...(signal === undefined ? {} : { signal }),
|
|
57
|
+
})
|
|
58
|
+
.catch((cause) => {
|
|
59
|
+
throw claimedError(cause);
|
|
60
|
+
});
|
|
61
|
+
const data = response.data?.data;
|
|
62
|
+
if (typeof data?.["answered"] !== "boolean" ||
|
|
63
|
+
typeof data["answeredBy"] !== "string" ||
|
|
64
|
+
typeof data["exclusive"] !== "boolean")
|
|
65
|
+
throw malformed("accept result");
|
|
66
|
+
return {
|
|
67
|
+
answered: data["answered"],
|
|
68
|
+
answeredBy: data["answeredBy"],
|
|
69
|
+
exclusive: data["exclusive"],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
reject(callId, signal) {
|
|
73
|
+
return this.#signaling.reject(callId, signal);
|
|
74
|
+
}
|
|
75
|
+
leave(callId, connectionId, signal) {
|
|
76
|
+
return this.#signaling.leave(callId, connectionId, signal);
|
|
77
|
+
}
|
|
78
|
+
end(callId, signal) {
|
|
79
|
+
return this.#signaling.end(callId, signal);
|
|
80
|
+
}
|
|
81
|
+
report(callId, report, signal) {
|
|
82
|
+
return this.#signaling.report(callId, report, signal);
|
|
83
|
+
}
|
|
84
|
+
async addParticipant(callId, to, signal) {
|
|
85
|
+
const response = await this.#transport
|
|
86
|
+
.request({
|
|
87
|
+
method: "POST",
|
|
88
|
+
path: `/messaging/voip/calls/${encodeURIComponent(callId)}/participants`,
|
|
89
|
+
body: { to },
|
|
90
|
+
...(signal === undefined ? {} : { signal }),
|
|
91
|
+
})
|
|
92
|
+
.catch((cause) => {
|
|
93
|
+
throw claimedError(cause);
|
|
94
|
+
});
|
|
95
|
+
const p = response.data?.data;
|
|
96
|
+
if (!isParticipant(p))
|
|
97
|
+
throw malformed("participant");
|
|
98
|
+
return p;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function malformed(field) {
|
|
102
|
+
return new CallsApiError(200, "malformed_response", `Calls response has an invalid ${field}.`);
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/calls/api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { CallEndReason, CallLifecycleEvent, CallsBackend, IncomingCall, PlaceCallInput } from "./controller.js";
|
|
2
|
+
import type { CallsSignaling } from "./signaling.js";
|
|
3
|
+
/**
|
|
4
|
+
* The parts of the `call.received` webhook payload the browser needs. The
|
|
5
|
+
* caller is a public identity reference; a bare string is accepted for hand-built
|
|
6
|
+
* payloads.
|
|
7
|
+
*/
|
|
8
|
+
export interface CallReceivedWebhookPayload {
|
|
9
|
+
readonly callId: string;
|
|
10
|
+
readonly from: string | {
|
|
11
|
+
readonly id?: string;
|
|
12
|
+
readonly phoneNumber?: string;
|
|
13
|
+
readonly bsuid?: string;
|
|
14
|
+
readonly username?: string;
|
|
15
|
+
};
|
|
16
|
+
readonly hasVideo?: boolean;
|
|
17
|
+
readonly capabilities?: {
|
|
18
|
+
readonly video?: boolean;
|
|
19
|
+
readonly invite?: boolean;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Turn a `call.received` webhook payload into an {@link IncomingCall}. The
|
|
24
|
+
* application receives the webhook on its server, relays it to the browser
|
|
25
|
+
* over its own realtime channel, and hands it to {@link IncomingCallRelay}.
|
|
26
|
+
* The caller is shown by phone number when present, otherwise by public ID.
|
|
27
|
+
*/
|
|
28
|
+
export declare function incomingCallFromWebhook(payload: CallReceivedWebhookPayload): IncomingCall;
|
|
29
|
+
/**
|
|
30
|
+
* Application-fed source of inbound call notifications for deployments that
|
|
31
|
+
* relay `call.received`, `call.accepted`, and `call.ended` webhook events over
|
|
32
|
+
* their own realtime channel. {@link CallsSocket} is the push alternative:
|
|
33
|
+
* it subscribes to the same lifecycle stream directly from the API.
|
|
34
|
+
*/
|
|
35
|
+
export declare class IncomingCallRelay {
|
|
36
|
+
#private;
|
|
37
|
+
/** Announce an inbound call; the controller moves to `incoming`. */
|
|
38
|
+
receive(call: IncomingCall): void;
|
|
39
|
+
/**
|
|
40
|
+
* Announce that a call was answered (from `call.accepted`). Pass its
|
|
41
|
+
* `answeredBy` and `exclusive` so other browsers stop ringing for a claimed
|
|
42
|
+
* call or offer Join for a shared one.
|
|
43
|
+
*/
|
|
44
|
+
accepted(callId: string, claim?: {
|
|
45
|
+
readonly answeredBy?: string;
|
|
46
|
+
readonly exclusive?: boolean;
|
|
47
|
+
/** The webhook's `capabilities`, when present. */
|
|
48
|
+
readonly capabilities?: {
|
|
49
|
+
readonly video?: boolean;
|
|
50
|
+
readonly invite?: boolean;
|
|
51
|
+
};
|
|
52
|
+
}): void;
|
|
53
|
+
/** Announce that the remote side ended a call (from `call.ended`). */
|
|
54
|
+
ended(callId: string, reason?: CallEndReason): void;
|
|
55
|
+
subscribe(listener: (event: CallLifecycleEvent) => void): () => void;
|
|
56
|
+
}
|
|
57
|
+
export interface SignalingCallsBackendOptions {
|
|
58
|
+
/** REST signaling client for the `/messaging/voip/calls/{id}` paths. */
|
|
59
|
+
readonly signaling: CallsSignaling;
|
|
60
|
+
/** Inbound call notifications: a {@link CallsSocket} or an application-fed {@link IncomingCallRelay}. */
|
|
61
|
+
readonly incoming: Pick<IncomingCallRelay, "subscribe">;
|
|
62
|
+
/**
|
|
63
|
+
* Places an outbound call through the application's own server and resolves
|
|
64
|
+
* the call id the platform assigned it.
|
|
65
|
+
*
|
|
66
|
+
* This custom backend delegates placement to the supplied hook. Use
|
|
67
|
+
* createBrowserCalls for built-in client-token placement. Without a hook,
|
|
68
|
+
* outbound calling reports place_failed instead of inventing a call id.
|
|
69
|
+
*/
|
|
70
|
+
readonly place?: (input: PlaceCallInput, signal: AbortSignal) => Promise<string | {
|
|
71
|
+
readonly callId: string;
|
|
72
|
+
}>;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A {@link CallsBackend} over the REST signaling surface alone. Answering an
|
|
76
|
+
* inbound call accepts it (claiming it only when the caller asks for
|
|
77
|
+
* `exclusive`), then the media factory offers SDP for its connection. Reject
|
|
78
|
+
* declines a ringing call and hang-up ends the call for everyone; leaving
|
|
79
|
+
* closes only this browser's connection. Everything runs on the client token.
|
|
80
|
+
*
|
|
81
|
+
* Supply place for custom outbound placement. createBrowserCalls provides
|
|
82
|
+
* direct client-token placement through the shared Calls client.
|
|
83
|
+
*/
|
|
84
|
+
export declare function createSignalingCallsBackend(options: SignalingCallsBackendOptions): CallsBackend;
|
|
85
|
+
//# sourceMappingURL=backend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/calls/backend.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,YAAY,EACZ,cAAc,EACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErD;;;;GAIG;AACH,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EACT,MAAM,GACN;QACE,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;KAC5B,CAAC;IACN,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE;QACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;KAC3B,CAAC;CACH;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,0BAA0B,GAClC,YAAY,CAad;AAED;;;;;GAKG;AACH,qBAAa,iBAAiB;;IAG5B,oEAAoE;IACpE,OAAO,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAIjC;;;;OAIG;IACH,QAAQ,CACN,MAAM,EAAE,MAAM,EACd,KAAK,GAAE;QACL,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;QAC7B,kDAAkD;QAClD,QAAQ,CAAC,YAAY,CAAC,EAAE;YACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;YACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;SAC3B,CAAC;KACE,GACL,IAAI;IAyBP,sEAAsE;IACtE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,IAAI;IAQnD,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,GAAG,MAAM,IAAI;CAQrE;AAED,MAAM,WAAW,4BAA4B;IAC3C,wEAAwE;IACxE,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC,yGAAyG;IACzG,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,iBAAiB,EAAE,WAAW,CAAC,CAAC;IACxD;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,CACf,KAAK,EAAE,cAAc,EACrB,MAAM,EAAE,WAAW,KAChB,OAAO,CAAC,MAAM,GAAG;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpD;AAED;;;;;;;;;GASG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,4BAA4B,GACpC,YAAY,CAqDd"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { capabilitiesFrom, } from "@polymorfa/sdk/calls/internal";
|
|
2
|
+
/**
|
|
3
|
+
* Turn a `call.received` webhook payload into an {@link IncomingCall}. The
|
|
4
|
+
* application receives the webhook on its server, relays it to the browser
|
|
5
|
+
* over its own realtime channel, and hands it to {@link IncomingCallRelay}.
|
|
6
|
+
* The caller is shown by phone number when present, otherwise by public ID.
|
|
7
|
+
*/
|
|
8
|
+
export function incomingCallFromWebhook(payload) {
|
|
9
|
+
// Empty strings fall through like absent fields, matching `peerFrom` on the
|
|
10
|
+
// socket path — the two inbound routes must agree on the same payload.
|
|
11
|
+
const from = typeof payload.from === "string"
|
|
12
|
+
? payload.from
|
|
13
|
+
: (firstNonEmpty(payload.from.phoneNumber, payload.from.id) ?? "");
|
|
14
|
+
return {
|
|
15
|
+
callId: payload.callId,
|
|
16
|
+
from,
|
|
17
|
+
video: payload.hasVideo === true,
|
|
18
|
+
capabilities: capabilitiesFrom(payload.capabilities),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Application-fed source of inbound call notifications for deployments that
|
|
23
|
+
* relay `call.received`, `call.accepted`, and `call.ended` webhook events over
|
|
24
|
+
* their own realtime channel. {@link CallsSocket} is the push alternative:
|
|
25
|
+
* it subscribes to the same lifecycle stream directly from the API.
|
|
26
|
+
*/
|
|
27
|
+
export class IncomingCallRelay {
|
|
28
|
+
#listeners = new Set();
|
|
29
|
+
/** Announce an inbound call; the controller moves to `incoming`. */
|
|
30
|
+
receive(call) {
|
|
31
|
+
this.#emit({ type: "incomingCall", call });
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Announce that a call was answered (from `call.accepted`). Pass its
|
|
35
|
+
* `answeredBy` and `exclusive` so other browsers stop ringing for a claimed
|
|
36
|
+
* call or offer Join for a shared one.
|
|
37
|
+
*/
|
|
38
|
+
accepted(callId, claim = {}) {
|
|
39
|
+
this.#emit({
|
|
40
|
+
type: "accepted",
|
|
41
|
+
callId,
|
|
42
|
+
...(claim.answeredBy === undefined
|
|
43
|
+
? {}
|
|
44
|
+
: { answeredBy: claim.answeredBy }),
|
|
45
|
+
exclusive: claim.exclusive === true,
|
|
46
|
+
// Reported fields only; the controller merges them with the call's
|
|
47
|
+
// current capabilities.
|
|
48
|
+
...(claim.capabilities === undefined
|
|
49
|
+
? {}
|
|
50
|
+
: {
|
|
51
|
+
capabilities: {
|
|
52
|
+
...(typeof claim.capabilities.video === "boolean"
|
|
53
|
+
? { video: claim.capabilities.video }
|
|
54
|
+
: {}),
|
|
55
|
+
...(typeof claim.capabilities.invite === "boolean"
|
|
56
|
+
? { invite: claim.capabilities.invite }
|
|
57
|
+
: {}),
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Announce that the remote side ended a call (from `call.ended`). */
|
|
63
|
+
ended(callId, reason) {
|
|
64
|
+
this.#emit(reason === undefined
|
|
65
|
+
? { type: "ended", callId }
|
|
66
|
+
: { type: "ended", callId, reason });
|
|
67
|
+
}
|
|
68
|
+
subscribe(listener) {
|
|
69
|
+
this.#listeners.add(listener);
|
|
70
|
+
return () => this.#listeners.delete(listener);
|
|
71
|
+
}
|
|
72
|
+
#emit(event) {
|
|
73
|
+
for (const listener of [...this.#listeners])
|
|
74
|
+
listener(event);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A {@link CallsBackend} over the REST signaling surface alone. Answering an
|
|
79
|
+
* inbound call accepts it (claiming it only when the caller asks for
|
|
80
|
+
* `exclusive`), then the media factory offers SDP for its connection. Reject
|
|
81
|
+
* declines a ringing call and hang-up ends the call for everyone; leaving
|
|
82
|
+
* closes only this browser's connection. Everything runs on the client token.
|
|
83
|
+
*
|
|
84
|
+
* Supply place for custom outbound placement. createBrowserCalls provides
|
|
85
|
+
* direct client-token placement through the shared Calls client.
|
|
86
|
+
*/
|
|
87
|
+
export function createSignalingCallsBackend(options) {
|
|
88
|
+
return {
|
|
89
|
+
subscribe: (listener) => options.incoming.subscribe(listener),
|
|
90
|
+
place: async (input, signal) => {
|
|
91
|
+
throwIfAborted(signal);
|
|
92
|
+
const placeWith = options.place;
|
|
93
|
+
if (placeWith === undefined)
|
|
94
|
+
throw new Error("This backend needs a `place` hook. Use createBrowserCalls for direct client-token placement.");
|
|
95
|
+
const placed = await placeWith(input, signal);
|
|
96
|
+
throwIfAborted(signal);
|
|
97
|
+
// `place` is application code behind a public interface: a response that
|
|
98
|
+
// did not match its declared shape must not reach signaling as an
|
|
99
|
+
// `undefined` call id.
|
|
100
|
+
const callId = typeof placed === "string"
|
|
101
|
+
? placed
|
|
102
|
+
: placed?.callId;
|
|
103
|
+
if (typeof callId !== "string" || callId === "")
|
|
104
|
+
throw new Error("`place` resolved without a call id.");
|
|
105
|
+
return { callId };
|
|
106
|
+
},
|
|
107
|
+
answer: async (callId, signal, input) => {
|
|
108
|
+
throwIfAborted(signal);
|
|
109
|
+
const accept = options.signaling.accept;
|
|
110
|
+
if (accept === undefined)
|
|
111
|
+
throw new Error("This signaling client cannot answer calls.");
|
|
112
|
+
return accept.call(options.signaling, callId, {
|
|
113
|
+
exclusive: input?.exclusive === true,
|
|
114
|
+
...(input === undefined ? {} : { video: input.video }),
|
|
115
|
+
}, signal);
|
|
116
|
+
},
|
|
117
|
+
reject: async (callId, signal) => {
|
|
118
|
+
const reject = options.signaling.reject;
|
|
119
|
+
if (reject === undefined)
|
|
120
|
+
throw new Error("This signaling client cannot decline calls.");
|
|
121
|
+
await reject.call(options.signaling, callId, signal);
|
|
122
|
+
},
|
|
123
|
+
hangup: (callId, signal) => options.signaling.end(callId, signal),
|
|
124
|
+
...(options.signaling.report === undefined
|
|
125
|
+
? {}
|
|
126
|
+
: {
|
|
127
|
+
report: (callId, report) => options.signaling.report(callId, report),
|
|
128
|
+
}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function firstNonEmpty(...values) {
|
|
132
|
+
for (const value of values)
|
|
133
|
+
if (typeof value === "string" && value.length > 0)
|
|
134
|
+
return value;
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
function throwIfAborted(signal) {
|
|
138
|
+
if (signal.aborted)
|
|
139
|
+
throw signal.reason;
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=backend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../../src/calls/backend.ts"],"names":[],"mappings":"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"}
|