@hainiva/agent-sdk 0.0.0-stage → 0.3.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.
Files changed (119) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/LICENSE +21 -0
  3. package/README.md +278 -2
  4. package/SECURITY.md +33 -0
  5. package/dist/cjs/agent.cjs +320 -0
  6. package/dist/cjs/agent.d.cts +239 -0
  7. package/dist/cjs/calls.cjs +277 -0
  8. package/dist/cjs/calls.d.cts +232 -0
  9. package/dist/cjs/contract.cjs +14 -0
  10. package/dist/cjs/contract.d.cts +225 -0
  11. package/dist/cjs/embed.cjs +253 -0
  12. package/dist/cjs/embed.d.cts +187 -0
  13. package/dist/cjs/emitter.cjs +50 -0
  14. package/dist/cjs/emitter.d.cts +22 -0
  15. package/dist/cjs/errors.cjs +28 -0
  16. package/dist/cjs/errors.d.cts +15 -0
  17. package/dist/cjs/events.cjs +97 -0
  18. package/dist/cjs/events.d.cts +97 -0
  19. package/dist/cjs/index.cjs +72 -0
  20. package/dist/cjs/index.d.cts +28 -0
  21. package/dist/cjs/livekit.cjs +56 -0
  22. package/dist/cjs/livekit.d.cts +35 -0
  23. package/dist/cjs/log.cjs +15 -0
  24. package/dist/cjs/log.d.cts +5 -0
  25. package/dist/cjs/react-native.cjs +32 -0
  26. package/dist/cjs/react-native.d.cts +39 -0
  27. package/dist/cjs/reactNativeDesk.cjs +107 -0
  28. package/dist/cjs/reactNativeDesk.d.cts +53 -0
  29. package/dist/cjs/redact.cjs +71 -0
  30. package/dist/cjs/redact.d.cts +21 -0
  31. package/dist/cjs/rest.cjs +286 -0
  32. package/dist/cjs/rest.d.cts +172 -0
  33. package/dist/cjs/session.cjs +159 -0
  34. package/dist/cjs/session.d.cts +101 -0
  35. package/dist/cjs/singleFlight.cjs +49 -0
  36. package/dist/cjs/singleFlight.d.cts +32 -0
  37. package/dist/cjs/url.cjs +39 -0
  38. package/dist/cjs/url.d.cts +13 -0
  39. package/dist/cjs/web.cjs +28 -0
  40. package/dist/cjs/web.d.cts +23 -0
  41. package/dist/cjs/ws.cjs +246 -0
  42. package/dist/cjs/ws.d.cts +134 -0
  43. package/dist/esm/agent.d.ts +240 -0
  44. package/dist/esm/agent.d.ts.map +1 -0
  45. package/dist/esm/agent.js +317 -0
  46. package/dist/esm/agent.js.map +1 -0
  47. package/dist/esm/calls.d.ts +233 -0
  48. package/dist/esm/calls.d.ts.map +1 -0
  49. package/dist/esm/calls.js +271 -0
  50. package/dist/esm/calls.js.map +1 -0
  51. package/dist/esm/contract.d.ts +226 -0
  52. package/dist/esm/contract.d.ts.map +1 -0
  53. package/dist/esm/contract.js +12 -0
  54. package/dist/esm/contract.js.map +1 -0
  55. package/dist/esm/embed.d.ts +188 -0
  56. package/dist/esm/embed.d.ts.map +1 -0
  57. package/dist/esm/embed.js +247 -0
  58. package/dist/esm/embed.js.map +1 -0
  59. package/dist/esm/emitter.d.ts +23 -0
  60. package/dist/esm/emitter.d.ts.map +1 -0
  61. package/dist/esm/emitter.js +47 -0
  62. package/dist/esm/emitter.js.map +1 -0
  63. package/dist/esm/errors.d.ts +16 -0
  64. package/dist/esm/errors.d.ts.map +1 -0
  65. package/dist/esm/errors.js +25 -0
  66. package/dist/esm/errors.js.map +1 -0
  67. package/dist/esm/events.d.ts +98 -0
  68. package/dist/esm/events.d.ts.map +1 -0
  69. package/dist/esm/events.js +90 -0
  70. package/dist/esm/events.js.map +1 -0
  71. package/dist/esm/index.d.ts +29 -0
  72. package/dist/esm/index.d.ts.map +1 -0
  73. package/dist/esm/index.js +29 -0
  74. package/dist/esm/index.js.map +1 -0
  75. package/dist/esm/livekit.d.ts +36 -0
  76. package/dist/esm/livekit.d.ts.map +1 -0
  77. package/dist/esm/livekit.js +54 -0
  78. package/dist/esm/livekit.js.map +1 -0
  79. package/dist/esm/log.d.ts +6 -0
  80. package/dist/esm/log.d.ts.map +1 -0
  81. package/dist/esm/log.js +13 -0
  82. package/dist/esm/log.js.map +1 -0
  83. package/dist/esm/react-native.d.ts +40 -0
  84. package/dist/esm/react-native.d.ts.map +1 -0
  85. package/dist/esm/react-native.js +26 -0
  86. package/dist/esm/react-native.js.map +1 -0
  87. package/dist/esm/reactNativeDesk.d.ts +54 -0
  88. package/dist/esm/reactNativeDesk.d.ts.map +1 -0
  89. package/dist/esm/reactNativeDesk.js +103 -0
  90. package/dist/esm/reactNativeDesk.js.map +1 -0
  91. package/dist/esm/redact.d.ts +22 -0
  92. package/dist/esm/redact.d.ts.map +1 -0
  93. package/dist/esm/redact.js +66 -0
  94. package/dist/esm/redact.js.map +1 -0
  95. package/dist/esm/rest.d.ts +173 -0
  96. package/dist/esm/rest.d.ts.map +1 -0
  97. package/dist/esm/rest.js +281 -0
  98. package/dist/esm/rest.js.map +1 -0
  99. package/dist/esm/session.d.ts +102 -0
  100. package/dist/esm/session.d.ts.map +1 -0
  101. package/dist/esm/session.js +155 -0
  102. package/dist/esm/session.js.map +1 -0
  103. package/dist/esm/singleFlight.d.ts +33 -0
  104. package/dist/esm/singleFlight.d.ts.map +1 -0
  105. package/dist/esm/singleFlight.js +46 -0
  106. package/dist/esm/singleFlight.js.map +1 -0
  107. package/dist/esm/url.d.ts +14 -0
  108. package/dist/esm/url.d.ts.map +1 -0
  109. package/dist/esm/url.js +37 -0
  110. package/dist/esm/url.js.map +1 -0
  111. package/dist/esm/web.d.ts +24 -0
  112. package/dist/esm/web.d.ts.map +1 -0
  113. package/dist/esm/web.js +23 -0
  114. package/dist/esm/web.js.map +1 -0
  115. package/dist/esm/ws.d.ts +135 -0
  116. package/dist/esm/ws.d.ts.map +1 -0
  117. package/dist/esm/ws.js +242 -0
  118. package/dist/esm/ws.js.map +1 -0
  119. package/package.json +117 -4
@@ -0,0 +1,320 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HainivaAgent = void 0;
4
+ /**
5
+ * HainivaAgent — one object for a staff member's app: sign-in with your
6
+ * backend's assertion, the REST API, the live event socket and the agent's
7
+ * calls, wired together.
8
+ *
9
+ * const agent = new HainivaAgent({ baseUrl, tenantId, getAssertion, media: joinCallRoom });
10
+ * agent.on("call.ringing", (r) => showRing(r));
11
+ * await agent.connect();
12
+ * await agent.calls.accept(callId); // answers, then joins the room with `media`
13
+ *
14
+ * Transport-agnostic: LiveKit, the DOM and React Native come in through options
15
+ * (`media`, `WebSocket`, `store`), so the core runs in Node too.
16
+ */
17
+ const calls_js_1 = require("./calls.cjs");
18
+ const emitter_js_1 = require("./emitter.cjs");
19
+ const errors_js_1 = require("./errors.cjs");
20
+ const events_js_1 = require("./events.cjs");
21
+ const log_js_1 = require("./log.cjs");
22
+ const redact_js_1 = require("./redact.cjs");
23
+ const rest_js_1 = require("./rest.cjs");
24
+ const session_js_1 = require("./session.cjs");
25
+ const ws_js_1 = require("./ws.cjs");
26
+ /**
27
+ * The staff member's app: session, REST, socket and calls.
28
+ */
29
+ class HainivaAgent {
30
+ constructor(o) {
31
+ this.o = o;
32
+ this.socket = null;
33
+ this.meDto = null;
34
+ this.media = new Map();
35
+ this.offs = [];
36
+ this.desk = {
37
+ /** A desk token for the embedded desk (`@hainiva/agent-sdk/embed` mountDesk / `<HainivaDesk/>` call this). */
38
+ session: () => this.guard(this.rest.deskSession()),
39
+ };
40
+ this.rings = {
41
+ /** Rings waiting for you now (also fed into the call state). */
42
+ pending: () => this.syncRings(),
43
+ };
44
+ this.calls = {
45
+ /**
46
+ * Answer: the server's answer race decides (409 = someone else was
47
+ * faster). With `media`, joins the room and returns the session.
48
+ */
49
+ accept: async (callId, ring) => {
50
+ const known = this.callStore.state.calls[callId];
51
+ if (!known || known.phase === "ended") {
52
+ // Answered from a push (CallKit / the Android call notification) before
53
+ // the socket told us about the ring: the state machine learns it now.
54
+ this.callStore.dispatch({ type: "ring", call: {
55
+ id: callId, direction: "inbound", path: "queue", from: ring?.from ?? "",
56
+ ...(ring?.conversationId ? { conversationId: ring.conversationId } : {}), expiresAt: Date.now() + this.ringMs,
57
+ } });
58
+ }
59
+ this.callStore.dispatch({ type: "accept", id: callId });
60
+ let accepted;
61
+ try {
62
+ accepted = await this.guard(this.rest.acceptCall(callId));
63
+ }
64
+ catch (e) {
65
+ const err = errors_js_1.HainivaAgentError.from(e, "accept_failed", this.redactor);
66
+ const reason = err.status === 409 ? "answered_elsewhere" : err.status === 404 || err.code === "call_ended" ? "gone" : "failed";
67
+ this.callStore.dispatch({ type: "accept_failed", id: callId, reason, error: err.code });
68
+ throw err;
69
+ }
70
+ if (!accepted.media || !this.o.media) {
71
+ this.callStore.dispatch({ type: "connected", id: callId, answered: true });
72
+ return { accepted, media: null };
73
+ }
74
+ const media = await this.joinMedia(callId, accepted.media);
75
+ return { accepted, media };
76
+ },
77
+ decline: async (callId) => {
78
+ this.callStore.dispatch({ type: "decline", id: callId });
79
+ return this.guard(this.rest.declineCall(callId));
80
+ },
81
+ /** Hang up here: media is left at once, then the server ends the call. */
82
+ end: async (callId) => {
83
+ this.callStore.dispatch({ type: "hang_up", id: callId });
84
+ return this.guard(this.rest.endCall(callId));
85
+ },
86
+ hold: async (callId, on) => {
87
+ const r = await this.guard(this.rest.holdCall(callId, on));
88
+ this.callStore.dispatch(on ? { type: "hold", id: callId } : { type: "resume", id: callId });
89
+ return r;
90
+ },
91
+ mute: async (callId, muted) => {
92
+ await this.media.get(callId)?.setMuted(muted);
93
+ this.callStore.dispatch({ type: "mute", id: callId, muted });
94
+ },
95
+ /** Warm or blind to a colleague, to a queue (always blind), or to a phone number. */
96
+ transfer: async (callId, target, mode = "warm") => {
97
+ this.callStore.dispatch({ type: "transfer_start", id: callId, mode, target });
98
+ try {
99
+ return await this.guard(this.rest.transferCall(callId, target.kind === "agent" ? { agentId: target.agentId, mode } : target.kind === "queue" ? { queueId: target.queueId } : { number: target.number, mode }));
100
+ }
101
+ catch (e) {
102
+ this.callStore.dispatch({ type: "transfer_cancelled", id: callId, notice: "transfer_failed" });
103
+ throw e;
104
+ }
105
+ },
106
+ get: (callId) => this.guard(this.rest.getCall(callId)),
107
+ };
108
+ this.conversations = {
109
+ list: (q) => this.guard(this.rest.listConversations(q)),
110
+ get: (id) => this.guard(this.rest.getConversation(id)),
111
+ /** Take it yourself (no agentId) or, a supervisor, give it to a colleague. */
112
+ assign: (id, agentId) => this.guard(this.rest.assignConversation(id, agentId)),
113
+ release: (id, note) => this.guard(this.rest.releaseConversation(id, note)),
114
+ transfer: (id, to) => this.guard(this.rest.transferConversation(id, to)),
115
+ close: (id) => this.guard(this.rest.closeConversation(id)),
116
+ };
117
+ this.messages = {
118
+ list: (conversationId, q) => this.guard(this.rest.listMessages(conversationId, q)),
119
+ send: (conversationId, text) => this.guard(this.rest.sendMessage(conversationId, text)),
120
+ typing: (conversationId) => this.guard(this.rest.typing(conversationId)),
121
+ };
122
+ this.supervisor = {
123
+ liveCalls: () => this.guard(this.rest.liveCalls()),
124
+ agents: () => this.guard(this.rest.teamAgents()),
125
+ /** Listen in (monitor), coach the agent (whisper) or join the call (barge). Join `media` with your adapter. */
126
+ supervise: (callId, mode = "monitor") => this.guard(this.rest.supervise(callId, mode)),
127
+ setMode: (callId, mode, supervisorTrackSid) => this.guard(this.rest.setSuperviseMode(callId, mode, supervisorTrackSid)),
128
+ };
129
+ this.push = {
130
+ /** Register this device's FCM token (Android) or PushKit/APNs token (iPhone) for YOUR app's credentials. */
131
+ register: (sub) => this.guard(this.rest.registerPush(sub)),
132
+ unregister: (token) => this.guard(this.rest.unregisterPush(token)),
133
+ };
134
+ this.redactor = new redact_js_1.Redactor();
135
+ this.log = (0, log_js_1.safeLogger)(o.log, this.redactor);
136
+ this.events = new emitter_js_1.TypedEmitter((e) => this.log("listener_error", e));
137
+ this.ringMs = o.ringTimeoutMs ?? 60000;
138
+ this.rest = new rest_js_1.AgentRestClient({
139
+ baseUrl: o.baseUrl, tenantId: o.tenantId, redactor: this.redactor,
140
+ ...(o.fetch ? { fetch: o.fetch } : {}), ...(o.timeoutMs ? { timeoutMs: o.timeoutMs } : {}),
141
+ ...(o.device ? { device: o.device } : {}), ...(o.log ? { log: o.log } : {}),
142
+ ...(o.allowInsecureLocalhost ? { allowInsecureLocalhost: true } : {}),
143
+ });
144
+ this.session = new session_js_1.SessionManager({
145
+ getAssertion: o.getAssertion, transport: this.rest.sessions, redactor: this.redactor,
146
+ ...(o.store ? { store: o.store } : {}), ...(o.log ? { log: o.log } : {}),
147
+ });
148
+ this.rest.useTokens(this.session);
149
+ this.callStore = new calls_js_1.CallStore();
150
+ this.callStore.events.on("change", (s) => this.events.emit("calls", s));
151
+ this.callStore.events.on("effect", (e) => this.onEffect(e));
152
+ }
153
+ /** The signed-in member (after connect()). */
154
+ get me() { return this.meDto; }
155
+ /** The calls now (the state machine): ringing, connecting, active, held, ended. */
156
+ get callsState() { return this.callStore.state; }
157
+ get connection() { return this.socket?.state ?? "idle"; }
158
+ on(event, fn) {
159
+ return this.events.on(event, fn);
160
+ }
161
+ /** Sign in (or restore the stored session), load who you are and the rings waiting, open the event socket. */
162
+ async connect() {
163
+ await this.session.signIn();
164
+ this.meDto = await this.guard(this.rest.me());
165
+ if (!this.socket)
166
+ this.openSocket();
167
+ this.socket.connect();
168
+ await this.syncRings().catch((e) => this.log("pending_rings_failed", e));
169
+ return this.meDto;
170
+ }
171
+ /**
172
+ * Close the socket and leave every call's media. `signOut: true` also ends
173
+ * the session on the server (and removes it from the store) — do that when
174
+ * the staff member signs out; unregister this device's push first.
175
+ */
176
+ async disconnect(opts = {}) {
177
+ this.socket?.disconnect();
178
+ this.socket = null;
179
+ for (const off of this.offs.splice(0))
180
+ off();
181
+ await Promise.all([...this.media.values()].map((m) => m.leave().catch(() => undefined)));
182
+ this.media.clear();
183
+ if (opts.signOut)
184
+ await this.session.signOut();
185
+ }
186
+ setStatus(status, o) { return this.guard(this.rest.setStatus(status, o)); }
187
+ /** Join (or re-join) a call's media room. Without credentials, a fresh LiveKit token is minted. */
188
+ async joinMedia(callId, creds) {
189
+ if (!this.o.media)
190
+ throw new errors_js_1.HainivaAgentError(0, "no_media_adapter", { hint: "pass options.media = joinCallRoom from @hainiva/agent-sdk/web or /react-native" });
191
+ const c = creds ?? await this.guard(this.rest.mediaToken(callId));
192
+ this.redactor.add(c.token);
193
+ const previous = this.media.get(callId);
194
+ if (previous)
195
+ await previous.leave().catch(() => undefined);
196
+ let m;
197
+ try {
198
+ m = await this.o.media({ url: c.livekitUrl, token: c.token });
199
+ }
200
+ catch (e) {
201
+ this.callStore.dispatch({ type: "accept_failed", id: callId, reason: "failed", error: "media_connect_failed" });
202
+ throw errors_js_1.HainivaAgentError.from(e, "media_connect_failed", this.redactor);
203
+ }
204
+ this.media.set(callId, m);
205
+ m.onDisconnected(() => {
206
+ if (this.media.get(callId) !== m)
207
+ return;
208
+ this.media.delete(callId);
209
+ this.callStore.dispatch({ type: "remote_end", id: callId });
210
+ });
211
+ // Media that comes up after the caller hung up is left at once (the machine's media.leave effect).
212
+ this.callStore.dispatch({ type: "connected", id: callId, answered: true });
213
+ return m;
214
+ }
215
+ /** After a call: a disposition code from dispositions() and notes. */
216
+ wrapUp(callId, w = {}) { return this.guard(this.rest.wrapUp(callId, w)); }
217
+ dispositions(channel) { return this.guard(this.rest.dispositions(channel)); }
218
+ // ── Internals ───────────────────────────────────────────────────────────────
219
+ /** A REST call; a final 401 session_revoked tells the app and forgets the session. */
220
+ async guard(p) {
221
+ try {
222
+ return await p;
223
+ }
224
+ catch (e) {
225
+ if (e instanceof errors_js_1.HainivaAgentError && e.status === 401 && e.code === "session_revoked")
226
+ await this.onRevoked("session_revoked");
227
+ throw e;
228
+ }
229
+ }
230
+ async onRevoked(reason) {
231
+ this.socket?.disconnect();
232
+ this.socket = null;
233
+ await this.session.clear();
234
+ this.events.emit("revoked", { reason });
235
+ }
236
+ openSocket() {
237
+ let sock;
238
+ sock = new ws_js_1.ReconnectingSocket({
239
+ url: this.rest.wsUrl,
240
+ getToken: () => this.session.accessToken(),
241
+ onTokenRejected: () => this.session.invalidate(),
242
+ redactor: this.redactor,
243
+ ...(this.o.WebSocket ? { WebSocket: this.o.WebSocket } : {}),
244
+ ...(this.o.log ? { log: this.o.log } : {}),
245
+ ...(this.o.allowInsecureLocalhost ? { allowInsecureLocalhost: true } : {}),
246
+ ...(0, events_js_1.agentSocketBindings)(() => sock.lastSeq),
247
+ });
248
+ this.socket = sock;
249
+ this.offs.push(sock.events.on("state", (s) => this.events.emit("connection", s)), sock.events.on("error", (e) => this.events.emit("error", e)), sock.events.on("revoked", () => { void this.onRevoked("socket_revoked"); }), sock.events.on("ready", (f) => {
250
+ const seq = f.type === "ready" && typeof f.seq === "number" ? f.seq : 0;
251
+ // A first connect starts at the server's newest event: old rings are not replayed.
252
+ if (sock.lastSeq === 0 && seq > 0)
253
+ sock.setLastSeq(seq);
254
+ this.events.emit("ready", { seq });
255
+ }), sock.events.on("frame", (f) => this.onFrame(sock, f)), this.session.events.on("token", (t) => { sock.updateToken(t); }));
256
+ }
257
+ onFrame(sock, f) {
258
+ switch (f.type) {
259
+ case "event": {
260
+ if (!(0, events_js_1.isEventFrame)(f))
261
+ return;
262
+ for (const ev of (0, events_js_1.toCallEvents)(f, this.callStore.state, Date.now(), this.ringMs))
263
+ this.callStore.dispatch(ev);
264
+ if ((0, events_js_1.isKnownEvent)(f.event))
265
+ this.events.emit(f.event, f.data);
266
+ else
267
+ this.events.emit("event.unknown", f);
268
+ return;
269
+ }
270
+ case "resync_required": {
271
+ const from = typeof f.fromSeq === "number" && f.fromSeq >= 0 ? f.fromSeq : 0;
272
+ sock.setLastSeq(from);
273
+ void this.syncRings().catch((e) => this.log("pending_rings_failed", e));
274
+ this.events.emit("resync", { fromSeq: from });
275
+ return;
276
+ }
277
+ case "session.expiring":
278
+ this.events.emit("session.expiring", { expiresAt: String(f.expiresAt ?? "") });
279
+ // The server's clock decides: refresh now even if ours thinks there is time.
280
+ // Refreshing emits "token", which hands the new token to this socket (token.refresh).
281
+ this.session.invalidate(this.session.current?.accessToken);
282
+ void this.session.accessToken().catch((e) => this.log("refresh_failed", e));
283
+ return;
284
+ case "ack":
285
+ if (!f.ok)
286
+ this.log("socket_ack_refused", { error: f.error });
287
+ return;
288
+ default:
289
+ return;
290
+ }
291
+ }
292
+ /** Rings waiting on the server → the call state (a cold start, a missed push, a resync). */
293
+ async syncRings() {
294
+ const rings = await this.guard(this.rest.pendingRings());
295
+ const now = Date.now();
296
+ for (const r of rings) {
297
+ if (!r || typeof r.callId !== "string")
298
+ continue;
299
+ this.callStore.dispatch({
300
+ type: "ring",
301
+ call: {
302
+ id: r.callId, conversationId: r.conversationId, direction: "inbound",
303
+ path: r.path === "direct" ? "direct" : "queue",
304
+ from: r.contactName ?? r.callerNumber ?? "", expiresAt: now + this.ringMs,
305
+ },
306
+ });
307
+ }
308
+ return rings;
309
+ }
310
+ onEffect(e) {
311
+ if (e.type === "media.leave") {
312
+ const m = this.media.get(e.id);
313
+ this.media.delete(e.id);
314
+ if (m)
315
+ void m.leave().catch(() => undefined);
316
+ }
317
+ this.events.emit("callEffect", e);
318
+ }
319
+ }
320
+ exports.HainivaAgent = HainivaAgent;
@@ -0,0 +1,239 @@
1
+ /**
2
+ * HainivaAgent — one object for a staff member's app: sign-in with your
3
+ * backend's assertion, the REST API, the live event socket and the agent's
4
+ * calls, wired together.
5
+ *
6
+ * const agent = new HainivaAgent({ baseUrl, tenantId, getAssertion, media: joinCallRoom });
7
+ * agent.on("call.ringing", (r) => showRing(r));
8
+ * await agent.connect();
9
+ * await agent.calls.accept(callId); // answers, then joins the room with `media`
10
+ *
11
+ * Transport-agnostic: LiveKit, the DOM and React Native come in through options
12
+ * (`media`, `WebSocket`, `store`), so the core runs in Node too.
13
+ */
14
+ import { CallStore, type CallEffect, type CallsState, type TransferTarget } from "./calls.cjs";
15
+ import type { AgentAcceptDto, AgentCallEndedData, AgentDeskSessionDto, AgentCallRingingData, AgentConversationDto, AgentDispositionDto, AgentMeDto, AgentMessageDto, AgentPage, AgentPushSubscriptionDto, AgentPushSubscriptionInput, AgentRingDto, AgentSessionDto, SupervisorAgentDto, SupervisorLiveCallDto, SupervisorSuperviseDto } from "./contract.cjs";
16
+ import { TypedEmitter } from "./emitter.cjs";
17
+ import { HainivaAgentError } from "./errors.cjs";
18
+ import { type AgentEventFrame } from "./events.cjs";
19
+ import { type Logger } from "./log.cjs";
20
+ import { AgentRestClient, type AgentStatus, type ConversationListQuery, type MediaCredentials, type RequestOptions } from "./rest.cjs";
21
+ import { SessionManager, type AgentSession, type GetAssertion, type SessionStore } from "./session.cjs";
22
+ import { type ConnectionState, type WebSocketCtor } from "./ws.cjs";
23
+ /** A joined media room — what `joinCallRoom` from `/web` or `/react-native` returns. */
24
+ export interface MediaSession {
25
+ leave(): Promise<void>;
26
+ setMuted(muted: boolean): Promise<void>;
27
+ onDisconnected(fn: () => void): () => void;
28
+ }
29
+ /** Join a room: pass `joinCallRoom` from `@hainiva/agent-sdk/web` or `/react-native`. */
30
+ export type JoinMedia = (join: {
31
+ url: string;
32
+ token: string;
33
+ }) => Promise<MediaSession>;
34
+ export interface HainivaAgentOptions {
35
+ /** https://api.hainiva.com (or your organisation's API host). */
36
+ baseUrl: string;
37
+ tenantId: string;
38
+ /** Ask YOUR backend for a fresh assertion — every time; never cache one. */
39
+ getAssertion: GetAssertion;
40
+ /** Where the session is kept. Default in memory (a restart signs in again). */
41
+ store?: SessionStore;
42
+ /** Joins call media. Without it, `calls.accept` answers and returns the credentials only. */
43
+ media?: JoinMedia;
44
+ /** WebSocket implementation (browsers and React Native have one; Node 22 too). */
45
+ WebSocket?: WebSocketCtor;
46
+ fetch?: typeof fetch;
47
+ device?: {
48
+ label?: string;
49
+ platform?: "ios" | "android" | "web" | "other";
50
+ };
51
+ /** Per REST request. Default 15 s. */
52
+ timeoutMs?: number;
53
+ /** How long a ring with no server-known end may ring here. Default 60 s. */
54
+ ringTimeoutMs?: number;
55
+ /** A redacted log of what the SDK does. Nothing is logged without it. */
56
+ log?: Logger;
57
+ /** Local development only (http://localhost, ws://localhost). */
58
+ allowInsecureLocalhost?: boolean;
59
+ }
60
+ export interface HainivaAgentEvents {
61
+ /** Socket state. */
62
+ connection: ConnectionState;
63
+ /** The socket is authenticated (after every reconnect). */
64
+ ready: {
65
+ seq: number;
66
+ };
67
+ "call.ringing": AgentCallRingingData;
68
+ "call.ended": AgentCallEndedData;
69
+ "conversation.assigned": Record<string, unknown>;
70
+ "message.created": Record<string, unknown>;
71
+ "wrapup.required": {
72
+ callId: string;
73
+ conversationId: string;
74
+ deadlineAt: string | null;
75
+ };
76
+ "presence.changed": Record<string, unknown>;
77
+ "supervisor.alert": Record<string, unknown>;
78
+ /** An event name this SDK version does not know yet (the server added one). */
79
+ "event.unknown": AgentEventFrame;
80
+ /** The calls changed (state machine). */
81
+ calls: CallsState;
82
+ /** Something the host must do for a call (stop the native ring, leave media). Handled for media already. */
83
+ callEffect: CallEffect;
84
+ /** Events were missed beyond what the server keeps: state was refetched (rings); refetch your own lists. */
85
+ resync: {
86
+ fromSeq: number;
87
+ };
88
+ /** The session expires within a minute: the SDK is refreshing it. */
89
+ "session.expiring": {
90
+ expiresAt: string;
91
+ };
92
+ /** The session was ended by HAINIVA (sign-out elsewhere, suspended, app paused, key rotated). Call connect() to sign in again. */
93
+ revoked: {
94
+ reason: string;
95
+ };
96
+ error: HainivaAgentError;
97
+ }
98
+ /**
99
+ * The staff member's app: session, REST, socket and calls.
100
+ */
101
+ export declare class HainivaAgent {
102
+ private readonly o;
103
+ readonly events: TypedEmitter<HainivaAgentEvents>;
104
+ readonly session: SessionManager<AgentSession & AgentSessionDto>;
105
+ readonly rest: AgentRestClient;
106
+ readonly callStore: CallStore;
107
+ private socket;
108
+ private meDto;
109
+ private readonly media;
110
+ private readonly offs;
111
+ private readonly redactor;
112
+ private readonly log;
113
+ private readonly ringMs;
114
+ constructor(o: HainivaAgentOptions);
115
+ /** The signed-in member (after connect()). */
116
+ get me(): AgentMeDto | null;
117
+ /** The calls now (the state machine): ringing, connecting, active, held, ended. */
118
+ get callsState(): CallsState;
119
+ get connection(): ConnectionState;
120
+ on<K extends keyof HainivaAgentEvents>(event: K, fn: (payload: HainivaAgentEvents[K]) => void): () => void;
121
+ /** Sign in (or restore the stored session), load who you are and the rings waiting, open the event socket. */
122
+ connect(): Promise<AgentMeDto>;
123
+ /**
124
+ * Close the socket and leave every call's media. `signOut: true` also ends
125
+ * the session on the server (and removes it from the store) — do that when
126
+ * the staff member signs out; unregister this device's push first.
127
+ */
128
+ disconnect(opts?: {
129
+ signOut?: boolean;
130
+ }): Promise<void>;
131
+ setStatus(status: AgentStatus, o?: RequestOptions): Promise<{
132
+ status: AgentStatus;
133
+ }>;
134
+ readonly desk: {
135
+ /** A desk token for the embedded desk (`@hainiva/agent-sdk/embed` mountDesk / `<HainivaDesk/>` call this). */
136
+ session: () => Promise<AgentDeskSessionDto>;
137
+ };
138
+ readonly rings: {
139
+ /** Rings waiting for you now (also fed into the call state). */
140
+ pending: () => Promise<AgentRingDto[]>;
141
+ };
142
+ readonly calls: {
143
+ /**
144
+ * Answer: the server's answer race decides (409 = someone else was
145
+ * faster). With `media`, joins the room and returns the session.
146
+ */
147
+ accept: (callId: string, ring?: {
148
+ from?: string;
149
+ conversationId?: string;
150
+ }) => Promise<{
151
+ accepted: AgentAcceptDto;
152
+ media: MediaSession | null;
153
+ }>;
154
+ decline: (callId: string) => Promise<{
155
+ callId: string;
156
+ declined: true;
157
+ }>;
158
+ /** Hang up here: media is left at once, then the server ends the call. */
159
+ end: (callId: string) => Promise<{
160
+ callId: string;
161
+ ended: true;
162
+ }>;
163
+ hold: (callId: string, on: boolean) => Promise<{
164
+ callId: string;
165
+ onHold: boolean;
166
+ }>;
167
+ mute: (callId: string, muted: boolean) => Promise<void>;
168
+ /** Warm or blind to a colleague, to a queue (always blind), or to a phone number. */
169
+ transfer: (callId: string, target: TransferTarget, mode?: "warm" | "blind") => Promise<{
170
+ callId: string;
171
+ transferred: true;
172
+ }>;
173
+ get: (callId: string) => Promise<import("./contract.cjs").AgentCallDto>;
174
+ };
175
+ /** Join (or re-join) a call's media room. Without credentials, a fresh LiveKit token is minted. */
176
+ joinMedia(callId: string, creds?: MediaCredentials): Promise<MediaSession>;
177
+ readonly conversations: {
178
+ list: (q?: ConversationListQuery) => Promise<AgentPage<AgentConversationDto>>;
179
+ get: (id: string) => Promise<AgentConversationDto>;
180
+ /** Take it yourself (no agentId) or, a supervisor, give it to a colleague. */
181
+ assign: (id: string, agentId?: string) => Promise<unknown>;
182
+ release: (id: string, note?: string) => Promise<{
183
+ conversationId: string;
184
+ released: true;
185
+ }>;
186
+ transfer: (id: string, to: {
187
+ agentId: string;
188
+ } | {
189
+ queueId: string;
190
+ note?: string;
191
+ }) => Promise<unknown>;
192
+ close: (id: string) => Promise<{
193
+ conversationId: string;
194
+ closed: true;
195
+ }>;
196
+ };
197
+ readonly messages: {
198
+ list: (conversationId: string, q?: {
199
+ after?: string;
200
+ limit?: number;
201
+ }) => Promise<AgentPage<AgentMessageDto>>;
202
+ send: (conversationId: string, text: string) => Promise<Pick<AgentMessageDto, "id" | "conversationId" | "interactionId" | "createdAt">>;
203
+ typing: (conversationId: string) => Promise<{
204
+ ok: true;
205
+ }>;
206
+ };
207
+ /** After a call: a disposition code from dispositions() and notes. */
208
+ wrapUp(callId: string, w?: {
209
+ dispositionCode?: string;
210
+ notes?: string;
211
+ }): Promise<{
212
+ callId: string;
213
+ wrappedUp: true;
214
+ }>;
215
+ dispositions(channel?: string): Promise<AgentDispositionDto[]>;
216
+ readonly supervisor: {
217
+ liveCalls: () => Promise<SupervisorLiveCallDto[]>;
218
+ agents: () => Promise<SupervisorAgentDto[]>;
219
+ /** Listen in (monitor), coach the agent (whisper) or join the call (barge). Join `media` with your adapter. */
220
+ supervise: (callId: string, mode?: "monitor" | "whisper" | "barge") => Promise<SupervisorSuperviseDto>;
221
+ setMode: (callId: string, mode: "monitor" | "whisper" | "barge", supervisorTrackSid?: string) => Promise<{
222
+ callId: string;
223
+ mode: string;
224
+ }>;
225
+ };
226
+ readonly push: {
227
+ /** Register this device's FCM token (Android) or PushKit/APNs token (iPhone) for YOUR app's credentials. */
228
+ register: (sub: AgentPushSubscriptionInput) => Promise<AgentPushSubscriptionDto>;
229
+ unregister: (token: string) => Promise<void>;
230
+ };
231
+ /** A REST call; a final 401 session_revoked tells the app and forgets the session. */
232
+ private guard;
233
+ private onRevoked;
234
+ private openSocket;
235
+ private onFrame;
236
+ /** Rings waiting on the server → the call state (a cold start, a missed push, a resync). */
237
+ private syncRings;
238
+ private onEffect;
239
+ }