@bircleai/widget-protocol 0.6.1 → 0.7.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/dist/index.cjs +104 -3
- package/dist/index.d.cts +136 -16
- package/dist/index.d.ts +136 -16
- package/dist/index.js +95 -2
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -21,9 +21,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
ALLOWED_UPLOAD_CONTENT_TYPES: () => ALLOWED_UPLOAD_CONTENT_TYPES,
|
|
24
|
+
DEFAULT_SURVEY_TEXT_LEN: () => DEFAULT_SURVEY_TEXT_LEN,
|
|
24
25
|
MAX_AGENT_NAME: () => MAX_AGENT_NAME,
|
|
25
26
|
MAX_QUICK_REPLIES: () => MAX_QUICK_REPLIES,
|
|
26
27
|
MAX_QUICK_REPLY_LABEL: () => MAX_QUICK_REPLY_LABEL,
|
|
28
|
+
MAX_SURVEY_CHOICE_OPTIONS: () => MAX_SURVEY_CHOICE_OPTIONS,
|
|
29
|
+
MAX_SURVEY_ID: () => MAX_SURVEY_ID,
|
|
30
|
+
MAX_SURVEY_LABEL: () => MAX_SURVEY_LABEL,
|
|
31
|
+
MAX_SURVEY_OPTION_LABEL: () => MAX_SURVEY_OPTION_LABEL,
|
|
32
|
+
MAX_SURVEY_STEPS: () => MAX_SURVEY_STEPS,
|
|
33
|
+
MAX_SURVEY_TEXT_LEN: () => MAX_SURVEY_TEXT_LEN,
|
|
27
34
|
MAX_THEME_FONT_FAMILY: () => MAX_THEME_FONT_FAMILY,
|
|
28
35
|
MAX_THEME_LABEL: () => MAX_THEME_LABEL,
|
|
29
36
|
MAX_THEME_TAGLINE: () => MAX_THEME_TAGLINE,
|
|
@@ -44,7 +51,8 @@ __export(index_exports, {
|
|
|
44
51
|
safeThemeUrl: () => safeThemeUrl,
|
|
45
52
|
sanitizeAssistantIdentity: () => sanitizeAssistantIdentity,
|
|
46
53
|
sanitizeQuickReplies: () => sanitizeQuickReplies,
|
|
47
|
-
sanitizeServerTheme: () => sanitizeServerTheme
|
|
54
|
+
sanitizeServerTheme: () => sanitizeServerTheme,
|
|
55
|
+
sanitizeSurvey: () => sanitizeSurvey
|
|
48
56
|
});
|
|
49
57
|
module.exports = __toCommonJS(index_exports);
|
|
50
58
|
var MEDIA_CONTENT_TYPES = [
|
|
@@ -131,6 +139,13 @@ function sanitizeThemeTexts(raw) {
|
|
|
131
139
|
}
|
|
132
140
|
return n > 0 ? out : null;
|
|
133
141
|
}
|
|
142
|
+
function safeLauncherToken(v) {
|
|
143
|
+
if (typeof v !== "string") return null;
|
|
144
|
+
const t = v.trim();
|
|
145
|
+
if (t === "bircle" || t === "chat") return t;
|
|
146
|
+
if (t.length > 0 && t.length <= 8 && !t.includes(":") && !t.includes("/")) return t;
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
134
149
|
function sanitizeServerTheme(raw) {
|
|
135
150
|
if (typeof raw !== "object" || raw === null) return {};
|
|
136
151
|
const o = raw;
|
|
@@ -139,7 +154,7 @@ function sanitizeServerTheme(raw) {
|
|
|
139
154
|
if (primaryColor !== null) out.primaryColor = primaryColor;
|
|
140
155
|
const accentColor = safeThemeColor(o.accentColor);
|
|
141
156
|
if (accentColor !== null) out.accentColor = accentColor;
|
|
142
|
-
const launcherIcon = safeThemeUrl(o.launcherIcon);
|
|
157
|
+
const launcherIcon = safeThemeUrl(o.launcherIcon) ?? safeLauncherToken(o.launcherIcon);
|
|
143
158
|
if (launcherIcon !== null) out.launcherIcon = launcherIcon;
|
|
144
159
|
const headerLogo = safeThemeUrl(o.headerLogo);
|
|
145
160
|
if (headerLogo !== null) out.headerLogo = headerLogo;
|
|
@@ -203,6 +218,76 @@ function sanitizeQuickReplies(raw) {
|
|
|
203
218
|
}
|
|
204
219
|
return out;
|
|
205
220
|
}
|
|
221
|
+
var MAX_SURVEY_STEPS = 5;
|
|
222
|
+
var MAX_SURVEY_CHOICE_OPTIONS = 10;
|
|
223
|
+
var MAX_SURVEY_LABEL = 100;
|
|
224
|
+
var MAX_SURVEY_OPTION_LABEL = 30;
|
|
225
|
+
var MAX_SURVEY_ID = 64;
|
|
226
|
+
var MAX_SURVEY_TEXT_LEN = 1e3;
|
|
227
|
+
var DEFAULT_SURVEY_TEXT_LEN = 500;
|
|
228
|
+
function surveyId(value, max = MAX_SURVEY_ID) {
|
|
229
|
+
if (typeof value !== "string") return null;
|
|
230
|
+
const v = value.trim();
|
|
231
|
+
return v !== "" && v.length <= max ? v : null;
|
|
232
|
+
}
|
|
233
|
+
function sanitizeSurveyStep(raw) {
|
|
234
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
235
|
+
const o = raw;
|
|
236
|
+
const key = surveyId(o.key);
|
|
237
|
+
const label = surveyId(o.label, MAX_SURVEY_LABEL);
|
|
238
|
+
if (key === null || label === null) return null;
|
|
239
|
+
switch (o.type) {
|
|
240
|
+
case "stars":
|
|
241
|
+
return { type: "stars", key, label };
|
|
242
|
+
case "choice": {
|
|
243
|
+
if (!Array.isArray(o.options)) return null;
|
|
244
|
+
if (o.options.length === 0 || o.options.length > MAX_SURVEY_CHOICE_OPTIONS) return null;
|
|
245
|
+
const options = [];
|
|
246
|
+
for (const item of o.options) {
|
|
247
|
+
if (typeof item !== "object" || item === null) return null;
|
|
248
|
+
const opt = item;
|
|
249
|
+
const id = surveyId(opt.id);
|
|
250
|
+
const optLabel = surveyId(opt.label, MAX_SURVEY_OPTION_LABEL);
|
|
251
|
+
if (id === null || optLabel === null) return null;
|
|
252
|
+
options.push({ id, label: optLabel });
|
|
253
|
+
}
|
|
254
|
+
return { type: "choice", key, label, options };
|
|
255
|
+
}
|
|
256
|
+
case "text": {
|
|
257
|
+
const step = { type: "text", key, label };
|
|
258
|
+
if (typeof o.max_len === "number" && Number.isInteger(o.max_len) && o.max_len > 0 && o.max_len <= MAX_SURVEY_TEXT_LEN) {
|
|
259
|
+
step.max_len = o.max_len;
|
|
260
|
+
}
|
|
261
|
+
return step;
|
|
262
|
+
}
|
|
263
|
+
default:
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function sanitizeSurvey(raw) {
|
|
268
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
|
|
269
|
+
const o = raw;
|
|
270
|
+
const survey_id = surveyId(o.survey_id);
|
|
271
|
+
const survey_msg_id = surveyId(o.survey_msg_id);
|
|
272
|
+
if (survey_id === null || survey_msg_id === null) return null;
|
|
273
|
+
if (typeof o.version !== "number" || !Number.isInteger(o.version) || o.version <= 0) return null;
|
|
274
|
+
if (!Array.isArray(o.steps)) return null;
|
|
275
|
+
if (o.steps.length === 0 || o.steps.length > MAX_SURVEY_STEPS) return null;
|
|
276
|
+
const steps = [];
|
|
277
|
+
for (const rawStep of o.steps) {
|
|
278
|
+
const step = sanitizeSurveyStep(rawStep);
|
|
279
|
+
if (step === null) return null;
|
|
280
|
+
steps.push(step);
|
|
281
|
+
}
|
|
282
|
+
let answered;
|
|
283
|
+
if (o.answered === void 0 || o.answered === null) answered = false;
|
|
284
|
+
else if (typeof o.answered === "boolean") answered = o.answered;
|
|
285
|
+
else return null;
|
|
286
|
+
const out = { survey_id, version: o.version, survey_msg_id, steps, answered };
|
|
287
|
+
const case_ref = surveyId(o.case_ref, MAX_SURVEY_LABEL);
|
|
288
|
+
if (case_ref !== null) out.case_ref = case_ref;
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
206
291
|
var MAX_AGENT_NAME = 40;
|
|
207
292
|
function sanitizeAssistantIdentity(raw, kind = "human") {
|
|
208
293
|
if (typeof raw !== "object" || raw === null) return null;
|
|
@@ -243,6 +328,7 @@ function isAssistantMessageEvent(v) {
|
|
|
243
328
|
if (v.caption !== void 0 && typeof v.caption !== "string") return false;
|
|
244
329
|
if (v.content_type !== void 0 && v.content_type !== "text" && !isMediaContentType(v.content_type))
|
|
245
330
|
return false;
|
|
331
|
+
if (v.survey !== void 0 && sanitizeSurvey(v.survey) === null) return false;
|
|
246
332
|
return v.turn_ids.every((t) => typeof t === "string");
|
|
247
333
|
}
|
|
248
334
|
function normalizeAssistantMessageEvent(raw, now = Date.now) {
|
|
@@ -261,6 +347,13 @@ function normalizeAssistantMessageEvent(raw, now = Date.now) {
|
|
|
261
347
|
const qr = sanitizeQuickReplies(raw.quick_replies);
|
|
262
348
|
return qr.length > 0 ? { quick_replies: qr } : {};
|
|
263
349
|
})(),
|
|
350
|
+
// La FUNCIÓN es la barrera (lección FAQs): un `survey` que no pasa el saneo
|
|
351
|
+
// no viaja — el mensaje conserva su `content` de texto fallback, así que
|
|
352
|
+
// una encuesta corrupta nunca cuesta la respuesta del agente.
|
|
353
|
+
...(() => {
|
|
354
|
+
const sv = sanitizeSurvey(raw.survey);
|
|
355
|
+
return sv !== null ? { survey: sv } : {};
|
|
356
|
+
})(),
|
|
264
357
|
// Cualquier valor que no sea exactamente "human" cuenta como bot: es el
|
|
265
358
|
// default seguro — decirle al usuario que lo atiende una persona cuando no
|
|
266
359
|
// es cierto es peor que lo contrario.
|
|
@@ -287,9 +380,16 @@ function parseWidgetEvent(raw, now = Date.now) {
|
|
|
287
380
|
// Annotate the CommonJS export names for ESM import in node:
|
|
288
381
|
0 && (module.exports = {
|
|
289
382
|
ALLOWED_UPLOAD_CONTENT_TYPES,
|
|
383
|
+
DEFAULT_SURVEY_TEXT_LEN,
|
|
290
384
|
MAX_AGENT_NAME,
|
|
291
385
|
MAX_QUICK_REPLIES,
|
|
292
386
|
MAX_QUICK_REPLY_LABEL,
|
|
387
|
+
MAX_SURVEY_CHOICE_OPTIONS,
|
|
388
|
+
MAX_SURVEY_ID,
|
|
389
|
+
MAX_SURVEY_LABEL,
|
|
390
|
+
MAX_SURVEY_OPTION_LABEL,
|
|
391
|
+
MAX_SURVEY_STEPS,
|
|
392
|
+
MAX_SURVEY_TEXT_LEN,
|
|
293
393
|
MAX_THEME_FONT_FAMILY,
|
|
294
394
|
MAX_THEME_LABEL,
|
|
295
395
|
MAX_THEME_TAGLINE,
|
|
@@ -310,5 +410,6 @@ function parseWidgetEvent(raw, now = Date.now) {
|
|
|
310
410
|
safeThemeUrl,
|
|
311
411
|
sanitizeAssistantIdentity,
|
|
312
412
|
sanitizeQuickReplies,
|
|
313
|
-
sanitizeServerTheme
|
|
413
|
+
sanitizeServerTheme,
|
|
414
|
+
sanitizeSurvey
|
|
314
415
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -194,6 +194,16 @@ interface HistoryMessageDto {
|
|
|
194
194
|
* elegir, al volver tiene que poder elegir igual.
|
|
195
195
|
*/
|
|
196
196
|
quick_replies?: QuickReply[];
|
|
197
|
+
/**
|
|
198
|
+
* Encuesta embebida del mensaje (`content_type: "survey"`), con su estado
|
|
199
|
+
* `answered` denormalizado EN el item (Red Team #4): reabrir el chat no
|
|
200
|
+
* re-ofrece una encuesta contestada.
|
|
201
|
+
*
|
|
202
|
+
* Mismo trato que en el evento WS: el valor crudo del wire pasa por
|
|
203
|
+
* `sanitizeSurvey` en el consumidor ANTES de renderizar — el tipo documenta
|
|
204
|
+
* el contrato, la función es la barrera.
|
|
205
|
+
*/
|
|
206
|
+
survey?: SurveyPayload;
|
|
197
207
|
/** Ausente ⇒ `bot`. Ver `MessageAuthorKind`. */
|
|
198
208
|
author?: MessageAuthorKind;
|
|
199
209
|
}
|
|
@@ -334,7 +344,7 @@ interface ChallengeUiConfig {
|
|
|
334
344
|
interface ThemeResponse {
|
|
335
345
|
/** Color de marca. `#rgb` o `#rrggbb`. Pasa por `safeThemeColor`. */
|
|
336
346
|
primaryColor?: string;
|
|
337
|
-
/** URL https
|
|
347
|
+
/** Ícono del launcher: URL https, un built-in ("bircle" | "chat") o un emoji corto. */
|
|
338
348
|
launcherIcon?: string;
|
|
339
349
|
position?: "bottom-right" | "bottom-left";
|
|
340
350
|
/** URL https del logo del header. */
|
|
@@ -460,19 +470,6 @@ declare function safeThemeColor(value: unknown): string | null;
|
|
|
460
470
|
* que termina en un `<img src>` es un vector de XSS en el sitio del cliente.
|
|
461
471
|
*/
|
|
462
472
|
declare function safeThemeUrl(value: unknown): string | null;
|
|
463
|
-
/**
|
|
464
|
-
* Tema del server, saneado campo por campo contra el contrato.
|
|
465
|
-
*
|
|
466
|
-
* ES EL ÚNICO CAMINO DE ENTRADA del branding remoto: lo que no sale de acá no
|
|
467
|
-
* debería aplicarse. Devuelve un objeto NUEVO con sólo los campos válidos —
|
|
468
|
-
* nunca el input— así ninguna clave extra del wire (una que el server agregue
|
|
469
|
-
* mañana, o una que un theme viejo tenga guardada) se cuela por un spread.
|
|
470
|
-
*
|
|
471
|
-
* NUNCA LANZA Y NUNCA RECHAZA EL TEMA ENTERO: un campo inválido se descarta y el
|
|
472
|
-
* consumidor cae a su default. Perder el branding completo —o peor, no pintar el
|
|
473
|
-
* chat— porque el `accentColor` está mal cargado sería mucho peor que mostrar el
|
|
474
|
-
* acento por default.
|
|
475
|
-
*/
|
|
476
473
|
declare function sanitizeServerTheme(raw: unknown): ServerThemeResponse;
|
|
477
474
|
/**
|
|
478
475
|
* Evento entregado por el WebSocket del widget (respuesta del asistente o del
|
|
@@ -513,6 +510,119 @@ declare const MAX_QUICK_REPLY_LABEL = 40;
|
|
|
513
510
|
* respuesta del agente por un botón mal formado sería mucho peor.
|
|
514
511
|
*/
|
|
515
512
|
declare function sanitizeQuickReplies(raw: unknown): QuickReply[];
|
|
513
|
+
/** Tope de pasos de una encuesta (límite del contrato de `surveys`). */
|
|
514
|
+
declare const MAX_SURVEY_STEPS = 5;
|
|
515
|
+
/** Tope de opciones de un paso `choice`. */
|
|
516
|
+
declare const MAX_SURVEY_CHOICE_OPTIONS = 10;
|
|
517
|
+
/** Tope del `label` de un paso. */
|
|
518
|
+
declare const MAX_SURVEY_LABEL = 100;
|
|
519
|
+
/** Tope del `label` de una opción de `choice` (entra en un chip de teléfono). */
|
|
520
|
+
declare const MAX_SURVEY_OPTION_LABEL = 30;
|
|
521
|
+
/** Tope de los ids/keys (`survey_id`, `survey_msg_id`, `key`, `option.id`). */
|
|
522
|
+
declare const MAX_SURVEY_ID = 64;
|
|
523
|
+
/** Tope duro de `max_len` de un paso `text` (mismo límite que el builder). */
|
|
524
|
+
declare const MAX_SURVEY_TEXT_LEN = 1000;
|
|
525
|
+
/** `max_len` efectivo cuando la definición no trae uno. */
|
|
526
|
+
declare const DEFAULT_SURVEY_TEXT_LEN = 500;
|
|
527
|
+
/** Paso de estrellas 1–5 (el rating principal). */
|
|
528
|
+
interface SurveyStepStars {
|
|
529
|
+
type: "stars";
|
|
530
|
+
/** Clave del answer en el POST de respuesta (ej. `rating`). */
|
|
531
|
+
key: string;
|
|
532
|
+
/** Pregunta a mostrar. Texto plano, nunca markup. */
|
|
533
|
+
label: string;
|
|
534
|
+
}
|
|
535
|
+
/** Una opción elegible de un paso `choice`. */
|
|
536
|
+
interface SurveyChoiceOption {
|
|
537
|
+
/** Id estable de la opción — es lo que se REPORTA, el label es lo que se ve. */
|
|
538
|
+
id: string;
|
|
539
|
+
label: string;
|
|
540
|
+
}
|
|
541
|
+
/** Paso de opciones (motivo de la calificación), render tipo chips. */
|
|
542
|
+
interface SurveyStepChoice {
|
|
543
|
+
type: "choice";
|
|
544
|
+
key: string;
|
|
545
|
+
label: string;
|
|
546
|
+
options: SurveyChoiceOption[];
|
|
547
|
+
}
|
|
548
|
+
/** Paso de texto libre (comentario). */
|
|
549
|
+
interface SurveyStepText {
|
|
550
|
+
type: "text";
|
|
551
|
+
key: string;
|
|
552
|
+
label: string;
|
|
553
|
+
/** Tope de caracteres del comentario. Ausente ⇒ `DEFAULT_SURVEY_TEXT_LEN`. */
|
|
554
|
+
max_len?: number;
|
|
555
|
+
}
|
|
556
|
+
type SurveyStep = SurveyStepStars | SurveyStepChoice | SurveyStepText;
|
|
557
|
+
/**
|
|
558
|
+
* Encuesta embebida en un mensaje del asistente. Campo hermano de
|
|
559
|
+
* `quick_replies` — viaja igual en el evento WS y en el DTO del historial.
|
|
560
|
+
*/
|
|
561
|
+
interface SurveyPayload {
|
|
562
|
+
/** Id de la DEFINICIÓN (tabla `surveys`). */
|
|
563
|
+
survey_id: string;
|
|
564
|
+
/** Versión de la definición con la que se generó este mensaje. */
|
|
565
|
+
version: number;
|
|
566
|
+
/**
|
|
567
|
+
* Id del MENSAJE survey. Es la clave con la que se responde
|
|
568
|
+
* (`POST /v1/widget/survey-response`) y con la que el server deduplica: el
|
|
569
|
+
* `response_id` determinístico sale de acá.
|
|
570
|
+
*/
|
|
571
|
+
survey_msg_id: string;
|
|
572
|
+
/** Pasos a renderizar, en orden. Nunca vacío (sin pasos no hay encuesta). */
|
|
573
|
+
steps: SurveyStep[];
|
|
574
|
+
/** Referencia legible del caso que se califica ("¿Cómo resolvimos tu caso #123?"). */
|
|
575
|
+
case_ref?: string;
|
|
576
|
+
/**
|
|
577
|
+
* `true` = ya respondida. Denormalizado EN el item del mensaje (Red Team #4)
|
|
578
|
+
* para que la rehidratación no re-ofrezca una encuesta contestada — la UI la
|
|
579
|
+
* muestra en su estado "¡Gracias!" directamente.
|
|
580
|
+
*/
|
|
581
|
+
answered: boolean;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Encuesta USABLE del wire, o `null`.
|
|
585
|
+
*
|
|
586
|
+
* ES LA BARRERA (no el tipo): todo `survey` que llega del server —evento WS o
|
|
587
|
+
* DTO de historial— pasa por acá antes de tocar la UI. Valida el contrato
|
|
588
|
+
* completo: ids/keys no vacíos ≤ 64, ≤ 5 pasos, choice con 1..10 opciones,
|
|
589
|
+
* labels ≤ 100 (≤ 30 en opciones), `version` entero positivo.
|
|
590
|
+
*
|
|
591
|
+
* TODO-O-NADA A PROPÓSITO (distinto de `sanitizeQuickReplies`): una encuesta
|
|
592
|
+
* con un paso corrupto no es "una encuesta más corta", es un instrumento que ya
|
|
593
|
+
* no coincide con la definición contra la que el Gateway valida la respuesta.
|
|
594
|
+
* Ofrecerla garantiza un 400 DESPUÉS de que el usuario contestó — peor que no
|
|
595
|
+
* ofrecerla (el mensaje conserva su `content` de texto fallback). NUNCA lanza.
|
|
596
|
+
*
|
|
597
|
+
* Tolerancias deliberadas (ausencia ≠ inválido):
|
|
598
|
+
* - `answered` ausente o `null` ⇒ `false` (un productor viejo que no
|
|
599
|
+
* denormaliza el estado no debe matar la encuesta); cualquier otro no-bool
|
|
600
|
+
* (`"true"`) sí es inválido — adivinar el estado re-ofrecería encuestas
|
|
601
|
+
* contestadas.
|
|
602
|
+
* - `case_ref` es decorativo: no-string/vacío/gigante ⇒ se omite el campo,
|
|
603
|
+
* la encuesta sigue (mismo criterio que el avatar en
|
|
604
|
+
* `sanitizeAssistantIdentity`).
|
|
605
|
+
*/
|
|
606
|
+
declare function sanitizeSurvey(raw: unknown): SurveyPayload | null;
|
|
607
|
+
/**
|
|
608
|
+
* `POST /v1/widget/survey-response` — body (auth: Bearer JWT de sesión, igual
|
|
609
|
+
* que `/messages`). `reason` debe ser un `option.id` de la definición y
|
|
610
|
+
* `comment` respetar el `max_len` — el Gateway valida contra la definición y
|
|
611
|
+
* responde 400 si no. 409 = ya respondida (el `response_id` determinístico hace
|
|
612
|
+
* el retry no-op). 404 = mensaje survey inexistente.
|
|
613
|
+
*/
|
|
614
|
+
interface SurveyResponseRequest {
|
|
615
|
+
survey_msg_id: string;
|
|
616
|
+
/** 1..5 (entero). */
|
|
617
|
+
rating: number;
|
|
618
|
+
/** `option.id` del paso choice, si el usuario eligió uno. */
|
|
619
|
+
reason?: string;
|
|
620
|
+
comment?: string;
|
|
621
|
+
}
|
|
622
|
+
/** `POST /v1/widget/survey-response` — respuesta 200. */
|
|
623
|
+
interface SurveyResponseAck {
|
|
624
|
+
answered: true;
|
|
625
|
+
}
|
|
516
626
|
/**
|
|
517
627
|
* Quién escribió un mensaje del lado del asistente.
|
|
518
628
|
*
|
|
@@ -558,6 +668,13 @@ interface AssistantMessageEvent {
|
|
|
558
668
|
ts: number;
|
|
559
669
|
/** Botones para responder sin escribir. Ver `QuickReply`. */
|
|
560
670
|
quick_replies?: QuickReply[];
|
|
671
|
+
/**
|
|
672
|
+
* Encuesta embebida (`content_type: "survey"`). Campo hermano de
|
|
673
|
+
* `quick_replies`, mismo patrón: sólo aparece si el wire trajo una encuesta
|
|
674
|
+
* VÁLIDA (pasó por `sanitizeSurvey`); un cliente que no la renderice sigue
|
|
675
|
+
* mostrando el `content` de texto fallback.
|
|
676
|
+
*/
|
|
677
|
+
survey?: SurveyPayload;
|
|
561
678
|
/**
|
|
562
679
|
* Quién lo escribió. Ausente ⇒ `bot` (todo lo que existía antes de que hubiera
|
|
563
680
|
* handover es del agente de IA).
|
|
@@ -582,12 +699,15 @@ interface AssistantMessageEvent {
|
|
|
582
699
|
* - `caption` `| null`: mismo motivo que `ts` (Python `None` ⇒ `null`).
|
|
583
700
|
* - `turn_ids` como `readonly unknown[]`: el guard solo verifica que sea un
|
|
584
701
|
* array; los elementos los filtra `normalizeAssistantMessageEvent`.
|
|
702
|
+
* - `survey` como `unknown`: mismo tratamiento que `quick_replies` — el guard
|
|
703
|
+
* no lo mira y `sanitizeSurvey` decide en la normalización.
|
|
585
704
|
*/
|
|
586
|
-
type RawAssistantMessageEvent = Omit<AssistantMessageEvent, "ts" | "turn_ids" | "content_type" | "caption"> & {
|
|
705
|
+
type RawAssistantMessageEvent = Omit<AssistantMessageEvent, "ts" | "turn_ids" | "content_type" | "caption" | "survey"> & {
|
|
587
706
|
ts?: number | null;
|
|
588
707
|
content_type?: string | null;
|
|
589
708
|
caption?: string | null;
|
|
590
709
|
turn_ids: readonly unknown[];
|
|
710
|
+
survey?: unknown;
|
|
591
711
|
};
|
|
592
712
|
type WidgetEvent = AssistantMessageEvent;
|
|
593
713
|
/** Union cruda del wire (hoy un solo tipo de evento; `typing`/etc. se sumarían acá). */
|
|
@@ -678,4 +798,4 @@ declare function normalizeAssistantMessageEvent(raw: RawAssistantMessageEvent, n
|
|
|
678
798
|
*/
|
|
679
799
|
declare function parseWidgetEvent(raw: string, now?: () => number): WidgetEvent | null;
|
|
680
800
|
|
|
681
|
-
export { ALLOWED_UPLOAD_CONTENT_TYPES, type AssistantIdentity, type AssistantMessageEvent, type ChallengeUiConfig, type HistoryMessageDto, type HistoryResponse, MAX_AGENT_NAME, MAX_QUICK_REPLIES, MAX_QUICK_REPLY_LABEL, MAX_THEME_FONT_FAMILY, MAX_THEME_LABEL, MAX_THEME_TAGLINE, MAX_THEME_TEXT_KEY, MAX_THEME_TEXT_KEYS, MAX_THEME_TEXT_VALUE, MAX_UPLOAD_SIZE_BYTES, MEDIA_CONTENT_TYPES, type MediaContentType, type MessageAccepted, type MessageAuthorKind, type MessageRequest, type QuickReply, type RawAssistantMessageEvent, type RawWidgetEvent, type RefreshRequest, type RefreshResponse, SERVER_PROVIDED_THEME_FIELDS, type ServerThemeResponse, type SessionRefreshRequest, type SessionRefreshResponse, type SessionRequest, type SessionResponse, type ThemeResponse, type UploadRequest, type UploadResponse, type WidgetEvent, isAllowedUploadContentType, isAssistantMessageEvent, isMediaContentType, isRawAssistantMessageEvent, mediaKindForContentType, normalizeAssistantMessageEvent, parseWidgetEvent, safeThemeColor, safeThemeUrl, sanitizeAssistantIdentity, sanitizeQuickReplies, sanitizeServerTheme };
|
|
801
|
+
export { ALLOWED_UPLOAD_CONTENT_TYPES, type AssistantIdentity, type AssistantMessageEvent, type ChallengeUiConfig, DEFAULT_SURVEY_TEXT_LEN, type HistoryMessageDto, type HistoryResponse, MAX_AGENT_NAME, MAX_QUICK_REPLIES, MAX_QUICK_REPLY_LABEL, MAX_SURVEY_CHOICE_OPTIONS, MAX_SURVEY_ID, MAX_SURVEY_LABEL, MAX_SURVEY_OPTION_LABEL, MAX_SURVEY_STEPS, MAX_SURVEY_TEXT_LEN, MAX_THEME_FONT_FAMILY, MAX_THEME_LABEL, MAX_THEME_TAGLINE, MAX_THEME_TEXT_KEY, MAX_THEME_TEXT_KEYS, MAX_THEME_TEXT_VALUE, MAX_UPLOAD_SIZE_BYTES, MEDIA_CONTENT_TYPES, type MediaContentType, type MessageAccepted, type MessageAuthorKind, type MessageRequest, type QuickReply, type RawAssistantMessageEvent, type RawWidgetEvent, type RefreshRequest, type RefreshResponse, SERVER_PROVIDED_THEME_FIELDS, type ServerThemeResponse, type SessionRefreshRequest, type SessionRefreshResponse, type SessionRequest, type SessionResponse, type SurveyChoiceOption, type SurveyPayload, type SurveyResponseAck, type SurveyResponseRequest, type SurveyStep, type SurveyStepChoice, type SurveyStepStars, type SurveyStepText, type ThemeResponse, type UploadRequest, type UploadResponse, type WidgetEvent, isAllowedUploadContentType, isAssistantMessageEvent, isMediaContentType, isRawAssistantMessageEvent, mediaKindForContentType, normalizeAssistantMessageEvent, parseWidgetEvent, safeThemeColor, safeThemeUrl, sanitizeAssistantIdentity, sanitizeQuickReplies, sanitizeServerTheme, sanitizeSurvey };
|
package/dist/index.d.ts
CHANGED
|
@@ -194,6 +194,16 @@ interface HistoryMessageDto {
|
|
|
194
194
|
* elegir, al volver tiene que poder elegir igual.
|
|
195
195
|
*/
|
|
196
196
|
quick_replies?: QuickReply[];
|
|
197
|
+
/**
|
|
198
|
+
* Encuesta embebida del mensaje (`content_type: "survey"`), con su estado
|
|
199
|
+
* `answered` denormalizado EN el item (Red Team #4): reabrir el chat no
|
|
200
|
+
* re-ofrece una encuesta contestada.
|
|
201
|
+
*
|
|
202
|
+
* Mismo trato que en el evento WS: el valor crudo del wire pasa por
|
|
203
|
+
* `sanitizeSurvey` en el consumidor ANTES de renderizar — el tipo documenta
|
|
204
|
+
* el contrato, la función es la barrera.
|
|
205
|
+
*/
|
|
206
|
+
survey?: SurveyPayload;
|
|
197
207
|
/** Ausente ⇒ `bot`. Ver `MessageAuthorKind`. */
|
|
198
208
|
author?: MessageAuthorKind;
|
|
199
209
|
}
|
|
@@ -334,7 +344,7 @@ interface ChallengeUiConfig {
|
|
|
334
344
|
interface ThemeResponse {
|
|
335
345
|
/** Color de marca. `#rgb` o `#rrggbb`. Pasa por `safeThemeColor`. */
|
|
336
346
|
primaryColor?: string;
|
|
337
|
-
/** URL https
|
|
347
|
+
/** Ícono del launcher: URL https, un built-in ("bircle" | "chat") o un emoji corto. */
|
|
338
348
|
launcherIcon?: string;
|
|
339
349
|
position?: "bottom-right" | "bottom-left";
|
|
340
350
|
/** URL https del logo del header. */
|
|
@@ -460,19 +470,6 @@ declare function safeThemeColor(value: unknown): string | null;
|
|
|
460
470
|
* que termina en un `<img src>` es un vector de XSS en el sitio del cliente.
|
|
461
471
|
*/
|
|
462
472
|
declare function safeThemeUrl(value: unknown): string | null;
|
|
463
|
-
/**
|
|
464
|
-
* Tema del server, saneado campo por campo contra el contrato.
|
|
465
|
-
*
|
|
466
|
-
* ES EL ÚNICO CAMINO DE ENTRADA del branding remoto: lo que no sale de acá no
|
|
467
|
-
* debería aplicarse. Devuelve un objeto NUEVO con sólo los campos válidos —
|
|
468
|
-
* nunca el input— así ninguna clave extra del wire (una que el server agregue
|
|
469
|
-
* mañana, o una que un theme viejo tenga guardada) se cuela por un spread.
|
|
470
|
-
*
|
|
471
|
-
* NUNCA LANZA Y NUNCA RECHAZA EL TEMA ENTERO: un campo inválido se descarta y el
|
|
472
|
-
* consumidor cae a su default. Perder el branding completo —o peor, no pintar el
|
|
473
|
-
* chat— porque el `accentColor` está mal cargado sería mucho peor que mostrar el
|
|
474
|
-
* acento por default.
|
|
475
|
-
*/
|
|
476
473
|
declare function sanitizeServerTheme(raw: unknown): ServerThemeResponse;
|
|
477
474
|
/**
|
|
478
475
|
* Evento entregado por el WebSocket del widget (respuesta del asistente o del
|
|
@@ -513,6 +510,119 @@ declare const MAX_QUICK_REPLY_LABEL = 40;
|
|
|
513
510
|
* respuesta del agente por un botón mal formado sería mucho peor.
|
|
514
511
|
*/
|
|
515
512
|
declare function sanitizeQuickReplies(raw: unknown): QuickReply[];
|
|
513
|
+
/** Tope de pasos de una encuesta (límite del contrato de `surveys`). */
|
|
514
|
+
declare const MAX_SURVEY_STEPS = 5;
|
|
515
|
+
/** Tope de opciones de un paso `choice`. */
|
|
516
|
+
declare const MAX_SURVEY_CHOICE_OPTIONS = 10;
|
|
517
|
+
/** Tope del `label` de un paso. */
|
|
518
|
+
declare const MAX_SURVEY_LABEL = 100;
|
|
519
|
+
/** Tope del `label` de una opción de `choice` (entra en un chip de teléfono). */
|
|
520
|
+
declare const MAX_SURVEY_OPTION_LABEL = 30;
|
|
521
|
+
/** Tope de los ids/keys (`survey_id`, `survey_msg_id`, `key`, `option.id`). */
|
|
522
|
+
declare const MAX_SURVEY_ID = 64;
|
|
523
|
+
/** Tope duro de `max_len` de un paso `text` (mismo límite que el builder). */
|
|
524
|
+
declare const MAX_SURVEY_TEXT_LEN = 1000;
|
|
525
|
+
/** `max_len` efectivo cuando la definición no trae uno. */
|
|
526
|
+
declare const DEFAULT_SURVEY_TEXT_LEN = 500;
|
|
527
|
+
/** Paso de estrellas 1–5 (el rating principal). */
|
|
528
|
+
interface SurveyStepStars {
|
|
529
|
+
type: "stars";
|
|
530
|
+
/** Clave del answer en el POST de respuesta (ej. `rating`). */
|
|
531
|
+
key: string;
|
|
532
|
+
/** Pregunta a mostrar. Texto plano, nunca markup. */
|
|
533
|
+
label: string;
|
|
534
|
+
}
|
|
535
|
+
/** Una opción elegible de un paso `choice`. */
|
|
536
|
+
interface SurveyChoiceOption {
|
|
537
|
+
/** Id estable de la opción — es lo que se REPORTA, el label es lo que se ve. */
|
|
538
|
+
id: string;
|
|
539
|
+
label: string;
|
|
540
|
+
}
|
|
541
|
+
/** Paso de opciones (motivo de la calificación), render tipo chips. */
|
|
542
|
+
interface SurveyStepChoice {
|
|
543
|
+
type: "choice";
|
|
544
|
+
key: string;
|
|
545
|
+
label: string;
|
|
546
|
+
options: SurveyChoiceOption[];
|
|
547
|
+
}
|
|
548
|
+
/** Paso de texto libre (comentario). */
|
|
549
|
+
interface SurveyStepText {
|
|
550
|
+
type: "text";
|
|
551
|
+
key: string;
|
|
552
|
+
label: string;
|
|
553
|
+
/** Tope de caracteres del comentario. Ausente ⇒ `DEFAULT_SURVEY_TEXT_LEN`. */
|
|
554
|
+
max_len?: number;
|
|
555
|
+
}
|
|
556
|
+
type SurveyStep = SurveyStepStars | SurveyStepChoice | SurveyStepText;
|
|
557
|
+
/**
|
|
558
|
+
* Encuesta embebida en un mensaje del asistente. Campo hermano de
|
|
559
|
+
* `quick_replies` — viaja igual en el evento WS y en el DTO del historial.
|
|
560
|
+
*/
|
|
561
|
+
interface SurveyPayload {
|
|
562
|
+
/** Id de la DEFINICIÓN (tabla `surveys`). */
|
|
563
|
+
survey_id: string;
|
|
564
|
+
/** Versión de la definición con la que se generó este mensaje. */
|
|
565
|
+
version: number;
|
|
566
|
+
/**
|
|
567
|
+
* Id del MENSAJE survey. Es la clave con la que se responde
|
|
568
|
+
* (`POST /v1/widget/survey-response`) y con la que el server deduplica: el
|
|
569
|
+
* `response_id` determinístico sale de acá.
|
|
570
|
+
*/
|
|
571
|
+
survey_msg_id: string;
|
|
572
|
+
/** Pasos a renderizar, en orden. Nunca vacío (sin pasos no hay encuesta). */
|
|
573
|
+
steps: SurveyStep[];
|
|
574
|
+
/** Referencia legible del caso que se califica ("¿Cómo resolvimos tu caso #123?"). */
|
|
575
|
+
case_ref?: string;
|
|
576
|
+
/**
|
|
577
|
+
* `true` = ya respondida. Denormalizado EN el item del mensaje (Red Team #4)
|
|
578
|
+
* para que la rehidratación no re-ofrezca una encuesta contestada — la UI la
|
|
579
|
+
* muestra en su estado "¡Gracias!" directamente.
|
|
580
|
+
*/
|
|
581
|
+
answered: boolean;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Encuesta USABLE del wire, o `null`.
|
|
585
|
+
*
|
|
586
|
+
* ES LA BARRERA (no el tipo): todo `survey` que llega del server —evento WS o
|
|
587
|
+
* DTO de historial— pasa por acá antes de tocar la UI. Valida el contrato
|
|
588
|
+
* completo: ids/keys no vacíos ≤ 64, ≤ 5 pasos, choice con 1..10 opciones,
|
|
589
|
+
* labels ≤ 100 (≤ 30 en opciones), `version` entero positivo.
|
|
590
|
+
*
|
|
591
|
+
* TODO-O-NADA A PROPÓSITO (distinto de `sanitizeQuickReplies`): una encuesta
|
|
592
|
+
* con un paso corrupto no es "una encuesta más corta", es un instrumento que ya
|
|
593
|
+
* no coincide con la definición contra la que el Gateway valida la respuesta.
|
|
594
|
+
* Ofrecerla garantiza un 400 DESPUÉS de que el usuario contestó — peor que no
|
|
595
|
+
* ofrecerla (el mensaje conserva su `content` de texto fallback). NUNCA lanza.
|
|
596
|
+
*
|
|
597
|
+
* Tolerancias deliberadas (ausencia ≠ inválido):
|
|
598
|
+
* - `answered` ausente o `null` ⇒ `false` (un productor viejo que no
|
|
599
|
+
* denormaliza el estado no debe matar la encuesta); cualquier otro no-bool
|
|
600
|
+
* (`"true"`) sí es inválido — adivinar el estado re-ofrecería encuestas
|
|
601
|
+
* contestadas.
|
|
602
|
+
* - `case_ref` es decorativo: no-string/vacío/gigante ⇒ se omite el campo,
|
|
603
|
+
* la encuesta sigue (mismo criterio que el avatar en
|
|
604
|
+
* `sanitizeAssistantIdentity`).
|
|
605
|
+
*/
|
|
606
|
+
declare function sanitizeSurvey(raw: unknown): SurveyPayload | null;
|
|
607
|
+
/**
|
|
608
|
+
* `POST /v1/widget/survey-response` — body (auth: Bearer JWT de sesión, igual
|
|
609
|
+
* que `/messages`). `reason` debe ser un `option.id` de la definición y
|
|
610
|
+
* `comment` respetar el `max_len` — el Gateway valida contra la definición y
|
|
611
|
+
* responde 400 si no. 409 = ya respondida (el `response_id` determinístico hace
|
|
612
|
+
* el retry no-op). 404 = mensaje survey inexistente.
|
|
613
|
+
*/
|
|
614
|
+
interface SurveyResponseRequest {
|
|
615
|
+
survey_msg_id: string;
|
|
616
|
+
/** 1..5 (entero). */
|
|
617
|
+
rating: number;
|
|
618
|
+
/** `option.id` del paso choice, si el usuario eligió uno. */
|
|
619
|
+
reason?: string;
|
|
620
|
+
comment?: string;
|
|
621
|
+
}
|
|
622
|
+
/** `POST /v1/widget/survey-response` — respuesta 200. */
|
|
623
|
+
interface SurveyResponseAck {
|
|
624
|
+
answered: true;
|
|
625
|
+
}
|
|
516
626
|
/**
|
|
517
627
|
* Quién escribió un mensaje del lado del asistente.
|
|
518
628
|
*
|
|
@@ -558,6 +668,13 @@ interface AssistantMessageEvent {
|
|
|
558
668
|
ts: number;
|
|
559
669
|
/** Botones para responder sin escribir. Ver `QuickReply`. */
|
|
560
670
|
quick_replies?: QuickReply[];
|
|
671
|
+
/**
|
|
672
|
+
* Encuesta embebida (`content_type: "survey"`). Campo hermano de
|
|
673
|
+
* `quick_replies`, mismo patrón: sólo aparece si el wire trajo una encuesta
|
|
674
|
+
* VÁLIDA (pasó por `sanitizeSurvey`); un cliente que no la renderice sigue
|
|
675
|
+
* mostrando el `content` de texto fallback.
|
|
676
|
+
*/
|
|
677
|
+
survey?: SurveyPayload;
|
|
561
678
|
/**
|
|
562
679
|
* Quién lo escribió. Ausente ⇒ `bot` (todo lo que existía antes de que hubiera
|
|
563
680
|
* handover es del agente de IA).
|
|
@@ -582,12 +699,15 @@ interface AssistantMessageEvent {
|
|
|
582
699
|
* - `caption` `| null`: mismo motivo que `ts` (Python `None` ⇒ `null`).
|
|
583
700
|
* - `turn_ids` como `readonly unknown[]`: el guard solo verifica que sea un
|
|
584
701
|
* array; los elementos los filtra `normalizeAssistantMessageEvent`.
|
|
702
|
+
* - `survey` como `unknown`: mismo tratamiento que `quick_replies` — el guard
|
|
703
|
+
* no lo mira y `sanitizeSurvey` decide en la normalización.
|
|
585
704
|
*/
|
|
586
|
-
type RawAssistantMessageEvent = Omit<AssistantMessageEvent, "ts" | "turn_ids" | "content_type" | "caption"> & {
|
|
705
|
+
type RawAssistantMessageEvent = Omit<AssistantMessageEvent, "ts" | "turn_ids" | "content_type" | "caption" | "survey"> & {
|
|
587
706
|
ts?: number | null;
|
|
588
707
|
content_type?: string | null;
|
|
589
708
|
caption?: string | null;
|
|
590
709
|
turn_ids: readonly unknown[];
|
|
710
|
+
survey?: unknown;
|
|
591
711
|
};
|
|
592
712
|
type WidgetEvent = AssistantMessageEvent;
|
|
593
713
|
/** Union cruda del wire (hoy un solo tipo de evento; `typing`/etc. se sumarían acá). */
|
|
@@ -678,4 +798,4 @@ declare function normalizeAssistantMessageEvent(raw: RawAssistantMessageEvent, n
|
|
|
678
798
|
*/
|
|
679
799
|
declare function parseWidgetEvent(raw: string, now?: () => number): WidgetEvent | null;
|
|
680
800
|
|
|
681
|
-
export { ALLOWED_UPLOAD_CONTENT_TYPES, type AssistantIdentity, type AssistantMessageEvent, type ChallengeUiConfig, type HistoryMessageDto, type HistoryResponse, MAX_AGENT_NAME, MAX_QUICK_REPLIES, MAX_QUICK_REPLY_LABEL, MAX_THEME_FONT_FAMILY, MAX_THEME_LABEL, MAX_THEME_TAGLINE, MAX_THEME_TEXT_KEY, MAX_THEME_TEXT_KEYS, MAX_THEME_TEXT_VALUE, MAX_UPLOAD_SIZE_BYTES, MEDIA_CONTENT_TYPES, type MediaContentType, type MessageAccepted, type MessageAuthorKind, type MessageRequest, type QuickReply, type RawAssistantMessageEvent, type RawWidgetEvent, type RefreshRequest, type RefreshResponse, SERVER_PROVIDED_THEME_FIELDS, type ServerThemeResponse, type SessionRefreshRequest, type SessionRefreshResponse, type SessionRequest, type SessionResponse, type ThemeResponse, type UploadRequest, type UploadResponse, type WidgetEvent, isAllowedUploadContentType, isAssistantMessageEvent, isMediaContentType, isRawAssistantMessageEvent, mediaKindForContentType, normalizeAssistantMessageEvent, parseWidgetEvent, safeThemeColor, safeThemeUrl, sanitizeAssistantIdentity, sanitizeQuickReplies, sanitizeServerTheme };
|
|
801
|
+
export { ALLOWED_UPLOAD_CONTENT_TYPES, type AssistantIdentity, type AssistantMessageEvent, type ChallengeUiConfig, DEFAULT_SURVEY_TEXT_LEN, type HistoryMessageDto, type HistoryResponse, MAX_AGENT_NAME, MAX_QUICK_REPLIES, MAX_QUICK_REPLY_LABEL, MAX_SURVEY_CHOICE_OPTIONS, MAX_SURVEY_ID, MAX_SURVEY_LABEL, MAX_SURVEY_OPTION_LABEL, MAX_SURVEY_STEPS, MAX_SURVEY_TEXT_LEN, MAX_THEME_FONT_FAMILY, MAX_THEME_LABEL, MAX_THEME_TAGLINE, MAX_THEME_TEXT_KEY, MAX_THEME_TEXT_KEYS, MAX_THEME_TEXT_VALUE, MAX_UPLOAD_SIZE_BYTES, MEDIA_CONTENT_TYPES, type MediaContentType, type MessageAccepted, type MessageAuthorKind, type MessageRequest, type QuickReply, type RawAssistantMessageEvent, type RawWidgetEvent, type RefreshRequest, type RefreshResponse, SERVER_PROVIDED_THEME_FIELDS, type ServerThemeResponse, type SessionRefreshRequest, type SessionRefreshResponse, type SessionRequest, type SessionResponse, type SurveyChoiceOption, type SurveyPayload, type SurveyResponseAck, type SurveyResponseRequest, type SurveyStep, type SurveyStepChoice, type SurveyStepStars, type SurveyStepText, type ThemeResponse, type UploadRequest, type UploadResponse, type WidgetEvent, isAllowedUploadContentType, isAssistantMessageEvent, isMediaContentType, isRawAssistantMessageEvent, mediaKindForContentType, normalizeAssistantMessageEvent, parseWidgetEvent, safeThemeColor, safeThemeUrl, sanitizeAssistantIdentity, sanitizeQuickReplies, sanitizeServerTheme, sanitizeSurvey };
|
package/dist/index.js
CHANGED
|
@@ -83,6 +83,13 @@ function sanitizeThemeTexts(raw) {
|
|
|
83
83
|
}
|
|
84
84
|
return n > 0 ? out : null;
|
|
85
85
|
}
|
|
86
|
+
function safeLauncherToken(v) {
|
|
87
|
+
if (typeof v !== "string") return null;
|
|
88
|
+
const t = v.trim();
|
|
89
|
+
if (t === "bircle" || t === "chat") return t;
|
|
90
|
+
if (t.length > 0 && t.length <= 8 && !t.includes(":") && !t.includes("/")) return t;
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
86
93
|
function sanitizeServerTheme(raw) {
|
|
87
94
|
if (typeof raw !== "object" || raw === null) return {};
|
|
88
95
|
const o = raw;
|
|
@@ -91,7 +98,7 @@ function sanitizeServerTheme(raw) {
|
|
|
91
98
|
if (primaryColor !== null) out.primaryColor = primaryColor;
|
|
92
99
|
const accentColor = safeThemeColor(o.accentColor);
|
|
93
100
|
if (accentColor !== null) out.accentColor = accentColor;
|
|
94
|
-
const launcherIcon = safeThemeUrl(o.launcherIcon);
|
|
101
|
+
const launcherIcon = safeThemeUrl(o.launcherIcon) ?? safeLauncherToken(o.launcherIcon);
|
|
95
102
|
if (launcherIcon !== null) out.launcherIcon = launcherIcon;
|
|
96
103
|
const headerLogo = safeThemeUrl(o.headerLogo);
|
|
97
104
|
if (headerLogo !== null) out.headerLogo = headerLogo;
|
|
@@ -155,6 +162,76 @@ function sanitizeQuickReplies(raw) {
|
|
|
155
162
|
}
|
|
156
163
|
return out;
|
|
157
164
|
}
|
|
165
|
+
var MAX_SURVEY_STEPS = 5;
|
|
166
|
+
var MAX_SURVEY_CHOICE_OPTIONS = 10;
|
|
167
|
+
var MAX_SURVEY_LABEL = 100;
|
|
168
|
+
var MAX_SURVEY_OPTION_LABEL = 30;
|
|
169
|
+
var MAX_SURVEY_ID = 64;
|
|
170
|
+
var MAX_SURVEY_TEXT_LEN = 1e3;
|
|
171
|
+
var DEFAULT_SURVEY_TEXT_LEN = 500;
|
|
172
|
+
function surveyId(value, max = MAX_SURVEY_ID) {
|
|
173
|
+
if (typeof value !== "string") return null;
|
|
174
|
+
const v = value.trim();
|
|
175
|
+
return v !== "" && v.length <= max ? v : null;
|
|
176
|
+
}
|
|
177
|
+
function sanitizeSurveyStep(raw) {
|
|
178
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
179
|
+
const o = raw;
|
|
180
|
+
const key = surveyId(o.key);
|
|
181
|
+
const label = surveyId(o.label, MAX_SURVEY_LABEL);
|
|
182
|
+
if (key === null || label === null) return null;
|
|
183
|
+
switch (o.type) {
|
|
184
|
+
case "stars":
|
|
185
|
+
return { type: "stars", key, label };
|
|
186
|
+
case "choice": {
|
|
187
|
+
if (!Array.isArray(o.options)) return null;
|
|
188
|
+
if (o.options.length === 0 || o.options.length > MAX_SURVEY_CHOICE_OPTIONS) return null;
|
|
189
|
+
const options = [];
|
|
190
|
+
for (const item of o.options) {
|
|
191
|
+
if (typeof item !== "object" || item === null) return null;
|
|
192
|
+
const opt = item;
|
|
193
|
+
const id = surveyId(opt.id);
|
|
194
|
+
const optLabel = surveyId(opt.label, MAX_SURVEY_OPTION_LABEL);
|
|
195
|
+
if (id === null || optLabel === null) return null;
|
|
196
|
+
options.push({ id, label: optLabel });
|
|
197
|
+
}
|
|
198
|
+
return { type: "choice", key, label, options };
|
|
199
|
+
}
|
|
200
|
+
case "text": {
|
|
201
|
+
const step = { type: "text", key, label };
|
|
202
|
+
if (typeof o.max_len === "number" && Number.isInteger(o.max_len) && o.max_len > 0 && o.max_len <= MAX_SURVEY_TEXT_LEN) {
|
|
203
|
+
step.max_len = o.max_len;
|
|
204
|
+
}
|
|
205
|
+
return step;
|
|
206
|
+
}
|
|
207
|
+
default:
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function sanitizeSurvey(raw) {
|
|
212
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
|
|
213
|
+
const o = raw;
|
|
214
|
+
const survey_id = surveyId(o.survey_id);
|
|
215
|
+
const survey_msg_id = surveyId(o.survey_msg_id);
|
|
216
|
+
if (survey_id === null || survey_msg_id === null) return null;
|
|
217
|
+
if (typeof o.version !== "number" || !Number.isInteger(o.version) || o.version <= 0) return null;
|
|
218
|
+
if (!Array.isArray(o.steps)) return null;
|
|
219
|
+
if (o.steps.length === 0 || o.steps.length > MAX_SURVEY_STEPS) return null;
|
|
220
|
+
const steps = [];
|
|
221
|
+
for (const rawStep of o.steps) {
|
|
222
|
+
const step = sanitizeSurveyStep(rawStep);
|
|
223
|
+
if (step === null) return null;
|
|
224
|
+
steps.push(step);
|
|
225
|
+
}
|
|
226
|
+
let answered;
|
|
227
|
+
if (o.answered === void 0 || o.answered === null) answered = false;
|
|
228
|
+
else if (typeof o.answered === "boolean") answered = o.answered;
|
|
229
|
+
else return null;
|
|
230
|
+
const out = { survey_id, version: o.version, survey_msg_id, steps, answered };
|
|
231
|
+
const case_ref = surveyId(o.case_ref, MAX_SURVEY_LABEL);
|
|
232
|
+
if (case_ref !== null) out.case_ref = case_ref;
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
158
235
|
var MAX_AGENT_NAME = 40;
|
|
159
236
|
function sanitizeAssistantIdentity(raw, kind = "human") {
|
|
160
237
|
if (typeof raw !== "object" || raw === null) return null;
|
|
@@ -195,6 +272,7 @@ function isAssistantMessageEvent(v) {
|
|
|
195
272
|
if (v.caption !== void 0 && typeof v.caption !== "string") return false;
|
|
196
273
|
if (v.content_type !== void 0 && v.content_type !== "text" && !isMediaContentType(v.content_type))
|
|
197
274
|
return false;
|
|
275
|
+
if (v.survey !== void 0 && sanitizeSurvey(v.survey) === null) return false;
|
|
198
276
|
return v.turn_ids.every((t) => typeof t === "string");
|
|
199
277
|
}
|
|
200
278
|
function normalizeAssistantMessageEvent(raw, now = Date.now) {
|
|
@@ -213,6 +291,13 @@ function normalizeAssistantMessageEvent(raw, now = Date.now) {
|
|
|
213
291
|
const qr = sanitizeQuickReplies(raw.quick_replies);
|
|
214
292
|
return qr.length > 0 ? { quick_replies: qr } : {};
|
|
215
293
|
})(),
|
|
294
|
+
// La FUNCIÓN es la barrera (lección FAQs): un `survey` que no pasa el saneo
|
|
295
|
+
// no viaja — el mensaje conserva su `content` de texto fallback, así que
|
|
296
|
+
// una encuesta corrupta nunca cuesta la respuesta del agente.
|
|
297
|
+
...(() => {
|
|
298
|
+
const sv = sanitizeSurvey(raw.survey);
|
|
299
|
+
return sv !== null ? { survey: sv } : {};
|
|
300
|
+
})(),
|
|
216
301
|
// Cualquier valor que no sea exactamente "human" cuenta como bot: es el
|
|
217
302
|
// default seguro — decirle al usuario que lo atiende una persona cuando no
|
|
218
303
|
// es cierto es peor que lo contrario.
|
|
@@ -238,9 +323,16 @@ function parseWidgetEvent(raw, now = Date.now) {
|
|
|
238
323
|
}
|
|
239
324
|
export {
|
|
240
325
|
ALLOWED_UPLOAD_CONTENT_TYPES,
|
|
326
|
+
DEFAULT_SURVEY_TEXT_LEN,
|
|
241
327
|
MAX_AGENT_NAME,
|
|
242
328
|
MAX_QUICK_REPLIES,
|
|
243
329
|
MAX_QUICK_REPLY_LABEL,
|
|
330
|
+
MAX_SURVEY_CHOICE_OPTIONS,
|
|
331
|
+
MAX_SURVEY_ID,
|
|
332
|
+
MAX_SURVEY_LABEL,
|
|
333
|
+
MAX_SURVEY_OPTION_LABEL,
|
|
334
|
+
MAX_SURVEY_STEPS,
|
|
335
|
+
MAX_SURVEY_TEXT_LEN,
|
|
244
336
|
MAX_THEME_FONT_FAMILY,
|
|
245
337
|
MAX_THEME_LABEL,
|
|
246
338
|
MAX_THEME_TAGLINE,
|
|
@@ -261,5 +353,6 @@ export {
|
|
|
261
353
|
safeThemeUrl,
|
|
262
354
|
sanitizeAssistantIdentity,
|
|
263
355
|
sanitizeQuickReplies,
|
|
264
|
-
sanitizeServerTheme
|
|
356
|
+
sanitizeServerTheme,
|
|
357
|
+
sanitizeSurvey
|
|
265
358
|
};
|
package/package.json
CHANGED