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