@botmesh/flow-schema/internal, sin garantía semver.
Un documento es solo datos. Nunca lleva funciones, eval, imports ni expresiones arbitrarias: el único cálculo es un lenguaje de expresiones seguro y plantillas {{ path }}.
Documento
Pasos
Las URLs de media deben ser http(s). Rutas locales, buffers y streams no se pueden representar a propósito: un documento guardado no puede leer el sistema de archivos del servidor. Lo mismo vale en ejecución para las fuentes
{ stateKey }: el valor del estado puede venir de la respuesta de un usuario final o de un paso http, así que todo lo que no sea una URL http(s) absoluta (/etc/passwd, C:\…, file:, data:) hace fallar el paso en vez de llegar al proveedor, que leería una cadena sin esquema como un archivo local. El proveedor, o un gateway autoalojado como Evolution o WPPConnect, descarga esa URL desde dentro de tu red, así que por defecto además debe ser https: y su host no puede ser (ni resolver a) una dirección privada, de loopback o link-local (127.0.0.1, 169.254.169.254, 10.x, localhost…). La opción de compilación media lo relaja a propósito: compileFlowDocument(doc, { media: { allowHttp: true, allowPrivateHosts: true, resolver } }). Las URLs escritas en el propio documento son del autor y no se comprueban.
El pattern de un campo de formulario se evalúa contra respuestas no confiables, así que el validador rechaza las referencias hacia atrás y los grupos repetidos cuyo cuerpo vuelve a repetirse o alternar ((a+)+, (\w+\s?)*, (a|aa)+: backtracking catastrófico). Escríbelos con una clase de caracteres ([a-z0-9-]+). Esa comprobación es solo un primer filtro: el campo compilado nunca ejecuta el patrón con el motor de backtracking de JavaScript. Compara la respuesta completa con un autómata de tiempo lineal (O(longitud de la respuesta × tamaño del patrón)), así que tampoco pueden congelar el proceso los cuantificadores seguidos (.*.*.*x) ni las cadenas largas de alternativas. La sintaxis admitida es el subconjunto regular: literales y escapes, clases de caracteres, ., grupos (de captura, (?:…) y con nombre), |, * + ? {n} {n,} {n,m} (también en su forma perezosa) y las anclas ^ $ \b \B. Se rechazan los lookarounds ((?=, (?!, (?<=, (?<!), las referencias hacia atrás, los escapes octales y los patrones cuyas repeticiones acotadas se despliegan en más de 2000 estados. Un campo de texto con pattern y sin maxLength queda limitado a 500 caracteres, y su maxLength puede ser como mucho 1000 (FLOW_DOCUMENT_LIMITS.maxPatternAnswerLength): el autómata es lineal, pero su coste sigue creciendo con la longitud de la respuesta por el tamaño del patrón.
El validador también rechaza los campos que ninguna respuesta puede cumplir: minLength mayor que maxLength, min mayor que max, un límite de date que no es un día real (2025-02-30) o un min posterior a su max, y un minSelected mayor que el número de opciones o que maxSelected.
Expresiones seguras
condition.if, guard.if y ask.validate.expr usan un lenguaje recursivo mínimo:
- literales (
1,'texto',true,false,null), rutas de estado (user.city),== != < <= > >= && || ! + -y paréntesis; - funciones:
len,lower,upper,trim,number,string,contains,startsWith,endsWith,empty,oneOf.
eval, sin Function, sin regexes, sin índices, sin asignación y sin acceso al prototipo (__proto__, prototype y constructor se rechazan). Límites: 1000 caracteres, 256 tokens, 32 niveles.
Las plantillas ({{ path }}) solo leen propiedades propias de c.state más $body, $from, $sender y $isGroup; un valor ausente se pinta como cadena vacía. Un valor ya pintado nunca se vuelve a leer como plantilla.
Una clave de estado puede llevar puntos: tras un ask con stateKey: 'user.name', tanto {{ user.name }} como len(user.name) leen c.state['user.name'] (si también existe un objeto anidado c.state.user.name, gana el anidado). Las claves con - o : son ids válidos pero no se pueden leer (first-name es una resta), así que usa letras, dígitos, _ y . en las claves que vayas a leer. Una variable $ desconocida, o $answer fuera de ask.validate.expr, es un error de validación.
Errores de validación
validateFlowDocument() devuelve rutas tipo JSON Pointer:
compileFlowDocument() lanza FlowDocumentError con los mismos problemas. El JSON Schema (FLOW_DOCUMENT_SCHEMA, draft 2020-12) lo sirve el manager en GET /v1/flows/schema para editores y generación de código.
Subflujos
c.state, sin un segundo trigger). Un documento que falta o un ciclo (a → b → a) es un error de compilación. Como un subflujo se inserta de nuevo en cada referencia, un flujo puede llegar como mucho a FLOW_DOCUMENT_LIMITS.maxInlinedSteps (5000) pasos con todos sus subflujos expandidos; una cadena de flujos que llaman varias veces al siguiente crece de forma exponencial y se rechaza al compilar (en @botmesh/manager, createBot/updateBot/addFlow responden INVALID).
Migraciones
version, la forma anterior a la versionada) pasa a la 1. La versión 1 pasa a la 2: en v1 retries de los pasos ask/form eran las respuestas en total, así que retries: N se reescribe como maxAttempts: N (un retries: 0, como maxAttempts: 1) y el flujo conserva su comportamiento; en v2 retries cuenta las respuestas después de la primera, como en el core. validateFlowDocument, compileFlowDocument y flowFromArtifact aplican esa migración a los documentos v1 al cargarlos; isFlowDocument solo acepta la versión 2. Una versión más nueva que el paquete se rechaza; las migraciones se pueden ampliar y encadenar (migrateFlowDocument(input, migrations, target)).
Exportación inversa
say, ask, choose, goto, delay, media, form, use); un do opaco o un guard con función lanzan FlowExportError con las rutas de los pasos en vez de descartarlos en silencio. Lo mismo pasa con un resultado que validateFlowDocument rechazaría (un .delay() del DSL de más de 5 minutos, un id con espacios): una exportación siempre se vuelve a cargar. El documento devuelto es una copia; cambiarlo no toca el flujo compilado.
Borradores y despliegues
publishFlow() usa el JSON del documento como artefacto y un flowDocumentGate() que parsea, valida y compila el candidato; options.gate puede encadenar otro gate (p. ej. transcripciones doradas). El almacenamiento es de @botmesh/deployments; este paquete solo aporta los helpers puros.
REST del manager
Los endpoints de flujos del manager (POST/PUT) aceptan exactamente estos documentos. GET /v1/flows/schema sirve el JSON Schema y GET/PUT/DELETE /v1/bots/{botId}/flows/{flowId} los guardan y reemplazan en caliente sin cambiar el contrato REST. Los pasos subflow se resuelven contra los demás flujos guardados del bot.