@bircleai/widget-protocol 0.4.3
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/LICENSE.md +50 -0
- package/README.md +13 -0
- package/dist/index.cjs +293 -0
- package/dist/index.d.cts +657 -0
- package/dist/index.d.ts +657 -0
- package/dist/index.js +244 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var MEDIA_CONTENT_TYPES = [
|
|
3
|
+
"image",
|
|
4
|
+
"audio",
|
|
5
|
+
"video",
|
|
6
|
+
"document"
|
|
7
|
+
];
|
|
8
|
+
function isMediaContentType(v) {
|
|
9
|
+
return typeof v === "string" && MEDIA_CONTENT_TYPES.includes(v);
|
|
10
|
+
}
|
|
11
|
+
var ALLOWED_UPLOAD_CONTENT_TYPES = {
|
|
12
|
+
"image/png": "image",
|
|
13
|
+
"image/jpeg": "image",
|
|
14
|
+
"image/webp": "image",
|
|
15
|
+
"image/gif": "image",
|
|
16
|
+
"audio/webm": "audio",
|
|
17
|
+
"audio/mp4": "audio",
|
|
18
|
+
"audio/mpeg": "audio",
|
|
19
|
+
"audio/ogg": "audio",
|
|
20
|
+
"video/mp4": "video",
|
|
21
|
+
"video/webm": "video",
|
|
22
|
+
"application/pdf": "document"
|
|
23
|
+
};
|
|
24
|
+
var MAX_UPLOAD_SIZE_BYTES = 10 * 1024 * 1024;
|
|
25
|
+
function isAllowedUploadContentType(contentType) {
|
|
26
|
+
return Object.prototype.hasOwnProperty.call(ALLOWED_UPLOAD_CONTENT_TYPES, contentType);
|
|
27
|
+
}
|
|
28
|
+
function mediaKindForContentType(contentType) {
|
|
29
|
+
return ALLOWED_UPLOAD_CONTENT_TYPES[contentType] ?? null;
|
|
30
|
+
}
|
|
31
|
+
var SERVER_PROVIDED_THEME_FIELDS = [
|
|
32
|
+
"primaryColor",
|
|
33
|
+
"accentColor",
|
|
34
|
+
"launcherIcon",
|
|
35
|
+
"launcherLabel",
|
|
36
|
+
"position",
|
|
37
|
+
"headerLogo",
|
|
38
|
+
"assistantName",
|
|
39
|
+
"assistantTagline",
|
|
40
|
+
"assistantAvatar",
|
|
41
|
+
"fontFamily",
|
|
42
|
+
"colorScheme",
|
|
43
|
+
"poweredBy",
|
|
44
|
+
"locale",
|
|
45
|
+
"texts"
|
|
46
|
+
];
|
|
47
|
+
var MAX_THEME_LABEL = 40;
|
|
48
|
+
var MAX_THEME_TAGLINE = 80;
|
|
49
|
+
var MAX_THEME_FONT_FAMILY = 120;
|
|
50
|
+
var MAX_THEME_TEXT_KEYS = 40;
|
|
51
|
+
var MAX_THEME_TEXT_KEY = 60;
|
|
52
|
+
var MAX_THEME_TEXT_VALUE = 200;
|
|
53
|
+
function safeThemeColor(value) {
|
|
54
|
+
if (typeof value !== "string") return null;
|
|
55
|
+
const v = value.trim();
|
|
56
|
+
return /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(v) ? v : null;
|
|
57
|
+
}
|
|
58
|
+
function safeThemeUrl(value) {
|
|
59
|
+
if (typeof value !== "string" || value.trim() === "") return null;
|
|
60
|
+
try {
|
|
61
|
+
const u = new URL(value.trim());
|
|
62
|
+
return u.protocol === "https:" ? u.toString() : null;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function safeThemeText(value, max) {
|
|
68
|
+
if (typeof value !== "string") return null;
|
|
69
|
+
const v = value.trim();
|
|
70
|
+
return v !== "" && v.length <= max ? v : null;
|
|
71
|
+
}
|
|
72
|
+
function sanitizeThemeTexts(raw) {
|
|
73
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
|
|
74
|
+
const out = {};
|
|
75
|
+
let n = 0;
|
|
76
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
77
|
+
if (n >= MAX_THEME_TEXT_KEYS) break;
|
|
78
|
+
if (k === "" || k.length > MAX_THEME_TEXT_KEY) continue;
|
|
79
|
+
if (typeof v !== "string" || v === "" || v.length > MAX_THEME_TEXT_VALUE) continue;
|
|
80
|
+
out[k] = v;
|
|
81
|
+
n += 1;
|
|
82
|
+
}
|
|
83
|
+
return n > 0 ? out : null;
|
|
84
|
+
}
|
|
85
|
+
function sanitizeServerTheme(raw) {
|
|
86
|
+
if (typeof raw !== "object" || raw === null) return {};
|
|
87
|
+
const o = raw;
|
|
88
|
+
const out = {};
|
|
89
|
+
const primaryColor = safeThemeColor(o.primaryColor);
|
|
90
|
+
if (primaryColor !== null) out.primaryColor = primaryColor;
|
|
91
|
+
const accentColor = safeThemeColor(o.accentColor);
|
|
92
|
+
if (accentColor !== null) out.accentColor = accentColor;
|
|
93
|
+
const launcherIcon = safeThemeUrl(o.launcherIcon);
|
|
94
|
+
if (launcherIcon !== null) out.launcherIcon = launcherIcon;
|
|
95
|
+
const headerLogo = safeThemeUrl(o.headerLogo);
|
|
96
|
+
if (headerLogo !== null) out.headerLogo = headerLogo;
|
|
97
|
+
const assistantAvatar = safeThemeUrl(o.assistantAvatar);
|
|
98
|
+
if (assistantAvatar !== null) out.assistantAvatar = assistantAvatar;
|
|
99
|
+
const launcherLabel = safeThemeText(o.launcherLabel, MAX_THEME_LABEL);
|
|
100
|
+
if (launcherLabel !== null) out.launcherLabel = launcherLabel;
|
|
101
|
+
const assistantName = safeThemeText(o.assistantName, MAX_THEME_LABEL);
|
|
102
|
+
if (assistantName !== null) out.assistantName = assistantName;
|
|
103
|
+
const assistantTagline = safeThemeText(o.assistantTagline, MAX_THEME_TAGLINE);
|
|
104
|
+
if (assistantTagline !== null) out.assistantTagline = assistantTagline;
|
|
105
|
+
const fontFamily = safeThemeText(o.fontFamily, MAX_THEME_FONT_FAMILY);
|
|
106
|
+
if (fontFamily !== null) out.fontFamily = fontFamily;
|
|
107
|
+
if (o.position === "bottom-right" || o.position === "bottom-left") out.position = o.position;
|
|
108
|
+
if (o.colorScheme === "light" || o.colorScheme === "dark") out.colorScheme = o.colorScheme;
|
|
109
|
+
if (typeof o.poweredBy === "boolean") out.poweredBy = o.poweredBy;
|
|
110
|
+
const locale = safeThemeLocale(o.locale);
|
|
111
|
+
if (locale !== null) out.locale = locale;
|
|
112
|
+
const texts = sanitizeThemeTexts(o.texts);
|
|
113
|
+
if (texts !== null) out.texts = texts;
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
function safeThemeLocale(value) {
|
|
117
|
+
if (typeof value !== "string") return null;
|
|
118
|
+
const v = value.trim();
|
|
119
|
+
return v.length >= 2 && v.length <= 5 && /^[a-zA-Z-]+$/.test(v) ? v : null;
|
|
120
|
+
}
|
|
121
|
+
var MAX_QUICK_REPLIES = 6;
|
|
122
|
+
var MAX_QUICK_REPLY_LABEL = 40;
|
|
123
|
+
function sanitizeQuickReplies(raw) {
|
|
124
|
+
if (!Array.isArray(raw)) return [];
|
|
125
|
+
const out = [];
|
|
126
|
+
for (const item of raw) {
|
|
127
|
+
if (out.length >= MAX_QUICK_REPLIES) break;
|
|
128
|
+
if (typeof item !== "object" || item === null) continue;
|
|
129
|
+
const o = item;
|
|
130
|
+
const label = typeof o.label === "string" ? o.label.trim() : "";
|
|
131
|
+
if (label === "" || label.length > MAX_QUICK_REPLY_LABEL) continue;
|
|
132
|
+
const value = typeof o.value === "string" && o.value.trim() !== "" ? o.value.trim() : void 0;
|
|
133
|
+
out.push(value !== void 0 ? { label, value } : { label });
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
var MAX_AGENT_NAME = 40;
|
|
138
|
+
function sanitizeAssistantIdentity(raw, kind = "human") {
|
|
139
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
140
|
+
const o = raw;
|
|
141
|
+
const rawName = typeof o.name === "string" ? o.name.trim() : "";
|
|
142
|
+
const name = rawName !== "" && rawName.length <= MAX_AGENT_NAME ? rawName : void 0;
|
|
143
|
+
let avatar;
|
|
144
|
+
if (typeof o.avatar === "string" && o.avatar.trim() !== "") {
|
|
145
|
+
try {
|
|
146
|
+
const u = new URL(o.avatar.trim());
|
|
147
|
+
if (u.protocol === "https:") avatar = u.toString();
|
|
148
|
+
} catch {
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (name === void 0 && avatar === void 0) return null;
|
|
152
|
+
return {
|
|
153
|
+
kind,
|
|
154
|
+
...name !== void 0 ? { name } : {},
|
|
155
|
+
...avatar !== void 0 ? { avatar } : {}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function isRawAssistantMessageEvent(v) {
|
|
159
|
+
if (typeof v !== "object" || v === null) return false;
|
|
160
|
+
const o = v;
|
|
161
|
+
if (o.type !== "assistant_message") return false;
|
|
162
|
+
if (typeof o.conversation_id !== "string") return false;
|
|
163
|
+
if (typeof o.content !== "string") return false;
|
|
164
|
+
if (!Array.isArray(o.turn_ids)) return false;
|
|
165
|
+
if (o.ts !== void 0 && o.ts !== null && typeof o.ts !== "number") return false;
|
|
166
|
+
if (o.content_type !== void 0 && o.content_type !== null && typeof o.content_type !== "string")
|
|
167
|
+
return false;
|
|
168
|
+
if (o.caption !== void 0 && o.caption !== null && typeof o.caption !== "string") return false;
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
function isAssistantMessageEvent(v) {
|
|
172
|
+
if (!isRawAssistantMessageEvent(v)) return false;
|
|
173
|
+
if (typeof v.ts !== "number") return false;
|
|
174
|
+
if (v.caption !== void 0 && typeof v.caption !== "string") return false;
|
|
175
|
+
if (v.content_type !== void 0 && v.content_type !== "text" && !isMediaContentType(v.content_type))
|
|
176
|
+
return false;
|
|
177
|
+
return v.turn_ids.every((t) => typeof t === "string");
|
|
178
|
+
}
|
|
179
|
+
function normalizeAssistantMessageEvent(raw, now = Date.now) {
|
|
180
|
+
const turnIds = raw.turn_ids.filter((t) => typeof t === "string");
|
|
181
|
+
return {
|
|
182
|
+
type: "assistant_message",
|
|
183
|
+
conversation_id: raw.conversation_id,
|
|
184
|
+
content: raw.content,
|
|
185
|
+
turn_ids: turnIds,
|
|
186
|
+
ts: typeof raw.ts === "number" && Number.isFinite(raw.ts) && raw.ts > 0 ? raw.ts : now(),
|
|
187
|
+
...raw.content_type != null ? {
|
|
188
|
+
content_type: isMediaContentType(raw.content_type) ? raw.content_type : "text"
|
|
189
|
+
} : {},
|
|
190
|
+
...raw.caption != null ? { caption: raw.caption } : {},
|
|
191
|
+
...(() => {
|
|
192
|
+
const qr = sanitizeQuickReplies(raw.quick_replies);
|
|
193
|
+
return qr.length > 0 ? { quick_replies: qr } : {};
|
|
194
|
+
})(),
|
|
195
|
+
// Cualquier valor que no sea exactamente "human" cuenta como bot: es el
|
|
196
|
+
// default seguro — decirle al usuario que lo atiende una persona cuando no
|
|
197
|
+
// es cierto es peor que lo contrario.
|
|
198
|
+
...raw.author === "human" ? { author: "human" } : {},
|
|
199
|
+
// La identidad SÓLO se propaga junto a `author: "human"`. Un `agent` en un
|
|
200
|
+
// mensaje del bot le pondría cara y nombre de persona a algo que escribió
|
|
201
|
+
// una máquina, que es exactamente la confusión que el handover evita.
|
|
202
|
+
...(() => {
|
|
203
|
+
if (raw.author !== "human") return {};
|
|
204
|
+
const id = sanitizeAssistantIdentity(raw.agent);
|
|
205
|
+
return id !== null ? { agent: id } : {};
|
|
206
|
+
})()
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function parseWidgetEvent(raw, now = Date.now) {
|
|
210
|
+
let v;
|
|
211
|
+
try {
|
|
212
|
+
v = JSON.parse(raw);
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return isRawAssistantMessageEvent(v) ? normalizeAssistantMessageEvent(v, now) : null;
|
|
217
|
+
}
|
|
218
|
+
export {
|
|
219
|
+
ALLOWED_UPLOAD_CONTENT_TYPES,
|
|
220
|
+
MAX_AGENT_NAME,
|
|
221
|
+
MAX_QUICK_REPLIES,
|
|
222
|
+
MAX_QUICK_REPLY_LABEL,
|
|
223
|
+
MAX_THEME_FONT_FAMILY,
|
|
224
|
+
MAX_THEME_LABEL,
|
|
225
|
+
MAX_THEME_TAGLINE,
|
|
226
|
+
MAX_THEME_TEXT_KEY,
|
|
227
|
+
MAX_THEME_TEXT_KEYS,
|
|
228
|
+
MAX_THEME_TEXT_VALUE,
|
|
229
|
+
MAX_UPLOAD_SIZE_BYTES,
|
|
230
|
+
MEDIA_CONTENT_TYPES,
|
|
231
|
+
SERVER_PROVIDED_THEME_FIELDS,
|
|
232
|
+
isAllowedUploadContentType,
|
|
233
|
+
isAssistantMessageEvent,
|
|
234
|
+
isMediaContentType,
|
|
235
|
+
isRawAssistantMessageEvent,
|
|
236
|
+
mediaKindForContentType,
|
|
237
|
+
normalizeAssistantMessageEvent,
|
|
238
|
+
parseWidgetEvent,
|
|
239
|
+
safeThemeColor,
|
|
240
|
+
safeThemeUrl,
|
|
241
|
+
sanitizeAssistantIdentity,
|
|
242
|
+
sanitizeQuickReplies,
|
|
243
|
+
sanitizeServerTheme
|
|
244
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bircleai/widget-protocol",
|
|
3
|
+
"version": "0.4.3",
|
|
4
|
+
"description": "Contrato compartido del Chat Gateway de BircleAI (tipos + helpers de parseo).",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
23
|
+
"build:debug": "tsup src/index.ts --format esm,cjs --dts --clean --sourcemap",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"registry": "https://registry.npmjs.org",
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/BircleAI/bircle-chat-widget.git"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"bircle",
|
|
37
|
+
"chat",
|
|
38
|
+
"protocol",
|
|
39
|
+
"types",
|
|
40
|
+
"typescript"
|
|
41
|
+
],
|
|
42
|
+
"homepage": "https://bircle.ai",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"email": "soporte@bircle.ai"
|
|
45
|
+
},
|
|
46
|
+
"author": "Bircle AI",
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18"
|
|
49
|
+
}
|
|
50
|
+
}
|