@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.
- package/dist/code-runner.d.ts +20 -0
- package/dist/code-runner.js +138 -0
- package/dist/contratos.d.ts +121 -0
- package/dist/contratos.js +24 -0
- package/dist/dto/output-node.dto.d.ts +19 -0
- package/dist/dto/output-node.dto.js +96 -0
- package/dist/ensure-meta.d.ts +22 -0
- package/dist/ensure-meta.js +35 -0
- package/dist/execute-with-iteration.d.ts +18 -0
- package/dist/execute-with-iteration.js +66 -0
- package/dist/filter-utils.d.ts +22 -0
- package/dist/filter-utils.js +178 -0
- package/dist/handler-helpers.d.ts +21 -0
- package/dist/handler-helpers.js +53 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +73 -0
- package/dist/log-metadata.d.ts +191 -0
- package/dist/log-metadata.js +375 -0
- package/dist/node-dispatch.registry.d.ts +32 -0
- package/dist/node-dispatch.registry.js +45 -0
- package/dist/node-executors.d.ts +299 -0
- package/dist/node-executors.js +555 -0
- package/dist/node-lifecycle.d.ts +399 -0
- package/dist/node-lifecycle.js +782 -0
- package/dist/normalize-nodes.d.ts +18 -0
- package/dist/normalize-nodes.js +22 -0
- package/dist/output-node-ref.schema.d.ts +82 -0
- package/dist/output-node-ref.schema.js +90 -0
- package/dist/output-webhook-scope.d.ts +36 -0
- package/dist/output-webhook-scope.js +42 -0
- package/dist/payload-preview.d.ts +10 -0
- package/dist/payload-preview.js +39 -0
- package/dist/pipeline.constants.d.ts +29 -0
- package/dist/pipeline.constants.js +51 -0
- package/dist/pre-request-pool.d.ts +58 -0
- package/dist/pre-request-pool.js +308 -0
- package/dist/pre-request-runner-source.d.ts +28 -0
- package/dist/pre-request-runner-source.js +411 -0
- package/dist/regex-de-inquilino.d.ts +15 -0
- package/dist/regex-de-inquilino.js +98 -0
- package/dist/request-context.d.ts +18 -0
- package/dist/request-context.js +34 -0
- package/dist/retry-transient.d.ts +54 -0
- package/dist/retry-transient.js +67 -0
- package/dist/retry-utils.d.ts +17 -0
- package/dist/retry-utils.js +23 -0
- package/dist/schema-validator-utils.d.ts +9 -0
- package/dist/schema-validator-utils.js +140 -0
- package/dist/ssrf-guard.d.ts +202 -0
- package/dist/ssrf-guard.js +917 -0
- package/dist/swallow.d.ts +52 -0
- package/dist/swallow.js +55 -0
- package/dist/template-render.d.ts +33 -0
- package/dist/template-render.js +43 -0
- package/dist/try-parse.d.ts +41 -0
- package/dist/try-parse.js +69 -0
- package/dist/workspace-payloads.d.ts +66 -0
- package/dist/workspace-payloads.js +496 -0
- package/package.json +35 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type CodeRunnerContext, type CodeRunnerResult } from './pre-request-pool';
|
|
2
|
+
/**
|
|
3
|
+
* Un fallo del CÓDIGO del inquilino, no del transporte.
|
|
4
|
+
*
|
|
5
|
+
* Se distingue a propósito: que el código del usuario lance no es motivo para
|
|
6
|
+
* caer al hijo — allí lanzaría igual, y reintentarlo sólo duplicaría efectos.
|
|
7
|
+
* Sólo el transporte justifica el respaldo.
|
|
8
|
+
*/
|
|
9
|
+
export declare class CodeNodeError extends Error {
|
|
10
|
+
readonly logs: string[];
|
|
11
|
+
constructor(message: string, logs: string[], name?: string);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Ejecutar el código, por el camino más seguro que esté disponible.
|
|
15
|
+
*
|
|
16
|
+
* Devuelve la misma forma corra donde corra — el llamante no se entera de
|
|
17
|
+
* cuál fue, que es la condición para que el respaldo sea un respaldo y no
|
|
18
|
+
* otro comportamiento.
|
|
19
|
+
*/
|
|
20
|
+
export declare function runTenantCode(code: string, ctx: CodeRunnerContext, timeoutMs: number): Promise<CodeRunnerResult>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CodeNodeError = void 0;
|
|
4
|
+
exports.runTenantCode = runTenantCode;
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
const pre_request_pool_1 = require("./pre-request-pool");
|
|
7
|
+
/**
|
|
8
|
+
* Dónde corre el código de un nodo de código.
|
|
9
|
+
*
|
|
10
|
+
* ## Por qué hay dos sitios
|
|
11
|
+
*
|
|
12
|
+
* Sacarlo del proceso de la API quitó los secretos y el disco: el hijo arranca
|
|
13
|
+
* con entorno vacío y, donde el runtime lo soporta, con ficheros denegados.
|
|
14
|
+
* Pero el modelo de permisos de Node NO cubre la red — medido: un escape del
|
|
15
|
+
* vm dentro del hijo encuentra `require('http')` disponible. Y ese hijo corre
|
|
16
|
+
* DENTRO de la red privada.
|
|
17
|
+
*
|
|
18
|
+
* El worker de Cloudflare no arregla el sandbox, arregla la POSICIÓN: allí no
|
|
19
|
+
* hay red privada que alcanzar. Por eso es el camino preferido.
|
|
20
|
+
*
|
|
21
|
+
* ## El respaldo, y por qué incomoda
|
|
22
|
+
*
|
|
23
|
+
* Si el worker no responde se cae al hijo. Eso protege la disponibilidad y
|
|
24
|
+
* cede exactamente lo que el worker vino a comprar, así que no puede ser
|
|
25
|
+
* silencioso ni permanente:
|
|
26
|
+
*
|
|
27
|
+
* - cada caída se registra a nivel ERROR, con el motivo. Un fallo sostenido
|
|
28
|
+
* tiene que verse, no acumularse;
|
|
29
|
+
* - `CODE_RUNNER_STRICT=1` lo desactiva. Con eso puesto, si el worker falla
|
|
30
|
+
* el nodo falla — que es lo que querrás en producción el día que confíes
|
|
31
|
+
* en el worker, porque así la seguridad deja de depender de que un
|
|
32
|
+
* servicio externo esté sano.
|
|
33
|
+
*
|
|
34
|
+
* Sin worker configurado no hay aviso ninguno: es el caso de desarrollo, y
|
|
35
|
+
* ahí el hijo es el camino normal, no una degradación.
|
|
36
|
+
*/
|
|
37
|
+
const logger = new common_1.Logger('CodeRunner');
|
|
38
|
+
/** Margen sobre el timeout del propio código, para distinguir "tu bucle no
|
|
39
|
+
* terminó" de "el worker no contestó". */
|
|
40
|
+
const MARGEN_DE_RED_MS = 3_000;
|
|
41
|
+
function urlDelWorker() {
|
|
42
|
+
return (process.env.CLOUDFLARE_CODE_RUNNER_URL ?? '').trim() || null;
|
|
43
|
+
}
|
|
44
|
+
function secretoDelWorker() {
|
|
45
|
+
return (process.env.CLOUDFLARE_CODE_RUNNER_SECRET ?? '').trim() || null;
|
|
46
|
+
}
|
|
47
|
+
/** Con esto puesto, un worker caído hace fallar el nodo en vez de caer al hijo. */
|
|
48
|
+
function esEstricto() {
|
|
49
|
+
const raw = (process.env.CODE_RUNNER_STRICT ?? '').trim().toLowerCase();
|
|
50
|
+
return raw === '1' || raw === 'true';
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Un fallo del CÓDIGO del inquilino, no del transporte.
|
|
54
|
+
*
|
|
55
|
+
* Se distingue a propósito: que el código del usuario lance no es motivo para
|
|
56
|
+
* caer al hijo — allí lanzaría igual, y reintentarlo sólo duplicaría efectos.
|
|
57
|
+
* Sólo el transporte justifica el respaldo.
|
|
58
|
+
*/
|
|
59
|
+
class CodeNodeError extends Error {
|
|
60
|
+
logs;
|
|
61
|
+
constructor(message, logs, name = 'Error') {
|
|
62
|
+
super(message);
|
|
63
|
+
this.logs = logs;
|
|
64
|
+
this.name = name;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.CodeNodeError = CodeNodeError;
|
|
68
|
+
async function correrEnWorker(url, secreto, code, ctx, timeoutMs) {
|
|
69
|
+
const abort = new AbortController();
|
|
70
|
+
const corte = setTimeout(() => abort.abort(), timeoutMs + MARGEN_DE_RED_MS);
|
|
71
|
+
let res;
|
|
72
|
+
try {
|
|
73
|
+
res = await fetch(url, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: {
|
|
76
|
+
'content-type': 'application/json',
|
|
77
|
+
authorization: `Bearer ${secreto}`,
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
code,
|
|
81
|
+
payload: ctx.payload,
|
|
82
|
+
workspacePayloads: ctx.workspacePayloads,
|
|
83
|
+
timeoutMs,
|
|
84
|
+
}),
|
|
85
|
+
signal: abort.signal,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
clearTimeout(corte);
|
|
90
|
+
}
|
|
91
|
+
if (!res.ok && res.status !== 200) {
|
|
92
|
+
// 401, 5xx, un 1102 de la plataforma… todo esto es transporte.
|
|
93
|
+
throw new Error(`code runner worker responded ${res.status}`);
|
|
94
|
+
}
|
|
95
|
+
const cuerpo = (await res.json());
|
|
96
|
+
const logs = Array.isArray(cuerpo.logs) ? cuerpo.logs.map(String) : [];
|
|
97
|
+
if (cuerpo.ok === true) {
|
|
98
|
+
return { output: cuerpo.output ?? null, logs };
|
|
99
|
+
}
|
|
100
|
+
// El worker contestó y dice que el código falló. Eso es un resultado, no una
|
|
101
|
+
// avería: se propaga tal cual y no se reintenta en el hijo.
|
|
102
|
+
throw new CodeNodeError(String(cuerpo.message ?? 'Code node failed'), logs, String(cuerpo.name ?? 'Error'));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Ejecutar el código, por el camino más seguro que esté disponible.
|
|
106
|
+
*
|
|
107
|
+
* Devuelve la misma forma corra donde corra — el llamante no se entera de
|
|
108
|
+
* cuál fue, que es la condición para que el respaldo sea un respaldo y no
|
|
109
|
+
* otro comportamiento.
|
|
110
|
+
*/
|
|
111
|
+
async function runTenantCode(code, ctx, timeoutMs) {
|
|
112
|
+
const url = urlDelWorker();
|
|
113
|
+
const secreto = secretoDelWorker();
|
|
114
|
+
if (!url || !secreto) {
|
|
115
|
+
// Desarrollo, o antes de desplegar el worker. El hijo es el camino normal
|
|
116
|
+
// aquí, no una degradación, así que no se avisa de nada.
|
|
117
|
+
return (0, pre_request_pool_1.runCodeNodeInChild)(code, ctx, timeoutMs);
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
return await correrEnWorker(url, secreto, code, ctx, timeoutMs);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
// El código del inquilino falló y el worker lo dijo. No es una avería.
|
|
124
|
+
if (err instanceof CodeNodeError)
|
|
125
|
+
throw err;
|
|
126
|
+
const motivo = err instanceof Error ? err.message : String(err);
|
|
127
|
+
if (esEstricto()) {
|
|
128
|
+
logger.error(`[code-runner] el worker no respondió y CODE_RUNNER_STRICT está puesto — el nodo falla: ${motivo}`);
|
|
129
|
+
throw new Error(`Code runner unavailable: ${motivo}`);
|
|
130
|
+
}
|
|
131
|
+
// A nivel ERROR a propósito: esto devuelve la ejecución a la red privada,
|
|
132
|
+
// que es justo lo que el worker vino a evitar. Un fallo sostenido tiene
|
|
133
|
+
// que doler en el panel, no acumularse en silencio.
|
|
134
|
+
logger.error(`[code-runner] el worker no respondió (${motivo}) — se ejecuta en el proceso hijo local. ` +
|
|
135
|
+
'Eso devuelve el código de inquilino a la red privada; si se repite, míralo.');
|
|
136
|
+
return (0, pre_request_pool_1.runCodeNodeInChild)(code, ctx, timeoutMs);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Los tipos que el SDK necesita del mundo de fuera, declarados AQUÍ.
|
|
3
|
+
*
|
|
4
|
+
* ## Por qué no se importan
|
|
5
|
+
*
|
|
6
|
+
* Porque el paquete no puede alcanzar `src/` de la api. Los siete vivían en
|
|
7
|
+
* entidades de Mongoose y servicios de Nest —`webhook.entity`,
|
|
8
|
+
* `telemetry.service`, `user.entity`…— y llegaban por `import type`, que se
|
|
9
|
+
* borra al compilar. Dentro del monolito eso funcionaba; en un paquete, no.
|
|
10
|
+
*
|
|
11
|
+
* ## Se declara la superficie USADA, no la entidad entera
|
|
12
|
+
*
|
|
13
|
+
* Está medido, no adivinado: de `TelemetryService` sólo se llaman dos métodos;
|
|
14
|
+
* de `UserDocument` sólo se leen cuatro campos. Copiar la entidad completa
|
|
15
|
+
* traería `@Prop`, el esquema, y con ellos media aplicación.
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ **La dirección de la dependencia se invierte.** A partir de aquí el
|
|
18
|
+
* contrato lo define el SDK y la api lo cumple: sus entidades reexportan
|
|
19
|
+
* estos tipos en vez de definirlos. Si alguien añade un campo en la entidad y
|
|
20
|
+
* no aquí, el SDK sencillamente no lo ve — que es lo correcto: el SDK no
|
|
21
|
+
* conoce las entidades de nadie.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Los operadores que el SDK sabe EVALUAR, sacados de `filter-utils`.
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ No coinciden con los `enum` de las entidades de la api, y esa es la
|
|
27
|
+
* lista buena: aquí vive el evaluador. Los enum de allá van de 7 a 32 segun
|
|
28
|
+
* la entidad —`webhook` admite 7 de estos 30— asi que hay filtros
|
|
29
|
+
* que el motor sabe resolver y la base de datos rechaza al guardar. Es un
|
|
30
|
+
* fallo de la api, anterior a este paquete, y se arregla aparte.
|
|
31
|
+
*/
|
|
32
|
+
export type FilterOperator = 'array_contains' | 'array_empty' | 'array_length_eq' | 'array_length_gt' | 'array_length_lt' | 'array_not_contains' | 'array_not_empty' | 'contains' | 'date_after' | 'date_before' | 'date_eq' | 'ends_with' | 'eq' | 'exists' | 'gt' | 'gte' | 'has_key' | 'is_empty' | 'is_false' | 'is_not_empty' | 'is_true' | 'lt' | 'lte' | 'matches_regex' | 'neq' | 'not_contains' | 'not_ends_with' | 'not_exists' | 'not_starts_with' | 'starts_with';
|
|
33
|
+
export interface PayloadFilter {
|
|
34
|
+
field: string;
|
|
35
|
+
operator: FilterOperator;
|
|
36
|
+
value?: string;
|
|
37
|
+
enabled?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface SchemaFieldConstraints {
|
|
40
|
+
minLength?: number;
|
|
41
|
+
maxLength?: number;
|
|
42
|
+
pattern?: string;
|
|
43
|
+
format?: 'email' | 'url' | 'uuid' | 'iso-date';
|
|
44
|
+
min?: number;
|
|
45
|
+
max?: number;
|
|
46
|
+
enum?: string[];
|
|
47
|
+
minItems?: number;
|
|
48
|
+
maxItems?: number;
|
|
49
|
+
}
|
|
50
|
+
export interface SchemaField {
|
|
51
|
+
path: string;
|
|
52
|
+
type: 'string' | 'number' | 'boolean' | 'array' | 'object' | 'any';
|
|
53
|
+
required: boolean;
|
|
54
|
+
constraints?: SchemaFieldConstraints;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `skipped` es un estado de primera y no un `failed` descafeinado: un paso que
|
|
58
|
+
* no llegó a correr porque el anterior falló no es lo mismo que uno que sí
|
|
59
|
+
* corrió y salió mal, y la pantalla necesita distinguirlos para enseñar dónde
|
|
60
|
+
* se cortó el flujo.
|
|
61
|
+
*/
|
|
62
|
+
export type RunStepStatus = 'success' | 'failed' | 'skipped';
|
|
63
|
+
export interface PasoEjecutado {
|
|
64
|
+
organizationId: string;
|
|
65
|
+
correlationId: string;
|
|
66
|
+
nodeId: string;
|
|
67
|
+
nodeName?: string;
|
|
68
|
+
nodeType?: string;
|
|
69
|
+
status: RunStepStatus;
|
|
70
|
+
startedAt: Date;
|
|
71
|
+
durationMs: number;
|
|
72
|
+
error?: string;
|
|
73
|
+
/** El origen de la corrida, para poder abrirla en la primera fila. */
|
|
74
|
+
sourceId?: string;
|
|
75
|
+
sourceName?: string;
|
|
76
|
+
workspaceId?: string;
|
|
77
|
+
entrada?: unknown;
|
|
78
|
+
salida?: unknown;
|
|
79
|
+
metadata?: Record<string, unknown>;
|
|
80
|
+
/** Cómo se disparó la corrida. Ausente = producción. */
|
|
81
|
+
modo?: 'pipeline';
|
|
82
|
+
}
|
|
83
|
+
/** Quien anota los pasos. El SDK sólo necesita saber que existe el método. */
|
|
84
|
+
export interface RegistradorDeHistorial {
|
|
85
|
+
registrarPaso(paso: PasoEjecutado): unknown;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* De dónde viene una línea de registro. Es `string` a propósito y no una
|
|
89
|
+
* unión cerrada: cada tipo de nodo aporta el suyo, y una unión obligaría a
|
|
90
|
+
* publicar el paquete cada vez que nace un nodo.
|
|
91
|
+
*/
|
|
92
|
+
export type LogSource = string;
|
|
93
|
+
/**
|
|
94
|
+
* Lo único que el ciclo de vida le pide a la telemetría.
|
|
95
|
+
*
|
|
96
|
+
* Dos métodos, medidos sobre `node-lifecycle`: `emit` para lo que puede
|
|
97
|
+
* perderse y `emitAndWait` para lo que no. El servicio real de la api tiene
|
|
98
|
+
* muchos más; ninguno se usa desde aquí.
|
|
99
|
+
*/
|
|
100
|
+
export interface EmisorDeTelemetria {
|
|
101
|
+
emit(...args: unknown[]): unknown;
|
|
102
|
+
emitAndWait(...args: unknown[]): Promise<unknown>;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Lo que `request-context` lee de `req.user`, y nada más.
|
|
106
|
+
*
|
|
107
|
+
* ⚠️ `__isApiKey` y `__apiKeyScopes` llevan doble guion bajo porque no son
|
|
108
|
+
* campos del documento: los pone la estrategia de autenticación al vuelo para
|
|
109
|
+
* distinguir una sesión del panel —que actúa por la cuenta entera— de una
|
|
110
|
+
* llave, que sólo tiene lo que se le acuñó.
|
|
111
|
+
*/
|
|
112
|
+
export interface IdentidadDePeticion {
|
|
113
|
+
_id: {
|
|
114
|
+
toString(): string;
|
|
115
|
+
};
|
|
116
|
+
organizationId?: {
|
|
117
|
+
toString(): string;
|
|
118
|
+
};
|
|
119
|
+
__isApiKey?: boolean;
|
|
120
|
+
__apiKeyScopes?: string[];
|
|
121
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Los tipos que el SDK necesita del mundo de fuera, declarados AQUÍ.
|
|
4
|
+
*
|
|
5
|
+
* ## Por qué no se importan
|
|
6
|
+
*
|
|
7
|
+
* Porque el paquete no puede alcanzar `src/` de la api. Los siete vivían en
|
|
8
|
+
* entidades de Mongoose y servicios de Nest —`webhook.entity`,
|
|
9
|
+
* `telemetry.service`, `user.entity`…— y llegaban por `import type`, que se
|
|
10
|
+
* borra al compilar. Dentro del monolito eso funcionaba; en un paquete, no.
|
|
11
|
+
*
|
|
12
|
+
* ## Se declara la superficie USADA, no la entidad entera
|
|
13
|
+
*
|
|
14
|
+
* Está medido, no adivinado: de `TelemetryService` sólo se llaman dos métodos;
|
|
15
|
+
* de `UserDocument` sólo se leen cuatro campos. Copiar la entidad completa
|
|
16
|
+
* traería `@Prop`, el esquema, y con ellos media aplicación.
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ **La dirección de la dependencia se invierte.** A partir de aquí el
|
|
19
|
+
* contrato lo define el SDK y la api lo cumple: sus entidades reexportan
|
|
20
|
+
* estos tipos en vez de definirlos. Si alguien añade un campo en la entidad y
|
|
21
|
+
* no aquí, el SDK sencillamente no lo ve — que es lo correcto: el SDK no
|
|
22
|
+
* conoce las entidades de nadie.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Common aliases LLMs and clients may send → canonical nodeType */
|
|
2
|
+
export declare const NODE_TYPE_ALIASES: Record<string, string>;
|
|
3
|
+
export declare function normalizeNodeType(type: string): string;
|
|
4
|
+
export declare class OutputNodeDto {
|
|
5
|
+
nodeType: string;
|
|
6
|
+
nodeId: string;
|
|
7
|
+
/**
|
|
8
|
+
* Which port the edge leaves through — see
|
|
9
|
+
* common/output-node-ref.schema.ts. Optional on the wire: absent means
|
|
10
|
+
* `main`, which is what every edge written before Phase 1 meant.
|
|
11
|
+
*
|
|
12
|
+
* Not validated against a fixed list on purpose. Container-owned ports
|
|
13
|
+
* are `<kind>:<container id>`, so the valid set is per-document and a
|
|
14
|
+
* whitelist here would either be wrong or duplicate the entity.
|
|
15
|
+
*/
|
|
16
|
+
port?: string;
|
|
17
|
+
sourceHandle?: string;
|
|
18
|
+
targetHandle?: string;
|
|
19
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.OutputNodeDto = exports.NODE_TYPE_ALIASES = void 0;
|
|
13
|
+
exports.normalizeNodeType = normalizeNodeType;
|
|
14
|
+
const class_validator_1 = require("class-validator");
|
|
15
|
+
const class_transformer_1 = require("class-transformer");
|
|
16
|
+
const node_types_1 = require("@hostwebhook/node-types");
|
|
17
|
+
/** Common aliases LLMs and clients may send → canonical nodeType */
|
|
18
|
+
exports.NODE_TYPE_ALIASES = {
|
|
19
|
+
mergeNode: 'merge',
|
|
20
|
+
approvalNode: 'approval',
|
|
21
|
+
filterNode: 'filter',
|
|
22
|
+
transformNode: 'transform',
|
|
23
|
+
delayNode: 'delay',
|
|
24
|
+
conditionalNode: 'conditional',
|
|
25
|
+
rateLimiterNode: 'rateLimiter',
|
|
26
|
+
aggregatorNode: 'aggregator',
|
|
27
|
+
cacheNode: 'cache',
|
|
28
|
+
schemaValidatorNode: 'schemaValidator',
|
|
29
|
+
email: 'emailAction',
|
|
30
|
+
http: 'httpAction',
|
|
31
|
+
mongo: 'mongoAction',
|
|
32
|
+
notification: 'notificationAction',
|
|
33
|
+
sheets: 'sheetsAction',
|
|
34
|
+
'scheduled-workflow': 'scheduledWorkflow',
|
|
35
|
+
scheduledwebhook: 'scheduledWorkflow',
|
|
36
|
+
aiNode: 'ai',
|
|
37
|
+
loopNode: 'loop',
|
|
38
|
+
splitNode: 'split',
|
|
39
|
+
markdownNode: 'markdown',
|
|
40
|
+
fileTransformNode: 'fileTransform',
|
|
41
|
+
calendar: 'calendarAction',
|
|
42
|
+
docs: 'docsAction',
|
|
43
|
+
firecrawl: 'firecrawlAction',
|
|
44
|
+
// Legacy alias — pre-2026-04 the source node was `voiceCall` (one
|
|
45
|
+
// per tool). The fold-voice-call-triggers migration replaced it with
|
|
46
|
+
// `voiceAgent` (one per agent, N output handles by sourceHandle=toolName).
|
|
47
|
+
// Old payloads / saved flows still send the literal "voiceCall" until
|
|
48
|
+
// the dashboard rewrites them on next save — normalise here so the
|
|
49
|
+
// pipeline doesn't reject inflight data.
|
|
50
|
+
voiceCall: 'voiceAgent',
|
|
51
|
+
};
|
|
52
|
+
function normalizeNodeType(type) {
|
|
53
|
+
return exports.NODE_TYPE_ALIASES[type] ?? type;
|
|
54
|
+
}
|
|
55
|
+
class OutputNodeDto {
|
|
56
|
+
nodeType;
|
|
57
|
+
nodeId;
|
|
58
|
+
/**
|
|
59
|
+
* Which port the edge leaves through — see
|
|
60
|
+
* common/output-node-ref.schema.ts. Optional on the wire: absent means
|
|
61
|
+
* `main`, which is what every edge written before Phase 1 meant.
|
|
62
|
+
*
|
|
63
|
+
* Not validated against a fixed list on purpose. Container-owned ports
|
|
64
|
+
* are `<kind>:<container id>`, so the valid set is per-document and a
|
|
65
|
+
* whitelist here would either be wrong or duplicate the entity.
|
|
66
|
+
*/
|
|
67
|
+
port;
|
|
68
|
+
sourceHandle;
|
|
69
|
+
targetHandle;
|
|
70
|
+
}
|
|
71
|
+
exports.OutputNodeDto = OutputNodeDto;
|
|
72
|
+
__decorate([
|
|
73
|
+
(0, class_transformer_1.Transform)(({ value }) => exports.NODE_TYPE_ALIASES[value] ?? value),
|
|
74
|
+
(0, class_validator_1.IsIn)(node_types_1.ALL_NODE_TYPES),
|
|
75
|
+
__metadata("design:type", String)
|
|
76
|
+
], OutputNodeDto.prototype, "nodeType", void 0);
|
|
77
|
+
__decorate([
|
|
78
|
+
(0, class_validator_1.IsMongoId)(),
|
|
79
|
+
__metadata("design:type", String)
|
|
80
|
+
], OutputNodeDto.prototype, "nodeId", void 0);
|
|
81
|
+
__decorate([
|
|
82
|
+
(0, class_validator_1.IsOptional)(),
|
|
83
|
+
(0, class_validator_1.IsString)(),
|
|
84
|
+
(0, class_validator_1.MaxLength)(80),
|
|
85
|
+
__metadata("design:type", String)
|
|
86
|
+
], OutputNodeDto.prototype, "port", void 0);
|
|
87
|
+
__decorate([
|
|
88
|
+
(0, class_validator_1.IsOptional)(),
|
|
89
|
+
(0, class_validator_1.IsString)(),
|
|
90
|
+
__metadata("design:type", String)
|
|
91
|
+
], OutputNodeDto.prototype, "sourceHandle", void 0);
|
|
92
|
+
__decorate([
|
|
93
|
+
(0, class_validator_1.IsOptional)(),
|
|
94
|
+
(0, class_validator_1.IsString)(),
|
|
95
|
+
__metadata("design:type", String)
|
|
96
|
+
], OutputNodeDto.prototype, "targetHandle", void 0);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backstop for the standardised `_meta` contract documented in
|
|
3
|
+
* `@hostwebhook/node-types` (`PayloadMeta`):
|
|
4
|
+
*
|
|
5
|
+
* { iterable: boolean; count: number; iterateField?: string }
|
|
6
|
+
*
|
|
7
|
+
* Most pipeline / transform / fetch nodes set `_meta` explicitly. Action
|
|
8
|
+
* nodes that proxy a third-party API (slack/discord/telegram/whatsapp/
|
|
9
|
+
* etc.) emit the raw API response as their output, and the contract
|
|
10
|
+
* was easy to forget when adding a new operation. Calling `ensureMeta`
|
|
11
|
+
* right before JSON-stringifying the result guarantees every downstream
|
|
12
|
+
* consumer (Limit / Loop / Filter / Aggregator / `$()` cross-node refs)
|
|
13
|
+
* sees a contract-compliant payload.
|
|
14
|
+
*
|
|
15
|
+
* Behavior:
|
|
16
|
+
* - If `output` already has a `_meta` field, returned unchanged.
|
|
17
|
+
* - If not, wraps with the safest default: a single, non-iterable
|
|
18
|
+
* object. Operations that DO return arrays should set
|
|
19
|
+
* `_meta: { iterable: true, iterateField, count }` themselves —
|
|
20
|
+
* this helper is the safety net, not a heuristic guesser.
|
|
21
|
+
*/
|
|
22
|
+
export declare function ensureMeta<T extends Record<string, unknown> | null | undefined>(output: T): Record<string, unknown>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ensureMeta = ensureMeta;
|
|
4
|
+
/**
|
|
5
|
+
* Backstop for the standardised `_meta` contract documented in
|
|
6
|
+
* `@hostwebhook/node-types` (`PayloadMeta`):
|
|
7
|
+
*
|
|
8
|
+
* { iterable: boolean; count: number; iterateField?: string }
|
|
9
|
+
*
|
|
10
|
+
* Most pipeline / transform / fetch nodes set `_meta` explicitly. Action
|
|
11
|
+
* nodes that proxy a third-party API (slack/discord/telegram/whatsapp/
|
|
12
|
+
* etc.) emit the raw API response as their output, and the contract
|
|
13
|
+
* was easy to forget when adding a new operation. Calling `ensureMeta`
|
|
14
|
+
* right before JSON-stringifying the result guarantees every downstream
|
|
15
|
+
* consumer (Limit / Loop / Filter / Aggregator / `$()` cross-node refs)
|
|
16
|
+
* sees a contract-compliant payload.
|
|
17
|
+
*
|
|
18
|
+
* Behavior:
|
|
19
|
+
* - If `output` already has a `_meta` field, returned unchanged.
|
|
20
|
+
* - If not, wraps with the safest default: a single, non-iterable
|
|
21
|
+
* object. Operations that DO return arrays should set
|
|
22
|
+
* `_meta: { iterable: true, iterateField, count }` themselves —
|
|
23
|
+
* this helper is the safety net, not a heuristic guesser.
|
|
24
|
+
*/
|
|
25
|
+
function ensureMeta(output) {
|
|
26
|
+
if (output &&
|
|
27
|
+
typeof output === 'object' &&
|
|
28
|
+
output._meta) {
|
|
29
|
+
return output;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
_meta: { iterable: false, count: 1 },
|
|
33
|
+
...(output ?? {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure-level iteration wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Nodes only implement single-item execution. This helper detects
|
|
5
|
+
* _meta.iterable and automatically iterates, aggregating results.
|
|
6
|
+
* Nodes never see _meta — they receive clean individual payloads.
|
|
7
|
+
*/
|
|
8
|
+
type SingleExecutor = (payload: Record<string, unknown>) => Promise<{
|
|
9
|
+
statusCode: number;
|
|
10
|
+
responseBody: string;
|
|
11
|
+
latencyMs: number;
|
|
12
|
+
}>;
|
|
13
|
+
export declare function executeWithIteration(payload: Record<string, unknown>, executeSingle: SingleExecutor): Promise<{
|
|
14
|
+
statusCode: number;
|
|
15
|
+
responseBody: string;
|
|
16
|
+
latencyMs: number;
|
|
17
|
+
}>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Infrastructure-level iteration wrapper.
|
|
4
|
+
*
|
|
5
|
+
* Nodes only implement single-item execution. This helper detects
|
|
6
|
+
* _meta.iterable and automatically iterates, aggregating results.
|
|
7
|
+
* Nodes never see _meta — they receive clean individual payloads.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.executeWithIteration = executeWithIteration;
|
|
11
|
+
async function executeWithIteration(payload, executeSingle) {
|
|
12
|
+
const meta = payload._meta;
|
|
13
|
+
if (meta?.iterable && meta.iterateField) {
|
|
14
|
+
const items = payload[meta.iterateField] ?? [];
|
|
15
|
+
const results = [];
|
|
16
|
+
let totalLatency = 0;
|
|
17
|
+
let lastStatus = 200;
|
|
18
|
+
for (const item of items) {
|
|
19
|
+
const clean = typeof item === 'object' && item !== null
|
|
20
|
+
? { ...item }
|
|
21
|
+
: { value: item };
|
|
22
|
+
delete clean._meta;
|
|
23
|
+
const res = await executeSingle(clean);
|
|
24
|
+
totalLatency += res.latencyMs;
|
|
25
|
+
lastStatus = res.statusCode;
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(res.responseBody);
|
|
28
|
+
// If executor returns an array, spread its items instead of nesting [[...]]
|
|
29
|
+
if (Array.isArray(parsed)) {
|
|
30
|
+
for (const entry of parsed)
|
|
31
|
+
results.push(entry);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
results.push(parsed);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
results.push(res.responseBody);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Single result from single iteration → unwrap as plain object
|
|
42
|
+
if (results.length === 1 &&
|
|
43
|
+
typeof results[0] === 'object' &&
|
|
44
|
+
results[0] !== null) {
|
|
45
|
+
const single = { _meta: { iterable: false, count: 1 }, ...results[0] };
|
|
46
|
+
return {
|
|
47
|
+
statusCode: lastStatus,
|
|
48
|
+
responseBody: JSON.stringify(single),
|
|
49
|
+
latencyMs: totalLatency,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const aggregated = {
|
|
53
|
+
_meta: { iterable: true, iterateField: 'results', count: results.length },
|
|
54
|
+
results,
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
statusCode: lastStatus,
|
|
58
|
+
responseBody: JSON.stringify(aggregated),
|
|
59
|
+
latencyMs: totalLatency,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// Single item — strip _meta and execute
|
|
63
|
+
const clean = { ...payload };
|
|
64
|
+
delete clean._meta;
|
|
65
|
+
return executeSingle(clean);
|
|
66
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PayloadFilter } from './contratos';
|
|
2
|
+
export declare function getNestedValue(obj: unknown, path: string): unknown;
|
|
3
|
+
export interface FilterResolveOpts {
|
|
4
|
+
/**
|
|
5
|
+
* @deprecated Sin efecto: el motor de plantillas retiró el evaluador de JS.
|
|
6
|
+
* Se mantiene el campo para no obligar a tocar todos los llamantes a la vez.
|
|
7
|
+
*/
|
|
8
|
+
allowJs?: boolean;
|
|
9
|
+
workspacePayloads?: Record<string, Record<string, unknown>>;
|
|
10
|
+
}
|
|
11
|
+
export declare function evaluateFilters(filters: PayloadFilter[], payload: unknown, mode?: 'and' | 'or', opts?: FilterResolveOpts): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Evaluate filters against a payload that may be iterable.
|
|
14
|
+
* If iterable: filters each item, returns a new iterable payload with only passing items.
|
|
15
|
+
* If not iterable: returns { passed, payload } with the original payload.
|
|
16
|
+
*/
|
|
17
|
+
export declare function evaluateFiltersIterable(filters: PayloadFilter[], payload: Record<string, unknown>, mode?: 'and' | 'or', outputAsSingle?: boolean, opts?: FilterResolveOpts): {
|
|
18
|
+
passed: boolean;
|
|
19
|
+
resultPayload: Record<string, unknown>;
|
|
20
|
+
totalItems: number;
|
|
21
|
+
passedItems: number;
|
|
22
|
+
};
|