@hostwebhook/node-sdk 0.1.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.
Files changed (59) hide show
  1. package/dist/code-runner.d.ts +20 -0
  2. package/dist/code-runner.js +138 -0
  3. package/dist/contratos.d.ts +121 -0
  4. package/dist/contratos.js +24 -0
  5. package/dist/dto/output-node.dto.d.ts +19 -0
  6. package/dist/dto/output-node.dto.js +96 -0
  7. package/dist/ensure-meta.d.ts +22 -0
  8. package/dist/ensure-meta.js +35 -0
  9. package/dist/execute-with-iteration.d.ts +18 -0
  10. package/dist/execute-with-iteration.js +66 -0
  11. package/dist/filter-utils.d.ts +22 -0
  12. package/dist/filter-utils.js +178 -0
  13. package/dist/handler-helpers.d.ts +21 -0
  14. package/dist/handler-helpers.js +53 -0
  15. package/dist/index.d.ts +51 -0
  16. package/dist/index.js +73 -0
  17. package/dist/log-metadata.d.ts +191 -0
  18. package/dist/log-metadata.js +375 -0
  19. package/dist/node-dispatch.registry.d.ts +32 -0
  20. package/dist/node-dispatch.registry.js +45 -0
  21. package/dist/node-executors.d.ts +299 -0
  22. package/dist/node-executors.js +555 -0
  23. package/dist/node-lifecycle.d.ts +399 -0
  24. package/dist/node-lifecycle.js +782 -0
  25. package/dist/normalize-nodes.d.ts +18 -0
  26. package/dist/normalize-nodes.js +22 -0
  27. package/dist/output-node-ref.schema.d.ts +82 -0
  28. package/dist/output-node-ref.schema.js +90 -0
  29. package/dist/output-webhook-scope.d.ts +36 -0
  30. package/dist/output-webhook-scope.js +42 -0
  31. package/dist/payload-preview.d.ts +10 -0
  32. package/dist/payload-preview.js +39 -0
  33. package/dist/pipeline.constants.d.ts +29 -0
  34. package/dist/pipeline.constants.js +51 -0
  35. package/dist/pre-request-pool.d.ts +58 -0
  36. package/dist/pre-request-pool.js +308 -0
  37. package/dist/pre-request-runner-source.d.ts +28 -0
  38. package/dist/pre-request-runner-source.js +411 -0
  39. package/dist/regex-de-inquilino.d.ts +15 -0
  40. package/dist/regex-de-inquilino.js +98 -0
  41. package/dist/request-context.d.ts +18 -0
  42. package/dist/request-context.js +34 -0
  43. package/dist/retry-transient.d.ts +54 -0
  44. package/dist/retry-transient.js +67 -0
  45. package/dist/retry-utils.d.ts +17 -0
  46. package/dist/retry-utils.js +23 -0
  47. package/dist/schema-validator-utils.d.ts +9 -0
  48. package/dist/schema-validator-utils.js +140 -0
  49. package/dist/ssrf-guard.d.ts +202 -0
  50. package/dist/ssrf-guard.js +917 -0
  51. package/dist/swallow.d.ts +52 -0
  52. package/dist/swallow.js +55 -0
  53. package/dist/template-render.d.ts +33 -0
  54. package/dist/template-render.js +43 -0
  55. package/dist/try-parse.d.ts +41 -0
  56. package/dist/try-parse.js +69 -0
  57. package/dist/workspace-payloads.d.ts +66 -0
  58. package/dist/workspace-payloads.js +496 -0
  59. package/package.json +35 -0
@@ -0,0 +1,411 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PRE_REQUEST_RUNNER_SOURCE = void 0;
4
+ /**
5
+ * Source of the child process that runs tenant code.
6
+ *
7
+ * Dos clases de trabajo, un solo hijo: el script pre-request de siempre
8
+ * (`kind: 'pre-request'`, el de por defecto) y el nodo de código
9
+ * (`kind: 'code'`). Comparten proceso porque comparten exactamente el motivo
10
+ * de estar aquí — son JS que escribe un inquilino — y porque el pool caliente
11
+ * ya montado es lo que hace que sacarlos del proceso no se note.
12
+ *
13
+ * Kept as a string, not a file, on purpose: the child is started with
14
+ * `node -e <this>`, so there is no path to resolve and the same code works
15
+ * under ts-node, jest and `dist/` alike. A file would have to exist at three
16
+ * different paths depending on how the API was started.
17
+ *
18
+ * The globals below are seeded from the child's own realm, and they are the
19
+ * same list, in the same order, that the in-process sandbox seeded before —
20
+ * including the cross-realm quirks that come with it (`[] instanceof Array` is
21
+ * false in here, because the literal's prototype is the vm context's while
22
+ * `Array` is the child's). Keeping the list identical is what makes a tenant
23
+ * script compute exactly what it computed before.
24
+ *
25
+ * These are real Node intrinsics, not shims. That is only safe because the
26
+ * realm they come from is worth nothing: the child is started with an empty
27
+ * environment and, where the runtime supports it, with the filesystem and
28
+ * process spawning denied. A script that breaks out of the vm — which is easy,
29
+ * vm was never a boundary — lands in a process that holds no secret.
30
+ */
31
+ exports.PRE_REQUEST_RUNNER_SOURCE = `
32
+ 'use strict';
33
+
34
+ /* La red se cierra ANTES que nada, y por eso está en la primera línea.
35
+ *
36
+ * El modelo de permisos de Node —\`--permission\`— cubre ficheros, spawn,
37
+ * worker_threads y addons. NO cubre la red: está medido, y un escape del vm
38
+ * dentro de este hijo encontraba \`require('http')\` y \`fetch\` disponibles.
39
+ * Y este hijo corre DENTRO de la red privada, así que desde ahí se alcanza
40
+ * Mongo, Redis, el servicio de metadatos de la nube y la propia API.
41
+ *
42
+ * El escape no es hipotético ni difícil: \`payload\` es un objeto del realm
43
+ * del hijo que entra al sandbox, así que
44
+ * \`payload.constructor.constructor('return process')()\` sale de golpe. El
45
+ * comentario de abajo ya lo decía —«vm nunca fue una frontera»—; lo que
46
+ * faltaba era que el realm al que se sale tampoco tuviera red.
47
+ *
48
+ * Esto NO convierte al hijo en un sandbox. La respuesta de verdad es la
49
+ * POSICIÓN —el worker de Cloudflare, donde no hay red privada que alcanzar—,
50
+ * y sigue siendo el camino preferido; ver \`code-runner.ts\`. Esto es la capa
51
+ * que queda puesta mientras tanto y el día que el worker no responda y se
52
+ * caiga al hijo, que es justo cuando hace falta.
53
+ *
54
+ * Se quitan también \`child_process\` y \`worker_threads\`, que el modelo de
55
+ * permisos ya niega, porque el escalón degradado —cuando el runtime no
56
+ * soporta el flag— se queda sin esa negación y con éstas no.
57
+ */
58
+ (function cerrarLaRed() {
59
+ const Module = require('module');
60
+
61
+ /* LISTA BLANCA, no lista negra — y el cambio importa más que su contenido.
62
+ *
63
+ * La primera versión enumeraba lo peligroso: http, https, net, tls, dns…
64
+ * Se rodeaba sin esfuerzo, porque Node expone los mismos transportes con
65
+ * otros nombres: \`_http_client\`, \`_tls_wrap\`, \`_http_common\` y
66
+ * \`_stream_wrap\` cargan los cuatro, y ninguno estaba en la lista.
67
+ *
68
+ * Enumerar lo prohibido es apostar a conocer TODOS los nombres, hoy y en la
69
+ * versión de Node de dentro de un año. Enumerar lo permitido es apostar a
70
+ * conocer lo que este hijo usa, que son tres módulos y están cinco líneas
71
+ * más abajo. La segunda apuesta se puede ganar.
72
+ */
73
+ const PERMITIDOS = new Set(['vm', 'crypto', 'module']);
74
+
75
+ const cargar = Module._load;
76
+ Module._load = function (peticion, padre, esPrincipal) {
77
+ const nombre = String(peticion).replace(/^node:/, '');
78
+ if (!PERMITIDOS.has(nombre)) {
79
+ throw new Error('module "' + peticion + '" is not available to node code');
80
+ }
81
+ return cargar.call(this, peticion, padre, esPrincipal);
82
+ };
83
+
84
+ /* \`process.binding\` devuelve los bindings nativos SIN pasar por el cargador
85
+ de módulos, así que la lista blanca de arriba no lo ve. Es una API vieja y
86
+ desaconsejada, y este hijo no la usa para nada. \`_linkedBinding\` es su
87
+ hermana.
88
+
89
+ \`getBuiltinModule\` es la misma idea con ropa nueva —Node 20.16 / 22.3—:
90
+ devuelve el builtin ya resuelto sin tocar \`Module._load\`, así que la
91
+ lista blanca tampoco lo veía. Está medido en este hijo:
92
+ \`getBuiltinModule('net')\` daba un socket de verdad, y los bytes llegaban
93
+ al otro extremo. \`dlopen\` carga un addon nativo, que trae red y todo lo
94
+ demás; el modelo de permisos ya lo niega, pero el escalón degradado —el
95
+ que se queda sin esa negación— no. Ninguna de las cuatro la usa este
96
+ hijo. */
97
+ for (const nombre of ['binding', '_linkedBinding', 'getBuiltinModule', 'dlopen']) {
98
+ if (typeof process[nombre] === 'function') {
99
+ process[nombre] = function () {
100
+ throw new Error('process.' + nombre + '() is not available to node code');
101
+ };
102
+ }
103
+ }
104
+
105
+ /* Registrar hooks de ESM abre un realm nuevo —otro hilo— donde
106
+ \`Module._load\` NO está parcheado, y el módulo de hooks importa lo que
107
+ quiera: basta una \`data:\` URL. También está medido, y también en el
108
+ escalón degradado, que es donde el modelo de permisos no lo niega por su
109
+ cuenta. \`registerHooks\` es la variante síncrona, que no existe en todas
110
+ las versiones; de ahí el \`typeof\`. */
111
+ for (const nombre of ['register', 'registerHooks']) {
112
+ if (typeof Module[nombre] === 'function') {
113
+ Module[nombre] = function () {
114
+ throw new Error('module.' + nombre + '() is not available to node code');
115
+ };
116
+ }
117
+ }
118
+
119
+ /* El último cargador que no pasa por \`Module._load\` es el \`import()\`
120
+ dinámico del contexto principal, y la llave la regala \`vm\`, que sí está
121
+ permitido porque este hijo lo necesita: pasando
122
+ \`vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER\` como
123
+ \`importModuleDynamically\` a \`Script\`, \`compileFunction\`,
124
+ \`runInThisContext\` o \`runInNewContext\`, \`import('net')\` resuelve —
125
+ medido igual que las de arriba, con bytes al otro lado. Sin esa llave no
126
+ queda camino: una callback propia exige \`--experimental-vm-modules\`, que
127
+ este hijo no lleva.
128
+
129
+ El objeto \`constants\` viene congelado, así que el símbolo no se puede
130
+ borrar de ahí; lo que sí se puede es dejar en \`vm.constants\` una copia
131
+ sin la llave. La copia conserva el resto de constantes y el prototipo
132
+ nulo que traía el original, para que lo que un script vea ahí sea lo
133
+ mismo de antes menos la llave. Si un día ninguna de las dos formas
134
+ funcionara no habría dónde avisar —este hijo corre con stdio cerrado—: lo
135
+ que cubre ese caso es el test de este fichero. */
136
+ const vmParaCerrar = require('vm');
137
+ const constantes = vmParaCerrar.constants;
138
+ if (constantes && constantes.USE_MAIN_CONTEXT_DEFAULT_LOADER) {
139
+ const copia = Object.create(null);
140
+ for (const clave of Object.getOwnPropertyNames(constantes)) {
141
+ if (clave !== 'USE_MAIN_CONTEXT_DEFAULT_LOADER') copia[clave] = constantes[clave];
142
+ }
143
+ Object.freeze(copia);
144
+ if (!Reflect.defineProperty(vmParaCerrar, 'constants', {
145
+ value: copia,
146
+ configurable: false,
147
+ writable: false,
148
+ enumerable: true,
149
+ })) {
150
+ Reflect.set(vmParaCerrar, 'constants', copia);
151
+ }
152
+ }
153
+
154
+ /* Los globales de red no pasan por \`require\`, así que hay que quitarlos
155
+ aparte. Con \`Reflect\` en vez de \`delete\` + asignación porque estos
156
+ globales son accesores perezosos: en modo estricto la asignación LANZA, y
157
+ \`Reflect\` devuelve \`false\` en vez de lanzar. Si las dos formas fallaran
158
+ —una versión de Node que los clave— no hay dónde avisar: este hijo corre
159
+ con stdio cerrado. Lo que cubre ese caso es el test de este fichero. */
160
+ for (const nombre of ['fetch', 'WebSocket', 'EventSource', 'XMLHttpRequest']) {
161
+ if (!Reflect.deleteProperty(globalThis, nombre)) {
162
+ Reflect.defineProperty(globalThis, nombre, {
163
+ value: undefined,
164
+ configurable: false,
165
+ writable: false,
166
+ });
167
+ }
168
+ }
169
+ })();
170
+
171
+ const vm = require('vm');
172
+ const crypto = require('crypto');
173
+
174
+ /* Una promesa rechazada que el inquilino deja suelta NO puede matar al hijo.
175
+ *
176
+ * Desde Node 15, el comportamiento por defecto de \`unhandledRejection\` es
177
+ * matar el proceso con código 1. Aquí eso es una carrera: el script termina,
178
+ * mandamos el resultado por IPC y, si Node recoge el rechazo antes de que el
179
+ * mensaje salga, el padre no ve respuesta — ve morir al hijo y contesta
180
+ * "Pre-request script worker stopped unexpectedly (1)". El mismo script, la
181
+ * misma petición, funciona o no según lo cargada que esté la máquina. En una
182
+ * portátil gana el envío casi siempre; en el runner del CI, no.
183
+ *
184
+ * Y basta con \`import('fs')\` —bloqueado, así que la promesa se rechaza— para
185
+ * dejar una suelta sin querer.
186
+ *
187
+ * El manejador está vacío a propósito. Lo tentador es recogerlo en los
188
+ * \`logs\` que el usuario ve, pero el rechazo aflora DESPUÉS de que el
189
+ * resultado ya salió: el script corre y se contesta dentro del mismo turno del
190
+ * bucle, y el evento llega en uno posterior. Un log escrito ahí no viajaría en
191
+ * esta ejecución, y como el pool reutiliza el proceso, acabaría contado en la
192
+ * siguiente — un mensaje falso sobre el script de otra persona. Se queda en lo
193
+ * que sí se puede prometer: el rechazo no mata al hijo.
194
+ */
195
+ process.on('unhandledRejection', function () {});
196
+
197
+ // A header value the structured clone cannot carry is replaced by the text the
198
+ // caller would have put on the wire anyway, so the send cannot fail on it.
199
+ function cloneable(headers) {
200
+ if (!headers || typeof headers !== 'object') return headers;
201
+ const out = Array.isArray(headers) ? [] : {};
202
+ for (const key of Object.keys(headers)) {
203
+ const value = headers[key];
204
+ const type = typeof value;
205
+ out[key] = type === 'function' || type === 'symbol' ? String(value) : value;
206
+ }
207
+ return out;
208
+ }
209
+
210
+ /**
211
+ * El sandbox del nodo de código.
212
+ *
213
+ * Misma lista de globales, en el mismo orden, que sembraba \`buildSandbox\` en
214
+ * proceso — incluidos los recortes deliberados: \`JSON\`, \`Array\` y \`Object\`
215
+ * entran con sólo unos métodos, no enteros. Mantenerla idéntica es lo que hace
216
+ * que el código de un inquilino calcule exactamente lo que calculaba antes.
217
+ *
218
+ * Aquí sí son los intrínsecos de verdad del hijo, y da igual: el realm del que
219
+ * salen no vale nada.
220
+ */
221
+ function buildCodeSandbox(ctx, logs) {
222
+ function stringify(v) {
223
+ if (v === undefined) return 'undefined';
224
+ if (v === null) return 'null';
225
+ if (typeof v === 'object') {
226
+ try { return JSON.stringify(v, null, 2); } catch (e) { return String(v); }
227
+ }
228
+ return String(v);
229
+ }
230
+
231
+ // \`$("nombre")\` — la salida de otro nodo del lienzo. Misma forma que en
232
+ // proceso: un iterable devuelve su campo, y \`_meta\` no se ve.
233
+ function nodeLookup(workspacePayloads) {
234
+ return function (nodeName) {
235
+ if (!workspacePayloads) throw new Error('$() requires workspace context');
236
+ const p = workspacePayloads[nodeName];
237
+ if (!p) throw new Error('Node "' + nodeName + '" not found');
238
+ const cloned = JSON.parse(JSON.stringify(p));
239
+ const m = cloned._meta;
240
+ if (m && m.iterable && m.iterateField) return cloned[m.iterateField] || [];
241
+ delete cloned._meta;
242
+ return cloned;
243
+ };
244
+ }
245
+
246
+ const sandbox = {
247
+ payload: ctx.payload,
248
+ $: nodeLookup(ctx.workspacePayloads),
249
+ headers: Object.assign({}, ctx.headers || {}),
250
+ meta: Object.assign({}, ctx.meta || {}),
251
+ JSON: { parse: JSON.parse, stringify: JSON.stringify },
252
+ Math: Math,
253
+ Date: Date,
254
+ Array: { isArray: Array.isArray, from: Array.from, of: Array.of },
255
+ Object: {
256
+ keys: Object.keys, values: Object.values, entries: Object.entries,
257
+ assign: Object.assign, freeze: Object.freeze,
258
+ },
259
+ String: String,
260
+ Number: Number,
261
+ Boolean: Boolean,
262
+ RegExp: RegExp,
263
+ parseInt: parseInt,
264
+ parseFloat: parseFloat,
265
+ isNaN: isNaN,
266
+ isFinite: isFinite,
267
+ encodeURIComponent: encodeURIComponent,
268
+ decodeURIComponent: decodeURIComponent,
269
+ Map: Map,
270
+ Set: Set,
271
+ undefined: undefined,
272
+ console: {
273
+ log: function () { logs.push(Array.prototype.map.call(arguments, stringify).join(' ')); },
274
+ warn: function () { logs.push('[warn] ' + Array.prototype.map.call(arguments, stringify).join(' ')); },
275
+ error: function () { logs.push('[error] ' + Array.prototype.map.call(arguments, stringify).join(' ')); },
276
+ },
277
+ };
278
+
279
+ return vm.createContext(sandbox);
280
+ }
281
+
282
+ function runCodeNode(msg) {
283
+ const logs = [];
284
+ let output;
285
+ try {
286
+ const sandbox = buildCodeSandbox(msg.ctx, logs);
287
+ // El envoltorio en función es lo que hace que \`return\` funcione, igual
288
+ // que en proceso.
289
+ output = new vm.Script('(function() {\\n' + msg.code + '\\n})()', {
290
+ filename: 'code-node.js',
291
+ }).runInContext(sandbox, { timeout: msg.timeoutMs });
292
+ } catch (err) {
293
+ process.send({
294
+ ok: false,
295
+ logs: logs,
296
+ name: err && err.name ? String(err.name) : 'Error',
297
+ message: err && err.message ? String(err.message) : String(err),
298
+ });
299
+ return;
300
+ }
301
+
302
+ // Lo que devuelva el inquilino cruza por structured clone. Lo que el clon no
303
+ // pueda llevar —una función, un símbolo— habría reventado el envío y matado
304
+ // la ejecución entera; el round-trip por JSON deja pasar lo que sí es
305
+ // representable y descarta el resto, que es lo que el llamante iba a
306
+ // serializar de todas formas.
307
+ try {
308
+ process.send({ ok: true, output: output === undefined ? null : output, logs: logs });
309
+ } catch (err) {
310
+ try {
311
+ process.send({
312
+ ok: true,
313
+ output: output === undefined || output === null
314
+ ? null
315
+ : JSON.parse(JSON.stringify(output)),
316
+ logs: logs,
317
+ });
318
+ } catch (again) {
319
+ process.send({
320
+ ok: false,
321
+ logs: logs,
322
+ name: 'TypeError',
323
+ message: 'Code node returned a value that cannot be returned: ' +
324
+ (again && again.message ? again.message : String(again)),
325
+ });
326
+ }
327
+ }
328
+ }
329
+
330
+ process.on('message', function (msg) {
331
+ if (msg && msg.kind === 'code') {
332
+ runCodeNode(msg);
333
+ return;
334
+ }
335
+
336
+ const ctx = msg.ctx;
337
+ const req = {
338
+ url: ctx.url,
339
+ method: ctx.method,
340
+ headers: Object.assign({}, ctx.headers),
341
+ body: ctx.body,
342
+ };
343
+
344
+ const sandbox = vm.createContext({
345
+ req,
346
+ payload: Object.freeze(ctx.payload),
347
+ // Crypto — needed for HMAC-SHA256 (S3, custom signatures)
348
+ crypto: {
349
+ createHmac: crypto.createHmac.bind(crypto),
350
+ createHash: crypto.createHash.bind(crypto),
351
+ randomUUID: crypto.randomUUID.bind(crypto),
352
+ },
353
+ // Standard globals
354
+ URL: URL,
355
+ URLSearchParams: URLSearchParams,
356
+ Buffer: Buffer,
357
+ Date: Date,
358
+ JSON: JSON,
359
+ Math: Math,
360
+ parseInt: parseInt,
361
+ parseFloat: parseFloat,
362
+ encodeURIComponent: encodeURIComponent,
363
+ decodeURIComponent: decodeURIComponent,
364
+ encodeURI: encodeURI,
365
+ decodeURI: decodeURI,
366
+ atob: globalThis.atob,
367
+ btoa: globalThis.btoa,
368
+ console: { log: function () {}, warn: function () {}, error: function () {} },
369
+ // String/Array/Object utilities
370
+ String: String,
371
+ Number: Number,
372
+ Boolean: Boolean,
373
+ Array: Array,
374
+ Object: Object,
375
+ RegExp: RegExp,
376
+ Map: Map,
377
+ Set: Set,
378
+ });
379
+
380
+ try {
381
+ new vm.Script(msg.code, { filename: 'pre-request-script.js' })
382
+ .runInContext(sandbox, { timeout: msg.timeoutMs });
383
+ } catch (err) {
384
+ process.send({
385
+ ok: false,
386
+ name: err && err.name ? String(err.name) : 'Error',
387
+ message: err && err.message ? String(err.message) : String(err),
388
+ });
389
+ return;
390
+ }
391
+
392
+ const result = { ok: true, url: req.url, headers: req.headers, body: req.body };
393
+ try {
394
+ process.send(result);
395
+ } catch (err) {
396
+ try {
397
+ result.headers = cloneable(req.headers);
398
+ process.send(result);
399
+ } catch (again) {
400
+ process.send({
401
+ ok: false,
402
+ name: 'TypeError',
403
+ message: 'Pre-request script produced a result that cannot be returned: ' +
404
+ (again && again.message ? again.message : String(again)),
405
+ });
406
+ }
407
+ }
408
+ });
409
+
410
+ process.send({ ready: true });
411
+ `;
@@ -0,0 +1,15 @@
1
+ import RE2 from 're2';
2
+ /**
3
+ * Compila el patrón con RE2, o devuelve `null` si no se puede.
4
+ *
5
+ * `null` cubre las dos formas de no poder, que desde fuera se tratan igual:
6
+ * el patrón está mal escrito, o usa algo que RE2 no implementa.
7
+ */
8
+ export declare function compilarRegexDeInquilino(patron: string, donde: string,
9
+ /** Los mismos de `RegExp`. El filtro de RSS usa `i`. */
10
+ flags?: string): RE2 | null;
11
+ /**
12
+ * `patron.test(cadena)` con el motor seguro. `false` cuando el patrón no
13
+ * compila, que es lo que estos dos sitios ya devolvían.
14
+ */
15
+ export declare function casaRegexDeInquilino(patron: string, cadena: unknown, donde: string): boolean;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.compilarRegexDeInquilino = compilarRegexDeInquilino;
7
+ exports.casaRegexDeInquilino = casaRegexDeInquilino;
8
+ const common_1 = require("@nestjs/common");
9
+ const re2_1 = __importDefault(require("re2"));
10
+ /**
11
+ * Las expresiones regulares que escribe un inquilino corren en RE2, no en el
12
+ * motor de JavaScript.
13
+ *
14
+ * ## El fallo
15
+ *
16
+ * Había dos sitios donde un patrón del inquilino se compilaba y se corría
17
+ * contra cadenas del payload, en el hilo de la petición:
18
+ *
19
+ * - `filter-utils.ts`, operador `matches_regex`;
20
+ * - `schema-validator-utils.ts`, la restricción `pattern`.
21
+ *
22
+ * El motor de JavaScript resuelve por retroceso, así que un patrón como
23
+ * `(a+)+$` contra una cadena de treinta letras tarda más que la vida útil del
24
+ * servidor. Y no hace falta ser el dueño del patrón para dispararlo: la cadena
25
+ * contra la que se prueba viene del payload, y el payload entra por
26
+ * `/api/in/:token`, que es público. Un solo evento deja un worker girando.
27
+ *
28
+ * Un `setTimeout` no lo corta: una expresión regular de JavaScript es
29
+ * síncrona, no cede el hilo, y no hay forma de interrumpirla desde dentro del
30
+ * proceso. Por eso el arreglo no puede ser «ponerle un límite de tiempo».
31
+ *
32
+ * ## Por qué RE2
33
+ *
34
+ * RE2 no retrocede: recorre el autómata en tiempo lineal sobre la longitud de
35
+ * la cadena, pase lo que pase con el patrón. No es un límite ni una heurística
36
+ * — es que la clase de fallo no existe en ese motor.
37
+ *
38
+ * ## Lo que RE2 NO acepta, que es el precio
39
+ *
40
+ * Precisamente porque no retrocede, RE2 no implementa lo que sólo se puede
41
+ * resolver retrocediendo: las referencias hacia atrás (`\1`) y las miradas
42
+ * adelante/atrás (`(?=…)`, `(?!…)`, `(?<=…)`). Un patrón que las use deja de
43
+ * compilar.
44
+ *
45
+ * Eso es un cambio de comportamiento y no se esconde: `compilar` devuelve
46
+ * `null` y deja que cada sitio decida cómo contarlo — el validador de esquema
47
+ * ya tenía un mensaje para «patrón inválido» y lo reutiliza; el filtro no
48
+ * casa, como ya hacía con cualquier patrón que no compilara, pero además lo
49
+ * registra, porque «este filtro dejó de casar en silencio» es un mal día para
50
+ * quien lo escribió.
51
+ *
52
+ * ## Sin caché, a propósito
53
+ *
54
+ * Tentaba guardar los compilados en un Map. La clave sería el patrón, y el
55
+ * patrón lo elige el inquilino: eso es exactamente un Map sin tope con clave
56
+ * que controla otro, que es otro de los hallazgos de este mismo informe. Los
57
+ * dos sitios ya compilaban en cada llamada antes de esto, así que no se pierde
58
+ * nada.
59
+ */
60
+ const logger = new common_1.Logger('RegexDeInquilino');
61
+ /** Máximo del patrón. Uno más largo que esto no es un filtro, es otra cosa. */
62
+ const MAX_PATRON = 1_000;
63
+ /**
64
+ * Compila el patrón con RE2, o devuelve `null` si no se puede.
65
+ *
66
+ * `null` cubre las dos formas de no poder, que desde fuera se tratan igual:
67
+ * el patrón está mal escrito, o usa algo que RE2 no implementa.
68
+ */
69
+ function compilarRegexDeInquilino(patron, donde,
70
+ /** Los mismos de `RegExp`. El filtro de RSS usa `i`. */
71
+ flags) {
72
+ if (typeof patron !== 'string' || patron.length === 0)
73
+ return null;
74
+ if (patron.length > MAX_PATRON) {
75
+ logger.warn(`[${donde}] patrón de ${patron.length} caracteres ignorado (máximo ${MAX_PATRON})`);
76
+ return null;
77
+ }
78
+ try {
79
+ return flags ? new re2_1.default(patron, flags) : new re2_1.default(patron);
80
+ }
81
+ catch (err) {
82
+ /* Se registra en vez de tragárselo porque el motivo casi siempre es
83
+ «RE2 no soporta esto», y sin la línea el patrón simplemente deja de
84
+ casar y nadie sabe por qué. */
85
+ logger.warn(`[${donde}] patrón rechazado: ${patron.slice(0, 120)} — ${err instanceof Error ? err.message : String(err)}`);
86
+ return null;
87
+ }
88
+ }
89
+ /**
90
+ * `patron.test(cadena)` con el motor seguro. `false` cuando el patrón no
91
+ * compila, que es lo que estos dos sitios ya devolvían.
92
+ */
93
+ function casaRegexDeInquilino(patron, cadena, donde) {
94
+ if (typeof cadena !== 'string')
95
+ return false;
96
+ const re = compilarRegexDeInquilino(patron, donde);
97
+ return re ? re.test(cadena) : false;
98
+ }
@@ -0,0 +1,18 @@
1
+ import type { Request } from 'express';
2
+ import type { IdentidadDePeticion as UserDocument } from './contratos';
3
+ export declare function getUser(req: Request): UserDocument;
4
+ export declare function getUserId(req: Request): string;
5
+ export declare function getOrganizationId(req: Request): string;
6
+ /**
7
+ * ¿Tiene quien llama este permiso?
8
+ *
9
+ * Una sesión del panel actúa por la cuenta entera, así que los tiene todos —
10
+ * es la misma distinción que lee `ScopesGuard`, y la que marca la estrategia
11
+ * de API key con `__isApiKey`. Una llave sólo tiene lo que se le acuñó.
12
+ *
13
+ * Existe porque un `@RequiredScopes(...)` en la ruta es todo o nada, y hay
14
+ * respuestas donde un CAMPO concreto vale más que el resto: el token de
15
+ * ingress de un webhook deja publicar eventos en él, así que no puede viajar
16
+ * en una respuesta que sólo pide permiso de lectura de eventos.
17
+ */
18
+ export declare function tienePermiso(req: Request, scope: string): boolean;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getUser = getUser;
4
+ exports.getUserId = getUserId;
5
+ exports.getOrganizationId = getOrganizationId;
6
+ exports.tienePermiso = tienePermiso;
7
+ function getUser(req) {
8
+ return req.user;
9
+ }
10
+ function getUserId(req) {
11
+ return getUser(req)._id.toString();
12
+ }
13
+ function getOrganizationId(req) {
14
+ const user = getUser(req);
15
+ return (user.organizationId ?? user._id).toString();
16
+ }
17
+ /**
18
+ * ¿Tiene quien llama este permiso?
19
+ *
20
+ * Una sesión del panel actúa por la cuenta entera, así que los tiene todos —
21
+ * es la misma distinción que lee `ScopesGuard`, y la que marca la estrategia
22
+ * de API key con `__isApiKey`. Una llave sólo tiene lo que se le acuñó.
23
+ *
24
+ * Existe porque un `@RequiredScopes(...)` en la ruta es todo o nada, y hay
25
+ * respuestas donde un CAMPO concreto vale más que el resto: el token de
26
+ * ingress de un webhook deja publicar eventos en él, así que no puede viajar
27
+ * en una respuesta que sólo pide permiso de lectura de eventos.
28
+ */
29
+ function tienePermiso(req, scope) {
30
+ const user = getUser(req);
31
+ if (!user?.__isApiKey)
32
+ return true;
33
+ return (user.__apiKeyScopes ?? []).includes(scope);
34
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Retry helper for pipeline nodes that make external HTTP requests.
3
+ * Retries on transient status codes (429, 502, 503, 504), waiting as long as
4
+ * the server asked when it said (`NodeResult.retryAfterMs`) and falling back
5
+ * to linear backoff when it didn't, plus jitter either way.
6
+ * Fire-and-forget safe — no state, no side effects.
7
+ *
8
+ * IMPORTANT: 500 is NOT retryable by default — a 500 from our own
9
+ * catch block (import error, parse failure, etc.) is NOT transient.
10
+ * Only external gateway errors (502/503/504) and rate limits (429) are.
11
+ *
12
+ * If fn() throws an exception (instead of returning a result),
13
+ * the error is caught and returned as statusCode 520 (internal error)
14
+ * WITHOUT retrying — thrown errors are bugs, not transient failures.
15
+ */
16
+ export interface NodeResult {
17
+ statusCode: number;
18
+ responseBody: string;
19
+ latencyMs: number;
20
+ /** Set to true when retryOnTransientError retried at least once */
21
+ _retriedTransient?: boolean;
22
+ /**
23
+ * How long the *server* said to wait before trying again, in ms — Discord's
24
+ * `retry_after`, a `Retry-After` header, anything of that shape. When set it
25
+ * replaces the computed backoff, because a guess that lands before the
26
+ * window reopens just burns an attempt. Control-plane only: it rides
27
+ * alongside the result like `_retriedTransient` and never reaches the
28
+ * downstream payload, which is built from `responseBody`.
29
+ */
30
+ retryAfterMs?: number;
31
+ }
32
+ export interface RetryOptions {
33
+ /** Max retries after first attempt (default 2 → 3 total attempts) */
34
+ maxRetries?: number;
35
+ /** Status codes to retry on (default: 429, 502, 503, 504) */
36
+ retryableStatuses?: Set<number>;
37
+ /** Base backoff delay in ms, multiplied by attempt (default 3000) */
38
+ backoffMs?: number;
39
+ /**
40
+ * Ceiling for a single wait, in ms (default 15000). Applies to a
41
+ * server-stated `retryAfterMs` too: a node that blocks for the four minutes
42
+ * a global rate limit can ask for is worse than a node that gives up.
43
+ */
44
+ maxBackoffMs?: number;
45
+ /** Logger for retry warnings */
46
+ logger?: {
47
+ warn: (msg: string) => void;
48
+ log: (msg: string) => void;
49
+ error?: (msg: string) => void;
50
+ };
51
+ /** Label for log messages (e.g. 'Firecrawl "Get Udemy"') */
52
+ label?: string;
53
+ }
54
+ export declare function retryOnTransientError(fn: () => Promise<NodeResult>, opts?: RetryOptions): Promise<NodeResult>;