@hostwebhook/node-types 1.76.0 → 1.78.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.
@@ -0,0 +1,446 @@
1
+ /**
2
+ * La respuesta que recibe quien llama a un webhook en modo `sync`, armada en
3
+ * UN solo sitio.
4
+ *
5
+ * ## Qué problema resuelve
6
+ *
7
+ * El dueño quiere unas pestañas para configurar qué recibe el cliente y una
8
+ * VISTA PREVIA que enseñe los bytes exactos. Una vista previa sólo vale si no
9
+ * puede desincronizarse del servidor: si son dos implementaciones, el día que
10
+ * difieran la pantalla miente con toda la confianza del mundo, y miente sobre
11
+ * lo único que el usuario no puede comprobar desde ahí.
12
+ *
13
+ * Ya hay un caso así en el dashboard —`FORMAT_OPTIONS`, cuatro formatos
14
+ * copiados a mano en `schema-validators/[id]/page.tsx` cuando el paquete
15
+ * publica dieciocho— y ahí se asume porque desincronizarse AVISA: el campo
16
+ * deja de ofrecer un formato que sí existe y alguien lo pide. Aquí no avisaría
17
+ * nada. La previa diría 422 con tres errores, el servidor mandaría otra cosa,
18
+ * y nadie se enteraría hasta que un cliente se quejara.
19
+ *
20
+ * Así que la función de abajo es la ÚNICA que sabe montar estos bytes. La
21
+ * llaman los tres: el handler del validador en hw-nodes, el endpoint de previa
22
+ * de la api, y el dashboard para pintarla.
23
+ *
24
+ * ## Por qué este paquete y no otro — medido el 2026-09-10
25
+ *
26
+ * Los tres candidatos, con los números delante:
27
+ *
28
+ * | | `platform-contracts` | `node-sdk` | **`node-types`** |
29
+ * |---|---|---|---|
30
+ * | lo pinea el dashboard | `^0.2.0` (0.2.0 instalada) | no es dependencia suya | `^1.74.0` (1.74.0 instalada) |
31
+ * | ¿el caret llega a lo nuevo? | **NO** — en `0.x` no cruza el minor: habría que subir a `^0.16` a mano | — | **SÍ** — en `1.x` sí lo cruza; basta refrescar el lock |
32
+ * | dependencias de ejecución | `dependencies: {}` **pero** su barril hace `require('@nestjs/common')` de verdad | mongoose, mongodb, express, re2 (binario nativo) | **ninguna: ni deps ni peers** |
33
+ * | ¿sirve en el navegador? | **NO** hoy | no | **sí** |
34
+ * | ficheros del dashboard que ya lo importan | 6 | 0 | **92** |
35
+ *
36
+ * ⚠️ Lo de `platform-contracts` conviene leerlo dos veces, porque la primera
37
+ * medición decía lo contrario. `dependencies: {}` es cierto, y por eso parecía
38
+ * servible en el navegador. Pero un PEER también se ejecuta: desde 0.3.0 el
39
+ * paquete trae `servicios/credenciales-del-gateway`, que importa
40
+ * `BadRequestException` como VALOR, y eso sale en el `dist` como un
41
+ * `require("@nestjs/common")` al que se llega desde `dist/index.js` —barril de
42
+ * CommonJS, sin mapa `exports` y sin `sideEffects`, o sea que no se sacude—.
43
+ * El dashboard entra por ese barril (`lib/store/hooks.ts` pide `tieneAddon`
44
+ * como valor, no como tipo), así que subirlo metería ~9,5 MB de framework de
45
+ * servidor —@nestjs/common 1,1 MB, rxjs 8,1 MB, reflect-metadata 265 kB— en un
46
+ * Next. Lo que NO es el problema, y también se midió: entre 0.2.0 y 0.15.0
47
+ * `addons.ts` y `operadores.ts` están byte a byte idénticos, así que de los 13
48
+ * minors de salto NO sale ni un breaking para lo que el dashboard usa hoy.
49
+ *
50
+ * Y aquí, además, cae en el sitio correcto por significado: la constante del
51
+ * gate es una lista de `NodeType`, y `NodeType` vive en este fichero de al
52
+ * lado. En cualquier otro paquete sería una lista de cadenas sueltas que nadie
53
+ * comprueba contra el catálogo de nodos.
54
+ */
55
+ /* ── El gate ──────────────────────────────────────────────────────── */
56
+ /**
57
+ * Los tipos de nodo que SABEN contestar en `sync`.
58
+ *
59
+ * Lo lee la api al guardar —para no dejar activar `responseMode: 'sync'` en un
60
+ * flujo que no lleva ninguno— y el dashboard para avisar antes de guardar.
61
+ *
62
+ * 🔥 Es una CONSTANTE y no el literal `'schemaValidator'` repartido por los dos
63
+ * sitios, y la razón no es estética: el día que un segundo nodo implemente
64
+ * `getSyncResult`, con literales el gate sigue compilando, sigue pasando los
65
+ * tests y **se queda mintiendo en silencio** —le dice al usuario que su flujo
66
+ * no puede contestar cuando sí puede—. Un fallo que no rompe nada es el que
67
+ * sobrevive años.
68
+ *
69
+ * ⚠️ Y lo que esta lista NO puede comprobar desde aquí: que sea la misma que
70
+ * la de los handlers que de verdad implementan `getSyncResult`. Esos viven en
71
+ * hw-nodes, que este paquete no ve ni debe ver. El guardián de esa atadura va
72
+ * ALLÍ, que es donde están los handlers — el mismo reparto que ya usa
73
+ * `pasaLoQueRecibe`: la tabla se fija aquí y
74
+ * `el-panel-lee-lo-mismo-que-el-handler.spec.ts` la ata en hw-nodes.
75
+ *
76
+ * ⚠️⚠️ Presencia no es paso. Que el flujo LLEVE un validador no garantiza que
77
+ * el evento pase por él: un conditional o un filter puede rutear alrededor. Por
78
+ * eso el gate es de producto y no de corrección, y por eso `'no-verdict'`
79
+ * existe ahí abajo — sin él, el flujo con el gate puesto se colgaría igual.
80
+ */
81
+ export const NODOS_QUE_CONTESTAN_EN_SYNC = [
82
+ 'schemaValidator',
83
+ ];
84
+ /**
85
+ * ¿Este tipo de nodo sabe contestar en `sync`?
86
+ *
87
+ * Acepta `string` y no `NodeType` a propósito: quien pregunta es la api con lo
88
+ * que trae un documento de mongo y el dashboard con lo que hay en el lienzo, y
89
+ * los dos manejan cadenas que todavía no han pasado por ningún tipo.
90
+ */
91
+ export function contestaEnSync(tipo) {
92
+ return NODOS_QUE_CONTESTAN_EN_SYNC.includes(tipo);
93
+ }
94
+ /* ── Lo que el validador dictamina ────────────────────────────────── */
95
+ /**
96
+ * Los cuatro finales posibles de una espera en `sync`. Son cuatro y no dos, y
97
+ * los dos de en medio son los que hoy faltan:
98
+ *
99
+ * - `valid` — el payload pasó. **Esto es lo que hoy no contesta nadie**: el
100
+ * `getSyncResult` del validador devuelve `null` cuando todo está bien y el
101
+ * paso 8.5 de `node-lifecycle.ts` sólo resuelve `if (syncResult)`. O sea que
102
+ * el camino FELIZ es el que se cuelga 120 s y sale con un 408. Pasar la
103
+ * validación era indistinguible de que no contestara nadie.
104
+ * - `invalid` — el payload no pasó. El único que funciona hoy.
105
+ * - `no-verdict` — la corrida TERMINÓ y ningún validador se pronunció (nadie
106
+ * pasó por él, o el flujo no lleva ninguno). Hay que contestar igual, ahí
107
+ * mismo, en vez de esperar el tope entero.
108
+ * - `timeout` — se acabó la espera de verdad.
109
+ */
110
+ export const RESULTADOS_DEL_VALIDADOR = [
111
+ 'valid',
112
+ 'invalid',
113
+ 'no-verdict',
114
+ 'timeout',
115
+ ];
116
+ /* ── Cómo se quiere la respuesta ──────────────────────────────────── */
117
+ /**
118
+ * Cuánto se le cuenta al cliente de POR QUÉ falló.
119
+ *
120
+ * Los valores van en inglés aunque el tipo se llame en español, por lo mismo
121
+ * que `FORMATOS_DEL_ESQUEMA`: acaban en un `enum` de mongoose, en un `@IsIn`
122
+ * de un DTO y en un `<select>`, y ninguno de esos tres sitios quiere una
123
+ * cadena en español dentro de un JSON que un cliente puede exportar.
124
+ *
125
+ * - `full` — ruta, regla y esperado. Lo que quiere quien integra con su propio
126
+ * equipo detrás.
127
+ * - `fields-only` — QUÉ campos fallaron, sin el porqué. El término medio para
128
+ * un formulario público: basta para señalar la casilla en rojo y no publica
129
+ * las reglas.
130
+ * - `opaque` — «Invalid payload» y ya. **Ni una ruta de campo sale de aquí.**
131
+ * Un esquema es un mapa de tu modelo de datos; repartirlo por un endpoint
132
+ * público es regalar el trabajo de enumeración a quien lo quiera.
133
+ */
134
+ export const NIVELES_DE_DETALLE = ['full', 'fields-only', 'opaque'];
135
+ /**
136
+ * Si se le devuelve al cliente lo que mandó, y cuánto.
137
+ *
138
+ * - `none` — no.
139
+ * - `full` — tal cual llegó. No revela nada: es SU payload.
140
+ * - `failed-fields` — sólo los campos que fallaron, **en plano**, con la ruta
141
+ * completa como clave: `{ "user.email": "nope", "items[0].sku": 12 }`.
142
+ * Plano y no reconstruido con su anidamiento por dos razones: reconstruirlo
143
+ * añade una segunda lectura de rutas que puede discrepar de la del
144
+ * validador, y porque quien está depurando quiere la lista de lo que falló
145
+ * junto a lo que mandó, no un recorte del original con la misma forma.
146
+ */
147
+ export const MODOS_DE_PAYLOAD = ['none', 'full', 'failed-fields'];
148
+ /**
149
+ * La forma del sobre.
150
+ *
151
+ * - `hostwebhook` — el nuestro, el que ya sale hoy por el cable.
152
+ * - `problem+json` — RFC 7807, con su `content-type: application/problem+json`.
153
+ * Lo piden los que meten esto detrás de un cliente generado.
154
+ */
155
+ export const SOBRES_DE_RESPUESTA = ['hostwebhook', 'problem+json'];
156
+ /**
157
+ * Los topes de los códigos de estado, exportados para que el dashboard ponga
158
+ * el `min`/`max` de cada caja desde AQUÍ y no a ojo.
159
+ *
160
+ * El del éxito acaba en 299 por lo que pidió el dueño: nada de un 500 en el
161
+ * éxito. Y no es una manía — un 5xx en la respuesta de «tu payload es válido»
162
+ * hace que el cliente reintente, y reintentar un webhook aceptado duplica el
163
+ * evento.
164
+ *
165
+ * El del error se queda en 4xx: la culpa del payload es de quien lo manda, y un
166
+ * 5xx ahí también invita al reintento de lo que nunca va a pasar.
167
+ *
168
+ * El del timeout llega hasta 599 porque ahí sí caben los dos: 408 (lo que sale
169
+ * hoy) y 504, que es lo que muchos proxies esperan.
170
+ */
171
+ export const TOPES_DE_ESTADO = {
172
+ success: { min: 200, max: 299, defecto: 200 },
173
+ error: { min: 400, max: 499, defecto: 422 },
174
+ timeout: { min: 400, max: 599, defecto: 408 },
175
+ };
176
+ /**
177
+ * Cuánto se puede tener la conexión viva.
178
+ *
179
+ * ⚠️ El `max` es `SYNC_TIMEOUT_SECONDS` de `@hostwebhook/node-sdk`
180
+ * (`topes/plan.constants.ts`), que es el `setTimeout` real del `waitForSync`
181
+ * del gateway. Está copiado y no importado porque este paquete **no tiene ni
182
+ * una dependencia** —es justo lo que lo hace servible en el navegador— y
183
+ * traerse node-sdk arrastraría mongoose, express y un binario nativo.
184
+ *
185
+ * Que la copia no se despiste lo ata un test EN node-sdk, que sí puede ver los
186
+ * dos: `el-tope-de-la-espera-es-uno.test.ts`. Sin esa atadura, configurar 200 s
187
+ * aquí daría una pantalla que promete 200 y un gateway que corta a los 120.
188
+ */
189
+ export const TOPE_DE_ESPERA_DEL_SYNC = {
190
+ min: 1,
191
+ max: 120,
192
+ defecto: 120,
193
+ };
194
+ /**
195
+ * Lo que se usa cuando no se configuró nada.
196
+ *
197
+ * Están elegidos para que un webhook que no toque nada se comporte IGUAL que
198
+ * antes de todo esto: 422 al fallar, 120 s de espera, 408 al agotarse, el sobre
199
+ * de siempre y sin devolver el payload. Lo único que cambia respecto a hoy es
200
+ * que el camino feliz ahora contesta en vez de colgarse, que era el bug.
201
+ *
202
+ * `detail: 'full'` por lo mismo: es lo que sale hoy por el cable.
203
+ */
204
+ export const RESPUESTA_DEL_SYNC_POR_DEFECTO = {
205
+ successStatus: TOPES_DE_ESTADO.success.defecto,
206
+ errorStatus: TOPES_DE_ESTADO.error.defecto,
207
+ detail: 'full',
208
+ includePayload: 'none',
209
+ envelope: 'hostwebhook',
210
+ problemType: 'about:blank',
211
+ timeoutSeconds: TOPE_DE_ESPERA_DEL_SYNC.defecto,
212
+ timeoutStatus: TOPES_DE_ESTADO.timeout.defecto,
213
+ };
214
+ /* ── La función ───────────────────────────────────────────────────── */
215
+ const TIPO_JSON = 'application/json; charset=utf-8';
216
+ const TIPO_PROBLEMA = 'application/problem+json; charset=utf-8';
217
+ /**
218
+ * Los huecos que la plantilla sabe rellenar. Cerrado a propósito: un hueco que
219
+ * no esté en esta lista se queda ESCRITO en la salida en vez de borrarse.
220
+ *
221
+ * Que parece lo feo, y es lo correcto: la plantilla la escribe el usuario en su
222
+ * propia caja, con la vista previa al lado. Un `{{fied}}` mal tecleado que se
223
+ * borra en silencio es un mensaje al que le falta un trozo y nadie sabe por
224
+ * qué; uno que se queda escrito se ve en la previa antes de guardar.
225
+ */
226
+ const HUECO = /\{\{\s*(field|rule|expected)\s*\}\}/g;
227
+ function rellenarPlantilla(plantilla, fallo) {
228
+ /* ⚠️ Con FUNCIÓN de reemplazo, no con cadena. Con cadena, un `$&` o un `$1`
229
+ dentro de una ruta de campo —que viene del payload de un desconocido— lo
230
+ interpretaría `String.replace` y saldría otra cosa por el cable. Con
231
+ función, lo que se devuelve se inserta literal. */
232
+ return plantilla.replace(HUECO, (_todo, hueco) => {
233
+ if (hueco === 'field')
234
+ return fallo.path;
235
+ if (hueco === 'rule')
236
+ return fallo.rule ?? '';
237
+ return fallo.expected ?? '';
238
+ });
239
+ }
240
+ /** El texto final de un fallo: override de campo, plantilla general, o el original. */
241
+ function mensajeDelFallo(fallo, config) {
242
+ const propia = config.fieldTemplates?.[fallo.path];
243
+ if (propia !== undefined)
244
+ return rellenarPlantilla(propia, fallo);
245
+ if (config.messageTemplate !== undefined) {
246
+ return rellenarPlantilla(config.messageTemplate, fallo);
247
+ }
248
+ return fallo.message;
249
+ }
250
+ function enRango(valor, tope) {
251
+ /* No lanza y no recorta al borde: vuelve al defecto. Lanzar dejaría la vista
252
+ previa en blanco justo cuando hace falta, y recortar (un 500 que se queda
253
+ en 299) inventa un número que nadie escribió. Volver al defecto se VE en la
254
+ previa —pusiste 500 y la previa dice 200—, que es el aviso que hace falta. */
255
+ if (typeof valor !== 'number' || !Number.isInteger(valor))
256
+ return tope.defecto;
257
+ return valor >= tope.min && valor <= tope.max ? valor : tope.defecto;
258
+ }
259
+ /**
260
+ * Lee el valor que hay en `payload` en la ruta `user.email` o `items[0].sku`.
261
+ *
262
+ * Misma partición que `getNestedValue` de `schema-validator-utils.ts`: las
263
+ * rutas las escribe el mismo panel, así que leerlas de otra manera aquí haría
264
+ * que `failed-fields` devolviera `undefined` justo en las rutas con corchetes.
265
+ */
266
+ function valorEnLaRuta(payload, ruta) {
267
+ const segmentos = ruta
268
+ .split(/\.|\[(\d+)\]/)
269
+ .filter((s) => s !== '' && s !== undefined);
270
+ return segmentos.reduce((actual, clave) => {
271
+ if (actual === null || actual === undefined)
272
+ return undefined;
273
+ if (Array.isArray(actual) && /^\d+$/.test(clave))
274
+ return actual[Number(clave)];
275
+ if (typeof actual === 'object') {
276
+ return actual[clave];
277
+ }
278
+ return undefined;
279
+ }, payload);
280
+ }
281
+ /**
282
+ * Arma la respuesta que recibe quien llamó en `sync`.
283
+ *
284
+ * PURA: sin nestjs, sin mongoose, sin `Date.now()`, sin nada de servidor. Entra
285
+ * un veredicto y una configuración, sale la respuesta. Por eso puede correr
286
+ * igual en el handler de hw-nodes y en el navegador del dashboard, y por eso la
287
+ * vista previa no puede desincronizarse: es literalmente la misma llamada.
288
+ */
289
+ export function armarRespuestaDelSync(veredicto, config = {}) {
290
+ const sobre = config.envelope ?? RESPUESTA_DEL_SYNC_POR_DEFECTO.envelope;
291
+ const detalle = config.detail ?? RESPUESTA_DEL_SYNC_POR_DEFECTO.detail;
292
+ /* ── Éxito ─────────────────────────────────────────────────────────
293
+ El sobre NO se aplica aquí, y es una decisión: RFC 7807 describe
294
+ PROBLEMAS. Un 200 con `application/problem+json` es un uso equivocado del
295
+ tipo, y el cliente generado que lo lea buscará un `title` que no existe. */
296
+ if (veredicto.outcome === 'valid' || veredicto.outcome === 'no-verdict') {
297
+ const body = { accepted: true };
298
+ if (veredicto.eventId !== undefined)
299
+ body.eventId = veredicto.eventId;
300
+ /* 🔥 `validated: false` SÓLO cuando nadie se pronunció. El bug que esto
301
+ viene a arreglar era precisamente que pasar la validación y que no
302
+ contestara nadie salían iguales; sería absurdo volver a juntarlos aquí,
303
+ un escalón más arriba. Con validador de por medio el cuerpo es el que
304
+ pidió el dueño, `{accepted, eventId}` y nada más. */
305
+ if (veredicto.outcome === 'no-verdict')
306
+ body.validated = false;
307
+ return {
308
+ status: enRango(config.successStatus, TOPES_DE_ESTADO.success),
309
+ headers: { 'content-type': TIPO_JSON },
310
+ body,
311
+ };
312
+ }
313
+ /* ── Timeout ───────────────────────────────────────────────────── */
314
+ if (veredicto.outcome === 'timeout') {
315
+ const status = enRango(config.timeoutStatus, TOPES_DE_ESTADO.timeout);
316
+ if (config.timeoutBody !== undefined) {
317
+ return {
318
+ status,
319
+ headers: {
320
+ 'content-type': sobre === 'problem+json' ? TIPO_PROBLEMA : TIPO_JSON,
321
+ },
322
+ body: { ...config.timeoutBody },
323
+ };
324
+ }
325
+ if (sobre === 'problem+json') {
326
+ const body = {
327
+ type: config.problemType ?? RESPUESTA_DEL_SYNC_POR_DEFECTO.problemType,
328
+ title: 'Pipeline timeout',
329
+ status,
330
+ };
331
+ if (veredicto.eventId !== undefined) {
332
+ body.instance = `/events/${veredicto.eventId}`;
333
+ }
334
+ return { status, headers: { 'content-type': TIPO_PROBLEMA }, body };
335
+ }
336
+ /* El cuerpo de hoy, letra por letra. */
337
+ const body = { error: 'Pipeline timeout' };
338
+ if (veredicto.eventId !== undefined)
339
+ body.eventId = veredicto.eventId;
340
+ return { status, headers: { 'content-type': TIPO_JSON }, body };
341
+ }
342
+ /* ── Inválido ──────────────────────────────────────────────────── */
343
+ const status = enRango(config.errorStatus, TOPES_DE_ESTADO.error);
344
+ const opaco = detalle === 'opaque';
345
+ /* 🔥 `failed-fields` se degrada a `none` cuando el detalle es `opaque`, y
346
+ esto es LA línea de seguridad de todo el fichero. Las CLAVES de ese objeto
347
+ son las rutas de los campos que fallaron: devolverlo es dar exactamente la
348
+ misma información que `fields-only`, sólo que por otra puerta. Un usuario
349
+ que elige `opaque` y deja el payload puesto no está pidiendo eso; está
350
+ pidiendo no filtrar.
351
+
352
+ `full` sí sigue permitido con `opaque`, y la razón es que NO PUEDE filtrar:
353
+ devuelve exactamente lo que entró, así que jamás contiene un nombre de
354
+ campo que quien llama no hubiera escrito él mismo. Quien intenta enumerar
355
+ el esquema manda `{"probe":1}` y le vuelve `{"probe":1}`. Degradarlo «por
356
+ si acaso» le quitaría a un usuario legítimo el poder ver su propia petición
357
+ en sus logs, a cambio de cero seguridad. */
358
+ const modoPayload = config.includePayload ??
359
+ RESPUESTA_DEL_SYNC_POR_DEFECTO.includePayload;
360
+ const payloadDevuelto = opaco && modoPayload === 'failed-fields' ? 'none' : modoPayload;
361
+ let payload;
362
+ if (payloadDevuelto === 'full') {
363
+ payload = veredicto.payload;
364
+ }
365
+ else if (payloadDevuelto === 'failed-fields') {
366
+ const recorte = {};
367
+ for (const fallo of veredicto.errors) {
368
+ recorte[fallo.path] = valorEnLaRuta(veredicto.payload, fallo.path);
369
+ }
370
+ payload = recorte;
371
+ }
372
+ const hayPayload = payloadDevuelto !== 'none' && payload !== undefined;
373
+ /* Los errores, según el nivel. `opaque` no llega aquí: se queda sin array. */
374
+ const errores = opaco
375
+ ? []
376
+ : veredicto.errors.map((fallo) => {
377
+ if (detalle === 'fields-only') {
378
+ /* Qué campos, sin el porqué. Ni `message`, ni `rule`, ni `expected`:
379
+ un `expected: "^EMP-\\d{6}$"` cuenta el formato entero de un id
380
+ interno, que es justo lo que este nivel viene a callar. */
381
+ return { path: fallo.path };
382
+ }
383
+ const salida = { path: fallo.path };
384
+ if (fallo.rule !== undefined)
385
+ salida.rule = fallo.rule;
386
+ if (fallo.expected !== undefined)
387
+ salida.expected = fallo.expected;
388
+ salida.message = mensajeDelFallo(fallo, config);
389
+ return salida;
390
+ });
391
+ if (sobre === 'problem+json') {
392
+ const body = {
393
+ type: config.problemType ?? RESPUESTA_DEL_SYNC_POR_DEFECTO.problemType,
394
+ title: opaco ? 'Invalid payload' : 'Schema Validation Failed',
395
+ status,
396
+ };
397
+ /* ⚠️ Ni `detail` ni un recuento con `opaque`. «3 fields failed» no dice
398
+ cuáles, pero sí dice cuántos campos tiene el esquema por lo bajo y
399
+ cuántos acertó quien está probando: es una señal de enumeración, que es
400
+ exactamente lo que este nivel corta. El dueño lo dijo así: «Invalid
401
+ payload» y ya. */
402
+ if (!opaco) {
403
+ const n = errores.length;
404
+ body.detail = `${n} field${n === 1 ? '' : 's'} failed validation`;
405
+ }
406
+ if (veredicto.eventId !== undefined) {
407
+ body.instance = `/events/${veredicto.eventId}`;
408
+ }
409
+ /* `errors` y `payload` son miembros de EXTENSIÓN del 7807, que el RFC
410
+ permite explícitamente. No se meten dentro de `detail` porque `detail`
411
+ es texto para un humano y esto es para una máquina. */
412
+ if (!opaco)
413
+ body.errors = errores;
414
+ if (hayPayload)
415
+ body.payload = payload;
416
+ return { status, headers: { 'content-type': TIPO_PROBLEMA }, body };
417
+ }
418
+ /* El sobre de siempre. El orden de las claves es el del 422 que ya sale hoy
419
+ por el cable, para que quien lo esté leyendo con un diff no vea ruido. */
420
+ const body = {
421
+ error: opaco ? 'Invalid payload' : 'Schema Validation Failed',
422
+ };
423
+ /* `eventId` va en TODOS los cuerpos cuando se conoce, y en `opaque` es lo que
424
+ hace que el nivel sea usable: el cliente no se entera de nada del esquema,
425
+ pero trae un identificador con el que tú sí puedes buscar la corrida cuando
426
+ te escriba. Opaco para fuera, trazable para dentro. */
427
+ if (veredicto.eventId !== undefined)
428
+ body.eventId = veredicto.eventId;
429
+ if (!opaco) {
430
+ if (veredicto.webhookName !== undefined) {
431
+ body.webhookName = veredicto.webhookName;
432
+ }
433
+ if (veredicto.nodeName !== undefined)
434
+ body.nodeName = veredicto.nodeName;
435
+ /* `message` como array de `ruta: texto` es lo que se manda hoy. Con
436
+ `fields-only` no hay texto que juntar, así que se queda sólo la ruta —y
437
+ no se omite el campo entero, porque quien lo lee hoy espera un array. */
438
+ body.message = veredicto.errors.map((fallo) => detalle === 'fields-only'
439
+ ? fallo.path
440
+ : `${fallo.path}: ${mensajeDelFallo(fallo, config)}`);
441
+ body.errors = errores;
442
+ }
443
+ if (hayPayload)
444
+ body.payload = payload;
445
+ return { status, headers: { 'content-type': TIPO_JSON }, body };
446
+ }
package/dist/index.d.ts CHANGED
@@ -66,3 +66,5 @@ export type { ModeloDeOpenRouter, PrecioDeOpenRouter, RespuestaDeModelosDeOpenRo
66
66
  export { URL_DE_MODELOS_DE_OPENROUTER, opcionesDeModelosDeOpenRouter, ventanaDeContextoDeOpenRouter, } from './openrouter.js';
67
67
  export type { CredentialTypeRegistration, CredentialType } from './credentials.js';
68
68
  export { CREDENTIAL_TYPES, CREDENTIAL_TYPE_VALUES, credentialTypeValues, getCredentialType, isCredentialType, } from './credentials.js';
69
+ export type { NodoQueContestaEnSync, ResultadoDelValidador, FalloDeValidacion, VeredictoDelValidador, NivelDeDetalle, ModoDePayload, SobreDeRespuesta, TopeDeNumero, RespuestaDelSync, RespuestaHttp, } from './respuesta-del-sync.js';
70
+ export { NODOS_QUE_CONTESTAN_EN_SYNC, contestaEnSync, RESULTADOS_DEL_VALIDADOR, NIVELES_DE_DETALLE, MODOS_DE_PAYLOAD, SOBRES_DE_RESPUESTA, TOPES_DE_ESTADO, TOPE_DE_ESPERA_DEL_SYNC, RESPUESTA_DEL_SYNC_POR_DEFECTO, armarRespuestaDelSync, } from './respuesta-del-sync.js';
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DRIVE_OPERATIONS = exports.GOOGLE_CALENDAR_TOOLKIT_BY_TOOL_NAME = exports.GOOGLE_CALENDAR_TOOLKIT_SPECS = exports.isGoogleCalendarOperation = exports.GOOGLE_CALENDAR_OPERATION_SPECS = exports.GOOGLE_CALENDAR_OPERATIONS = exports.isGmailOperation = exports.resolveGmailSendFields = exports.GMAIL_SEND_LEGACY_FIELDS = exports.NATIVE_EMAIL_TOOLKIT_BY_TOOL_NAME = exports.GMAIL_TOOLKIT_BY_TOOL_NAME = exports.NATIVE_EMAIL_TOOLKIT_SPECS = exports.GMAIL_SEND_AND_WAIT_TOOL_SPEC = exports.GMAIL_ALL_TOOLKIT_SPECS = exports.GMAIL_TOOLKIT_SPECS = exports.GMAIL_TOOLKIT_OPERATIONS = exports.GMAIL_DROPDOWN_OPERATIONS = exports.GMAIL_OPERATION_GROUPS = exports.GMAIL_OPERATION_SPECS = exports.GMAIL_OPERATIONS = exports.versionCatalogErrors = exports.fieldsLost = exports.fieldsLostBetween = exports.currentVersion = exports.versionSpec = exports.versionsOf = exports.isVersioned = exports.NODE_TYPE_TO_PREFIX = exports.PREFIX_TO_NODE_TYPE = exports.NODE_STATE_KEYS = exports.NODE_COLORS = exports.NODE_DETAIL_PATHS = exports.getNodeRegistryEntry = exports.NODE_REGISTRY = exports.pasaLoQueRecibe = exports.getNodeDispatchConfig = exports.getAllNodeCollections = exports.NODE_DISPATCH = exports.resolveNodeId = exports.PREFIX_TO_TYPE = exports.NODE_UI = exports.ALL_NODE_TYPES = exports.isNodeType = exports.isTerminal = exports.canSendToNodes = exports.canReceiveFromNodes = exports.canReceiveFrom = exports.NODE_CONNECTIONS = exports.iterableMeta = exports.singleMeta = void 0;
4
4
  exports.BUCKET_ITERABLE_OPERATIONS = exports.BUCKET_OPERATION_SPECS = exports.BUCKET_OPERATIONS = exports.isJiraOperation = exports.JIRA_ITERABLE_OPERATIONS = exports.JIRA_DROPDOWN_OPERATIONS = exports.JIRA_OPERATION_SPECS = exports.JIRA_OPERATIONS = exports.isGithubOperation = exports.GITHUB_ITERABLE_OPERATIONS = exports.GITHUB_DROPDOWN_OPERATIONS = exports.GITHUB_OPERATION_SPECS = exports.GITHUB_OPERATIONS = exports.scopesQueFaltanEnShopify = exports.isShopifyOperation = exports.SHOPIFY_PRODUCT_STATUSES = exports.SHOPIFY_CANCEL_REASONS = exports.SHOPIFY_SEARCHABLE_RESOURCES = exports.SHOPIFY_TAGGABLE_RESOURCES = exports.SHOPIFY_OPERATION_SCOPES = exports.SHOPIFY_OPERATION_SPECS = exports.SHOPIFY_OPERATIONS = exports.isMailchimpOperation = exports.MAILCHIMP_CONTACT_STATUSES = exports.MAILCHIMP_OPERATION_SPECS = exports.MAILCHIMP_OPERATIONS = exports.herramientasDeDiscordPara = exports.DISCORD_TOOLKIT_BY_TOOL_NAME = exports.DISCORD_TOOLKIT_SPECS = exports.isDiscordOperation = exports.camposDeDiscordNoDisponibles = exports.discordPuedeEjecutar = exports.operacionesDeDiscordPara = exports.DISCORD_CAPACIDADES_POR_CREDENCIAL = exports.DISCORD_OPERATION_SPECS = exports.DISCORD_OPERATIONS = exports.camposNoDisponiblesPara = exports.puedeEjecutar = exports.operacionesPara = exports.isWhatsAppOperation = exports.WHATSAPP_OPERATIONS = exports.TELEGRAM_TOOLKIT_BY_TOOL_NAME = exports.TELEGRAM_TOOLKIT_SPECS = exports.isTelegramOperation = exports.TELEGRAM_OPERATION_SPECS = exports.TELEGRAM_OPERATIONS = exports.DRIVE_TOOLKIT_BY_TOOL_NAME = exports.DRIVE_TOOLKIT_SPECS = exports.isDriveOperation = exports.DRIVE_OPERATION_SPECS = void 0;
5
5
  exports.getDefaultModel = exports.getModelsFor = exports.MODEL_CONTEXT_WINDOWS = exports.LLM_MODELS = exports.LLM_PROVIDERS = exports.DOCS_TOOLKIT_DEFAULTABLE = exports.DOCS_TOOLKIT_BY_TOOL_NAME = exports.DOCS_TOOLKIT_SPECS = exports.isDocsOperation = exports.DOCS_OPERATION_SPECS = exports.DOCS_OPERATIONS = exports.isMongoOperation = exports.MONGO_OPERATION_SPECS = exports.MONGO_OPERATIONS = exports.isPostgresOperation = exports.POSTGRES_OPERATION_SPECS = exports.POSTGRES_MODES = exports.POSTGRES_OPERATIONS = exports.isNotionOperation = exports.NOTION_DROPDOWN_OPERATIONS = exports.NOTION_OPERATION_SPECS = exports.NOTION_OPERATIONS = exports.isGoogleAnalyticsOperation = exports.GOOGLE_ANALYTICS_DROPDOWN_OPERATIONS = exports.GOOGLE_ANALYTICS_OPERATION_SPECS = exports.GOOGLE_ANALYTICS_OPERATIONS = exports.isGoogleContactsOperation = exports.GOOGLE_CONTACTS_DEFAULT_PERSON_FIELDS = exports.GOOGLE_CONTACTS_OPERATION_GROUPS = exports.GOOGLE_CONTACTS_OPERATION_SPECS = exports.GOOGLE_CONTACTS_OPERATIONS_V2 = exports.GOOGLE_CONTACTS_OPERATIONS_V1 = exports.GOOGLE_CONTACTS_OPERATIONS = exports.SHEETS_TOOLKIT_DEFAULTABLE = exports.SHEETS_TOOLKIT_BY_TOOL_NAME = exports.SHEETS_TOOLKIT_SPECS = exports.isSheetsOperation = exports.SHEETS_OPERATION_SPECS = exports.SHEETS_OPERATIONS = exports.herramientasDeSlackPara = exports.SLACK_TOOLKIT_BY_TOOL_NAME = exports.SLACK_TOOLKIT_SPECS = exports.isSlackOperation = exports.camposDeSlackNoDisponibles = exports.slackPuedeEjecutar = exports.operacionesDeSlackPara = exports.SLACK_CAPACIDADES_POR_CREDENCIAL = exports.SLACK_OPERATION_SPECS = exports.SLACK_OPERATIONS = exports.isBucketOperation = void 0;
6
- exports.isCredentialType = exports.getCredentialType = exports.credentialTypeValues = exports.CREDENTIAL_TYPE_VALUES = exports.CREDENTIAL_TYPES = exports.ventanaDeContextoDeOpenRouter = exports.opcionesDeModelosDeOpenRouter = exports.URL_DE_MODELOS_DE_OPENROUTER = exports.getModelLabel = void 0;
6
+ exports.armarRespuestaDelSync = exports.RESPUESTA_DEL_SYNC_POR_DEFECTO = exports.TOPE_DE_ESPERA_DEL_SYNC = exports.TOPES_DE_ESTADO = exports.SOBRES_DE_RESPUESTA = exports.MODOS_DE_PAYLOAD = exports.NIVELES_DE_DETALLE = exports.RESULTADOS_DEL_VALIDADOR = exports.contestaEnSync = exports.NODOS_QUE_CONTESTAN_EN_SYNC = exports.isCredentialType = exports.getCredentialType = exports.credentialTypeValues = exports.CREDENTIAL_TYPE_VALUES = exports.CREDENTIAL_TYPES = exports.ventanaDeContextoDeOpenRouter = exports.opcionesDeModelosDeOpenRouter = exports.URL_DE_MODELOS_DE_OPENROUTER = exports.getModelLabel = void 0;
7
7
  var types_js_1 = require("./types.js");
8
8
  Object.defineProperty(exports, "singleMeta", { enumerable: true, get: function () { return types_js_1.singleMeta; } });
9
9
  Object.defineProperty(exports, "iterableMeta", { enumerable: true, get: function () { return types_js_1.iterableMeta; } });
@@ -216,3 +216,14 @@ Object.defineProperty(exports, "CREDENTIAL_TYPE_VALUES", { enumerable: true, get
216
216
  Object.defineProperty(exports, "credentialTypeValues", { enumerable: true, get: function () { return credentials_js_1.credentialTypeValues; } });
217
217
  Object.defineProperty(exports, "getCredentialType", { enumerable: true, get: function () { return credentials_js_1.getCredentialType; } });
218
218
  Object.defineProperty(exports, "isCredentialType", { enumerable: true, get: function () { return credentials_js_1.isCredentialType; } });
219
+ var respuesta_del_sync_js_1 = require("./respuesta-del-sync.js");
220
+ Object.defineProperty(exports, "NODOS_QUE_CONTESTAN_EN_SYNC", { enumerable: true, get: function () { return respuesta_del_sync_js_1.NODOS_QUE_CONTESTAN_EN_SYNC; } });
221
+ Object.defineProperty(exports, "contestaEnSync", { enumerable: true, get: function () { return respuesta_del_sync_js_1.contestaEnSync; } });
222
+ Object.defineProperty(exports, "RESULTADOS_DEL_VALIDADOR", { enumerable: true, get: function () { return respuesta_del_sync_js_1.RESULTADOS_DEL_VALIDADOR; } });
223
+ Object.defineProperty(exports, "NIVELES_DE_DETALLE", { enumerable: true, get: function () { return respuesta_del_sync_js_1.NIVELES_DE_DETALLE; } });
224
+ Object.defineProperty(exports, "MODOS_DE_PAYLOAD", { enumerable: true, get: function () { return respuesta_del_sync_js_1.MODOS_DE_PAYLOAD; } });
225
+ Object.defineProperty(exports, "SOBRES_DE_RESPUESTA", { enumerable: true, get: function () { return respuesta_del_sync_js_1.SOBRES_DE_RESPUESTA; } });
226
+ Object.defineProperty(exports, "TOPES_DE_ESTADO", { enumerable: true, get: function () { return respuesta_del_sync_js_1.TOPES_DE_ESTADO; } });
227
+ Object.defineProperty(exports, "TOPE_DE_ESPERA_DEL_SYNC", { enumerable: true, get: function () { return respuesta_del_sync_js_1.TOPE_DE_ESPERA_DEL_SYNC; } });
228
+ Object.defineProperty(exports, "RESPUESTA_DEL_SYNC_POR_DEFECTO", { enumerable: true, get: function () { return respuesta_del_sync_js_1.RESPUESTA_DEL_SYNC_POR_DEFECTO; } });
229
+ Object.defineProperty(exports, "armarRespuestaDelSync", { enumerable: true, get: function () { return respuesta_del_sync_js_1.armarRespuestaDelSync; } });