@hainiva/agent-sdk 0.0.0-stage → 0.2.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/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +201 -2
- package/SECURITY.md +33 -0
- package/dist/cjs/agent.cjs +316 -0
- package/dist/cjs/agent.d.cts +235 -0
- package/dist/cjs/calls.cjs +277 -0
- package/dist/cjs/calls.d.cts +232 -0
- package/dist/cjs/contract.cjs +14 -0
- package/dist/cjs/contract.d.cts +215 -0
- package/dist/cjs/embed.cjs +11 -0
- package/dist/cjs/embed.d.cts +13 -0
- package/dist/cjs/emitter.cjs +50 -0
- package/dist/cjs/emitter.d.cts +22 -0
- package/dist/cjs/errors.cjs +28 -0
- package/dist/cjs/errors.d.cts +15 -0
- package/dist/cjs/events.cjs +97 -0
- package/dist/cjs/events.d.cts +97 -0
- package/dist/cjs/index.cjs +72 -0
- package/dist/cjs/index.d.cts +28 -0
- package/dist/cjs/livekit.cjs +56 -0
- package/dist/cjs/livekit.d.cts +35 -0
- package/dist/cjs/log.cjs +15 -0
- package/dist/cjs/log.d.cts +5 -0
- package/dist/cjs/react-native.cjs +27 -0
- package/dist/cjs/react-native.d.cts +38 -0
- package/dist/cjs/redact.cjs +71 -0
- package/dist/cjs/redact.d.cts +21 -0
- package/dist/cjs/rest.cjs +280 -0
- package/dist/cjs/rest.d.cts +166 -0
- package/dist/cjs/session.cjs +159 -0
- package/dist/cjs/session.d.cts +101 -0
- package/dist/cjs/singleFlight.cjs +49 -0
- package/dist/cjs/singleFlight.d.cts +32 -0
- package/dist/cjs/url.cjs +39 -0
- package/dist/cjs/url.d.cts +13 -0
- package/dist/cjs/web.cjs +28 -0
- package/dist/cjs/web.d.cts +23 -0
- package/dist/cjs/ws.cjs +246 -0
- package/dist/cjs/ws.d.cts +134 -0
- package/dist/esm/agent.d.ts +236 -0
- package/dist/esm/agent.d.ts.map +1 -0
- package/dist/esm/agent.js +313 -0
- package/dist/esm/agent.js.map +1 -0
- package/dist/esm/calls.d.ts +233 -0
- package/dist/esm/calls.d.ts.map +1 -0
- package/dist/esm/calls.js +271 -0
- package/dist/esm/calls.js.map +1 -0
- package/dist/esm/contract.d.ts +216 -0
- package/dist/esm/contract.d.ts.map +1 -0
- package/dist/esm/contract.js +12 -0
- package/dist/esm/contract.js.map +1 -0
- package/dist/esm/embed.d.ts +14 -0
- package/dist/esm/embed.d.ts.map +1 -0
- package/dist/esm/embed.js +9 -0
- package/dist/esm/embed.js.map +1 -0
- package/dist/esm/emitter.d.ts +23 -0
- package/dist/esm/emitter.d.ts.map +1 -0
- package/dist/esm/emitter.js +47 -0
- package/dist/esm/emitter.js.map +1 -0
- package/dist/esm/errors.d.ts +16 -0
- package/dist/esm/errors.d.ts.map +1 -0
- package/dist/esm/errors.js +25 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/events.d.ts +98 -0
- package/dist/esm/events.d.ts.map +1 -0
- package/dist/esm/events.js +90 -0
- package/dist/esm/events.js.map +1 -0
- package/dist/esm/index.d.ts +29 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +29 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/livekit.d.ts +36 -0
- package/dist/esm/livekit.d.ts.map +1 -0
- package/dist/esm/livekit.js +54 -0
- package/dist/esm/livekit.js.map +1 -0
- package/dist/esm/log.d.ts +6 -0
- package/dist/esm/log.d.ts.map +1 -0
- package/dist/esm/log.js +13 -0
- package/dist/esm/log.js.map +1 -0
- package/dist/esm/react-native.d.ts +39 -0
- package/dist/esm/react-native.d.ts.map +1 -0
- package/dist/esm/react-native.js +23 -0
- package/dist/esm/react-native.js.map +1 -0
- package/dist/esm/redact.d.ts +22 -0
- package/dist/esm/redact.d.ts.map +1 -0
- package/dist/esm/redact.js +66 -0
- package/dist/esm/redact.js.map +1 -0
- package/dist/esm/rest.d.ts +167 -0
- package/dist/esm/rest.d.ts.map +1 -0
- package/dist/esm/rest.js +275 -0
- package/dist/esm/rest.js.map +1 -0
- package/dist/esm/session.d.ts +102 -0
- package/dist/esm/session.d.ts.map +1 -0
- package/dist/esm/session.js +155 -0
- package/dist/esm/session.js.map +1 -0
- package/dist/esm/singleFlight.d.ts +33 -0
- package/dist/esm/singleFlight.d.ts.map +1 -0
- package/dist/esm/singleFlight.js +46 -0
- package/dist/esm/singleFlight.js.map +1 -0
- package/dist/esm/url.d.ts +14 -0
- package/dist/esm/url.d.ts.map +1 -0
- package/dist/esm/url.js +37 -0
- package/dist/esm/url.js.map +1 -0
- package/dist/esm/web.d.ts +24 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +23 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/esm/ws.d.ts +135 -0
- package/dist/esm/ws.d.ts.map +1 -0
- package/dist/esm/ws.js +242 -0
- package/dist/esm/ws.js.map +1 -0
- package/package.json +80 -4
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AgentRestClient = void 0;
|
|
4
|
+
exports.newIdempotencyKey = newIdempotencyKey;
|
|
5
|
+
exports.errorFromResponse = errorFromResponse;
|
|
6
|
+
const errors_js_1 = require("./errors.cjs");
|
|
7
|
+
const log_js_1 = require("./log.cjs");
|
|
8
|
+
const redact_js_1 = require("./redact.cjs");
|
|
9
|
+
const url_js_1 = require("./url.cjs");
|
|
10
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11
|
+
const TENANT = /^[A-Za-z0-9_-]{1,64}$/;
|
|
12
|
+
/** A random UUID v4: crypto.randomUUID where it exists (browsers, Node), else getRandomValues, else Math.random (Idempotency-Key only, not a secret). */
|
|
13
|
+
function newIdempotencyKey() {
|
|
14
|
+
const c = globalThis.crypto;
|
|
15
|
+
if (c?.randomUUID)
|
|
16
|
+
return c.randomUUID();
|
|
17
|
+
const b = new Uint8Array(16);
|
|
18
|
+
if (c?.getRandomValues)
|
|
19
|
+
c.getRandomValues(b);
|
|
20
|
+
else
|
|
21
|
+
for (let i = 0; i < 16; i++)
|
|
22
|
+
b[i] = Math.floor(Math.random() * 256);
|
|
23
|
+
b[6] = (b[6] & 0x0f) | 0x40;
|
|
24
|
+
b[8] = (b[8] & 0x3f) | 0x80;
|
|
25
|
+
const h = [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
26
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
27
|
+
}
|
|
28
|
+
/** An id that goes into a path: a UUID, or the request is refused before it leaves (no path tricks). */
|
|
29
|
+
function id(v, what) {
|
|
30
|
+
if (typeof v !== "string" || !UUID.test(v))
|
|
31
|
+
throw new errors_js_1.HainivaAgentError(0, "invalid_id", { field: what });
|
|
32
|
+
return v.toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
function isObject(v) {
|
|
35
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
36
|
+
}
|
|
37
|
+
/** Pure: the error a non-2xx answer becomes. Exported for tests. */
|
|
38
|
+
function errorFromResponse(status, data, redactor, retryAfter) {
|
|
39
|
+
const e = isObject(data) ? data["error"] : undefined;
|
|
40
|
+
let code = status === 401 ? "unauthorized" : status === 403 ? "forbidden" : status === 404 ? "not_found" : status === 409 ? "conflict" : status === 429 ? "rate_limited" : status >= 500 ? "server_error" : "invalid_request";
|
|
41
|
+
let message;
|
|
42
|
+
if (isObject(e)) {
|
|
43
|
+
if (typeof e["code"] === "string" && e["code"])
|
|
44
|
+
code = e["code"].slice(0, 80);
|
|
45
|
+
if (typeof e["message"] === "string")
|
|
46
|
+
message = e["message"].slice(0, 500);
|
|
47
|
+
}
|
|
48
|
+
else if (typeof e === "string") {
|
|
49
|
+
message = e.slice(0, 500);
|
|
50
|
+
}
|
|
51
|
+
const body = {};
|
|
52
|
+
if (message)
|
|
53
|
+
body["message"] = message;
|
|
54
|
+
if (retryAfter && /^\d{1,6}$/.test(retryAfter))
|
|
55
|
+
body["retryAfterSec"] = Number(retryAfter);
|
|
56
|
+
return new errors_js_1.HainivaAgentError(status, code, body, redactor);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The Agent API over fetch. `tokens` is normally the SessionManager; the
|
|
60
|
+
* client's own `sessions` transport is what that SessionManager signs in with.
|
|
61
|
+
*/
|
|
62
|
+
class AgentRestClient {
|
|
63
|
+
constructor(o, tokens) {
|
|
64
|
+
this.o = o;
|
|
65
|
+
// ── Sessions (the SessionManager's transport) ───────────────────────────────
|
|
66
|
+
this.sessions = {
|
|
67
|
+
exchange: (assertion) => this.raw("POST", "/agent/v1/sessions", {
|
|
68
|
+
body: { tenantId: this.o.tenantId, assertion, ...(this.o.device ? { device: this.o.device } : {}) },
|
|
69
|
+
}, null).then((s) => s),
|
|
70
|
+
refresh: (refreshToken) => this.raw("POST", "/agent/v1/sessions/refresh", {
|
|
71
|
+
body: { tenantId: this.o.tenantId, refreshToken },
|
|
72
|
+
}, null).then((s) => s),
|
|
73
|
+
revoke: async (session) => {
|
|
74
|
+
await this.raw("DELETE", "/agent/v1/sessions/current", {}, session.accessToken);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
const u = (0, url_js_1.assertSafeUrl)(o.baseUrl, { kind: "http", ...(o.allowInsecureLocalhost ? { allowInsecureLocalhost: true } : {}) });
|
|
78
|
+
if (u.search || u.hash)
|
|
79
|
+
throw new errors_js_1.HainivaAgentError(0, "invalid_url", { hint: "baseUrl takes no query or fragment" });
|
|
80
|
+
if (typeof o.tenantId !== "string" || !TENANT.test(o.tenantId))
|
|
81
|
+
throw new errors_js_1.HainivaAgentError(0, "invalid_tenant_id");
|
|
82
|
+
this.baseUrl = u.origin + u.pathname.replace(/\/+$/, "");
|
|
83
|
+
const f = o.fetch ?? globalThis.fetch;
|
|
84
|
+
if (!f)
|
|
85
|
+
throw new errors_js_1.HainivaAgentError(0, "no_fetch", { hint: "pass options.fetch" });
|
|
86
|
+
this.fetchFn = f.bind(globalThis);
|
|
87
|
+
this.timeoutMs = o.timeoutMs ?? 15000;
|
|
88
|
+
this.redactor = o.redactor ?? new redact_js_1.Redactor();
|
|
89
|
+
this.log = (0, log_js_1.safeLogger)(o.log, this.redactor);
|
|
90
|
+
this.tokens = tokens ?? null;
|
|
91
|
+
}
|
|
92
|
+
/** Attach the token source after construction (the SessionManager needs `sessions` first). */
|
|
93
|
+
useTokens(tokens) { this.tokens = tokens; }
|
|
94
|
+
/** The wss:// URL of the event stream, derived from baseUrl (never from a server answer). */
|
|
95
|
+
get wsUrl() {
|
|
96
|
+
return `${this.baseUrl.replace(/^http/, "ws")}/agent/v1/ws`;
|
|
97
|
+
}
|
|
98
|
+
// ── The generic request ─────────────────────────────────────────────────────
|
|
99
|
+
/** An authenticated request: refresh-and-retry once on an expired token. */
|
|
100
|
+
async request(method, path, opts = {}) {
|
|
101
|
+
if (!this.tokens)
|
|
102
|
+
throw new errors_js_1.HainivaAgentError(0, "not_signed_in");
|
|
103
|
+
const idem = method === "POST" ? newIdempotencyKey() : undefined;
|
|
104
|
+
const token = await this.tokens.accessToken();
|
|
105
|
+
try {
|
|
106
|
+
return await this.raw(method, path, opts, token, idem);
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
if (!(e instanceof errors_js_1.HainivaAgentError) || e.status !== 401 || (e.code !== "token_expired" && e.code !== "token_invalid"))
|
|
110
|
+
throw e;
|
|
111
|
+
this.log("access_token_refused", { code: e.code });
|
|
112
|
+
this.tokens.invalidate(token);
|
|
113
|
+
const next = await this.tokens.accessToken();
|
|
114
|
+
return this.raw(method, path, opts, next, idem);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async raw(method, path, opts, token, idem) {
|
|
118
|
+
let url = `${this.baseUrl}${path}`;
|
|
119
|
+
if (opts.query) {
|
|
120
|
+
const q = new URLSearchParams();
|
|
121
|
+
for (const [k, v] of Object.entries(opts.query))
|
|
122
|
+
if (v !== undefined && v !== "")
|
|
123
|
+
q.set(k, String(v));
|
|
124
|
+
const s = q.toString();
|
|
125
|
+
if (s)
|
|
126
|
+
url += `?${s}`;
|
|
127
|
+
}
|
|
128
|
+
const headers = { accept: "application/json" };
|
|
129
|
+
if (token) {
|
|
130
|
+
this.redactor.add(token);
|
|
131
|
+
headers["authorization"] = `Bearer ${token}`;
|
|
132
|
+
}
|
|
133
|
+
if (idem)
|
|
134
|
+
headers["idempotency-key"] = idem;
|
|
135
|
+
let body;
|
|
136
|
+
if (opts.body !== undefined) {
|
|
137
|
+
headers["content-type"] = "application/json";
|
|
138
|
+
body = JSON.stringify(opts.body);
|
|
139
|
+
}
|
|
140
|
+
const ctl = new AbortController();
|
|
141
|
+
const timeout = opts.timeoutMs ?? this.timeoutMs;
|
|
142
|
+
let timedOut = false;
|
|
143
|
+
const timer = setTimeout(() => { timedOut = true; ctl.abort(); }, timeout);
|
|
144
|
+
const onAbort = () => ctl.abort();
|
|
145
|
+
if (opts.signal) {
|
|
146
|
+
if (opts.signal.aborted) {
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
throw new errors_js_1.HainivaAgentError(0, "aborted");
|
|
149
|
+
}
|
|
150
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
151
|
+
}
|
|
152
|
+
let res;
|
|
153
|
+
try {
|
|
154
|
+
res = await this.fetchFn(url, { method, headers, ...(body !== undefined ? { body } : {}), signal: ctl.signal });
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
if (timedOut)
|
|
158
|
+
throw new errors_js_1.HainivaAgentError(0, "timeout", { timeoutMs: timeout });
|
|
159
|
+
if (opts.signal?.aborted)
|
|
160
|
+
throw new errors_js_1.HainivaAgentError(0, "aborted");
|
|
161
|
+
throw errors_js_1.HainivaAgentError.from(e, "network_error", this.redactor);
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
166
|
+
}
|
|
167
|
+
if (res.status === 204)
|
|
168
|
+
return undefined;
|
|
169
|
+
let data = null;
|
|
170
|
+
const text = await res.text().catch(() => "");
|
|
171
|
+
if (text) {
|
|
172
|
+
try {
|
|
173
|
+
data = JSON.parse(text);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
data = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (!res.ok)
|
|
180
|
+
throw errorFromResponse(res.status, data, this.redactor, res.headers?.get?.("retry-after") ?? null);
|
|
181
|
+
if (data === null || typeof data !== "object")
|
|
182
|
+
throw new errors_js_1.HainivaAgentError(res.status, "invalid_response", {}, this.redactor);
|
|
183
|
+
return data;
|
|
184
|
+
}
|
|
185
|
+
// ── Me ──────────────────────────────────────────────────────────────────────
|
|
186
|
+
async me(o) { return this.request("GET", "/agent/v1/me", o); }
|
|
187
|
+
async setStatus(status, o) {
|
|
188
|
+
return this.request("PUT", "/agent/v1/me/status", { ...o, body: { status } });
|
|
189
|
+
}
|
|
190
|
+
/** Rings for this member now (after a cold start, a missed push or a resync). */
|
|
191
|
+
async pendingRings(o) {
|
|
192
|
+
const r = await this.request("GET", "/agent/v1/rings/pending", o);
|
|
193
|
+
return Array.isArray(r.data) ? r.data : [];
|
|
194
|
+
}
|
|
195
|
+
// ── Calls ───────────────────────────────────────────────────────────────────
|
|
196
|
+
async getCall(callId, o) { return this.request("GET", `/agent/v1/calls/${id(callId, "callId")}`, o); }
|
|
197
|
+
async acceptCall(callId, o) { return this.request("POST", `/agent/v1/calls/${id(callId, "callId")}/accept`, { ...o, body: {} }); }
|
|
198
|
+
async declineCall(callId, o) { return this.request("POST", `/agent/v1/calls/${id(callId, "callId")}/decline`, { ...o, body: {} }); }
|
|
199
|
+
async endCall(callId, o) { return this.request("POST", `/agent/v1/calls/${id(callId, "callId")}/end`, { ...o, body: {} }); }
|
|
200
|
+
async holdCall(callId, on, o) {
|
|
201
|
+
return this.request("POST", `/agent/v1/calls/${id(callId, "callId")}/hold`, { ...o, body: { on } });
|
|
202
|
+
}
|
|
203
|
+
async transferCall(callId, target, o) {
|
|
204
|
+
let body;
|
|
205
|
+
if ("agentId" in target)
|
|
206
|
+
body = { targetAgentId: id(target.agentId, "agentId"), mode: target.mode ?? "warm" };
|
|
207
|
+
else if ("queueId" in target)
|
|
208
|
+
body = { queueId: id(target.queueId, "queueId") };
|
|
209
|
+
else
|
|
210
|
+
body = { number: target.number, mode: target.mode ?? "warm" };
|
|
211
|
+
return this.request("POST", `/agent/v1/calls/${id(callId, "callId")}/transfer`, { ...o, body });
|
|
212
|
+
}
|
|
213
|
+
/** Re-mint the LiveKit token while still a participant (reconnect, token near expiry). */
|
|
214
|
+
async mediaToken(callId, o) { return this.request("POST", `/agent/v1/calls/${id(callId, "callId")}/media-token`, { ...o, body: {} }); }
|
|
215
|
+
// ── Wrap-up ─────────────────────────────────────────────────────────────────
|
|
216
|
+
async dispositions(channel, o) {
|
|
217
|
+
const r = await this.request("GET", "/agent/v1/dispositions", { ...o, ...(channel ? { query: { channel } } : {}) });
|
|
218
|
+
return Array.isArray(r.data) ? r.data : [];
|
|
219
|
+
}
|
|
220
|
+
async wrapUp(callId, w = {}, o) {
|
|
221
|
+
return this.request("POST", `/agent/v1/calls/${id(callId, "callId")}/wrapup`, { ...o, body: w });
|
|
222
|
+
}
|
|
223
|
+
// ── Conversations and messages ──────────────────────────────────────────────
|
|
224
|
+
async listConversations(q = {}, o) {
|
|
225
|
+
return this.request("GET", "/agent/v1/conversations", { ...o, query: { scope: q.scope, status: q.status, cursor: q.cursor, limit: q.limit } }).then((page));
|
|
226
|
+
}
|
|
227
|
+
async getConversation(conversationId, o) {
|
|
228
|
+
return this.request("GET", `/agent/v1/conversations/${id(conversationId, "conversationId")}`, o);
|
|
229
|
+
}
|
|
230
|
+
/** Claim it (no agentId), or — a supervisor — assign it to a colleague. */
|
|
231
|
+
async assignConversation(conversationId, agentId, o) {
|
|
232
|
+
return this.request("POST", `/agent/v1/conversations/${id(conversationId, "conversationId")}/assign`, { ...o, body: agentId ? { agentId: id(agentId, "agentId") } : {} });
|
|
233
|
+
}
|
|
234
|
+
async releaseConversation(conversationId, note, o) {
|
|
235
|
+
return this.request("POST", `/agent/v1/conversations/${id(conversationId, "conversationId")}/release`, { ...o, body: note ? { note } : {} });
|
|
236
|
+
}
|
|
237
|
+
async transferConversation(conversationId, to, o) {
|
|
238
|
+
const body = "agentId" in to ? { agentId: id(to.agentId, "agentId") } : { queueId: id(to.queueId, "queueId"), ...(to.note ? { note: to.note } : {}) };
|
|
239
|
+
return this.request("POST", `/agent/v1/conversations/${id(conversationId, "conversationId")}/transfer`, { ...o, body });
|
|
240
|
+
}
|
|
241
|
+
async closeConversation(conversationId, o) {
|
|
242
|
+
return this.request("POST", `/agent/v1/conversations/${id(conversationId, "conversationId")}/close`, { ...o, body: {} });
|
|
243
|
+
}
|
|
244
|
+
async listMessages(conversationId, q = {}, o) {
|
|
245
|
+
return this.request("GET", `/agent/v1/conversations/${id(conversationId, "conversationId")}/messages`, { ...o, query: { after: q.after, limit: q.limit } }).then((page));
|
|
246
|
+
}
|
|
247
|
+
async sendMessage(conversationId, text, o) {
|
|
248
|
+
return this.request("POST", `/agent/v1/conversations/${id(conversationId, "conversationId")}/messages`, { ...o, body: { text } });
|
|
249
|
+
}
|
|
250
|
+
async typing(conversationId, o) {
|
|
251
|
+
return this.request("POST", `/agent/v1/conversations/${id(conversationId, "conversationId")}/typing`, { ...o, body: {} });
|
|
252
|
+
}
|
|
253
|
+
// ── Supervisor ──────────────────────────────────────────────────────────────
|
|
254
|
+
async liveCalls(o) {
|
|
255
|
+
const r = await this.request("GET", "/agent/v1/supervisor/live-calls", o);
|
|
256
|
+
return Array.isArray(r.data) ? r.data : [];
|
|
257
|
+
}
|
|
258
|
+
async teamAgents(o) {
|
|
259
|
+
const r = await this.request("GET", "/agent/v1/supervisor/agents", o);
|
|
260
|
+
return Array.isArray(r.data) ? r.data : [];
|
|
261
|
+
}
|
|
262
|
+
async supervise(callId, mode = "monitor", o) {
|
|
263
|
+
return this.request("POST", `/agent/v1/supervisor/calls/${id(callId, "callId")}/supervise`, { ...o, body: { mode } });
|
|
264
|
+
}
|
|
265
|
+
async setSuperviseMode(callId, mode, supervisorTrackSid, o) {
|
|
266
|
+
return this.request("PUT", `/agent/v1/supervisor/calls/${id(callId, "callId")}/supervise-mode`, { ...o, body: { mode, ...(supervisorTrackSid ? { supervisorTrackSid } : {}) } });
|
|
267
|
+
}
|
|
268
|
+
// ── Push (your app's own FCM / APNs credentials, uploaded by your admin) ─────
|
|
269
|
+
async registerPush(sub, o) {
|
|
270
|
+
return this.request("POST", "/agent/v1/push/subscriptions", { ...o, body: sub });
|
|
271
|
+
}
|
|
272
|
+
async unregisterPush(token, o) {
|
|
273
|
+
await this.request("DELETE", "/agent/v1/push/subscriptions", { ...o, body: { token } });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
exports.AgentRestClient = AgentRestClient;
|
|
277
|
+
function page(r) {
|
|
278
|
+
const x = isObject(r) ? r : {};
|
|
279
|
+
return { data: Array.isArray(x["data"]) ? x["data"] : [], nextCursor: typeof x["nextCursor"] === "string" ? x["nextCursor"] : null };
|
|
280
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The REST client for the Agent API (`/agent/v1/*`).
|
|
3
|
+
*
|
|
4
|
+
* - `baseUrl` is checked with assertSafeUrl (HTTPS only, no credential in it);
|
|
5
|
+
* - the access token goes in the Authorization header only — never in a URL;
|
|
6
|
+
* - a 401 `token_expired` (or `token_invalid`) refreshes ONCE (single flight
|
|
7
|
+
* across every caller: SessionManager) and retries the request once; a
|
|
8
|
+
* refused refresh signs in again with a NEW assertion from `getAssertion`;
|
|
9
|
+
* a 401 `session_revoked` is final (the facade emits `revoked`);
|
|
10
|
+
* - every POST carries an `Idempotency-Key` (the retry after a refresh reuses
|
|
11
|
+
* it, so the server never does the action twice);
|
|
12
|
+
* - each request has a timeout (default 15 s) and honours an AbortSignal;
|
|
13
|
+
* - any non-2xx becomes a HainivaAgentError {status, code, body} from the
|
|
14
|
+
* server's `{error:{code,message}}` envelope, redacted; non-JSON or
|
|
15
|
+
* non-object bodies are handled defensively.
|
|
16
|
+
*/
|
|
17
|
+
import type { AgentAcceptDto, AgentCallDto, AgentConversationDto, AgentDispositionDto, AgentMeDto, AgentMediaTokenDto, AgentMessageDto, AgentPage, AgentPresenceStatus, AgentPushSubscriptionDto, AgentPushSubscriptionInput, AgentRingDto, AgentSessionDto, SupervisorAgentDto, SupervisorLiveCallDto, SupervisorSuperviseDto } from "./contract.cjs";
|
|
18
|
+
import { HainivaAgentError } from "./errors.cjs";
|
|
19
|
+
import { type Logger } from "./log.cjs";
|
|
20
|
+
import { Redactor } from "./redact.cjs";
|
|
21
|
+
import type { AgentSession, SessionTransport } from "./session.cjs";
|
|
22
|
+
export interface AgentRestOptions {
|
|
23
|
+
/** e.g. https://api.hainiva.com (HTTPS only). */
|
|
24
|
+
baseUrl: string;
|
|
25
|
+
/** Your HAINIVA organisation id: sign-in and refresh name it. */
|
|
26
|
+
tenantId: string;
|
|
27
|
+
fetch?: typeof fetch;
|
|
28
|
+
/** Per request, default 15 000 ms. */
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
/** Local development only: http://localhost. */
|
|
31
|
+
allowInsecureLocalhost?: boolean;
|
|
32
|
+
/** Device shown to the organisation's admins in the session list. */
|
|
33
|
+
device?: {
|
|
34
|
+
label?: string;
|
|
35
|
+
platform?: "ios" | "android" | "web" | "other";
|
|
36
|
+
};
|
|
37
|
+
redactor?: Redactor;
|
|
38
|
+
log?: Logger;
|
|
39
|
+
}
|
|
40
|
+
/** Where the client gets its access token, and how it reports a refused one. */
|
|
41
|
+
export interface TokenSource {
|
|
42
|
+
accessToken(): Promise<string>;
|
|
43
|
+
/** The server refused this token: refresh before the next request. */
|
|
44
|
+
invalidate(refused?: string): void;
|
|
45
|
+
}
|
|
46
|
+
export interface RequestOptions {
|
|
47
|
+
body?: unknown;
|
|
48
|
+
query?: Record<string, string | number | undefined>;
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
/** Overrides the client's timeout for this request. */
|
|
51
|
+
timeoutMs?: number;
|
|
52
|
+
}
|
|
53
|
+
/** LiveKit room credentials for a call. */
|
|
54
|
+
export type MediaCredentials = AgentMediaTokenDto;
|
|
55
|
+
export type AgentStatus = AgentPresenceStatus;
|
|
56
|
+
/** Where a call is transferred. */
|
|
57
|
+
export type CallTransferTarget = {
|
|
58
|
+
agentId: string;
|
|
59
|
+
mode?: "warm" | "blind";
|
|
60
|
+
} | {
|
|
61
|
+
queueId: string;
|
|
62
|
+
} | {
|
|
63
|
+
number: string;
|
|
64
|
+
mode?: "warm" | "blind";
|
|
65
|
+
};
|
|
66
|
+
export interface ConversationListQuery {
|
|
67
|
+
scope?: "all" | "mine";
|
|
68
|
+
status?: "open" | "closed" | "all";
|
|
69
|
+
cursor?: string;
|
|
70
|
+
limit?: number;
|
|
71
|
+
}
|
|
72
|
+
/** A random UUID v4: crypto.randomUUID where it exists (browsers, Node), else getRandomValues, else Math.random (Idempotency-Key only, not a secret). */
|
|
73
|
+
export declare function newIdempotencyKey(): string;
|
|
74
|
+
/** Pure: the error a non-2xx answer becomes. Exported for tests. */
|
|
75
|
+
export declare function errorFromResponse(status: number, data: unknown, redactor?: Redactor, retryAfter?: string | null): HainivaAgentError;
|
|
76
|
+
/**
|
|
77
|
+
* The Agent API over fetch. `tokens` is normally the SessionManager; the
|
|
78
|
+
* client's own `sessions` transport is what that SessionManager signs in with.
|
|
79
|
+
*/
|
|
80
|
+
export declare class AgentRestClient {
|
|
81
|
+
private readonly o;
|
|
82
|
+
readonly baseUrl: string;
|
|
83
|
+
private readonly fetchFn;
|
|
84
|
+
private readonly timeoutMs;
|
|
85
|
+
private readonly redactor;
|
|
86
|
+
private readonly log;
|
|
87
|
+
private tokens;
|
|
88
|
+
constructor(o: AgentRestOptions, tokens?: TokenSource);
|
|
89
|
+
/** Attach the token source after construction (the SessionManager needs `sessions` first). */
|
|
90
|
+
useTokens(tokens: TokenSource): void;
|
|
91
|
+
/** The wss:// URL of the event stream, derived from baseUrl (never from a server answer). */
|
|
92
|
+
get wsUrl(): string;
|
|
93
|
+
readonly sessions: SessionTransport<AgentSession & AgentSessionDto>;
|
|
94
|
+
/** An authenticated request: refresh-and-retry once on an expired token. */
|
|
95
|
+
request<T>(method: "GET" | "POST" | "PUT" | "DELETE", path: string, opts?: RequestOptions): Promise<T>;
|
|
96
|
+
private raw;
|
|
97
|
+
me(o?: RequestOptions): Promise<AgentMeDto>;
|
|
98
|
+
setStatus(status: AgentStatus, o?: RequestOptions): Promise<{
|
|
99
|
+
status: AgentStatus;
|
|
100
|
+
}>;
|
|
101
|
+
/** Rings for this member now (after a cold start, a missed push or a resync). */
|
|
102
|
+
pendingRings(o?: RequestOptions): Promise<AgentRingDto[]>;
|
|
103
|
+
getCall(callId: string, o?: RequestOptions): Promise<AgentCallDto>;
|
|
104
|
+
acceptCall(callId: string, o?: RequestOptions): Promise<AgentAcceptDto>;
|
|
105
|
+
declineCall(callId: string, o?: RequestOptions): Promise<{
|
|
106
|
+
callId: string;
|
|
107
|
+
declined: true;
|
|
108
|
+
}>;
|
|
109
|
+
endCall(callId: string, o?: RequestOptions): Promise<{
|
|
110
|
+
callId: string;
|
|
111
|
+
ended: true;
|
|
112
|
+
}>;
|
|
113
|
+
holdCall(callId: string, on: boolean, o?: RequestOptions): Promise<{
|
|
114
|
+
callId: string;
|
|
115
|
+
onHold: boolean;
|
|
116
|
+
}>;
|
|
117
|
+
transferCall(callId: string, target: CallTransferTarget, o?: RequestOptions): Promise<{
|
|
118
|
+
callId: string;
|
|
119
|
+
transferred: true;
|
|
120
|
+
}>;
|
|
121
|
+
/** Re-mint the LiveKit token while still a participant (reconnect, token near expiry). */
|
|
122
|
+
mediaToken(callId: string, o?: RequestOptions): Promise<MediaCredentials>;
|
|
123
|
+
dispositions(channel?: string, o?: RequestOptions): Promise<AgentDispositionDto[]>;
|
|
124
|
+
wrapUp(callId: string, w?: {
|
|
125
|
+
dispositionCode?: string;
|
|
126
|
+
notes?: string;
|
|
127
|
+
}, o?: RequestOptions): Promise<{
|
|
128
|
+
callId: string;
|
|
129
|
+
wrappedUp: true;
|
|
130
|
+
}>;
|
|
131
|
+
listConversations(q?: ConversationListQuery, o?: RequestOptions): Promise<AgentPage<AgentConversationDto>>;
|
|
132
|
+
getConversation(conversationId: string, o?: RequestOptions): Promise<AgentConversationDto>;
|
|
133
|
+
/** Claim it (no agentId), or — a supervisor — assign it to a colleague. */
|
|
134
|
+
assignConversation(conversationId: string, agentId?: string, o?: RequestOptions): Promise<unknown>;
|
|
135
|
+
releaseConversation(conversationId: string, note?: string, o?: RequestOptions): Promise<{
|
|
136
|
+
conversationId: string;
|
|
137
|
+
released: true;
|
|
138
|
+
}>;
|
|
139
|
+
transferConversation(conversationId: string, to: {
|
|
140
|
+
agentId: string;
|
|
141
|
+
} | {
|
|
142
|
+
queueId: string;
|
|
143
|
+
note?: string;
|
|
144
|
+
}, o?: RequestOptions): Promise<unknown>;
|
|
145
|
+
closeConversation(conversationId: string, o?: RequestOptions): Promise<{
|
|
146
|
+
conversationId: string;
|
|
147
|
+
closed: true;
|
|
148
|
+
}>;
|
|
149
|
+
listMessages(conversationId: string, q?: {
|
|
150
|
+
after?: string;
|
|
151
|
+
limit?: number;
|
|
152
|
+
}, o?: RequestOptions): Promise<AgentPage<AgentMessageDto>>;
|
|
153
|
+
sendMessage(conversationId: string, text: string, o?: RequestOptions): Promise<Pick<AgentMessageDto, "id" | "conversationId" | "interactionId" | "createdAt">>;
|
|
154
|
+
typing(conversationId: string, o?: RequestOptions): Promise<{
|
|
155
|
+
ok: true;
|
|
156
|
+
}>;
|
|
157
|
+
liveCalls(o?: RequestOptions): Promise<SupervisorLiveCallDto[]>;
|
|
158
|
+
teamAgents(o?: RequestOptions): Promise<SupervisorAgentDto[]>;
|
|
159
|
+
supervise(callId: string, mode?: "monitor" | "whisper" | "barge", o?: RequestOptions): Promise<SupervisorSuperviseDto>;
|
|
160
|
+
setSuperviseMode(callId: string, mode: "monitor" | "whisper" | "barge", supervisorTrackSid?: string, o?: RequestOptions): Promise<{
|
|
161
|
+
callId: string;
|
|
162
|
+
mode: string;
|
|
163
|
+
}>;
|
|
164
|
+
registerPush(sub: AgentPushSubscriptionInput, o?: RequestOptions): Promise<AgentPushSubscriptionDto>;
|
|
165
|
+
unregisterPush(token: string, o?: RequestOptions): Promise<void>;
|
|
166
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SessionManager = void 0;
|
|
4
|
+
exports.memoryStore = memoryStore;
|
|
5
|
+
const emitter_js_1 = require("./emitter.cjs");
|
|
6
|
+
const errors_js_1 = require("./errors.cjs");
|
|
7
|
+
const log_js_1 = require("./log.cjs");
|
|
8
|
+
const redact_js_1 = require("./redact.cjs");
|
|
9
|
+
const singleFlight_js_1 = require("./singleFlight.cjs");
|
|
10
|
+
/** Keeps the session for the life of the page / JS context only. The default. */
|
|
11
|
+
function memoryStore() {
|
|
12
|
+
let v = null;
|
|
13
|
+
return { get: () => v, set: (x) => { v = x; }, remove: () => { v = null; } };
|
|
14
|
+
}
|
|
15
|
+
const isSession = (v) => {
|
|
16
|
+
const s = v;
|
|
17
|
+
return !!s && typeof s === "object" && typeof s["accessToken"] === "string" && typeof s["refreshToken"] === "string"
|
|
18
|
+
&& typeof s["accessExpiresAt"] === "string" && typeof s["refreshExpiresAt"] === "string";
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The staff session: sign in with a fresh assertion, keep it in the store,
|
|
22
|
+
* refresh before expiry (single flight — one refresh however many callers),
|
|
23
|
+
* sign in again with a NEW assertion when the refresh is refused.
|
|
24
|
+
*/
|
|
25
|
+
class SessionManager {
|
|
26
|
+
constructor(o) {
|
|
27
|
+
this.o = o;
|
|
28
|
+
this.events = new emitter_js_1.TypedEmitter();
|
|
29
|
+
this.session = null;
|
|
30
|
+
this.forceNext = false;
|
|
31
|
+
this.freshSignIn = (0, singleFlight_js_1.singleFlight)(async () => {
|
|
32
|
+
// Always a new assertion: never cached, never stored, never reused.
|
|
33
|
+
let assertion;
|
|
34
|
+
try {
|
|
35
|
+
assertion = await this.o.getAssertion();
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
throw errors_js_1.HainivaAgentError.from(e, "assertion_failed", this.redactor);
|
|
39
|
+
}
|
|
40
|
+
if (typeof assertion !== "string" || assertion.split(".").length !== 3) {
|
|
41
|
+
throw new errors_js_1.HainivaAgentError(0, "invalid_assertion", { hint: "getAssertion must resolve to a signed JWT from your backend" });
|
|
42
|
+
}
|
|
43
|
+
this.redactor.add(assertion);
|
|
44
|
+
try {
|
|
45
|
+
const s = await this.o.transport.exchange(assertion);
|
|
46
|
+
if (!isSession(s))
|
|
47
|
+
throw new errors_js_1.HainivaAgentError(0, "invalid_session", {}, this.redactor);
|
|
48
|
+
await this.keep(s);
|
|
49
|
+
return s;
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
throw errors_js_1.HainivaAgentError.from(e, "sign_in_failed", this.redactor);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
this.redactor.remove(assertion);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
this.store = o.store ?? memoryStore();
|
|
59
|
+
this.skew = o.refreshSkewMs ?? 60000;
|
|
60
|
+
this.now = o.now ?? Date.now;
|
|
61
|
+
this.redactor = o.redactor ?? new redact_js_1.Redactor();
|
|
62
|
+
this.log = (0, log_js_1.safeLogger)(o.log, this.redactor);
|
|
63
|
+
this.refreshOnce = (0, singleFlight_js_1.singleFlight)(() => this.doRefresh());
|
|
64
|
+
}
|
|
65
|
+
get current() { return this.session; }
|
|
66
|
+
/** Restore the stored session if it can still be refreshed, else sign in afresh. */
|
|
67
|
+
async signIn() {
|
|
68
|
+
const saved = await Promise.resolve().then(() => this.store.get()).catch((e) => { this.log("store_read_failed", e); return null; });
|
|
69
|
+
if (saved) {
|
|
70
|
+
try {
|
|
71
|
+
const s = JSON.parse(saved);
|
|
72
|
+
if (isSession(s) && Date.parse(s.refreshExpiresAt) > this.now() + this.skew) {
|
|
73
|
+
this.adopt(s);
|
|
74
|
+
return await this.fresh();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch { /* corrupt or refused: sign in afresh */ }
|
|
78
|
+
await Promise.resolve().then(() => this.store.remove()).catch(() => undefined);
|
|
79
|
+
}
|
|
80
|
+
return this.freshSignIn();
|
|
81
|
+
}
|
|
82
|
+
/** A valid access token, refreshing first when it is about to expire. */
|
|
83
|
+
async accessToken() {
|
|
84
|
+
return (await this.fresh()).accessToken;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The server refused the access token (401 / socket 4001): the next
|
|
88
|
+
* accessToken() refreshes. Pass the token that was refused: a refusal of an
|
|
89
|
+
* OLDER token (a request that was in flight while another caller already
|
|
90
|
+
* refreshed) is ignored, so ten parallel 401s cause one refresh, not ten.
|
|
91
|
+
*/
|
|
92
|
+
invalidate(refused) {
|
|
93
|
+
if (refused && this.session && this.session.accessToken !== refused)
|
|
94
|
+
return;
|
|
95
|
+
this.forceNext = true;
|
|
96
|
+
}
|
|
97
|
+
/** Forget the session here only (the server already ended it): no revoke call. */
|
|
98
|
+
async clear() {
|
|
99
|
+
this.adopt(null);
|
|
100
|
+
await Promise.resolve().then(() => this.store.remove()).catch(() => undefined);
|
|
101
|
+
}
|
|
102
|
+
/** End the session here and (best effort) on the server. */
|
|
103
|
+
async signOut() {
|
|
104
|
+
const s = this.session;
|
|
105
|
+
this.adopt(null);
|
|
106
|
+
await Promise.resolve().then(() => this.store.remove()).catch(() => undefined);
|
|
107
|
+
if (s && this.o.transport.revoke)
|
|
108
|
+
await this.o.transport.revoke(s).catch((e) => this.log("revoke_failed", e));
|
|
109
|
+
this.redactor.clear();
|
|
110
|
+
}
|
|
111
|
+
async fresh() {
|
|
112
|
+
if (!this.session)
|
|
113
|
+
return this.freshSignIn();
|
|
114
|
+
const left = Date.parse(this.session.accessExpiresAt) - this.now();
|
|
115
|
+
if (!this.forceNext && left > this.skew)
|
|
116
|
+
return this.session;
|
|
117
|
+
return this.refreshOnce();
|
|
118
|
+
}
|
|
119
|
+
async doRefresh() {
|
|
120
|
+
const s = this.session;
|
|
121
|
+
if (!s)
|
|
122
|
+
return this.freshSignIn();
|
|
123
|
+
this.forceNext = false;
|
|
124
|
+
try {
|
|
125
|
+
const r = await this.o.transport.refresh(s.refreshToken);
|
|
126
|
+
if (!r || typeof r.accessToken !== "string" || !r.accessToken)
|
|
127
|
+
throw new errors_js_1.HainivaAgentError(0, "refresh_failed", {}, this.redactor);
|
|
128
|
+
const next = { ...s, ...r };
|
|
129
|
+
await this.keep(next);
|
|
130
|
+
return next;
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
// Refused (expired, revoked, reused): a new session from a NEW assertion.
|
|
134
|
+
if (e instanceof errors_js_1.HainivaAgentError && (e.status === 401 || e.status === 403)) {
|
|
135
|
+
this.log("refresh_refused", { status: e.status });
|
|
136
|
+
await Promise.resolve().then(() => this.store.remove()).catch(() => undefined);
|
|
137
|
+
this.adopt(null);
|
|
138
|
+
return this.freshSignIn();
|
|
139
|
+
}
|
|
140
|
+
this.forceNext = true;
|
|
141
|
+
throw errors_js_1.HainivaAgentError.from(e, "refresh_failed", this.redactor);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async keep(s) {
|
|
145
|
+
this.adopt(s);
|
|
146
|
+
await Promise.resolve().then(() => this.store.set(JSON.stringify(s))).catch((e) => this.log("store_write_failed", e));
|
|
147
|
+
}
|
|
148
|
+
adopt(s) {
|
|
149
|
+
const prev = this.session;
|
|
150
|
+
this.session = s;
|
|
151
|
+
// Old tokens stay redacted: they may still appear in an in-flight error.
|
|
152
|
+
if (s)
|
|
153
|
+
this.redactor.add(s.accessToken, s.refreshToken);
|
|
154
|
+
this.events.emit("session", s);
|
|
155
|
+
if (s && s.accessToken !== prev?.accessToken)
|
|
156
|
+
this.events.emit("token", s.accessToken);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
exports.SessionManager = SessionManager;
|