HandoffRouter
ChatGateway lets a browser hold a text conversation. HandoffRouter is the other
half the SignalWire address widget expects: three routes, served at the same URL prefix as the
gateway, that move that conversation to a phone call and back, and let a visitor type into a live
call. Without these routes, a gateway answers the widget’s chat requests and returns 404 for the
rest.
Every route checks the request origin against the gateway’s allowlist. Unknown, expired, and
already-redeemed nonces all answer 404, so a caller can’t probe whether a given call is live.
This class implements the wire contract: the routes, the nonce, the ordering guarantee, and the spend limits. What a conversation is belongs to your application, and arrives as callbacks: where a leg’s transcript is written, and how a call is ended or spoken into.
How the nonce works
A browser can’t be trusted to name a call, since a page-supplied call ID would let anyone inject
speech into a stranger’s call. Instead the browser proves which call it is on. Your application puts
a random handoff_nonce in the user variables of one dial, registers it against that
call from a per-call config callback (see addPerCallConfig()), and the
browser presents it later. The same dial carries the chat_handle, so the callback recovers the
conversation id with readHandle().
Exchanging a nonce for a chat handle works once. Typing is repeatable, up to maxMessagesPerCall,
until /handoff uses the nonce or nonceTtl seconds pass from its registration, even if the call is
still live.
The ordering guarantee
A new medium doesn’t start until the one it replaces has ended and its record is written.
/handoff ends the call and waits for your captureLeg callback to confirm the capture before it
issues a handle. /escalate waits for the chat leg’s capture before it returns. Without the wait,
the new medium’s config fetch can run before the record exists, and the new leg starts with no
history.
The registry lives in the serving process, like the gateway’s caps. A redemption must reach the
replica that served the dial: run one replica, use sticky routing, or pass a shared registry.
Registering, redeeming, and taking a typing slot each read the table and write it back, so across
replicas that share a registry a nonce can be redeemed once by each, and the typing cap can be
passed. Use one nonce table, or route each call’s requests to one replica.
Parameters
The constructor takes one options object, HandoffRouterOptions.
gateway
The gateway that owns the conversations. Used to issue and read handles and to check origins, so all routes on the URL apply the same origin policy.
captureLeg
Ends a leg and writes its record: captureLeg(conversationId, medium), where medium is voice
or chat. Return true only once the record is written. Sync or async. When omitted, nothing
waits and the ordering guarantee doesn’t hold.
endCall
Hangs a call up server-side so its teardown hooks run now: endCall(callId).
sendMessage
Delivers typed text into a live call as if the caller said it: sendMessage(callId, text). When
omitted, typing is off and /say returns 404.
nextConversationId
Returns the id for the new leg, from the current one. An ended conversation can’t be reopened, so
each leg needs a new id. Defaults to appending .1, or incrementing an existing .N suffix: root
becomes root.1, and root.2 becomes root.3. Keep . as the separator.
nonceTtl
Seconds a nonce stays usable for /handoff and /say, from registration.
maxMessagesPerCall
Typed messages allowed per call. Each is a billed turn, so this limits spending as well as abuse.
captureTimeout
Seconds to wait for captureLeg. An upper limit; a capture normally takes under a second. On
timeout the next medium starts without this leg’s record, and a warning is logged.
registry
The nonce table. Supply one backed by shared storage to run more than one replica. It may return
copies of its entries, because the router writes every change back with set().
Properties
gateway, captureLeg, endCall, sendMessage, nextConversationId, nonceTtl,
maxMessagesPerCall, and captureTimeout are readable and writable on the instance. captureLeg,
endCall, and sendMessage are null when not set.
Exports
All of these are importable from @signalwire/sdk.
NonceEntry
What a registered nonce gives access to.
NonceEntry.conversationId
The conversation the call belongs to.
NonceEntry.callId
The platform call id, from the request the platform sent.
NonceEntry.issuedAt
When it was registered, in monotonic seconds, used for expiry.
NonceEntry.messages
Typed messages delivered so far.
NonceEntry.redeemed
true once /handoff has used the nonce. The entry stays in the table until it would have
expired, so registering the nonce again can’t make it usable again.
CaptureLeg
The type of captureLeg.
EndCall
The type of endCall.
SendMessage
The type of sendMessage.
HandoffRouterOptions
The constructor’s options object, described under Parameters.
MAX_REQUEST_BODY_BYTES (64 KiB) and MAX_MESSAGE_BYTES (8 KiB of UTF-8) apply to these routes as
they do to the gateway. A body over the first, or /say text over the second, is answered with
413. Both are checked before the nonce is looked up, so the answer says nothing about whether the
nonce is live.
Methods
Examples
Register the nonce from the per-call config callback of the dial that carried it, reading the call id from the platform’s request and never from anything the browser sent.
A captureLeg, endCall, or sendMessage that throws is logged and treated as a failure. A
sendMessage that throws or returns false doesn’t count against maxMessagesPerCall.
The option types and a shared registry: