@hostwebhook/node-types 1.87.0 → 1.89.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/esm/google-contacts-toolkit.d.ts +46 -0
- package/dist/esm/google-contacts-toolkit.js +180 -0
- package/dist/esm/index.d.ts +5 -1
- package/dist/esm/index.js +10 -1
- package/dist/esm/postgres-ssl.d.ts +125 -0
- package/dist/esm/postgres-ssl.js +276 -0
- package/dist/esm/toolkits.d.ts +69 -5
- package/dist/esm/toolkits.js +109 -5
- package/dist/google-contacts-toolkit.d.ts +46 -0
- package/dist/google-contacts-toolkit.js +183 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +27 -1
- package/dist/postgres-ssl.d.ts +125 -0
- package/dist/postgres-ssl.js +283 -0
- package/dist/toolkits.d.ts +69 -5
- package/dist/toolkits.js +117 -6
- package/package.json +1 -1
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* El SSL de una cadena de conexión de Postgres: qué hará el driver con ella,
|
|
3
|
+
* y cómo escribir el modo que se elija.
|
|
4
|
+
*
|
|
5
|
+
* ## Por qué vive aquí (2026-09-24)
|
|
6
|
+
*
|
|
7
|
+
* Ariel conectó un Postgres de Railway eligiendo «Require» y el panel del nodo
|
|
8
|
+
* dijo `self-signed certificate in certificate chain`. Medido: el servidor
|
|
9
|
+
* enseña un certificado `CN=localhost` firmado por su propio `root-ca`, y
|
|
10
|
+
* «Require» escribía `sslmode=require`, que para `pg` 8 es VERIFICAR el
|
|
11
|
+
* certificado. Y pidió que, si la cadena ya trae el SSL, el panel lo lea y lo
|
|
12
|
+
* deje fijo; y si no lo trae, que se elija a mano.
|
|
13
|
+
*
|
|
14
|
+
* Para fijarlo, el panel tiene que saber lo que hará EL DRIVER, no lo que
|
|
15
|
+
* parece que dice la cadena — y el driver tiene reglas que no se adivinan:
|
|
16
|
+
* `sslmode=prefer` también verifica, `ssl=false` ENCIENDE el SSL (queda como
|
|
17
|
+
* el texto `"false"`, que es verdadero), y `uselibpqcompat=true` le da la
|
|
18
|
+
* vuelta a `require`. El panel sólo conocía `require` y `no-verify`; lo demás
|
|
19
|
+
* lo pintaba como «Off».
|
|
20
|
+
*
|
|
21
|
+
* Una sola función para el dashboard y para los servicios, y el api la compara
|
|
22
|
+
* caso a caso con el `pg` que de verdad corre (`el-ssl-de-postgres-es-el-del-
|
|
23
|
+
* driver.spec.ts`): si una actualización de `pg` cambia sus reglas, ese spec
|
|
24
|
+
* se pone rojo antes de que el panel enseñe una cosa y el driver haga otra.
|
|
25
|
+
*
|
|
26
|
+
* ## Las reglas, copiadas de `pg-connection-string` 2.12 y `pg` 8.20
|
|
27
|
+
*
|
|
28
|
+
* 1. Los parámetros salen de `new URL(cadena, 'postgres://base')` (con el
|
|
29
|
+
* mismo arreglo de espacios y el mismo host de relleno para `@/`); si una
|
|
30
|
+
* clave se repite, gana la ÚLTIMA. Las claves distinguen mayúsculas:
|
|
31
|
+
* `SSLMODE` no existe para el driver.
|
|
32
|
+
* 2. `ssl=true`/`ssl=1` → sí; `ssl=0` → no; cualquier otro texto se queda
|
|
33
|
+
* como texto (`ssl=false` → `"false"` → ENCENDIDO).
|
|
34
|
+
* 3. Si hay `sslmode` (no vacío) o un fichero (`sslcert`, `sslkey`,
|
|
35
|
+
* `sslrootcert`), el SSL pasa a ser un objeto y `ssl=` deja de contar.
|
|
36
|
+
* 4. `sslmode`: `disable` → no; `no-verify` → cifrar sin comprobar; todo lo
|
|
37
|
+
* demás (`prefer`, `require`, `verify-ca`, `verify-full`, y cualquier
|
|
38
|
+
* valor raro) → cifrar Y comprobar el certificado.
|
|
39
|
+
* 5. Con `uselibpqcompat=true`, las de libpq: `prefer` y `require` → sin
|
|
40
|
+
* comprobar; `verify-full` → comprobando; `verify-ca` sin `sslrootcert`
|
|
41
|
+
* → el driver se NIEGA; `no-verify` no existe en libpq → comprobando.
|
|
42
|
+
* 6. Sin nada de eso, `pg` mira `PGSSLMODE` del entorno; en los servicios no
|
|
43
|
+
* está puesta (medido el 2026-09-24), así que es SIN SSL.
|
|
44
|
+
*
|
|
45
|
+
* ## Los ficheros (`sslcert`, `sslkey`, `sslrootcert`)
|
|
46
|
+
*
|
|
47
|
+
* El driver los LEE DEL DISCO de quien conecta — nuestro servidor, no la
|
|
48
|
+
* máquina de quien pegó la cadena. Nunca pueden valer: aquí salen como
|
|
49
|
+
* `problema` y el panel no deja guardarlos.
|
|
50
|
+
*/
|
|
51
|
+
const FICHEROS = ['sslcert', 'sslkey', 'sslrootcert'];
|
|
52
|
+
/** Los parámetros de la cadena, como los lee `pg-connection-string`. `null`
|
|
53
|
+
* si no se puede leer (el driver también fallaría) o si es un socket. */
|
|
54
|
+
function parametrosDe(cadena) {
|
|
55
|
+
let str = (cadena ?? '').trim();
|
|
56
|
+
if (!str || str.charAt(0) === '/')
|
|
57
|
+
return null;
|
|
58
|
+
if (/ |%[^a-f0-9]|%[a-f0-9][^a-f0-9]/i.test(str)) {
|
|
59
|
+
str = encodeURI(str).replace(/%25(\d\d)/g, '%$1');
|
|
60
|
+
}
|
|
61
|
+
let url;
|
|
62
|
+
try {
|
|
63
|
+
try {
|
|
64
|
+
url = new URL(str, 'postgres://base');
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
url = new URL(str.replace('@/', '@___DUMMY___/'), 'postgres://base');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/* Con `socket:` el driver devuelve antes de mirar el SSL. */
|
|
74
|
+
if (url.protocol === 'socket:')
|
|
75
|
+
return null;
|
|
76
|
+
const p = {};
|
|
77
|
+
url.searchParams.forEach((valor, clave) => {
|
|
78
|
+
p[clave] = valor;
|
|
79
|
+
});
|
|
80
|
+
return p;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Qué hará el driver con el SSL de esta cadena.
|
|
84
|
+
*
|
|
85
|
+
* `{ dice: false }` cuando la cadena no dice nada: ni `ssl`, ni `sslmode`, ni
|
|
86
|
+
* ficheros (un `sslmode=` o `ssl=` vacío tampoco dicen nada: el driver los
|
|
87
|
+
* ignora).
|
|
88
|
+
*/
|
|
89
|
+
export function leerSslPostgres(cadena) {
|
|
90
|
+
const p = parametrosDe(cadena);
|
|
91
|
+
if (!p)
|
|
92
|
+
return { dice: false };
|
|
93
|
+
const ficheros = FICHEROS.filter((k) => p[k]);
|
|
94
|
+
if (ficheros.length > 0) {
|
|
95
|
+
return {
|
|
96
|
+
dice: true,
|
|
97
|
+
modo: null,
|
|
98
|
+
por: `${ficheros[0]}=…`,
|
|
99
|
+
problema: `\`${ficheros[0]}\` points to a file, and the driver reads it from the disk of the ` +
|
|
100
|
+
`machine that connects — our servers, not yours. Remove it from the connection string ` +
|
|
101
|
+
`and pick the SSL mode here instead.`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const libpq = p.uselibpqcompat === 'true';
|
|
105
|
+
const conLibpq = libpq ? ' with uselibpqcompat=true' : '';
|
|
106
|
+
if (p.sslmode) {
|
|
107
|
+
const por = `sslmode=${p.sslmode}${conLibpq}`;
|
|
108
|
+
if (libpq) {
|
|
109
|
+
switch (p.sslmode) {
|
|
110
|
+
case 'disable':
|
|
111
|
+
return { dice: true, modo: 'off', por };
|
|
112
|
+
case 'prefer':
|
|
113
|
+
case 'require':
|
|
114
|
+
return { dice: true, modo: 'no-verify', por };
|
|
115
|
+
case 'verify-ca':
|
|
116
|
+
return {
|
|
117
|
+
dice: true,
|
|
118
|
+
modo: null,
|
|
119
|
+
por,
|
|
120
|
+
problema: 'With uselibpqcompat=true, sslmode=verify-ca needs sslrootcert, and a certificate file ' +
|
|
121
|
+
"can't be read from our servers. Use sslmode=verify-full, or remove sslmode and pick " +
|
|
122
|
+
'the SSL mode here.',
|
|
123
|
+
};
|
|
124
|
+
default:
|
|
125
|
+
/* `verify-full`, `no-verify` (que libpq no conoce) o un valor raro:
|
|
126
|
+
el objeto se queda vacío → comprobar. */
|
|
127
|
+
return { dice: true, modo: 'require', por };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
switch (p.sslmode) {
|
|
131
|
+
case 'disable':
|
|
132
|
+
return { dice: true, modo: 'off', por };
|
|
133
|
+
case 'no-verify':
|
|
134
|
+
return { dice: true, modo: 'no-verify', por };
|
|
135
|
+
default:
|
|
136
|
+
return { dice: true, modo: 'require', por };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (p.ssl) {
|
|
140
|
+
const por = `ssl=${p.ssl}`;
|
|
141
|
+
if (p.ssl === '0')
|
|
142
|
+
return { dice: true, modo: 'off', por };
|
|
143
|
+
/* `pg` convierte `ssl=no-verify` en «sin comprobar». */
|
|
144
|
+
if (p.ssl === 'no-verify')
|
|
145
|
+
return { dice: true, modo: 'no-verify', por };
|
|
146
|
+
/* `true`, `1`… y también `false`: queda como el texto `"false"`, que es
|
|
147
|
+
verdadero, y el driver cifra y comprueba. */
|
|
148
|
+
return { dice: true, modo: 'require', por };
|
|
149
|
+
}
|
|
150
|
+
return { dice: false };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Escribe el modo elegido en la cadena, quitando antes `ssl` y `sslmode` (en
|
|
154
|
+
* una cadena que «no dice nada», los que hubiera están vacíos). No toca el
|
|
155
|
+
* resto de parámetros, ni los recodifica.
|
|
156
|
+
*
|
|
157
|
+
* Lo que escribe, y por qué:
|
|
158
|
+
* - `off` → `sslmode=disable`, explícito: la cadena guardada dice lo
|
|
159
|
+
* que hace sin depender de `PGSSLMODE`.
|
|
160
|
+
* - `require` → `sslmode=verify-full`, no `require`: para `pg` 8 es lo
|
|
161
|
+
* mismo, pero `require` avisa en cada conexión de que en la
|
|
162
|
+
* versión 9 pasará a NO comprobar. `verify-full` significa
|
|
163
|
+
* lo mismo en las dos.
|
|
164
|
+
* - `no-verify` → `sslmode=no-verify`; con `uselibpqcompat=true`, que no lo
|
|
165
|
+
* conoce, `sslmode=require` (lo mismo en libpq).
|
|
166
|
+
*
|
|
167
|
+
* Una cadena vacía se devuelve tal cual.
|
|
168
|
+
*/
|
|
169
|
+
export function ponerSslPostgres(cadena, modo) {
|
|
170
|
+
if (!cadena)
|
|
171
|
+
return cadena;
|
|
172
|
+
const i = cadena.indexOf('?');
|
|
173
|
+
const base = i >= 0 ? cadena.slice(0, i) : cadena;
|
|
174
|
+
const partes = i >= 0 ? cadena.slice(i + 1).split('&') : [];
|
|
175
|
+
const clave = (parte) => {
|
|
176
|
+
const k = parte.split('=')[0];
|
|
177
|
+
try {
|
|
178
|
+
return decodeURIComponent(k);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return k;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
const libpq = partes.some((parte) => clave(parte) === 'uselibpqcompat' && /=true$/.test(parte));
|
|
185
|
+
const quedan = partes.filter((parte) => parte !== '' && clave(parte) !== 'ssl' && clave(parte) !== 'sslmode');
|
|
186
|
+
const valor = modo === 'off' ? 'disable' : modo === 'require' ? 'verify-full' : libpq ? 'require' : 'no-verify';
|
|
187
|
+
quedan.push(`sslmode=${valor}`);
|
|
188
|
+
return `${base}?${quedan.join('&')}`;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* La frase que le falta a un error de conexión de SSL: qué cambiar. `null` si
|
|
192
|
+
* el error no es de SSL.
|
|
193
|
+
*
|
|
194
|
+
* Los tres casos que se ven al conectar:
|
|
195
|
+
* - el certificado no se puede comprobar (autofirmado, CA privada, nombre
|
|
196
|
+
* que no casa): Railway (`CN=localhost` de su `root-ca`) y Supabase (su
|
|
197
|
+
* `Supabase Root 2021 CA`) —medidos los dos el 2026-09-24—, Heroku y
|
|
198
|
+
* RDS. Neon sí se comprueba (Let's Encrypt);
|
|
199
|
+
* - el servidor no acepta SSL;
|
|
200
|
+
* - el servidor exige SSL y se conectó sin él.
|
|
201
|
+
*/
|
|
202
|
+
export function pistaDeSslPostgres(mensaje) {
|
|
203
|
+
const m = mensaje ?? '';
|
|
204
|
+
if (/self[- ]signed certificate|unable to verify the first certificate|unable to get local issuer certificate|certificate has expired|Hostname\/IP does not match certificate|SELF_SIGNED_CERT_IN_CHAIN|DEPTH_ZERO_SELF_SIGNED_CERT|UNABLE_TO_VERIFY_LEAF_SIGNATURE|ERR_TLS_CERT_ALTNAME_INVALID|CERT_HAS_EXPIRED/i.test(m)) {
|
|
205
|
+
return ("The server's SSL certificate can't be verified (self-signed or from a private CA — Railway, " +
|
|
206
|
+
'Supabase, Heroku and RDS do this). Set the credential\'s SSL mode to "Require, skip cert" ' +
|
|
207
|
+
'(sslmode=no-verify): the connection stays encrypted, only the certificate check is skipped.');
|
|
208
|
+
}
|
|
209
|
+
if (/server does not support SSL connections/i.test(m)) {
|
|
210
|
+
return ('This server does not accept SSL. Set the credential\'s SSL mode to "Off" (sslmode=disable) — ' +
|
|
211
|
+
'only for a private network or a tunnel.');
|
|
212
|
+
}
|
|
213
|
+
if (/no pg_hba\.conf entry.*(no encryption|SSL off)|connection is insecure|SSL (connection )?is required|requires? SSL/i.test(m)) {
|
|
214
|
+
return ('This server requires SSL. Set the credential\'s SSL mode to "Require" (sslmode=verify-full), ' +
|
|
215
|
+
'or "Require, skip cert" (sslmode=no-verify) if its certificate is self-signed.');
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
/** El mensaje con su pista, si la tiene. Lo que devuelven los servicios. Si el
|
|
220
|
+
* error ya pasó por aquí (dos capas que lo envuelven), no la repite. */
|
|
221
|
+
export function conPistaDeSslPostgres(mensaje) {
|
|
222
|
+
const pista = pistaDeSslPostgres(mensaje);
|
|
223
|
+
if (!pista || mensaje.includes(pista))
|
|
224
|
+
return mensaje;
|
|
225
|
+
return `${mensaje} — ${pista}`;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Los casos con los que se comprueba la regla — aquí y contra el driver de
|
|
229
|
+
* verdad en el api (`el-ssl-de-postgres-es-el-del-driver.spec.ts`). Uno nuevo
|
|
230
|
+
* se añade AQUÍ y lo miran los dos.
|
|
231
|
+
*
|
|
232
|
+
* `esperado`: el modo, `'nada'` si la cadena no dice nada, `'problema'` si no
|
|
233
|
+
* puede funcionar.
|
|
234
|
+
*/
|
|
235
|
+
export const CASOS_DE_SSL_POSTGRES = [
|
|
236
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db', esperado: 'nada' },
|
|
237
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?application_name=x', esperado: 'nada' },
|
|
238
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?sslmode=', esperado: 'nada' },
|
|
239
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?ssl=', esperado: 'nada' },
|
|
240
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?SSLMODE=require', esperado: 'nada' },
|
|
241
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?uselibpqcompat=true', esperado: 'nada' },
|
|
242
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?sslmode=disable', esperado: 'off' },
|
|
243
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?sslmode=allow', esperado: 'require' },
|
|
244
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?sslmode=prefer', esperado: 'require' },
|
|
245
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?sslmode=require', esperado: 'require' },
|
|
246
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?sslmode=verify-ca', esperado: 'require' },
|
|
247
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?sslmode=verify-full', esperado: 'require' },
|
|
248
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?sslmode=no-verify', esperado: 'no-verify' },
|
|
249
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?sslmode=REQUIRE', esperado: 'require' },
|
|
250
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?ssl=true', esperado: 'require' },
|
|
251
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?ssl=1', esperado: 'require' },
|
|
252
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?ssl=false', esperado: 'require' },
|
|
253
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?ssl=0', esperado: 'off' },
|
|
254
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?ssl=no-verify', esperado: 'no-verify' },
|
|
255
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?ssl=true&sslmode=disable', esperado: 'off' },
|
|
256
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?sslmode=disable&ssl=true', esperado: 'off' },
|
|
257
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?sslmode=no-verify&ssl=true', esperado: 'no-verify' },
|
|
258
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?sslmode=require&sslmode=disable', esperado: 'off' },
|
|
259
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?uselibpqcompat=true&sslmode=disable', esperado: 'off' },
|
|
260
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?uselibpqcompat=true&sslmode=prefer', esperado: 'no-verify' },
|
|
261
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?uselibpqcompat=true&sslmode=require', esperado: 'no-verify' },
|
|
262
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?uselibpqcompat=true&sslmode=verify-full', esperado: 'require' },
|
|
263
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?uselibpqcompat=true&sslmode=no-verify', esperado: 'require' },
|
|
264
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?uselibpqcompat=true&sslmode=verify-ca', esperado: 'problema' },
|
|
265
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?sslrootcert=/etc/ssl/ca.pem', esperado: 'problema' },
|
|
266
|
+
{ cadena: 'postgresql://u:p@h.example:5432/db?sslmode=require&sslkey=key.pem', esperado: 'problema' },
|
|
267
|
+
{ cadena: 'postgres://u:p@h.example/db?sslmode=require', esperado: 'require' },
|
|
268
|
+
{ cadena: 'postgres://u:p%40x@h.example/db?sslmode=no-verify', esperado: 'no-verify' },
|
|
269
|
+
{ cadena: 'postgres://u:p@/db?host=/var/run/postgresql&sslmode=disable', esperado: 'off' },
|
|
270
|
+
/* Varios hosts: `pg` ni la lee (`ERR_INVALID_URL`), así que no hay SSL que
|
|
271
|
+
fijar — falla al conectar con su propio error. */
|
|
272
|
+
{ cadena: 'postgres://u:p@h1.example:5432,h2.example:5433/db?sslmode=require', esperado: 'nada' },
|
|
273
|
+
{ cadena: 'postgres://u:p w@h.example/db?sslmode=no-verify', esperado: 'no-verify' },
|
|
274
|
+
{ cadena: '/var/run/postgresql mydb', esperado: 'nada' },
|
|
275
|
+
{ cadena: '', esperado: 'nada' },
|
|
276
|
+
];
|
package/dist/esm/toolkits.d.ts
CHANGED
|
@@ -14,11 +14,30 @@
|
|
|
14
14
|
*
|
|
15
15
|
* ## Lo que falta aquí, a propósito
|
|
16
16
|
*
|
|
17
|
-
* WhatsApp y
|
|
18
|
-
*
|
|
19
|
-
* registro
|
|
20
|
-
* devuelve `null` para ellos, que
|
|
21
|
-
* destructiva».
|
|
17
|
+
* WhatsApp y Social siguen con sus specs fuera de este paquete (WhatsApp
|
|
18
|
+
* escrito a mano en la api y el dashboard; Social armado por entidad desde el
|
|
19
|
+
* registro de proveedores). Quien consulte este registro tiene que componerlos
|
|
20
|
+
* por su lado — {@link esOperacionDestructiva} devuelve `null` para ellos, que
|
|
21
|
+
* significa «no lo sé», no «no es destructiva». Contacts se mudó el
|
|
22
|
+
* 2026-09-24.
|
|
23
|
+
*
|
|
24
|
+
* ## El bloqueo por nodo y la puerta sin herramienta (2026-09-24)
|
|
25
|
+
*
|
|
26
|
+
* Abajo viven también las dos reglas que se aplican a TODA llamada a un nodo en
|
|
27
|
+
* modo toolkit, en un solo sitio para que la api, hw-nodes, node-sdk y el
|
|
28
|
+
* dashboard pregunten lo mismo:
|
|
29
|
+
*
|
|
30
|
+
* - «Block these tool calls»: el dueño bloquea herramientas en el nodo
|
|
31
|
+
* (`blockedTools`, por nombre). Vale para todo tipo de este registro
|
|
32
|
+
* ({@link admiteBloqueo}).
|
|
33
|
+
* - Sin herramienta no se corre: en modo toolkit un nodo sólo ejecuta la
|
|
34
|
+
* herramienta que le nombran en `operation`. Un evento sin ella —una arista
|
|
35
|
+
* vieja del lienzo— ya no corre la operación de «Single operation», que en
|
|
36
|
+
* ese modo no se ve. Decisión de Ariel para los catorce toolkits
|
|
37
|
+
* ({@link TIPOS_CON_TOOLKIT}), no sólo los de este registro.
|
|
38
|
+
*
|
|
39
|
+
* {@link motivoParaNoCorrer} junta las dos y la llama node-sdk al registrar
|
|
40
|
+
* cada handler, así que cubre todo camino que acabe en `handler.execute`.
|
|
22
41
|
*/
|
|
23
42
|
/** Lo mínimo de una herramienta de toolkit que hace falta para decidir. */
|
|
24
43
|
export interface OperacionDeToolkit {
|
|
@@ -45,3 +64,48 @@ export declare function operacionDeToolkit(nodeType: string | undefined | null,
|
|
|
45
64
|
* las herramientas que no son de toolkit (MCP, HTTP…).
|
|
46
65
|
*/
|
|
47
66
|
export declare function esOperacionDestructiva(nodeType: string | undefined | null, operacionONombre: string | undefined | null): boolean | null;
|
|
67
|
+
/**
|
|
68
|
+
* Los tipos con modo AI toolkit de VARIAS herramientas: los de este registro
|
|
69
|
+
* más WhatsApp y Social, cuyas herramientas aún no viven aquí. Es la lista de
|
|
70
|
+
* quién se niega a correr sin herramienta pedida.
|
|
71
|
+
*
|
|
72
|
+
* RSS, Mailchimp o Shopify también tienen `aiEnabled`, pero exponen el nodo
|
|
73
|
+
* como UNA herramienta sin `operation`: no están aquí a propósito.
|
|
74
|
+
*/
|
|
75
|
+
export declare const TIPOS_CON_TOOLKIT: readonly string[];
|
|
76
|
+
export declare function tieneModoToolkit(nodeType: string | undefined | null): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* ¿Admite «Block these tool calls»? Los de este registro: para bloquear hay
|
|
79
|
+
* que saber traducir la operación que guarda el AI Node al nombre de la
|
|
80
|
+
* herramienta que guarda el nodo, y eso sólo se sabe aquí.
|
|
81
|
+
*/
|
|
82
|
+
export declare function admiteBloqueo(nodeType: string | undefined | null): boolean;
|
|
83
|
+
/** Los nombres de herramienta de un toolkit: lo único que `blockedTools` acepta. */
|
|
84
|
+
export declare function nombresDeHerramientas(nodeType: string | undefined | null): string[];
|
|
85
|
+
/** Los nombres bloqueados de un nodo, tolerando documentos sin el campo. */
|
|
86
|
+
export declare function bloqueadasDe(entidad: unknown): string[];
|
|
87
|
+
/**
|
|
88
|
+
* ¿Está bloqueada en este nodo? Acepta la operación (`deleteOne`, lo que guarda
|
|
89
|
+
* el AI Node en `operationOverride`) o el nombre (`delete_document`, lo que
|
|
90
|
+
* recibe el servidor MCP).
|
|
91
|
+
*/
|
|
92
|
+
export declare function herramientaBloqueadaEn(entidad: unknown, nodeType: string | undefined | null, operacionONombre: string | undefined | null): boolean;
|
|
93
|
+
/** Lo que se contesta a una llamada sin herramienta. Lo lee una persona en el
|
|
94
|
+
* historial —una arista vieja— o el modelo, así que dice qué hacer. */
|
|
95
|
+
export declare const MENSAJE_SIN_HERRAMIENTA = "This node is an AI toolkit: it only runs the tool an AI Node or an MCP client names, and this call named none. If a connection in the flow leads here, remove it \u2014 in toolkit mode the node is not part of the pipeline.";
|
|
96
|
+
export declare function mensajeDeHerramientaBloqueada(toolName: string): string;
|
|
97
|
+
/**
|
|
98
|
+
* Por qué una llamada NO debe correr en este nodo, o `null` si puede.
|
|
99
|
+
*
|
|
100
|
+
* Sólo mira nodos en modo toolkit (`aiEnabled` y un tipo de
|
|
101
|
+
* {@link TIPOS_CON_TOOLKIT}); cualquier otro nodo corre como siempre. En modo
|
|
102
|
+
* toolkit:
|
|
103
|
+
* - sin `operation` → {@link MENSAJE_SIN_HERRAMIENTA};
|
|
104
|
+
* - con una herramienta bloqueada en el nodo → su mensaje.
|
|
105
|
+
*
|
|
106
|
+
* Una operación que el registro no conoce NO se rechaza aquí: cada servicio
|
|
107
|
+
* sabe qué operaciones acepta (Mongo y Postgres las validan con más detalle),
|
|
108
|
+
* y rechazar lo desconocido aquí rompería operaciones legítimas que no son
|
|
109
|
+
* herramientas.
|
|
110
|
+
*/
|
|
111
|
+
export declare function motivoParaNoCorrer(nodeType: string | undefined | null, entidad: unknown, payload: unknown): string | null;
|
package/dist/esm/toolkits.js
CHANGED
|
@@ -14,11 +14,30 @@
|
|
|
14
14
|
*
|
|
15
15
|
* ## Lo que falta aquí, a propósito
|
|
16
16
|
*
|
|
17
|
-
* WhatsApp y
|
|
18
|
-
*
|
|
19
|
-
* registro
|
|
20
|
-
* devuelve `null` para ellos, que
|
|
21
|
-
* destructiva».
|
|
17
|
+
* WhatsApp y Social siguen con sus specs fuera de este paquete (WhatsApp
|
|
18
|
+
* escrito a mano en la api y el dashboard; Social armado por entidad desde el
|
|
19
|
+
* registro de proveedores). Quien consulte este registro tiene que componerlos
|
|
20
|
+
* por su lado — {@link esOperacionDestructiva} devuelve `null` para ellos, que
|
|
21
|
+
* significa «no lo sé», no «no es destructiva». Contacts se mudó el
|
|
22
|
+
* 2026-09-24.
|
|
23
|
+
*
|
|
24
|
+
* ## El bloqueo por nodo y la puerta sin herramienta (2026-09-24)
|
|
25
|
+
*
|
|
26
|
+
* Abajo viven también las dos reglas que se aplican a TODA llamada a un nodo en
|
|
27
|
+
* modo toolkit, en un solo sitio para que la api, hw-nodes, node-sdk y el
|
|
28
|
+
* dashboard pregunten lo mismo:
|
|
29
|
+
*
|
|
30
|
+
* - «Block these tool calls»: el dueño bloquea herramientas en el nodo
|
|
31
|
+
* (`blockedTools`, por nombre). Vale para todo tipo de este registro
|
|
32
|
+
* ({@link admiteBloqueo}).
|
|
33
|
+
* - Sin herramienta no se corre: en modo toolkit un nodo sólo ejecuta la
|
|
34
|
+
* herramienta que le nombran en `operation`. Un evento sin ella —una arista
|
|
35
|
+
* vieja del lienzo— ya no corre la operación de «Single operation», que en
|
|
36
|
+
* ese modo no se ve. Decisión de Ariel para los catorce toolkits
|
|
37
|
+
* ({@link TIPOS_CON_TOOLKIT}), no sólo los de este registro.
|
|
38
|
+
*
|
|
39
|
+
* {@link motivoParaNoCorrer} junta las dos y la llama node-sdk al registrar
|
|
40
|
+
* cada handler, así que cubre todo camino que acabe en `handler.execute`.
|
|
22
41
|
*/
|
|
23
42
|
import { GMAIL_ALL_TOOLKIT_SPECS, NATIVE_EMAIL_TOOLKIT_SPECS } from './gmail-operations.js';
|
|
24
43
|
import { GOOGLE_CALENDAR_TOOLKIT_SPECS } from './calendar-toolkit.js';
|
|
@@ -30,6 +49,7 @@ import { SHEETS_TOOLKIT_SPECS } from './sheets-toolkit.js';
|
|
|
30
49
|
import { DOCS_TOOLKIT_SPECS } from './docs-toolkit.js';
|
|
31
50
|
import { MONGO_TOOLKIT_SPECS } from './mongo-toolkit.js';
|
|
32
51
|
import { POSTGRES_TOOLKIT_SPECS } from './postgres-toolkit.js';
|
|
52
|
+
import { GOOGLE_CONTACTS_TOOLKIT_SPECS } from './google-contacts-toolkit.js';
|
|
33
53
|
export const TOOLKITS_POR_TIPO = Object.freeze({
|
|
34
54
|
gmailAction: GMAIL_ALL_TOOLKIT_SPECS,
|
|
35
55
|
emailAction: NATIVE_EMAIL_TOOLKIT_SPECS,
|
|
@@ -42,6 +62,7 @@ export const TOOLKITS_POR_TIPO = Object.freeze({
|
|
|
42
62
|
docsAction: DOCS_TOOLKIT_SPECS,
|
|
43
63
|
mongoAction: MONGO_TOOLKIT_SPECS,
|
|
44
64
|
postgresAction: POSTGRES_TOOLKIT_SPECS,
|
|
65
|
+
googleContactsAction: GOOGLE_CONTACTS_TOOLKIT_SPECS,
|
|
45
66
|
});
|
|
46
67
|
/**
|
|
47
68
|
* La operación de un toolkit, buscada por su nombre interno (`updateRow`) O
|
|
@@ -72,3 +93,86 @@ export function esOperacionDestructiva(nodeType, operacionONombre) {
|
|
|
72
93
|
const op = operacionDeToolkit(nodeType, operacionONombre);
|
|
73
94
|
return op ? op.destructive === true : null;
|
|
74
95
|
}
|
|
96
|
+
/* ── Quién tiene toolkit, el bloqueo por nodo y la puerta ───────────────── */
|
|
97
|
+
/**
|
|
98
|
+
* Los tipos con modo AI toolkit de VARIAS herramientas: los de este registro
|
|
99
|
+
* más WhatsApp y Social, cuyas herramientas aún no viven aquí. Es la lista de
|
|
100
|
+
* quién se niega a correr sin herramienta pedida.
|
|
101
|
+
*
|
|
102
|
+
* RSS, Mailchimp o Shopify también tienen `aiEnabled`, pero exponen el nodo
|
|
103
|
+
* como UNA herramienta sin `operation`: no están aquí a propósito.
|
|
104
|
+
*/
|
|
105
|
+
export const TIPOS_CON_TOOLKIT = Object.freeze([
|
|
106
|
+
...Object.keys(TOOLKITS_POR_TIPO),
|
|
107
|
+
'whatsappAction',
|
|
108
|
+
'socialMediaAction',
|
|
109
|
+
]);
|
|
110
|
+
export function tieneModoToolkit(nodeType) {
|
|
111
|
+
return !!nodeType && TIPOS_CON_TOOLKIT.includes(nodeType);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* ¿Admite «Block these tool calls»? Los de este registro: para bloquear hay
|
|
115
|
+
* que saber traducir la operación que guarda el AI Node al nombre de la
|
|
116
|
+
* herramienta que guarda el nodo, y eso sólo se sabe aquí.
|
|
117
|
+
*/
|
|
118
|
+
export function admiteBloqueo(nodeType) {
|
|
119
|
+
return !!nodeType && Object.prototype.hasOwnProperty.call(TOOLKITS_POR_TIPO, nodeType);
|
|
120
|
+
}
|
|
121
|
+
/** Los nombres de herramienta de un toolkit: lo único que `blockedTools` acepta. */
|
|
122
|
+
export function nombresDeHerramientas(nodeType) {
|
|
123
|
+
if (!admiteBloqueo(nodeType))
|
|
124
|
+
return [];
|
|
125
|
+
return TOOLKITS_POR_TIPO[nodeType].map((s) => s.toolName);
|
|
126
|
+
}
|
|
127
|
+
/** Los nombres bloqueados de un nodo, tolerando documentos sin el campo. */
|
|
128
|
+
export function bloqueadasDe(entidad) {
|
|
129
|
+
const lista = entidad?.blockedTools;
|
|
130
|
+
return Array.isArray(lista) ? lista.filter((x) => typeof x === 'string') : [];
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* ¿Está bloqueada en este nodo? Acepta la operación (`deleteOne`, lo que guarda
|
|
134
|
+
* el AI Node en `operationOverride`) o el nombre (`delete_document`, lo que
|
|
135
|
+
* recibe el servidor MCP).
|
|
136
|
+
*/
|
|
137
|
+
export function herramientaBloqueadaEn(entidad, nodeType, operacionONombre) {
|
|
138
|
+
if (!operacionONombre)
|
|
139
|
+
return false;
|
|
140
|
+
const bloqueadas = bloqueadasDe(entidad);
|
|
141
|
+
if (bloqueadas.length === 0)
|
|
142
|
+
return false;
|
|
143
|
+
const op = operacionDeToolkit(nodeType, operacionONombre);
|
|
144
|
+
return bloqueadas.includes(op?.toolName ?? operacionONombre);
|
|
145
|
+
}
|
|
146
|
+
/** Lo que se contesta a una llamada sin herramienta. Lo lee una persona en el
|
|
147
|
+
* historial —una arista vieja— o el modelo, así que dice qué hacer. */
|
|
148
|
+
export const MENSAJE_SIN_HERRAMIENTA = 'This node is an AI toolkit: it only runs the tool an AI Node or an MCP client names, and this call named none. If a connection in the flow leads here, remove it — in toolkit mode the node is not part of the pipeline.';
|
|
149
|
+
export function mensajeDeHerramientaBloqueada(toolName) {
|
|
150
|
+
return `The tool "${toolName}" is blocked on this node by its owner. Do not retry it; tell the user it is not allowed here.`;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Por qué una llamada NO debe correr en este nodo, o `null` si puede.
|
|
154
|
+
*
|
|
155
|
+
* Sólo mira nodos en modo toolkit (`aiEnabled` y un tipo de
|
|
156
|
+
* {@link TIPOS_CON_TOOLKIT}); cualquier otro nodo corre como siempre. En modo
|
|
157
|
+
* toolkit:
|
|
158
|
+
* - sin `operation` → {@link MENSAJE_SIN_HERRAMIENTA};
|
|
159
|
+
* - con una herramienta bloqueada en el nodo → su mensaje.
|
|
160
|
+
*
|
|
161
|
+
* Una operación que el registro no conoce NO se rechaza aquí: cada servicio
|
|
162
|
+
* sabe qué operaciones acepta (Mongo y Postgres las validan con más detalle),
|
|
163
|
+
* y rechazar lo desconocido aquí rompería operaciones legítimas que no son
|
|
164
|
+
* herramientas.
|
|
165
|
+
*/
|
|
166
|
+
export function motivoParaNoCorrer(nodeType, entidad, payload) {
|
|
167
|
+
const e = entidad;
|
|
168
|
+
if (!e || e.aiEnabled !== true || !tieneModoToolkit(nodeType))
|
|
169
|
+
return null;
|
|
170
|
+
const pedida = payload?.operation;
|
|
171
|
+
if (typeof pedida !== 'string' || !pedida.trim())
|
|
172
|
+
return MENSAJE_SIN_HERRAMIENTA;
|
|
173
|
+
if (herramientaBloqueadaEn(entidad, nodeType, pedida)) {
|
|
174
|
+
const op = operacionDeToolkit(nodeType, pedida);
|
|
175
|
+
return mensajeDeHerramientaBloqueada(op?.toolName ?? pedida);
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Contacts AI Toolkit — las herramientas que un googleContactsAction
|
|
3
|
+
* expone cuando `aiEnabled` está encendido.
|
|
4
|
+
*
|
|
5
|
+
* **Se escribe una sola vez** (2026-09-24). Vivía dos veces: en
|
|
6
|
+
* `api/src/mcp-servers/toolkit-specs.ts` y en
|
|
7
|
+
* `dashboard/components/google-contacts-actions/google-contacts-operations-schemas.ts`,
|
|
8
|
+
* las dos a mano. Al mudarlas coincidían exactamente —las 15 operaciones, sus
|
|
9
|
+
* nombres, textos y parámetros—; el `group` del dashboard ya lo lleva el spec
|
|
10
|
+
* de formulario (`GOOGLE_CONTACTS_OPERATION_SPECS[op].group`).
|
|
11
|
+
*
|
|
12
|
+
* Se llama `GOOGLE_CONTACTS_TOOLKIT_SPECS` y no `…_OPERATION_SPECS` porque ese
|
|
13
|
+
* nombre ya es el del FORMULARIO del nodo en este paquete — la misma pareja que
|
|
14
|
+
* Discord y Slack.
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ El `destructive` ya no se escribe a mano: lo pone `marcarDestructivas` por
|
|
17
|
+
* el verbo. Cuatro que la copia vieja marcaba dejan de serlo —crear o añadir no
|
|
18
|
+
* pisa nada—: `create_contact`, `add_contact_to_group`,
|
|
19
|
+
* `batch_create_contacts` y `create_contact_group`. El gate del AI Node ya las
|
|
20
|
+
* leía así (por el verbo del nombre); lo que cambia es la etiqueta del panel.
|
|
21
|
+
*
|
|
22
|
+
* Los textos van en inglés porque los lee el modelo.
|
|
23
|
+
*/
|
|
24
|
+
import type { GoogleContactsOperation } from './google-contacts-operations.js';
|
|
25
|
+
export interface GoogleContactsToolkitParameter {
|
|
26
|
+
name: string;
|
|
27
|
+
type: 'string' | 'number' | 'boolean';
|
|
28
|
+
description: string;
|
|
29
|
+
required: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface GoogleContactsToolkitSpec {
|
|
32
|
+
operation: GoogleContactsOperation;
|
|
33
|
+
/** Etiqueta corta de la fila en la lista de herramientas. */
|
|
34
|
+
label: string;
|
|
35
|
+
/** Nombre con el que el LLM llama a la herramienta. */
|
|
36
|
+
toolName: string;
|
|
37
|
+
/** Descripción (más reglas de uso) que ve el LLM. */
|
|
38
|
+
description: string;
|
|
39
|
+
parameters: GoogleContactsToolkitParameter[];
|
|
40
|
+
/** Pisa o borra. Lo pone `marcarDestructivas` leyendo el verbo del
|
|
41
|
+
* nombre (ver `clase-de-herramienta.ts`), no se escribe a mano. */
|
|
42
|
+
destructive?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export declare const GOOGLE_CONTACTS_TOOLKIT_SPECS: GoogleContactsToolkitSpec[];
|
|
45
|
+
/** Las herramientas por nombre, para despachar una llamada del modelo. */
|
|
46
|
+
export declare const GOOGLE_CONTACTS_TOOLKIT_BY_TOOL_NAME: Record<string, GoogleContactsToolkitSpec>;
|