@fonoster/voice 0.30.0 → 0.31.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/README.md +1 -1
- package/dist/Session.d.ts +21 -6
- package/dist/Session.js +52 -12
- package/dist/VoiceServer.d.ts +2 -2
- package/dist/VoiceServer.js +3 -3
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/signature.d.ts +2 -2
- package/dist/signature.js +2 -2
- package/dist/types.d.ts +5 -1
- package/package.json +6 -7
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ $ npm install --save @fonoster/voice
|
|
|
26
26
|
```typescript
|
|
27
27
|
import { VoiceServer } from "@fonoster/voice";
|
|
28
28
|
|
|
29
|
-
//
|
|
29
|
+
// AppBridge signs every connection with the app's signing secret.
|
|
30
30
|
new VoiceServer({ port: 3000, signingSecret: process.env.APP_SIGNING_SECRET }).listen(
|
|
31
31
|
async (session) => {
|
|
32
32
|
await session.say("Hi there! What's your name?");
|
package/dist/Session.d.ts
CHANGED
|
@@ -37,13 +37,15 @@ export declare class Session {
|
|
|
37
37
|
readonly from: string;
|
|
38
38
|
readonly to: string;
|
|
39
39
|
readonly metadata: Record<string, unknown>;
|
|
40
|
+
/** The W3C traceparent of the call's span (session.start `traceparent`), when sent. */
|
|
41
|
+
readonly traceparent?: string;
|
|
40
42
|
/** The payload of the handoff that started this session (session.start `handoff`), or null. */
|
|
41
43
|
readonly handoffPayload: Record<string, unknown> | null;
|
|
42
44
|
readonly speech: SessionStart["speech"];
|
|
43
45
|
readonly audioFormat: SessionStart["audio"];
|
|
44
46
|
readonly tracks: SessionStart["tracks"];
|
|
45
47
|
/**
|
|
46
|
-
* The app and workspace
|
|
48
|
+
* The app and workspace AppBridge's app-bound signature named, when the server verified one
|
|
47
49
|
* (VoiceServer); undefined otherwise.
|
|
48
50
|
*/
|
|
49
51
|
readonly signedFor?: {
|
|
@@ -69,6 +71,8 @@ export declare class Session {
|
|
|
69
71
|
private gatherState?;
|
|
70
72
|
private buffered;
|
|
71
73
|
private lastWarmTransfer?;
|
|
74
|
+
/** A pending completeTransfer(), rejected when AppBridge refuses it. */
|
|
75
|
+
private completing?;
|
|
72
76
|
private endedReason?;
|
|
73
77
|
private listening;
|
|
74
78
|
constructor(start: SessionStart, transport: SessionTransport, signedFor?: {
|
|
@@ -84,7 +88,7 @@ export declare class Session {
|
|
|
84
88
|
/** Waits for speech (turn.ended) or digits (dtmf), whichever comes first. */
|
|
85
89
|
gather(options?: GatherOptions): Promise<GatherResult>;
|
|
86
90
|
/**
|
|
87
|
-
* Starts listening or changes how
|
|
91
|
+
* Starts listening or changes how AppBridge listens (`listen.update`): recognition language,
|
|
88
92
|
* hints, and the turn-taking patience knob (`low`, `normal` or `high`).
|
|
89
93
|
*/
|
|
90
94
|
listen(options?: {
|
|
@@ -97,9 +101,14 @@ export declare class Session {
|
|
|
97
101
|
on<E extends SessionEventName>(event: E, cb: (data: SessionEventMap[E]) => void): this;
|
|
98
102
|
off<E extends SessionEventName>(event: E, cb: (data: SessionEventMap[E]) => void): this;
|
|
99
103
|
playDtmf(digits: string): Promise<void>;
|
|
100
|
-
/** Transfers the call; resolves when
|
|
104
|
+
/** Transfers the call; resolves when AppBridge ends the session with reason transfer. */
|
|
101
105
|
transfer(to: string, options?: TransferOptions): Promise<void>;
|
|
102
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* Completes the pending warm transfer: the agent leaves, caller and new party stay. Resolves
|
|
108
|
+
* when the session ends with reason transfer; rejects with a SessionError
|
|
109
|
+
* (`transfer_not_answered`) when the new party has not answered yet, which leaves the transfer
|
|
110
|
+
* pending: wait for the `transfer.answered` event and call it again.
|
|
111
|
+
*/
|
|
103
112
|
completeTransfer(): Promise<void>;
|
|
104
113
|
hold(options?: {
|
|
105
114
|
music?: string;
|
|
@@ -115,7 +124,13 @@ export declare class Session {
|
|
|
115
124
|
* @internal Reserved for Fonoster's own apps (Autopilot); not part of the public SDK and may change.
|
|
116
125
|
*/
|
|
117
126
|
note(data: Record<string, unknown>): Promise<void>;
|
|
118
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* Adds model token usage to the call (usage.report, add-usage-metering).
|
|
129
|
+
*
|
|
130
|
+
* @internal Reserved for Fonoster's own Autopilot; AppBridge refuses it from other apps.
|
|
131
|
+
*/
|
|
132
|
+
reportUsage(lines: Record<string, unknown>[]): Promise<void>;
|
|
133
|
+
/** Resolves at once: AppBridge answers the call on connect (voice-sdk S2). */
|
|
119
134
|
answer(): Promise<void>;
|
|
120
135
|
/** Feeds one frame from the socket. */
|
|
121
136
|
receive(data: Buffer, isBinary: boolean): void;
|
|
@@ -133,7 +148,7 @@ export declare class Session {
|
|
|
133
148
|
private command;
|
|
134
149
|
private awaitOutput;
|
|
135
150
|
/**
|
|
136
|
-
* The app owns interruption (
|
|
151
|
+
* The app owns interruption (appbridge B22): when the caller starts talking over an interruptible
|
|
137
152
|
* say or play, the SDK cuts it off with clear; a non-interruptible one keeps playing.
|
|
138
153
|
*/
|
|
139
154
|
private bargeIn;
|
package/dist/Session.js
CHANGED
|
@@ -30,7 +30,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
30
30
|
exports.Session = void 0;
|
|
31
31
|
const errors_1 = require("./errors");
|
|
32
32
|
const DEFAULT_GATHER_TIMEOUT_MS = 4000;
|
|
33
|
-
/** Commands that make
|
|
33
|
+
/** Commands that make AppBridge start listening (speech recognition and turn events). */
|
|
34
34
|
const SPEECH_COMMANDS = new Set([
|
|
35
35
|
"speak",
|
|
36
36
|
"play",
|
|
@@ -63,6 +63,7 @@ class Session {
|
|
|
63
63
|
this.from = start.from;
|
|
64
64
|
this.to = start.to;
|
|
65
65
|
this.metadata = ((_a = start.metadata) !== null && _a !== void 0 ? _a : {});
|
|
66
|
+
this.traceparent = start.traceparent;
|
|
66
67
|
this.handoffPayload = ((_b = start.handoff) !== null && _b !== void 0 ? _b : null);
|
|
67
68
|
this.speech = start.speech;
|
|
68
69
|
this.audioFormat = start.audio;
|
|
@@ -125,7 +126,7 @@ class Session {
|
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
catch (e) {
|
|
128
|
-
// End the utterance so
|
|
129
|
+
// End the utterance so AppBridge does not wait for more pieces.
|
|
129
130
|
this.send(Object.assign({ type: "speak", id, text: "", last: true }, fields));
|
|
130
131
|
this.outputs.delete(id);
|
|
131
132
|
done.catch(() => undefined);
|
|
@@ -162,7 +163,7 @@ class Session {
|
|
|
162
163
|
if (this.gatherState)
|
|
163
164
|
return Promise.reject(new Error("a gather is already pending"));
|
|
164
165
|
const source = (_a = options.source) !== null && _a !== void 0 ? _a : "speech_and_dtmf";
|
|
165
|
-
//
|
|
166
|
+
// AppBridge listens only after a Speech-layer command; a gather may be the first one.
|
|
166
167
|
if (source !== "dtmf" && !this.listening)
|
|
167
168
|
this.send({ type: "listen.update" });
|
|
168
169
|
return new Promise((resolve, reject) => {
|
|
@@ -190,7 +191,7 @@ class Session {
|
|
|
190
191
|
});
|
|
191
192
|
}
|
|
192
193
|
/**
|
|
193
|
-
* Starts listening or changes how
|
|
194
|
+
* Starts listening or changes how AppBridge listens (`listen.update`): recognition language,
|
|
194
195
|
* hints, and the turn-taking patience knob (`low`, `normal` or `high`).
|
|
195
196
|
*/
|
|
196
197
|
listen() {
|
|
@@ -222,7 +223,7 @@ class Session {
|
|
|
222
223
|
this.command({ type: "dtmf.send", digits });
|
|
223
224
|
});
|
|
224
225
|
}
|
|
225
|
-
/** Transfers the call; resolves when
|
|
226
|
+
/** Transfers the call; resolves when AppBridge ends the session with reason transfer. */
|
|
226
227
|
transfer(to, options = {}) {
|
|
227
228
|
const id = this.nextId("transfer");
|
|
228
229
|
if (options.mode === "warm")
|
|
@@ -230,12 +231,32 @@ class Session {
|
|
|
230
231
|
return this.untilEnd(["transfer"], id, () => this.send(Object.assign({ type: "transfer", id,
|
|
231
232
|
to }, this.compact({ mode: options.mode, headers: options.headers }))));
|
|
232
233
|
}
|
|
233
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* Completes the pending warm transfer: the agent leaves, caller and new party stay. Resolves
|
|
236
|
+
* when the session ends with reason transfer; rejects with a SessionError
|
|
237
|
+
* (`transfer_not_answered`) when the new party has not answered yet, which leaves the transfer
|
|
238
|
+
* pending: wait for the `transfer.answered` event and call it again.
|
|
239
|
+
*/
|
|
234
240
|
completeTransfer() {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
241
|
+
const id = this.lastWarmTransfer;
|
|
242
|
+
if (!id)
|
|
243
|
+
return Promise.reject(new Error("no warm transfer is pending"));
|
|
244
|
+
return new Promise((resolve, reject) => {
|
|
245
|
+
if (this.endedReason)
|
|
246
|
+
return reject(new errors_1.SessionEndedError(this.endedReason));
|
|
247
|
+
const waiter = { reasons: ["transfer"], resolve, reject };
|
|
248
|
+
this.endWaiters.push(waiter);
|
|
249
|
+
this.completing = (e) => {
|
|
250
|
+
this.endWaiters = this.endWaiters.filter((w) => w !== waiter);
|
|
251
|
+
reject(e);
|
|
252
|
+
};
|
|
253
|
+
try {
|
|
254
|
+
this.send({ type: "transfer.complete", id });
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
this.completing = undefined;
|
|
258
|
+
reject(e);
|
|
259
|
+
}
|
|
239
260
|
});
|
|
240
261
|
}
|
|
241
262
|
hold() {
|
|
@@ -269,7 +290,17 @@ class Session {
|
|
|
269
290
|
this.command({ type: "record.note", data });
|
|
270
291
|
});
|
|
271
292
|
}
|
|
272
|
-
/**
|
|
293
|
+
/**
|
|
294
|
+
* Adds model token usage to the call (usage.report, add-usage-metering).
|
|
295
|
+
*
|
|
296
|
+
* @internal Reserved for Fonoster's own Autopilot; AppBridge refuses it from other apps.
|
|
297
|
+
*/
|
|
298
|
+
reportUsage(lines) {
|
|
299
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
300
|
+
this.command({ type: "usage.report", lines });
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/** Resolves at once: AppBridge answers the call on connect (voice-sdk S2). */
|
|
273
304
|
answer() {
|
|
274
305
|
return __awaiter(this, void 0, void 0, function* () { });
|
|
275
306
|
}
|
|
@@ -352,6 +383,7 @@ class Session {
|
|
|
352
383
|
case "transcript.partial":
|
|
353
384
|
case "turn.estimate":
|
|
354
385
|
case "amd.result":
|
|
386
|
+
case "transfer.answered":
|
|
355
387
|
return this.emit(type, this.without(msg, "type"));
|
|
356
388
|
default:
|
|
357
389
|
// Unknown types are ignored (additive-only protocol).
|
|
@@ -361,6 +393,13 @@ class Session {
|
|
|
361
393
|
var _a;
|
|
362
394
|
const error = new errors_1.SessionError(String(msg.code), String((_a = msg.message) !== null && _a !== void 0 ? _a : msg.code), msg.fatal === true);
|
|
363
395
|
const id = msg.relatesTo;
|
|
396
|
+
// A refused completeTransfer() fails that call, not the pending transfer.
|
|
397
|
+
if (error.code === "transfer_not_answered" && this.completing) {
|
|
398
|
+
const completing = this.completing;
|
|
399
|
+
this.completing = undefined;
|
|
400
|
+
completing(error);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
364
403
|
const output = id ? this.take(id) : undefined;
|
|
365
404
|
const command = id ? this.commands.get(id) : undefined;
|
|
366
405
|
if (output)
|
|
@@ -450,7 +489,7 @@ class Session {
|
|
|
450
489
|
});
|
|
451
490
|
}
|
|
452
491
|
/**
|
|
453
|
-
* The app owns interruption (
|
|
492
|
+
* The app owns interruption (appbridge B22): when the caller starts talking over an interruptible
|
|
454
493
|
* say or play, the SDK cuts it off with clear; a non-interruptible one keeps playing.
|
|
455
494
|
*/
|
|
456
495
|
bargeIn() {
|
|
@@ -487,6 +526,7 @@ class Session {
|
|
|
487
526
|
if (this.endedReason)
|
|
488
527
|
return;
|
|
489
528
|
this.endedReason = reason;
|
|
529
|
+
this.completing = undefined;
|
|
490
530
|
const ended = new errors_1.SessionEndedError(reason);
|
|
491
531
|
for (const w of this.endWaiters) {
|
|
492
532
|
if (w.reasons.includes(reason))
|
package/dist/VoiceServer.d.ts
CHANGED
|
@@ -46,8 +46,8 @@ export type VoiceServerOptions = {
|
|
|
46
46
|
startTimeoutMs?: number;
|
|
47
47
|
};
|
|
48
48
|
/**
|
|
49
|
-
* Accepts
|
|
50
|
-
* (voice-sdk, "VoiceServer accepts
|
|
49
|
+
* Accepts AppBridge sessions over WebSocket and runs the handler once per session
|
|
50
|
+
* (voice-sdk, "VoiceServer accepts AppBridge sessions over WebSocket").
|
|
51
51
|
*/
|
|
52
52
|
export declare class VoiceServer {
|
|
53
53
|
private readonly options;
|
package/dist/VoiceServer.js
CHANGED
|
@@ -71,8 +71,8 @@ const Session_1 = require("./Session");
|
|
|
71
71
|
const signature_1 = require("./signature");
|
|
72
72
|
const logger = (0, logger_1.getLogger)({ service: "voice", filePath: __filename });
|
|
73
73
|
/**
|
|
74
|
-
* Accepts
|
|
75
|
-
* (voice-sdk, "VoiceServer accepts
|
|
74
|
+
* Accepts AppBridge sessions over WebSocket and runs the handler once per session
|
|
75
|
+
* (voice-sdk, "VoiceServer accepts AppBridge sessions over WebSocket").
|
|
76
76
|
*/
|
|
77
77
|
class VoiceServer {
|
|
78
78
|
constructor(options = {}) {
|
|
@@ -133,7 +133,7 @@ class VoiceServer {
|
|
|
133
133
|
let binding;
|
|
134
134
|
if (this.verify) {
|
|
135
135
|
const urls = this.candidateUrls(req);
|
|
136
|
-
// The app-bound signature when
|
|
136
|
+
// The app-bound signature when AppBridge sent one (or it is required); a bad one is
|
|
137
137
|
// refused even if the URL-only signature matches.
|
|
138
138
|
const bound = this.options.requireAppBinding || (0, signature_1.hasAppSignature)(req.headers)
|
|
139
139
|
? (0, signature_1.verifyAppSignature)(urls, req.headers, this.secret)
|
package/dist/errors.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* See the License for the specific language governing permissions and
|
|
17
17
|
* limitations under the License.
|
|
18
18
|
*/
|
|
19
|
-
/** A command failed:
|
|
19
|
+
/** A command failed: AppBridge sent `error` with `relatesTo` set to the command's id. */
|
|
20
20
|
export declare class SessionError extends Error {
|
|
21
21
|
readonly code: string;
|
|
22
22
|
readonly fatal: boolean;
|
package/dist/errors.js
CHANGED
|
@@ -19,7 +19,7 @@ exports.SessionEndedError = exports.SessionError = void 0;
|
|
|
19
19
|
* See the License for the specific language governing permissions and
|
|
20
20
|
* limitations under the License.
|
|
21
21
|
*/
|
|
22
|
-
/** A command failed:
|
|
22
|
+
/** A command failed: AppBridge sent `error` with `relatesTo` set to the command's id. */
|
|
23
23
|
class SessionError extends Error {
|
|
24
24
|
constructor(code, message, fatal = false) {
|
|
25
25
|
super(message);
|
package/dist/signature.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export type Verification = {
|
|
|
10
10
|
reason?: "missing" | "stale" | "mismatch";
|
|
11
11
|
};
|
|
12
12
|
export declare function sign(url: string, timestamp: number, secret: string): string;
|
|
13
|
-
/** Checks
|
|
13
|
+
/** Checks AppBridge's signature against each candidate URL (the URL as configured on the app). */
|
|
14
14
|
export declare function verifySignature(urls: string[], headers: IncomingHttpHeaders, secret: string, nowSeconds?: number): Verification;
|
|
15
15
|
/** The app and workspace a bound signature was made for. */
|
|
16
16
|
export type AppBinding = {
|
|
@@ -25,7 +25,7 @@ export declare function signApp(url: string, timestamp: number, binding: AppBind
|
|
|
25
25
|
/** Whether the request carries the app-bound signature headers at all. */
|
|
26
26
|
export declare function hasAppSignature(headers: IncomingHttpHeaders): boolean;
|
|
27
27
|
/**
|
|
28
|
-
* Checks
|
|
28
|
+
* Checks AppBridge's app-bound signature against each candidate URL and returns the app and
|
|
29
29
|
* workspace it was made for; the session must then be for that app.
|
|
30
30
|
*/
|
|
31
31
|
export declare function verifyAppSignature(urls: string[], headers: IncomingHttpHeaders, secret: string, nowSeconds?: number): AppVerification;
|
package/dist/signature.js
CHANGED
|
@@ -38,7 +38,7 @@ function sign(url, timestamp, secret) {
|
|
|
38
38
|
.update(`${timestamp}.${url}`)
|
|
39
39
|
.digest("hex");
|
|
40
40
|
}
|
|
41
|
-
/** Checks
|
|
41
|
+
/** Checks AppBridge's signature against each candidate URL (the URL as configured on the app). */
|
|
42
42
|
function verifySignature(urls, headers, secret, nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
43
43
|
const signature = headers[exports.SIGNATURE_HEADER];
|
|
44
44
|
const timestamp = Number(headers[exports.TIMESTAMP_HEADER]);
|
|
@@ -69,7 +69,7 @@ function hasAppSignature(headers) {
|
|
|
69
69
|
return header(headers, exports.APP_SIGNATURE_HEADER) !== undefined;
|
|
70
70
|
}
|
|
71
71
|
/**
|
|
72
|
-
* Checks
|
|
72
|
+
* Checks AppBridge's app-bound signature against each candidate URL and returns the app and
|
|
73
73
|
* workspace it was made for; the session must then be for that app.
|
|
74
74
|
*/
|
|
75
75
|
function verifyAppSignature(urls, headers, secret, nowSeconds = Math.floor(Date.now() / 1000)) {
|
package/dist/types.d.ts
CHANGED
|
@@ -78,7 +78,7 @@ export type SessionEventMap = {
|
|
|
78
78
|
speaking: boolean;
|
|
79
79
|
kind: string;
|
|
80
80
|
};
|
|
81
|
-
/** Answering-machine detection, once per call, for apps with `amd` (add-
|
|
81
|
+
/** Answering-machine detection, once per call, for apps with `amd` (add-roomr 4.3). */
|
|
82
82
|
"amd.result": {
|
|
83
83
|
status: "human" | "voicemail" | "ivr" | "machine" | "unknown";
|
|
84
84
|
confidence: number;
|
|
@@ -89,6 +89,10 @@ export type SessionEventMap = {
|
|
|
89
89
|
dtmf: {
|
|
90
90
|
digit: string;
|
|
91
91
|
};
|
|
92
|
+
/** A warm transfer's new party answered: completeTransfer() may follow. */
|
|
93
|
+
"transfer.answered": {
|
|
94
|
+
relatesTo: string;
|
|
95
|
+
};
|
|
92
96
|
error: {
|
|
93
97
|
code: string;
|
|
94
98
|
message: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fonoster/voice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Session SDK for Fonoster voice applications",
|
|
5
5
|
"author": "Pedro Sanders <psanders@fonoster.com>",
|
|
6
6
|
"homepage": "https://github.com/fonoster/fonoster#readme",
|
|
@@ -14,12 +14,11 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"prebuild": "rimraf ./dist tsconfig.tsbuildinfo",
|
|
16
16
|
"build": "tsc -b tsconfig.json",
|
|
17
|
-
"
|
|
18
|
-
"generate:readme": "node ../../.scripts/gen-readme.cjs"
|
|
17
|
+
"generate:readme": "fonoster-readme"
|
|
19
18
|
},
|
|
20
19
|
"dependencies": {
|
|
21
|
-
"@fonoster/common": "^0.
|
|
22
|
-
"@fonoster/logger": "^0.
|
|
20
|
+
"@fonoster/common": "^0.31.0",
|
|
21
|
+
"@fonoster/logger": "^0.31.0",
|
|
23
22
|
"ws": "^8.18.0"
|
|
24
23
|
},
|
|
25
24
|
"files": [
|
|
@@ -37,8 +36,8 @@
|
|
|
37
36
|
"bugs": {
|
|
38
37
|
"url": "https://github.com/fonoster/fonoster/issues"
|
|
39
38
|
},
|
|
40
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "f0a5102dddeea0f70f659234108805bc10690f2e",
|
|
41
40
|
"devDependencies": {
|
|
42
|
-
"@fonoster/conformance": "^0.
|
|
41
|
+
"@fonoster/conformance": "^0.31.0"
|
|
43
42
|
}
|
|
44
43
|
}
|