@hostwebhook/node-types 1.77.0 → 1.79.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/README.md +0 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/registry.js +63 -0
- package/dist/esm/respuesta-del-sync.d.ts +353 -0
- package/dist/esm/respuesta-del-sync.js +446 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +12 -1
- package/dist/registry.js +63 -0
- package/dist/respuesta-del-sync.d.ts +353 -0
- package/dist/respuesta-del-sync.js +451 -0
- package/package.json +1 -1
|
@@ -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