@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.
- package/CHANGELOG.md +73 -0
- package/LICENSE +21 -0
- package/README.md +278 -2
- package/SECURITY.md +33 -0
- package/dist/cjs/agent.cjs +320 -0
- package/dist/cjs/agent.d.cts +239 -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 +225 -0
- package/dist/cjs/embed.cjs +253 -0
- package/dist/cjs/embed.d.cts +187 -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 +32 -0
- package/dist/cjs/react-native.d.cts +39 -0
- package/dist/cjs/reactNativeDesk.cjs +107 -0
- package/dist/cjs/reactNativeDesk.d.cts +53 -0
- package/dist/cjs/redact.cjs +71 -0
- package/dist/cjs/redact.d.cts +21 -0
- package/dist/cjs/rest.cjs +286 -0
- package/dist/cjs/rest.d.cts +172 -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 +240 -0
- package/dist/esm/agent.d.ts.map +1 -0
- package/dist/esm/agent.js +317 -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 +226 -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 +188 -0
- package/dist/esm/embed.d.ts.map +1 -0
- package/dist/esm/embed.js +247 -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 +40 -0
- package/dist/esm/react-native.d.ts.map +1 -0
- package/dist/esm/react-native.js +26 -0
- package/dist/esm/react-native.js.map +1 -0
- package/dist/esm/reactNativeDesk.d.ts +54 -0
- package/dist/esm/reactNativeDesk.d.ts.map +1 -0
- package/dist/esm/reactNativeDesk.js +103 -0
- package/dist/esm/reactNativeDesk.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 +173 -0
- package/dist/esm/rest.d.ts.map +1 -0
- package/dist/esm/rest.js +281 -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 +117 -4
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED from the HAINIVA monorepo (@aicc/types agentApi.ts and
|
|
3
|
+
* agentEvents.ts) — do not edit by hand. The Agent API's response shapes and
|
|
4
|
+
* event names: additive changes only within a major version.
|
|
5
|
+
*/
|
|
6
|
+
export type AgentPresenceStatus = "online" | "busy" | "away" | "offline";
|
|
7
|
+
export type AgentRole = "agent" | "supervisor";
|
|
8
|
+
export interface AgentMeDto {
|
|
9
|
+
id: string;
|
|
10
|
+
externalId: string | null;
|
|
11
|
+
displayName: string | null;
|
|
12
|
+
role: AgentRole;
|
|
13
|
+
/** Team ids this member works in; null = the whole organisation. */
|
|
14
|
+
teams: string[] | null;
|
|
15
|
+
status: AgentPresenceStatus | null;
|
|
16
|
+
session: {
|
|
17
|
+
id: string;
|
|
18
|
+
agentAppId: string;
|
|
19
|
+
mode: "live" | "test";
|
|
20
|
+
expiresAt: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export interface AgentRingDto {
|
|
24
|
+
callId: string;
|
|
25
|
+
conversationId: string;
|
|
26
|
+
kind: "voice" | "video" | "chat";
|
|
27
|
+
channel: string;
|
|
28
|
+
contactName: string | null;
|
|
29
|
+
callerNumber: string | null;
|
|
30
|
+
/** direct = routed to you; queue = offered to your queue; escalation = a live call handed to you. */
|
|
31
|
+
path: "direct" | "queue" | "escalation";
|
|
32
|
+
startedAt: string;
|
|
33
|
+
}
|
|
34
|
+
export interface AgentCallDto {
|
|
35
|
+
callId: string;
|
|
36
|
+
conversationId: string;
|
|
37
|
+
kind: "voice" | "video" | "chat";
|
|
38
|
+
channel: string;
|
|
39
|
+
direction: "inbound" | "outbound" | "internal";
|
|
40
|
+
status: string;
|
|
41
|
+
assignedToMe: boolean;
|
|
42
|
+
startedAt: string;
|
|
43
|
+
endedAt: string | null;
|
|
44
|
+
contactName: string | null;
|
|
45
|
+
callerNumber: string | null;
|
|
46
|
+
}
|
|
47
|
+
export interface AgentMediaTokenDto {
|
|
48
|
+
token: string;
|
|
49
|
+
roomName: string;
|
|
50
|
+
livekitUrl: string;
|
|
51
|
+
expiresAt: string;
|
|
52
|
+
}
|
|
53
|
+
export interface AgentConversationDto {
|
|
54
|
+
id: string;
|
|
55
|
+
status: string;
|
|
56
|
+
subject: string | null;
|
|
57
|
+
channel: string | null;
|
|
58
|
+
chatType: string | null;
|
|
59
|
+
contact: {
|
|
60
|
+
id: string;
|
|
61
|
+
name: string | null;
|
|
62
|
+
phone: string | null;
|
|
63
|
+
};
|
|
64
|
+
assignedAgentId: string | null;
|
|
65
|
+
assignedToMe: boolean;
|
|
66
|
+
queueId: string | null;
|
|
67
|
+
/** The open interaction a message or file goes to (null when none is open). */
|
|
68
|
+
activeInteractionId: string | null;
|
|
69
|
+
lastMessageAt: string | null;
|
|
70
|
+
createdAt: string;
|
|
71
|
+
closedAt: string | null;
|
|
72
|
+
}
|
|
73
|
+
export interface AgentMessageDto {
|
|
74
|
+
id: string;
|
|
75
|
+
conversationId: string;
|
|
76
|
+
interactionId: string;
|
|
77
|
+
direction: string;
|
|
78
|
+
senderType: string;
|
|
79
|
+
/** agents.id of the sender when an agent wrote it. */
|
|
80
|
+
senderAgentId: string | null;
|
|
81
|
+
senderName: string | null;
|
|
82
|
+
type: string;
|
|
83
|
+
text: string | null;
|
|
84
|
+
attachment: {
|
|
85
|
+
name: string | null;
|
|
86
|
+
mimeType: string | null;
|
|
87
|
+
size: number | null;
|
|
88
|
+
} | null;
|
|
89
|
+
createdAt: string;
|
|
90
|
+
}
|
|
91
|
+
export interface AgentDispositionDto {
|
|
92
|
+
code: string;
|
|
93
|
+
label: string;
|
|
94
|
+
channel: string | null;
|
|
95
|
+
required: boolean;
|
|
96
|
+
}
|
|
97
|
+
export interface SupervisorLiveCallDto {
|
|
98
|
+
callId: string;
|
|
99
|
+
conversationId: string;
|
|
100
|
+
agentId: string | null;
|
|
101
|
+
agentName: string | null;
|
|
102
|
+
channel: string;
|
|
103
|
+
status: string;
|
|
104
|
+
startedAt: string;
|
|
105
|
+
contactName: string | null;
|
|
106
|
+
callerNumber: string | null;
|
|
107
|
+
queueId: string | null;
|
|
108
|
+
}
|
|
109
|
+
export interface SupervisorAgentDto {
|
|
110
|
+
id: string;
|
|
111
|
+
displayName: string;
|
|
112
|
+
externalId: string | null;
|
|
113
|
+
role: AgentRole;
|
|
114
|
+
teamId: string | null;
|
|
115
|
+
status: AgentPresenceStatus;
|
|
116
|
+
activeCalls: number;
|
|
117
|
+
openConversations: number;
|
|
118
|
+
}
|
|
119
|
+
export interface AgentPage<T> {
|
|
120
|
+
data: T[];
|
|
121
|
+
nextCursor: string | null;
|
|
122
|
+
}
|
|
123
|
+
/** POST /agent/v1/sessions (201) and POST /agent/v1/sessions/refresh (200). */
|
|
124
|
+
export interface AgentSessionDto {
|
|
125
|
+
accessToken: string;
|
|
126
|
+
accessExpiresAt: string;
|
|
127
|
+
/** hnv_rt_…, rotating: each refresh answers a new one and the old one stops working. */
|
|
128
|
+
refreshToken: string;
|
|
129
|
+
refreshExpiresAt: string;
|
|
130
|
+
sessionId: string;
|
|
131
|
+
member: {
|
|
132
|
+
id: string;
|
|
133
|
+
externalId: string | null;
|
|
134
|
+
displayName: string;
|
|
135
|
+
role: AgentRole;
|
|
136
|
+
};
|
|
137
|
+
app: {
|
|
138
|
+
id: string;
|
|
139
|
+
name: string;
|
|
140
|
+
mode: "live" | "test";
|
|
141
|
+
};
|
|
142
|
+
/** The event socket on the host the app reached; SDKs derive their own from their base URL. */
|
|
143
|
+
wsUrl: string;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* POST /agent/v1/desk-session (201) — partner platform S4, the embeddable desk.
|
|
147
|
+
* A 15-minute token for the HAINIVA desk shown inside the partner's product,
|
|
148
|
+
* handed to the desk over postMessage only (never in a URL, never stored).
|
|
149
|
+
* Bound to the agent session it came from: ends when that session ends.
|
|
150
|
+
*/
|
|
151
|
+
export interface AgentDeskSessionDto {
|
|
152
|
+
token: string;
|
|
153
|
+
expiresAt: string;
|
|
154
|
+
}
|
|
155
|
+
/** POST /agent/v1/calls/:id/accept. `media` is null for a call without a room (chat). */
|
|
156
|
+
export interface AgentAcceptDto {
|
|
157
|
+
callId: string;
|
|
158
|
+
accepted: true;
|
|
159
|
+
media: AgentMediaTokenDto | null;
|
|
160
|
+
}
|
|
161
|
+
/** POST /agent/v1/supervisor/calls/:id/supervise. */
|
|
162
|
+
export interface SupervisorSuperviseDto {
|
|
163
|
+
callId: string;
|
|
164
|
+
mode: "monitor" | "whisper" | "barge";
|
|
165
|
+
media: AgentMediaTokenDto;
|
|
166
|
+
}
|
|
167
|
+
/** POST /agent/v1/push/subscriptions body. */
|
|
168
|
+
export interface AgentPushSubscriptionInput {
|
|
169
|
+
platform: "ios" | "android";
|
|
170
|
+
/** FCM registration token (Android) or APNs device token, hex (iPhone). */
|
|
171
|
+
token: string;
|
|
172
|
+
/** voip = rings (PushKit on iPhone, high-priority data on Android); alert = ordinary notices. */
|
|
173
|
+
kind: "voip" | "alert";
|
|
174
|
+
/** Must be one of the agent app's allowedBundleIds. */
|
|
175
|
+
bundleId: string;
|
|
176
|
+
/** A stable id for this install: a new token from the same install replaces the old one. */
|
|
177
|
+
deviceId?: string;
|
|
178
|
+
}
|
|
179
|
+
/** POST /agent/v1/push/subscriptions (201). The token is never echoed. */
|
|
180
|
+
export interface AgentPushSubscriptionDto {
|
|
181
|
+
id: string;
|
|
182
|
+
platform: "ios" | "android";
|
|
183
|
+
kind: "voip" | "alert";
|
|
184
|
+
bundleId: string;
|
|
185
|
+
/** Whether the agent app has working push credentials for this platform right now. */
|
|
186
|
+
deliverable: boolean;
|
|
187
|
+
createdAt: string;
|
|
188
|
+
}
|
|
189
|
+
/** Event data of the stream's call.ringing. */
|
|
190
|
+
export interface AgentCallRingingData {
|
|
191
|
+
callId: string;
|
|
192
|
+
conversationId: string;
|
|
193
|
+
path: "direct" | "queue" | "transfer";
|
|
194
|
+
channel?: string | null;
|
|
195
|
+
line?: string | null;
|
|
196
|
+
contactName: string | null;
|
|
197
|
+
callerNumber?: string | null;
|
|
198
|
+
fromAgentId?: string | null;
|
|
199
|
+
fromAgentName?: string | null;
|
|
200
|
+
transferMode?: "warm" | "blind";
|
|
201
|
+
/** ISO time the ring ends, when the server knows it. */
|
|
202
|
+
expiresAt: string | null;
|
|
203
|
+
}
|
|
204
|
+
/** Event data of the stream's call.ended. callId is null when only the conversation is known. */
|
|
205
|
+
export interface AgentCallEndedData {
|
|
206
|
+
callId: string | null;
|
|
207
|
+
conversationId: string | null;
|
|
208
|
+
reason: "ended" | "failed" | "answered_elsewhere" | "declined" | "expired" | "transfer_ended";
|
|
209
|
+
answeredBy?: string | null;
|
|
210
|
+
failure?: {
|
|
211
|
+
reason: string;
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
export declare const AGENT_EVENT_NAMES: readonly ["call.ringing", "call.ended", "conversation.assigned", "message.created", "wrapup.required", "presence.changed", "supervisor.alert"];
|
|
215
|
+
export type AgentEventName = (typeof AGENT_EVENT_NAMES)[number];
|
|
216
|
+
export interface AgentEvent {
|
|
217
|
+
event: AgentEventName;
|
|
218
|
+
data: Record<string, unknown>;
|
|
219
|
+
}
|
|
220
|
+
/** What the stream delivers: the event plus its position (resume from it). */
|
|
221
|
+
export interface AgentStreamFrame extends AgentEvent {
|
|
222
|
+
type: "event";
|
|
223
|
+
seq: number;
|
|
224
|
+
at: string;
|
|
225
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DESK_ALLOW = exports.DESK_SANDBOX = exports.DeskHost = void 0;
|
|
4
|
+
exports.parseDeskMessage = parseDeskMessage;
|
|
5
|
+
exports.deskUrl = deskUrl;
|
|
6
|
+
exports.mountDesk = mountDesk;
|
|
7
|
+
const errors_js_1 = require("./errors.cjs");
|
|
8
|
+
const url_js_1 = require("./url.cjs");
|
|
9
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
10
|
+
const END_REASONS = new Set(["not_embedded", "no_host", "expired", "revoked", "host_ended", "misconfigured"]);
|
|
11
|
+
const PHONE = /^[0-9+*#() .-]{1,40}$/;
|
|
12
|
+
function keysAre(o, required, optional = []) {
|
|
13
|
+
const k = Object.keys(o);
|
|
14
|
+
return required.every((r) => k.includes(r)) && k.every((x) => required.includes(x) || optional.includes(x));
|
|
15
|
+
}
|
|
16
|
+
const optId = (v) => v === undefined || (typeof v === "string" && UUID.test(v));
|
|
17
|
+
/** Exactly one of the desk's messages, or null. */
|
|
18
|
+
function parseDeskMessage(data) {
|
|
19
|
+
let d = data;
|
|
20
|
+
if (typeof d === "string") {
|
|
21
|
+
if (d.length > 8192)
|
|
22
|
+
return null;
|
|
23
|
+
try {
|
|
24
|
+
d = JSON.parse(d);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (!d || typeof d !== "object" || Array.isArray(d))
|
|
31
|
+
return null;
|
|
32
|
+
const o = d;
|
|
33
|
+
if (o["v"] !== 1)
|
|
34
|
+
return null;
|
|
35
|
+
switch (o["type"]) {
|
|
36
|
+
case "hainiva:ready":
|
|
37
|
+
return keysAre(o, ["v", "type", "appId"]) && typeof o["appId"] === "string" && UUID.test(o["appId"]) ? { v: 1, type: "hainiva:ready", appId: o["appId"] } : null;
|
|
38
|
+
case "hainiva:token-request":
|
|
39
|
+
return keysAre(o, ["v", "type", "reason"]) && (o["reason"] === "expiring" || o["reason"] === "unauthorized") ? { v: 1, type: "hainiva:token-request", reason: o["reason"] } : null;
|
|
40
|
+
case "hainiva:ended":
|
|
41
|
+
return keysAre(o, ["v", "type", "reason"]) && typeof o["reason"] === "string" && END_REASONS.has(o["reason"])
|
|
42
|
+
? { v: 1, type: "hainiva:ended", reason: o["reason"] } : null;
|
|
43
|
+
case "hainiva:dial":
|
|
44
|
+
if (!keysAre(o, ["v", "type", "number"], ["contactId"]) || typeof o["number"] !== "string" || !PHONE.test(o["number"]) || !optId(o["contactId"]))
|
|
45
|
+
return null;
|
|
46
|
+
return { v: 1, type: "hainiva:dial", number: o["number"], ...(o["contactId"] ? { contactId: o["contactId"] } : {}) };
|
|
47
|
+
case "hainiva:dialer":
|
|
48
|
+
if (!keysAre(o, ["v", "type"], ["number", "contactId"]) || !optId(o["contactId"]))
|
|
49
|
+
return null;
|
|
50
|
+
if (o["number"] !== undefined && (typeof o["number"] !== "string" || !PHONE.test(o["number"])))
|
|
51
|
+
return null;
|
|
52
|
+
return { v: 1, type: "hainiva:dialer", ...(o["number"] ? { number: o["number"] } : {}), ...(o["contactId"] ? { contactId: o["contactId"] } : {}) };
|
|
53
|
+
case "hainiva:open-url": {
|
|
54
|
+
if (!keysAre(o, ["v", "type", "url"]) || typeof o["url"] !== "string" || o["url"].length > 2048)
|
|
55
|
+
return null;
|
|
56
|
+
let u;
|
|
57
|
+
try {
|
|
58
|
+
u = new URL(o["url"]);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return ["https:", "http:", "mailto:", "tel:"].includes(u.protocol) ? { v: 1, type: "hainiva:open-url", url: u.href } : null;
|
|
64
|
+
}
|
|
65
|
+
case "hainiva:media-error":
|
|
66
|
+
return keysAre(o, ["v", "type", "name"]) && typeof o["name"] === "string" && o["name"].length <= 60 ? { v: 1, type: "hainiva:media-error", name: o["name"] } : null;
|
|
67
|
+
default:
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The host side of the desk protocol, transport-agnostic: `mountDesk` (iframe)
|
|
73
|
+
* and `<HainivaDesk/>` (React Native WebView) both drive one.
|
|
74
|
+
* @internal exported for the React Native entry and tests.
|
|
75
|
+
*/
|
|
76
|
+
class DeskHost {
|
|
77
|
+
constructor(appId, source, send, cb) {
|
|
78
|
+
this.appId = appId;
|
|
79
|
+
this.source = source;
|
|
80
|
+
this.send = send;
|
|
81
|
+
this.cb = cb;
|
|
82
|
+
this.ended = false;
|
|
83
|
+
this.ready = false;
|
|
84
|
+
this.inflight = null;
|
|
85
|
+
}
|
|
86
|
+
get isReady() { return this.ready; }
|
|
87
|
+
get isEnded() { return this.ended; }
|
|
88
|
+
/** A raw message from the desk (already origin/source-checked by the transport). */
|
|
89
|
+
receive(raw) {
|
|
90
|
+
if (this.ended)
|
|
91
|
+
return;
|
|
92
|
+
const m = parseDeskMessage(raw);
|
|
93
|
+
if (!m)
|
|
94
|
+
return;
|
|
95
|
+
switch (m.type) {
|
|
96
|
+
case "hainiva:ready":
|
|
97
|
+
if (m.appId.toLowerCase() !== this.appId.toLowerCase())
|
|
98
|
+
return;
|
|
99
|
+
this.ready = true;
|
|
100
|
+
void this.supplyToken();
|
|
101
|
+
return;
|
|
102
|
+
case "hainiva:token-request":
|
|
103
|
+
void this.supplyToken();
|
|
104
|
+
return;
|
|
105
|
+
case "hainiva:ended":
|
|
106
|
+
this.finish(m.reason, false);
|
|
107
|
+
return;
|
|
108
|
+
case "hainiva:dial":
|
|
109
|
+
this.cb.onDial?.(m.number, m.contactId);
|
|
110
|
+
return;
|
|
111
|
+
case "hainiva:dialer":
|
|
112
|
+
this.cb.onDialer?.({ ...(m.number ? { number: m.number } : {}), ...(m.contactId ? { contactId: m.contactId } : {}) });
|
|
113
|
+
return;
|
|
114
|
+
case "hainiva:open-url":
|
|
115
|
+
this.cb.onOpenUrl?.(m.url);
|
|
116
|
+
return;
|
|
117
|
+
case "hainiva:media-error":
|
|
118
|
+
this.cb.onMediaError?.(m.name);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** One desk-session request at a time; the token goes to the desk only. */
|
|
123
|
+
supplyToken() {
|
|
124
|
+
if (this.ended)
|
|
125
|
+
return Promise.resolve();
|
|
126
|
+
this.inflight ?? (this.inflight = (async () => {
|
|
127
|
+
try {
|
|
128
|
+
const d = await this.source.desk.session();
|
|
129
|
+
if (!this.ended)
|
|
130
|
+
this.send({ v: 1, type: "hainiva:token", token: d.token, expiresAt: d.expiresAt });
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
// A final refusal of the agent session: the desk ends with it. A
|
|
134
|
+
// transient failure: the desk asks again (or ends on its own timeout).
|
|
135
|
+
const final = e instanceof errors_js_1.HainivaAgentError && (e.status === 401 || e.status === 403);
|
|
136
|
+
if (final)
|
|
137
|
+
this.finish("session_ended", true);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
this.inflight = null;
|
|
141
|
+
}
|
|
142
|
+
})());
|
|
143
|
+
return this.inflight;
|
|
144
|
+
}
|
|
145
|
+
setTheme(mode) { if (mode === "light" || mode === "dark")
|
|
146
|
+
this.send({ v: 1, type: "hainiva:theme", mode }); }
|
|
147
|
+
setLocale(locale) { if (locale === "vi" || locale === "en")
|
|
148
|
+
this.send({ v: 1, type: "hainiva:locale", locale }); }
|
|
149
|
+
finish(reason, tellDesk) {
|
|
150
|
+
if (this.ended)
|
|
151
|
+
return;
|
|
152
|
+
if (tellDesk) {
|
|
153
|
+
try {
|
|
154
|
+
this.send({ v: 1, type: "hainiva:end" });
|
|
155
|
+
}
|
|
156
|
+
catch { /* gone */ }
|
|
157
|
+
}
|
|
158
|
+
this.ended = true;
|
|
159
|
+
this.cb.onEnded?.(reason);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
exports.DeskHost = DeskHost;
|
|
163
|
+
/** The desk page for an app. The URL carries the app id ONLY. */
|
|
164
|
+
function deskUrl(baseUrl, appId, opts = {}) {
|
|
165
|
+
if (!UUID.test(appId))
|
|
166
|
+
throw new errors_js_1.HainivaAgentError(0, "invalid_id", { field: "appId" });
|
|
167
|
+
const base = (0, url_js_1.assertSafeUrl)(baseUrl, { kind: "http", ...(opts.allowInsecureLocalhost ? { allowInsecureLocalhost: true } : {}) });
|
|
168
|
+
const u = new URL("/embed", base.origin);
|
|
169
|
+
u.searchParams.set("app", appId.toLowerCase());
|
|
170
|
+
return u;
|
|
171
|
+
}
|
|
172
|
+
// ── Web: the iframe ──────────────────────────────────────────────────────────
|
|
173
|
+
/**
|
|
174
|
+
* The iframe's sandbox — every flag the desk needs and nothing more:
|
|
175
|
+
* allow-scripts the desk is an application;
|
|
176
|
+
* allow-same-origin the desk keeps ITS OWN origin (app.hainiva.com):
|
|
177
|
+
* its same-origin API calls, its session storage
|
|
178
|
+
* and the gateway's Origin check depend on it. It
|
|
179
|
+
* gives the desk no access to your page — that is
|
|
180
|
+
* a different origin;
|
|
181
|
+
* allow-forms the composer and every dialog submit forms;
|
|
182
|
+
* allow-popups links, recordings and help open in a new tab…
|
|
183
|
+
* allow-popups-to-escape-sandbox …as normal pages, not sandboxed copies;
|
|
184
|
+
* allow-downloads call recordings, attachments, exports;
|
|
185
|
+
* allow-modals the desk confirms destructive actions (confirm()).
|
|
186
|
+
* Deliberately NOT: allow-top-navigation(-by-user-activation) — the desk can
|
|
187
|
+
* never navigate your page; allow-pointer-lock, allow-presentation,
|
|
188
|
+
* allow-orientation-lock, allow-storage-access-by-user-activation.
|
|
189
|
+
*/
|
|
190
|
+
exports.DESK_SANDBOX = "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads allow-modals";
|
|
191
|
+
/** Calls in the iframe are WebRTC: the microphone (and camera for video) must be delegated. */
|
|
192
|
+
exports.DESK_ALLOW = "microphone; camera; autoplay; clipboard-write";
|
|
193
|
+
/** Show the HAINIVA desk in `container`, signed in as your agent session's staff member. */
|
|
194
|
+
function mountDesk(container, o) {
|
|
195
|
+
if (!container || typeof container.appendChild !== "function")
|
|
196
|
+
throw new errors_js_1.HainivaAgentError(0, "invalid_container", {});
|
|
197
|
+
if (!o?.agent?.desk || typeof o.agent.desk.session !== "function")
|
|
198
|
+
throw new errors_js_1.HainivaAgentError(0, "invalid_agent", {});
|
|
199
|
+
const url = deskUrl(o.baseUrl, o.appId, o.allowInsecureLocalhost ? { allowInsecureLocalhost: true } : {});
|
|
200
|
+
const deskOrigin = url.origin;
|
|
201
|
+
const doc = container.ownerDocument ?? globalThis.document;
|
|
202
|
+
const win = (doc?.defaultView ?? globalThis);
|
|
203
|
+
if (!doc)
|
|
204
|
+
throw new errors_js_1.HainivaAgentError(0, "no_document", {});
|
|
205
|
+
if (win.location?.origin === deskOrigin)
|
|
206
|
+
throw new errors_js_1.HainivaAgentError(0, "same_origin_host", { origin: deskOrigin });
|
|
207
|
+
const iframe = doc.createElement("iframe");
|
|
208
|
+
iframe.setAttribute("sandbox", exports.DESK_SANDBOX);
|
|
209
|
+
iframe.setAttribute("allow", exports.DESK_ALLOW);
|
|
210
|
+
iframe.setAttribute("title", o.title ?? "HAINIVA");
|
|
211
|
+
iframe.setAttribute("referrerpolicy", "origin");
|
|
212
|
+
iframe.style.border = "0";
|
|
213
|
+
iframe.style.width = "100%";
|
|
214
|
+
iframe.style.height = "100%";
|
|
215
|
+
const host = new DeskHost(o.appId, o.agent, (m) => { iframe.contentWindow?.postMessage(m, deskOrigin); }, {
|
|
216
|
+
...o,
|
|
217
|
+
onEnded: (r) => { clearTimeout(loadTimer); o.onEnded?.(r); },
|
|
218
|
+
});
|
|
219
|
+
const onMessage = (e) => {
|
|
220
|
+
if (e.source !== iframe.contentWindow || e.origin !== deskOrigin)
|
|
221
|
+
return;
|
|
222
|
+
const wasReady = host.isReady;
|
|
223
|
+
host.receive(e.data);
|
|
224
|
+
if (!wasReady && host.isReady) {
|
|
225
|
+
clearTimeout(loadTimer);
|
|
226
|
+
if (o.theme)
|
|
227
|
+
host.setTheme(o.theme);
|
|
228
|
+
if (o.locale)
|
|
229
|
+
host.setLocale(o.locale);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
win.addEventListener("message", onMessage);
|
|
233
|
+
const loadTimer = setTimeout(() => { if (!host.isReady)
|
|
234
|
+
host.finish("load_failed", false); }, o.loadTimeoutMs ?? 20000);
|
|
235
|
+
iframe.setAttribute("src", url.href);
|
|
236
|
+
container.appendChild(iframe);
|
|
237
|
+
return {
|
|
238
|
+
iframe,
|
|
239
|
+
setTheme: (m) => host.setTheme(m),
|
|
240
|
+
setLocale: (l) => host.setLocale(l),
|
|
241
|
+
destroy: () => {
|
|
242
|
+
clearTimeout(loadTimer);
|
|
243
|
+
if (!host.isEnded) {
|
|
244
|
+
try {
|
|
245
|
+
iframe.contentWindow?.postMessage({ v: 1, type: "hainiva:end" }, deskOrigin);
|
|
246
|
+
}
|
|
247
|
+
catch { /* gone */ }
|
|
248
|
+
}
|
|
249
|
+
win.removeEventListener("message", onMessage);
|
|
250
|
+
iframe.remove();
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hainiva/agent-sdk/embed — the HAINIVA desk inside your web product.
|
|
3
|
+
*
|
|
4
|
+
* import { HainivaAgent } from "@hainiva/agent-sdk";
|
|
5
|
+
* import { mountDesk } from "@hainiva/agent-sdk/embed";
|
|
6
|
+
*
|
|
7
|
+
* const agent = new HainivaAgent({ baseUrl: "https://api.hainiva.com", tenantId, getAssertion });
|
|
8
|
+
* await agent.connect();
|
|
9
|
+
* const desk = mountDesk(document.getElementById("desk")!, {
|
|
10
|
+
* agent, appId: "<your agent app id>", baseUrl: "https://app.hainiva.com",
|
|
11
|
+
* onEnded: (reason) => showSignedOut(reason),
|
|
12
|
+
* });
|
|
13
|
+
* // later: desk.setTheme("dark"); desk.destroy();
|
|
14
|
+
*
|
|
15
|
+
* Your staff member is signed in with YOUR agent session: the SDK exchanges it
|
|
16
|
+
* for a 15-minute desk token (POST /agent/v1/desk-session) and hands that to
|
|
17
|
+
* the desk over postMessage — never in the URL, never stored. The desk asks
|
|
18
|
+
* for a fresh one before it expires; when the session ends (signed out,
|
|
19
|
+
* suspended, app paused) the desk shows "ended" and `onEnded` fires.
|
|
20
|
+
*
|
|
21
|
+
* Your page's origin must be in the agent app's `allowedOrigins`: that list is
|
|
22
|
+
* also the desk's CSP frame-ancestors, so any other site gets a blank frame.
|
|
23
|
+
*
|
|
24
|
+
* Security, both directions:
|
|
25
|
+
* - only messages whose `event.source` is THIS iframe's window and whose
|
|
26
|
+
* `event.origin` is the HAINIVA desk origin are read, and only in the exact
|
|
27
|
+
* message shapes below;
|
|
28
|
+
* - everything we send goes to the desk origin only (never "*").
|
|
29
|
+
*/
|
|
30
|
+
import type { AgentDeskSessionDto } from "./contract.cjs";
|
|
31
|
+
export type DeskEndReason = "not_embedded" | "no_host" | "expired" | "revoked" | "host_ended" | "misconfigured"
|
|
32
|
+
/** The desk never answered (wrong origin: the CSP blocked the frame; network). */
|
|
33
|
+
| "load_failed"
|
|
34
|
+
/** Your agent session could not provide a desk token. */
|
|
35
|
+
| "session_ended";
|
|
36
|
+
/** From the desk. */
|
|
37
|
+
export type DeskToHost = {
|
|
38
|
+
v: 1;
|
|
39
|
+
type: "hainiva:ready";
|
|
40
|
+
appId: string;
|
|
41
|
+
} | {
|
|
42
|
+
v: 1;
|
|
43
|
+
type: "hainiva:token-request";
|
|
44
|
+
reason: "expiring" | "unauthorized";
|
|
45
|
+
} | {
|
|
46
|
+
v: 1;
|
|
47
|
+
type: "hainiva:ended";
|
|
48
|
+
reason: Exclude<DeskEndReason, "load_failed" | "session_ended">;
|
|
49
|
+
} | {
|
|
50
|
+
v: 1;
|
|
51
|
+
type: "hainiva:dial";
|
|
52
|
+
number: string;
|
|
53
|
+
contactId?: string;
|
|
54
|
+
} | {
|
|
55
|
+
v: 1;
|
|
56
|
+
type: "hainiva:dialer";
|
|
57
|
+
number?: string;
|
|
58
|
+
contactId?: string;
|
|
59
|
+
} | {
|
|
60
|
+
v: 1;
|
|
61
|
+
type: "hainiva:open-url";
|
|
62
|
+
url: string;
|
|
63
|
+
} | {
|
|
64
|
+
v: 1;
|
|
65
|
+
type: "hainiva:media-error";
|
|
66
|
+
name: string;
|
|
67
|
+
};
|
|
68
|
+
/** To the desk. */
|
|
69
|
+
export type HostToDesk = {
|
|
70
|
+
v: 1;
|
|
71
|
+
type: "hainiva:token";
|
|
72
|
+
token: string;
|
|
73
|
+
expiresAt: string;
|
|
74
|
+
} | {
|
|
75
|
+
v: 1;
|
|
76
|
+
type: "hainiva:theme";
|
|
77
|
+
mode: "light" | "dark";
|
|
78
|
+
} | {
|
|
79
|
+
v: 1;
|
|
80
|
+
type: "hainiva:locale";
|
|
81
|
+
locale: "vi" | "en";
|
|
82
|
+
} | {
|
|
83
|
+
v: 1;
|
|
84
|
+
type: "hainiva:end";
|
|
85
|
+
};
|
|
86
|
+
/** Exactly one of the desk's messages, or null. */
|
|
87
|
+
export declare function parseDeskMessage(data: unknown): DeskToHost | null;
|
|
88
|
+
/** Where the desk token comes from: `HainivaAgent` (its `desk.session()`), or anything shaped like it. */
|
|
89
|
+
export interface DeskTokenSource {
|
|
90
|
+
desk: {
|
|
91
|
+
session(): Promise<AgentDeskSessionDto>;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export interface DeskCallbacks {
|
|
95
|
+
/** The desk wants to place a call (React Native: your native call; web: the desk calls itself and never sends this). */
|
|
96
|
+
onDial?: (number: string, contactId?: string) => void;
|
|
97
|
+
/** Open your dialer, optionally pre-filled. */
|
|
98
|
+
onDialer?: (prefill: {
|
|
99
|
+
number?: string;
|
|
100
|
+
contactId?: string;
|
|
101
|
+
}) => void;
|
|
102
|
+
/** The desk's session is over. Show your own signed-out state; mount again after a new sign-in. */
|
|
103
|
+
onEnded?: (reason: DeskEndReason) => void;
|
|
104
|
+
/** A link the desk cannot open itself (React Native). Default: ignored. */
|
|
105
|
+
onOpenUrl?: (url: string) => void;
|
|
106
|
+
/** The microphone/camera was refused (web): check your iframe's `allow` and the browser's permission. */
|
|
107
|
+
onMediaError?: (name: string) => void;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The host side of the desk protocol, transport-agnostic: `mountDesk` (iframe)
|
|
111
|
+
* and `<HainivaDesk/>` (React Native WebView) both drive one.
|
|
112
|
+
* @internal exported for the React Native entry and tests.
|
|
113
|
+
*/
|
|
114
|
+
export declare class DeskHost {
|
|
115
|
+
private readonly appId;
|
|
116
|
+
private readonly source;
|
|
117
|
+
private readonly send;
|
|
118
|
+
private readonly cb;
|
|
119
|
+
private ended;
|
|
120
|
+
private ready;
|
|
121
|
+
private inflight;
|
|
122
|
+
constructor(appId: string, source: DeskTokenSource, send: (m: HostToDesk) => void, cb: DeskCallbacks);
|
|
123
|
+
get isReady(): boolean;
|
|
124
|
+
get isEnded(): boolean;
|
|
125
|
+
/** A raw message from the desk (already origin/source-checked by the transport). */
|
|
126
|
+
receive(raw: unknown): void;
|
|
127
|
+
/** One desk-session request at a time; the token goes to the desk only. */
|
|
128
|
+
supplyToken(): Promise<void>;
|
|
129
|
+
setTheme(mode: "light" | "dark"): void;
|
|
130
|
+
setLocale(locale: "vi" | "en"): void;
|
|
131
|
+
finish(reason: DeskEndReason, tellDesk: boolean): void;
|
|
132
|
+
}
|
|
133
|
+
/** The desk page for an app. The URL carries the app id ONLY. */
|
|
134
|
+
export declare function deskUrl(baseUrl: string, appId: string, opts?: {
|
|
135
|
+
allowInsecureLocalhost?: boolean;
|
|
136
|
+
}): URL;
|
|
137
|
+
/**
|
|
138
|
+
* The iframe's sandbox — every flag the desk needs and nothing more:
|
|
139
|
+
* allow-scripts the desk is an application;
|
|
140
|
+
* allow-same-origin the desk keeps ITS OWN origin (app.hainiva.com):
|
|
141
|
+
* its same-origin API calls, its session storage
|
|
142
|
+
* and the gateway's Origin check depend on it. It
|
|
143
|
+
* gives the desk no access to your page — that is
|
|
144
|
+
* a different origin;
|
|
145
|
+
* allow-forms the composer and every dialog submit forms;
|
|
146
|
+
* allow-popups links, recordings and help open in a new tab…
|
|
147
|
+
* allow-popups-to-escape-sandbox …as normal pages, not sandboxed copies;
|
|
148
|
+
* allow-downloads call recordings, attachments, exports;
|
|
149
|
+
* allow-modals the desk confirms destructive actions (confirm()).
|
|
150
|
+
* Deliberately NOT: allow-top-navigation(-by-user-activation) — the desk can
|
|
151
|
+
* never navigate your page; allow-pointer-lock, allow-presentation,
|
|
152
|
+
* allow-orientation-lock, allow-storage-access-by-user-activation.
|
|
153
|
+
*/
|
|
154
|
+
export declare const DESK_SANDBOX = "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads allow-modals";
|
|
155
|
+
/** Calls in the iframe are WebRTC: the microphone (and camera for video) must be delegated. */
|
|
156
|
+
export declare const DESK_ALLOW = "microphone; camera; autoplay; clipboard-write";
|
|
157
|
+
export interface MountDeskOptions extends DeskCallbacks {
|
|
158
|
+
/** A connected HainivaAgent (or anything with `desk.session()`). */
|
|
159
|
+
agent: DeskTokenSource;
|
|
160
|
+
/** Your agent app id (its allowedOrigins must list this page's origin). */
|
|
161
|
+
appId: string;
|
|
162
|
+
/** The HAINIVA desk, e.g. https://app.hainiva.com. */
|
|
163
|
+
baseUrl: string;
|
|
164
|
+
theme?: "light" | "dark";
|
|
165
|
+
locale?: "vi" | "en";
|
|
166
|
+
/** The iframe's accessible title. Default "HAINIVA". */
|
|
167
|
+
title?: string;
|
|
168
|
+
/** No `hainiva:ready` within this long = onEnded("load_failed"). Default 20 s. */
|
|
169
|
+
loadTimeoutMs?: number;
|
|
170
|
+
/** Local development only (http://localhost desk). */
|
|
171
|
+
allowInsecureLocalhost?: boolean;
|
|
172
|
+
}
|
|
173
|
+
export interface MountedDesk {
|
|
174
|
+
/** Remove the desk (it is told to drop its token first). */
|
|
175
|
+
destroy(): void;
|
|
176
|
+
setTheme(mode: "light" | "dark"): void;
|
|
177
|
+
setLocale(locale: "vi" | "en"): void;
|
|
178
|
+
/** The iframe, for sizing. */
|
|
179
|
+
readonly iframe: HTMLIFrameElement;
|
|
180
|
+
}
|
|
181
|
+
interface ContainerLike {
|
|
182
|
+
appendChild(n: unknown): unknown;
|
|
183
|
+
ownerDocument?: Document | null;
|
|
184
|
+
}
|
|
185
|
+
/** Show the HAINIVA desk in `container`, signed in as your agent session's staff member. */
|
|
186
|
+
export declare function mountDesk(container: ContainerLike, o: MountDeskOptions): MountedDesk;
|
|
187
|
+
export {};
|