@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 CHANGED
@@ -26,7 +26,7 @@ $ npm install --save @fonoster/voice
26
26
  ```typescript
27
27
  import { VoiceServer } from "@fonoster/voice";
28
28
 
29
- // The gateway signs every connection with the app's signing secret.
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 the gateway's app-bound signature named, when the server verified one
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 the gateway listens (`listen.update`): recognition language,
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 the gateway ends the session with reason transfer. */
104
+ /** Transfers the call; resolves when AppBridge ends the session with reason transfer. */
101
105
  transfer(to: string, options?: TransferOptions): Promise<void>;
102
- /** Completes the pending warm transfer: the agent leaves, caller and new party stay. */
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
- /** Resolves at once: the gateway answers the call on connect (voice-sdk S2). */
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 (gateway B22): when the caller starts talking over an interruptible
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 the gateway start listening (speech recognition and turn events). */
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 the gateway does not wait for more pieces.
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
- // The gateway listens only after a Speech-layer command; a gather may be the first one.
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 the gateway listens (`listen.update`): recognition language,
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 the gateway ends the session with reason transfer. */
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
- /** Completes the pending warm transfer: the agent leaves, caller and new party stay. */
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
- return __awaiter(this, void 0, void 0, function* () {
236
- if (!this.lastWarmTransfer)
237
- throw new Error("no warm transfer is pending");
238
- this.send({ type: "transfer.complete", id: this.lastWarmTransfer });
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
- /** Resolves at once: the gateway answers the call on connect (voice-sdk S2). */
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 (gateway B22): when the caller starts talking over an interruptible
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))
@@ -46,8 +46,8 @@ export type VoiceServerOptions = {
46
46
  startTimeoutMs?: number;
47
47
  };
48
48
  /**
49
- * Accepts VoiceSessionGateway sessions over WebSocket and runs the handler once per session
50
- * (voice-sdk, "VoiceServer accepts gateway sessions over WebSocket").
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;
@@ -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 VoiceSessionGateway sessions over WebSocket and runs the handler once per session
75
- * (voice-sdk, "VoiceServer accepts gateway sessions over WebSocket").
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 the gateway sent one (or it is required); a bad one is
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: the gateway sent `error` with `relatesTo` set to the command's id. */
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: the gateway sent `error` with `relatesTo` set to the command's id. */
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);
@@ -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 the gateway's signature against each candidate URL (the URL as configured on the app). */
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 the gateway's app-bound signature against each candidate URL and returns the app and
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 the gateway's signature against each candidate URL (the URL as configured on the app). */
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 the gateway's app-bound signature against each candidate URL and returns the app and
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-media-node 4.3). */
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.30.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
- "clean": "rimraf ./dist node_modules tsconfig.tsbuildinfo",
18
- "generate:readme": "node ../../.scripts/gen-readme.cjs"
17
+ "generate:readme": "fonoster-readme"
19
18
  },
20
19
  "dependencies": {
21
- "@fonoster/common": "^0.30.0",
22
- "@fonoster/logger": "^0.30.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": "b59b06a3ef325abe3f188c0fb87cc22d50ed5471",
39
+ "gitHead": "f0a5102dddeea0f70f659234108805bc10690f2e",
41
40
  "devDependencies": {
42
- "@fonoster/conformance": "^0.30.0"
41
+ "@fonoster/conformance": "^0.31.0"
43
42
  }
44
43
  }