@hostwebhook/platform-node 0.1.0 → 0.3.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/busqueda-literal.d.ts +52 -0
- package/dist/busqueda-literal.js +61 -0
- package/dist/escapar-regex.d.ts +28 -0
- package/dist/escapar-regex.js +33 -0
- package/dist/escape-html.d.ts +15 -0
- package/dist/escape-html.js +25 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +26 -0
- package/dist/mongo-databases.d.ts +26 -0
- package/dist/mongo-databases.js +45 -0
- package/dist/postgres-databases.d.ts +30 -0
- package/dist/postgres-databases.js +38 -0
- package/dist/presets-de-oauth/github-oauth-presets.d.ts +199 -0
- package/dist/presets-de-oauth/github-oauth-presets.js +142 -0
- package/dist/presets-de-oauth/linkedin-oauth-presets.d.ts +314 -0
- package/dist/presets-de-oauth/linkedin-oauth-presets.js +338 -0
- package/dist/presets-de-oauth/mailchimp-oauth-presets.d.ts +135 -0
- package/dist/presets-de-oauth/mailchimp-oauth-presets.js +96 -0
- package/dist/presets-de-oauth/shopify-oauth-presets.d.ts +209 -0
- package/dist/presets-de-oauth/shopify-oauth-presets.js +223 -0
- package/package.json +1 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lo que se teclea en un buscador es TEXTO, no una expresión regular.
|
|
3
|
+
*
|
|
4
|
+
* ## El fallo que cierra
|
|
5
|
+
*
|
|
6
|
+
* `GET /api/credentials?search=` y `GET /api/signing-secrets?search=` metían la
|
|
7
|
+
* cadena de la query directamente en un `$regex` de MongoDB. O sea que quien
|
|
8
|
+
* llamaba elegía la expresión regular que ejecuta el motor de la base.
|
|
9
|
+
*
|
|
10
|
+
* Y el motor de Mongo resuelve por retroceso, igual que el de JavaScript:
|
|
11
|
+
*
|
|
12
|
+
* 1. se crea una credencial con `name` = 100 000 letras 'a'
|
|
13
|
+
* (ni el DTO ni el esquema ponían tope a la longitud);
|
|
14
|
+
* 2. `GET /api/credentials?search=(a%2B)%2Bb`
|
|
15
|
+
*
|
|
16
|
+
* y `(a+)+b` contra esa cadena es exponencial. Una petición barata deja un
|
|
17
|
+
* worker de mongod girando, y la base es la MISMA para todos los inquilinos del
|
|
18
|
+
* clúster. Repitiendo hasta el tope de peticiones —que además se esquiva
|
|
19
|
+
* variando `X-Forwarded-For`, cosa que el propio `client-ip.ts` avisa de que es
|
|
20
|
+
* falsificable— la CPU no baja.
|
|
21
|
+
*
|
|
22
|
+
* Lo más llamativo es que el escape correcto ya estaba escrito noventa líneas
|
|
23
|
+
* más abajo en el mismo fichero, para el buscador de etiquetas. Estaba resuelto
|
|
24
|
+
* y copiado a medias.
|
|
25
|
+
*
|
|
26
|
+
* ## Lo que hace
|
|
27
|
+
*
|
|
28
|
+
* Escapa los metacaracteres —para que el patrón case LITERALMENTE lo que se
|
|
29
|
+
* escribió— y acota la longitud. Lo segundo importa aparte: el coste de una
|
|
30
|
+
* búsqueda crece con el patrón además de con el texto, y un buscador no
|
|
31
|
+
* necesita mil caracteres.
|
|
32
|
+
*
|
|
33
|
+
* ⚠️ El escape en sí NO vive aquí: es `escaparRegex`, en el módulo de al
|
|
34
|
+
* lado. Esta función es «escapar Y acotar»; hay un puñado de sitios que
|
|
35
|
+
* incrustan un valor interno en un patrón y no pueden recortarlo sin cambiar
|
|
36
|
+
* lo que significa, y ésos usan el primitivo pelado. Una sola expresión de
|
|
37
|
+
* escape en todo el repositorio, y dos maneras de pedirla.
|
|
38
|
+
*
|
|
39
|
+
* No se usa `$text`: exigiría un índice de texto por colección y cambiaría la
|
|
40
|
+
* semántica de «contiene» a «palabras completas», que no es lo que el panel
|
|
41
|
+
* ofrece hoy.
|
|
42
|
+
*/
|
|
43
|
+
/** Un buscador no necesita más. Por encima, se recorta. */
|
|
44
|
+
export declare const MAX_BUSQUEDA = 200;
|
|
45
|
+
/**
|
|
46
|
+
* Devuelve la cadena lista para meter en un `$regex`, casando literalmente.
|
|
47
|
+
*
|
|
48
|
+
* Se exporta también `MAX_BUSQUEDA` para que quien quiera pueda rechazar en vez
|
|
49
|
+
* de recortar; aquí se recorta porque un buscador que devuelve un error por
|
|
50
|
+
* escribir de más es peor que uno que busca por los primeros 200 caracteres.
|
|
51
|
+
*/
|
|
52
|
+
export declare function busquedaLiteral(bruto: unknown): string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Lo que se teclea en un buscador es TEXTO, no una expresión regular.
|
|
4
|
+
*
|
|
5
|
+
* ## El fallo que cierra
|
|
6
|
+
*
|
|
7
|
+
* `GET /api/credentials?search=` y `GET /api/signing-secrets?search=` metían la
|
|
8
|
+
* cadena de la query directamente en un `$regex` de MongoDB. O sea que quien
|
|
9
|
+
* llamaba elegía la expresión regular que ejecuta el motor de la base.
|
|
10
|
+
*
|
|
11
|
+
* Y el motor de Mongo resuelve por retroceso, igual que el de JavaScript:
|
|
12
|
+
*
|
|
13
|
+
* 1. se crea una credencial con `name` = 100 000 letras 'a'
|
|
14
|
+
* (ni el DTO ni el esquema ponían tope a la longitud);
|
|
15
|
+
* 2. `GET /api/credentials?search=(a%2B)%2Bb`
|
|
16
|
+
*
|
|
17
|
+
* y `(a+)+b` contra esa cadena es exponencial. Una petición barata deja un
|
|
18
|
+
* worker de mongod girando, y la base es la MISMA para todos los inquilinos del
|
|
19
|
+
* clúster. Repitiendo hasta el tope de peticiones —que además se esquiva
|
|
20
|
+
* variando `X-Forwarded-For`, cosa que el propio `client-ip.ts` avisa de que es
|
|
21
|
+
* falsificable— la CPU no baja.
|
|
22
|
+
*
|
|
23
|
+
* Lo más llamativo es que el escape correcto ya estaba escrito noventa líneas
|
|
24
|
+
* más abajo en el mismo fichero, para el buscador de etiquetas. Estaba resuelto
|
|
25
|
+
* y copiado a medias.
|
|
26
|
+
*
|
|
27
|
+
* ## Lo que hace
|
|
28
|
+
*
|
|
29
|
+
* Escapa los metacaracteres —para que el patrón case LITERALMENTE lo que se
|
|
30
|
+
* escribió— y acota la longitud. Lo segundo importa aparte: el coste de una
|
|
31
|
+
* búsqueda crece con el patrón además de con el texto, y un buscador no
|
|
32
|
+
* necesita mil caracteres.
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ El escape en sí NO vive aquí: es `escaparRegex`, en el módulo de al
|
|
35
|
+
* lado. Esta función es «escapar Y acotar»; hay un puñado de sitios que
|
|
36
|
+
* incrustan un valor interno en un patrón y no pueden recortarlo sin cambiar
|
|
37
|
+
* lo que significa, y ésos usan el primitivo pelado. Una sola expresión de
|
|
38
|
+
* escape en todo el repositorio, y dos maneras de pedirla.
|
|
39
|
+
*
|
|
40
|
+
* No se usa `$text`: exigiría un índice de texto por colección y cambiaría la
|
|
41
|
+
* semántica de «contiene» a «palabras completas», que no es lo que el panel
|
|
42
|
+
* ofrece hoy.
|
|
43
|
+
*/
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.MAX_BUSQUEDA = void 0;
|
|
46
|
+
exports.busquedaLiteral = busquedaLiteral;
|
|
47
|
+
const escapar_regex_1 = require("./escapar-regex");
|
|
48
|
+
/** Un buscador no necesita más. Por encima, se recorta. */
|
|
49
|
+
exports.MAX_BUSQUEDA = 200;
|
|
50
|
+
/**
|
|
51
|
+
* Devuelve la cadena lista para meter en un `$regex`, casando literalmente.
|
|
52
|
+
*
|
|
53
|
+
* Se exporta también `MAX_BUSQUEDA` para que quien quiera pueda rechazar en vez
|
|
54
|
+
* de recortar; aquí se recorta porque un buscador que devuelve un error por
|
|
55
|
+
* escribir de más es peor que uno que busca por los primeros 200 caracteres.
|
|
56
|
+
*/
|
|
57
|
+
function busquedaLiteral(bruto) {
|
|
58
|
+
if (typeof bruto !== 'string' || bruto.length === 0)
|
|
59
|
+
return '';
|
|
60
|
+
return (0, escapar_regex_1.escaparRegex)(bruto.slice(0, exports.MAX_BUSQUEDA));
|
|
61
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lo que escribe el usuario es TEXTO, no un patrón.
|
|
3
|
+
*
|
|
4
|
+
* Sin escapar pasan dos cosas, y ninguna es «no encuentra nada»: un
|
|
5
|
+
* paréntesis suelto hace que Mongo rechace la consulta entera con un error
|
|
6
|
+
* de sintaxis, y un patrón anidado del tipo «grupo repetido dentro de grupo
|
|
7
|
+
* repetido» se convierte en un ReDoS — el regex lo evalúa el servidor de
|
|
8
|
+
* base de datos, así que quien se queda colgado es él, no el proceso que
|
|
9
|
+
* recibió la petición.
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ **Ésta es la única expresión regular de escape del repositorio.** No
|
|
12
|
+
* escribas otra: `busquedaLiteral` la usa, y todo lo demás usa una de las
|
|
13
|
+
* dos. Cuál de las dos:
|
|
14
|
+
*
|
|
15
|
+
* - `busquedaLiteral` — texto que teclea alguien en un buscador. Escapa
|
|
16
|
+
* **y acota** la longitud, porque el coste de la búsqueda crece con el
|
|
17
|
+
* patrón además de con el texto. Es la que quieres casi siempre.
|
|
18
|
+
* - `escaparRegex` (ésta) — un valor interno que se incrusta en un patrón
|
|
19
|
+
* y que NO puede recortarse sin cambiar lo que significa.
|
|
20
|
+
*
|
|
21
|
+
* El 2026-08-27 este módulo decía que quedaban «tres copias literales» por
|
|
22
|
+
* migrar. Eran DOCE: se habían contado buscando los nombres `escapeRegex` y
|
|
23
|
+
* `escaparRegex`, y la mitad estaban escritas a pelo dentro de la consulta,
|
|
24
|
+
* sin función que buscar. Contar por el nombre no es inventariar — hay que
|
|
25
|
+
* contar por la clase de caracteres. `una-sola-copia-del-escape.spec.ts` lo
|
|
26
|
+
* cuenta ahora por ti y no deja aparecer la trece.
|
|
27
|
+
*/
|
|
28
|
+
export declare function escaparRegex(s: string): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.escaparRegex = escaparRegex;
|
|
4
|
+
/**
|
|
5
|
+
* Lo que escribe el usuario es TEXTO, no un patrón.
|
|
6
|
+
*
|
|
7
|
+
* Sin escapar pasan dos cosas, y ninguna es «no encuentra nada»: un
|
|
8
|
+
* paréntesis suelto hace que Mongo rechace la consulta entera con un error
|
|
9
|
+
* de sintaxis, y un patrón anidado del tipo «grupo repetido dentro de grupo
|
|
10
|
+
* repetido» se convierte en un ReDoS — el regex lo evalúa el servidor de
|
|
11
|
+
* base de datos, así que quien se queda colgado es él, no el proceso que
|
|
12
|
+
* recibió la petición.
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ **Ésta es la única expresión regular de escape del repositorio.** No
|
|
15
|
+
* escribas otra: `busquedaLiteral` la usa, y todo lo demás usa una de las
|
|
16
|
+
* dos. Cuál de las dos:
|
|
17
|
+
*
|
|
18
|
+
* - `busquedaLiteral` — texto que teclea alguien en un buscador. Escapa
|
|
19
|
+
* **y acota** la longitud, porque el coste de la búsqueda crece con el
|
|
20
|
+
* patrón además de con el texto. Es la que quieres casi siempre.
|
|
21
|
+
* - `escaparRegex` (ésta) — un valor interno que se incrusta en un patrón
|
|
22
|
+
* y que NO puede recortarse sin cambiar lo que significa.
|
|
23
|
+
*
|
|
24
|
+
* El 2026-08-27 este módulo decía que quedaban «tres copias literales» por
|
|
25
|
+
* migrar. Eran DOCE: se habían contado buscando los nombres `escapeRegex` y
|
|
26
|
+
* `escaparRegex`, y la mitad estaban escritas a pelo dentro de la consulta,
|
|
27
|
+
* sin función que buscar. Contar por el nombre no es inventariar — hay que
|
|
28
|
+
* contar por la clase de caracteres. `una-sola-copia-del-escape.spec.ts` lo
|
|
29
|
+
* cuenta ahora por ti y no deja aparecer la trece.
|
|
30
|
+
*/
|
|
31
|
+
function escaparRegex(s) {
|
|
32
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
33
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escapar HTML, una sola vez y en un solo sitio.
|
|
3
|
+
*
|
|
4
|
+
* Había TRES copias de esta función en el repo —en `credentials.controller`,
|
|
5
|
+
* en `approval-nodes.controller` y en `send-and-wait.controller`— idénticas
|
|
6
|
+
* carácter por carácter. Que hoy coincidan no es garantía de nada: son tres
|
|
7
|
+
* sitios donde alguien puede añadir un caso, olvidarse de los otros dos, y
|
|
8
|
+
* dejar dos escapes peores que el tercero sin que nada lo señale.
|
|
9
|
+
*
|
|
10
|
+
* Y estaba a punto de haber una cuarta, para el email de invitación.
|
|
11
|
+
*
|
|
12
|
+
* Los cinco caracteres son los de siempre, y el orden importa: `&` va primero
|
|
13
|
+
* o acabaría escapando los `&` que introducen los demás reemplazos.
|
|
14
|
+
*/
|
|
15
|
+
export declare function escapeHtml(valor: unknown): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.escapeHtml = escapeHtml;
|
|
4
|
+
/**
|
|
5
|
+
* Escapar HTML, una sola vez y en un solo sitio.
|
|
6
|
+
*
|
|
7
|
+
* Había TRES copias de esta función en el repo —en `credentials.controller`,
|
|
8
|
+
* en `approval-nodes.controller` y en `send-and-wait.controller`— idénticas
|
|
9
|
+
* carácter por carácter. Que hoy coincidan no es garantía de nada: son tres
|
|
10
|
+
* sitios donde alguien puede añadir un caso, olvidarse de los otros dos, y
|
|
11
|
+
* dejar dos escapes peores que el tercero sin que nada lo señale.
|
|
12
|
+
*
|
|
13
|
+
* Y estaba a punto de haber una cuarta, para el email de invitación.
|
|
14
|
+
*
|
|
15
|
+
* Los cinco caracteres son los de siempre, y el orden importa: `&` va primero
|
|
16
|
+
* o acabaría escapando los `&` que introducen los demás reemplazos.
|
|
17
|
+
*/
|
|
18
|
+
function escapeHtml(valor) {
|
|
19
|
+
return String(valor)
|
|
20
|
+
.replace(/&/g, '&')
|
|
21
|
+
.replace(/</g, '<')
|
|
22
|
+
.replace(/>/g, '>')
|
|
23
|
+
.replace(/"/g, '"')
|
|
24
|
+
.replace(/'/g, ''');
|
|
25
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
1
|
export { encrypt, decrypt, type Llaves } from './crypto';
|
|
2
|
+
export * from './escape-html';
|
|
3
|
+
export * from './mongo-databases';
|
|
4
|
+
export * from './postgres-databases';
|
|
5
|
+
export * from './busqueda-literal';
|
|
6
|
+
export * from './escapar-regex';
|
|
7
|
+
export * from './presets-de-oauth/github-oauth-presets';
|
|
8
|
+
export * from './presets-de-oauth/linkedin-oauth-presets';
|
|
9
|
+
export * from './presets-de-oauth/mailchimp-oauth-presets';
|
|
10
|
+
export * from './presets-de-oauth/shopify-oauth-presets';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
2
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
17
|
exports.decrypt = exports.encrypt = void 0;
|
|
4
18
|
var crypto_1 = require("./crypto");
|
|
5
19
|
Object.defineProperty(exports, "encrypt", { enumerable: true, get: function () { return crypto_1.encrypt; } });
|
|
6
20
|
Object.defineProperty(exports, "decrypt", { enumerable: true, get: function () { return crypto_1.decrypt; } });
|
|
21
|
+
__exportStar(require("./escape-html"), exports);
|
|
22
|
+
__exportStar(require("./mongo-databases"), exports);
|
|
23
|
+
__exportStar(require("./postgres-databases"), exports);
|
|
24
|
+
__exportStar(require("./busqueda-literal"), exports);
|
|
25
|
+
__exportStar(require("./escapar-regex"), exports);
|
|
26
|
+
/* Los presets de OAuth: datos puros, cero dependencias externas (shopify usa el
|
|
27
|
+
`crypto` de Node, que es incorporado). Los comparten la api, `nodes/` y los
|
|
28
|
+
ayudantes de `common/` que hablan con cada plataforma. */
|
|
29
|
+
__exportStar(require("./presets-de-oauth/github-oauth-presets"), exports);
|
|
30
|
+
__exportStar(require("./presets-de-oauth/linkedin-oauth-presets"), exports);
|
|
31
|
+
__exportStar(require("./presets-de-oauth/mailchimp-oauth-presets"), exports);
|
|
32
|
+
__exportStar(require("./presets-de-oauth/shopify-oauth-presets"), exports);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Las bases que MongoDB trae siempre y que nadie elige como destino.
|
|
3
|
+
*
|
|
4
|
+
* Vive en `common` y no junto al nodo porque la usan los dos lados y en
|
|
5
|
+
* direcciones opuestas: el nodo de Mongo al listar las bases del servidor, y
|
|
6
|
+
* `CredentialsService` al comprobar que la base tecleada en el formulario
|
|
7
|
+
* existe. Importarla desde `nodes/` en `credentials/` invertiría la
|
|
8
|
+
* dependencia (los nodos importan credenciales, no al revés).
|
|
9
|
+
*/
|
|
10
|
+
export declare const BASES_INTERNAS: ReadonlySet<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Por qué rechazar la base que declara una credencial de Mongo, o `null` si
|
|
13
|
+
* no hay motivo.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ La distinción que lo es todo: `disponibles === undefined` significa **no
|
|
16
|
+
* se pudo mirar** (credencial acotada a una base, servidor caído), y eso
|
|
17
|
+
* **nunca** bloquea. Una lista vacía sí es una respuesta. Tratar los dos
|
|
18
|
+
* casos igual impediría guardar credenciales perfectamente buenas.
|
|
19
|
+
*
|
|
20
|
+
* Existe porque `client.db("no_existe")` no falla en Mongo —las bases se
|
|
21
|
+
* crean al escribir— así que un nombre mal tecleado se guarda tan campante y
|
|
22
|
+
* después toda lectura devuelve cero, sin error. Medido el 2026-08-12: una
|
|
23
|
+
* credencial decía `my_hostwebhook_testing_database` y el servidor sólo tenía
|
|
24
|
+
* `test`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function motivoDeBaseInvalida(pedida: string | null | undefined, disponibles: string[] | undefined): string | null;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BASES_INTERNAS = void 0;
|
|
4
|
+
exports.motivoDeBaseInvalida = motivoDeBaseInvalida;
|
|
5
|
+
/**
|
|
6
|
+
* Las bases que MongoDB trae siempre y que nadie elige como destino.
|
|
7
|
+
*
|
|
8
|
+
* Vive en `common` y no junto al nodo porque la usan los dos lados y en
|
|
9
|
+
* direcciones opuestas: el nodo de Mongo al listar las bases del servidor, y
|
|
10
|
+
* `CredentialsService` al comprobar que la base tecleada en el formulario
|
|
11
|
+
* existe. Importarla desde `nodes/` en `credentials/` invertiría la
|
|
12
|
+
* dependencia (los nodos importan credenciales, no al revés).
|
|
13
|
+
*/
|
|
14
|
+
exports.BASES_INTERNAS = new Set([
|
|
15
|
+
'admin',
|
|
16
|
+
'local',
|
|
17
|
+
'config',
|
|
18
|
+
]);
|
|
19
|
+
/**
|
|
20
|
+
* Por qué rechazar la base que declara una credencial de Mongo, o `null` si
|
|
21
|
+
* no hay motivo.
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ La distinción que lo es todo: `disponibles === undefined` significa **no
|
|
24
|
+
* se pudo mirar** (credencial acotada a una base, servidor caído), y eso
|
|
25
|
+
* **nunca** bloquea. Una lista vacía sí es una respuesta. Tratar los dos
|
|
26
|
+
* casos igual impediría guardar credenciales perfectamente buenas.
|
|
27
|
+
*
|
|
28
|
+
* Existe porque `client.db("no_existe")` no falla en Mongo —las bases se
|
|
29
|
+
* crean al escribir— así que un nombre mal tecleado se guarda tan campante y
|
|
30
|
+
* después toda lectura devuelve cero, sin error. Medido el 2026-08-12: una
|
|
31
|
+
* credencial decía `my_hostwebhook_testing_database` y el servidor sólo tenía
|
|
32
|
+
* `test`.
|
|
33
|
+
*/
|
|
34
|
+
function motivoDeBaseInvalida(pedida, disponibles) {
|
|
35
|
+
const nombre = (pedida ?? '').trim();
|
|
36
|
+
if (!nombre)
|
|
37
|
+
return null;
|
|
38
|
+
if (!disponibles)
|
|
39
|
+
return null;
|
|
40
|
+
if (disponibles.includes(nombre))
|
|
41
|
+
return null;
|
|
42
|
+
return disponibles.length > 0
|
|
43
|
+
? `Database "${nombre}" doesn't exist on this server. Available: ${disponibles.join(', ')}`
|
|
44
|
+
: `Database "${nombre}" doesn't exist on this server, and the server has no databases yet`;
|
|
45
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qué bases ofrece el selector del formulario de una credencial de Postgres.
|
|
3
|
+
*
|
|
4
|
+
* Aparte del servicio, como `mongo-databases.ts`, para que la consulta y el
|
|
5
|
+
* filtrado se puedan probar sin levantar un Postgres.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Las bases a las que un cliente se puede conectar de verdad.
|
|
9
|
+
*
|
|
10
|
+
* Los dos filtros están por un motivo concreto, y quitarlos no rompe nada
|
|
11
|
+
* hasta que alguien elige la opción equivocada:
|
|
12
|
+
*
|
|
13
|
+
* - `datistemplate = false` deja fuera `template0` y `template1`, que son
|
|
14
|
+
* plantillas y no destinos.
|
|
15
|
+
* - `datallowconn = true` es el que de verdad importa: **`template0` rechaza
|
|
16
|
+
* conexiones por diseño**. Ofrecerla es ofrecer una opción que sólo puede
|
|
17
|
+
* fallar, y el error (`FATAL: database "template0" is not currently
|
|
18
|
+
* accepting connections`) no se parece en nada a «esa no la elijas».
|
|
19
|
+
*/
|
|
20
|
+
export declare const SQL_BASES_DE_POSTGRES = "SELECT datname FROM pg_database\n WHERE datistemplate = false AND datallowconn = true\n ORDER BY datname";
|
|
21
|
+
/**
|
|
22
|
+
* Los nombres, de lo que devuelva el driver.
|
|
23
|
+
*
|
|
24
|
+
* Se valida el tipo en vez de castear: `pg` tipa las filas por lo que le
|
|
25
|
+
* digas, no por lo que llegue, así que una fila rara entraría como `undefined`
|
|
26
|
+
* y pintaría una opción vacía en el desplegable.
|
|
27
|
+
*/
|
|
28
|
+
export declare function nombresDeBases(filas: ReadonlyArray<{
|
|
29
|
+
datname?: unknown;
|
|
30
|
+
}>): string[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Qué bases ofrece el selector del formulario de una credencial de Postgres.
|
|
4
|
+
*
|
|
5
|
+
* Aparte del servicio, como `mongo-databases.ts`, para que la consulta y el
|
|
6
|
+
* filtrado se puedan probar sin levantar un Postgres.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.SQL_BASES_DE_POSTGRES = void 0;
|
|
10
|
+
exports.nombresDeBases = nombresDeBases;
|
|
11
|
+
/**
|
|
12
|
+
* Las bases a las que un cliente se puede conectar de verdad.
|
|
13
|
+
*
|
|
14
|
+
* Los dos filtros están por un motivo concreto, y quitarlos no rompe nada
|
|
15
|
+
* hasta que alguien elige la opción equivocada:
|
|
16
|
+
*
|
|
17
|
+
* - `datistemplate = false` deja fuera `template0` y `template1`, que son
|
|
18
|
+
* plantillas y no destinos.
|
|
19
|
+
* - `datallowconn = true` es el que de verdad importa: **`template0` rechaza
|
|
20
|
+
* conexiones por diseño**. Ofrecerla es ofrecer una opción que sólo puede
|
|
21
|
+
* fallar, y el error (`FATAL: database "template0" is not currently
|
|
22
|
+
* accepting connections`) no se parece en nada a «esa no la elijas».
|
|
23
|
+
*/
|
|
24
|
+
exports.SQL_BASES_DE_POSTGRES = `SELECT datname FROM pg_database
|
|
25
|
+
WHERE datistemplate = false AND datallowconn = true
|
|
26
|
+
ORDER BY datname`;
|
|
27
|
+
/**
|
|
28
|
+
* Los nombres, de lo que devuelva el driver.
|
|
29
|
+
*
|
|
30
|
+
* Se valida el tipo en vez de castear: `pg` tipa las filas por lo que le
|
|
31
|
+
* digas, no por lo que llegue, así que una fila rara entraría como `undefined`
|
|
32
|
+
* y pintaría una opción vacía en el desplegable.
|
|
33
|
+
*/
|
|
34
|
+
function nombresDeBases(filas) {
|
|
35
|
+
return filas
|
|
36
|
+
.map((f) => f.datname)
|
|
37
|
+
.filter((n) => typeof n === 'string' && n.trim() !== '');
|
|
38
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constantes y formas del OAuth de HostWebhook contra GitHub.
|
|
3
|
+
*
|
|
4
|
+
* ── Qué se guarda, y por qué es distinto a los demás proveedores ──
|
|
5
|
+
*
|
|
6
|
+
* Casi todos los proveedores de esta carpeta guardan un `access_token` del
|
|
7
|
+
* usuario y lo refrescan. Aquí NO. Lo que se guarda es un `installationId`, y
|
|
8
|
+
* **ningún secreto**: cada llamada mina un token de instalación firmando un
|
|
9
|
+
* JWT con la private key de la app, que vive en el entorno.
|
|
10
|
+
*
|
|
11
|
+
* La razón no es estética. Un token de usuario muere cuando esa persona deja
|
|
12
|
+
* la organización, y una automatización que lleva meses corriendo se cae con
|
|
13
|
+
* ella. El token de instalación pertenece a la instalación, no a quien la hizo.
|
|
14
|
+
*
|
|
15
|
+
* Consecuencia práctica: aquí no hay nada que caduque en la base de datos, y
|
|
16
|
+
* el flujo de OAuth existe para dos cosas —comprobar que el usuario autorizó y
|
|
17
|
+
* quedarse con el `installation_id` que vuelve en el callback—, no para
|
|
18
|
+
* quedarse con su token.
|
|
19
|
+
*
|
|
20
|
+
* ── Contra qué docs está escrito (verificado 2026-08-25) ──
|
|
21
|
+
*
|
|
22
|
+
* - JWT: RS256; `iss` es el App ID (los docs admiten también el client id);
|
|
23
|
+
* `iat` **60 segundos en el pasado** por deriva de reloj; `exp` como mucho
|
|
24
|
+
* 10 minutos en el futuro.
|
|
25
|
+
* - Token de instalación: `POST /app/installations/{id}/access_tokens` con
|
|
26
|
+
* el JWT en `Authorization: Bearer`. Dura **1 hora**.
|
|
27
|
+
* - Token de usuario: 8 h (28800 s), refresh de 6 meses. No se usa más allá
|
|
28
|
+
* del intercambio inicial — ver arriba.
|
|
29
|
+
*
|
|
30
|
+
* ── ⚠️ Las variables de entorno ──
|
|
31
|
+
*
|
|
32
|
+
* Son `GITHUB_APP_*`. `GITHUB_OAUTH_CLIENT_ID` y `GITHUB_OAUTH_CLIENT_SECRET`
|
|
33
|
+
* ya existían y son de OTRA app: la OAuth App que usa `MCP_OAUTH_PRESETS.github`
|
|
34
|
+
* para llegar al servidor MCP de GitHub. Reusar ese par rompería las dos cosas.
|
|
35
|
+
*/
|
|
36
|
+
export declare const GITHUB_APP_OAUTH_CONFIG: {
|
|
37
|
+
readonly authorizationEndpoint: "https://github.com/login/oauth/authorize";
|
|
38
|
+
readonly tokenEndpoint: "https://github.com/login/oauth/access_token";
|
|
39
|
+
readonly apiBase: "https://api.github.com";
|
|
40
|
+
/** La versión de la API REST que fijan todas las llamadas. Fijarla es lo que
|
|
41
|
+
* evita que un cambio de GitHub llegue sin avisar. */
|
|
42
|
+
readonly apiVersion: "2026-03-10";
|
|
43
|
+
/** Nombres de las variables con las credenciales registradas de HW. */
|
|
44
|
+
readonly clientIdEnv: "GITHUB_APP_CLIENT_ID";
|
|
45
|
+
readonly clientSecretEnv: "GITHUB_APP_CLIENT_SECRET";
|
|
46
|
+
readonly appIdEnv: "GITHUB_APP_ID";
|
|
47
|
+
/** La private key va en base64: un PEM en crudo dentro de una variable de
|
|
48
|
+
* entorno se parte por los saltos de línea y sólo se descubre en runtime. */
|
|
49
|
+
readonly privateKeyBase64Env: "GITHUB_APP_PRIVATE_KEY_BASE64";
|
|
50
|
+
/**
|
|
51
|
+
* El secreto con el que GitHub firma cada entrega del webhook.
|
|
52
|
+
*
|
|
53
|
+
* ⚠️ Es el mismo valor que hay escrito en **Webhook → Secret** del registro
|
|
54
|
+
* de la app en github.com. Si los dos no dicen exactamente lo mismo, TODAS
|
|
55
|
+
* las entregas se rechazan con 401 y en GitHub se ven en rojo — el síntoma
|
|
56
|
+
* es un trigger activo que no dispara nunca.
|
|
57
|
+
*
|
|
58
|
+
* Sin la variable no se deja pasar nada: un despliegue al que le falte no
|
|
59
|
+
* debe convertirse en un endpoint abierto por el que cualquiera pueda
|
|
60
|
+
* inyectar eventos. Ver `verificarFirmaGithub`.
|
|
61
|
+
*/
|
|
62
|
+
readonly webhookSecretEnv: "GITHUB_APP_WEBHOOK_SECRET";
|
|
63
|
+
/** El JWT no puede pedir más de 10 minutos. Se usan 9 para dejar margen. */
|
|
64
|
+
readonly jwtLifetimeSeconds: number;
|
|
65
|
+
/** Los docs piden retrasar `iat` para absorber la deriva de reloj. */
|
|
66
|
+
readonly jwtClockSkewSeconds: 60;
|
|
67
|
+
/** Un token de instalación dura 1 h; se re-mina un minuto antes. */
|
|
68
|
+
readonly installationTokenTtlSeconds: number;
|
|
69
|
+
readonly installationTokenSafetySeconds: 60;
|
|
70
|
+
};
|
|
71
|
+
/** El `type` de la credencial que crea este flujo. */
|
|
72
|
+
export declare const GITHUB_APP_CREDENTIAL_TYPE = "github_app";
|
|
73
|
+
/** El `type` de la credencial de token pegado. Mismas operaciones, otra vía. */
|
|
74
|
+
export declare const GITHUB_PAT_CREDENTIAL_TYPE = "github_pat";
|
|
75
|
+
/**
|
|
76
|
+
* Lo que devuelve `POST /login/oauth/access_token`.
|
|
77
|
+
*
|
|
78
|
+
* Todo opcional a propósito: GitHub responde `200` con un cuerpo de error
|
|
79
|
+
* cuando el `code` ya se usó, así que tratar `access_token` como obligatorio
|
|
80
|
+
* convierte un error explicable en un fallo de parseo.
|
|
81
|
+
*/
|
|
82
|
+
export interface GithubTokenResponse {
|
|
83
|
+
access_token?: string;
|
|
84
|
+
token_type?: string;
|
|
85
|
+
scope?: string;
|
|
86
|
+
expires_in?: number;
|
|
87
|
+
refresh_token?: string;
|
|
88
|
+
refresh_token_expires_in?: number;
|
|
89
|
+
error?: string;
|
|
90
|
+
error_description?: string;
|
|
91
|
+
}
|
|
92
|
+
/** Lo que devuelve `POST /app/installations/{id}/access_tokens`. */
|
|
93
|
+
export interface GithubInstallationTokenResponse {
|
|
94
|
+
token?: string;
|
|
95
|
+
/** ISO-8601. Una hora después de emitirlo. */
|
|
96
|
+
expires_at?: string;
|
|
97
|
+
permissions?: Record<string, string>;
|
|
98
|
+
repository_selection?: 'all' | 'selected';
|
|
99
|
+
message?: string;
|
|
100
|
+
}
|
|
101
|
+
/** La instalación, tal como la devuelve `GET /app/installations/{id}`. */
|
|
102
|
+
export interface GithubInstallation {
|
|
103
|
+
id?: number;
|
|
104
|
+
account?: {
|
|
105
|
+
login?: string;
|
|
106
|
+
type?: string;
|
|
107
|
+
avatar_url?: string;
|
|
108
|
+
};
|
|
109
|
+
repository_selection?: 'all' | 'selected';
|
|
110
|
+
app_id?: number;
|
|
111
|
+
target_type?: string;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* El blob cifrado de una credencial `github_app`.
|
|
115
|
+
*
|
|
116
|
+
* No lleva token. Es deliberado — ver la cabecera de este fichero.
|
|
117
|
+
*/
|
|
118
|
+
export interface GithubAppStoredPayload {
|
|
119
|
+
/** La instalación. Es lo único que hace falta para minar tokens. */
|
|
120
|
+
installationId: string;
|
|
121
|
+
/** La cuenta donde se instaló, para poder nombrar la credencial y para que
|
|
122
|
+
* el usuario distinga dos instalaciones en la lista. */
|
|
123
|
+
accountLogin?: string;
|
|
124
|
+
accountType?: string;
|
|
125
|
+
/** `all` o `selected`. Cambia el texto que se le enseña al usuario cuando el
|
|
126
|
+
* selector de repos sale corto. */
|
|
127
|
+
repositorySelection?: 'all' | 'selected';
|
|
128
|
+
authMode: 'app';
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* El blob cifrado de una credencial `github_pat`.
|
|
132
|
+
*
|
|
133
|
+
* Aquí SÍ hay un secreto del usuario, y además caduca: GitHub obliga a poner
|
|
134
|
+
* fecha a los fine-grained. Guardar la fecha permite avisar antes de que una
|
|
135
|
+
* corrida se caiga por una credencial muerta.
|
|
136
|
+
*/
|
|
137
|
+
export interface GithubPatStoredPayload {
|
|
138
|
+
token: string;
|
|
139
|
+
/** ISO-8601, cuando GitHub la ha dicho. */
|
|
140
|
+
expiresAt?: string | null;
|
|
141
|
+
authMode: 'pat';
|
|
142
|
+
}
|
|
143
|
+
export interface PendingGithubOAuthState {
|
|
144
|
+
orgId: string;
|
|
145
|
+
userId: string;
|
|
146
|
+
credentialName: string;
|
|
147
|
+
/** En reconexión, para actualizar la credencial en vez de crear otra. */
|
|
148
|
+
credentialId?: string;
|
|
149
|
+
tags?: string[];
|
|
150
|
+
/** La carpeta elegida en el formulario. Viaja por Redis entre `start` y el
|
|
151
|
+
* callback; sin esto la elección se perdía por el camino. */
|
|
152
|
+
folderId?: string;
|
|
153
|
+
clientId: string;
|
|
154
|
+
clientSecret: string;
|
|
155
|
+
/** Marca absoluta en ms de cuándo caduca este estado pendiente. */
|
|
156
|
+
expiresAt: number;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* A dónde se manda al usuario: a **INSTALAR**, no a autorizar.
|
|
160
|
+
*
|
|
161
|
+
* En GitHub son dos gestos distintos y sólo uno sirve para lo que hace este
|
|
162
|
+
* nodo. `/login/oauth/authorize` le da a HostWebhook permiso para actuar en su
|
|
163
|
+
* nombre; **no instala nada**, así que después no hay ninguna instalación de la
|
|
164
|
+
* que sacar un token y la conexión muere con "no lo has instalado en ninguna
|
|
165
|
+
* cuenta". Pasó en producción, con el mensaje correcto y la puerta equivocada.
|
|
166
|
+
*
|
|
167
|
+
* `/apps/{slug}/installations/new` es la buena: lleva por elegir cuenta y
|
|
168
|
+
* elegir repositorios, que es justo la promesa que le hacemos al usuario en el
|
|
169
|
+
* texto de al lado. Y con "Request user authorization (OAuth) during
|
|
170
|
+
* installation" encendido en el registro de la app, al terminar vuelve al
|
|
171
|
+
* Callback URL con el `code` — así que el resto del flujo no cambia.
|
|
172
|
+
*
|
|
173
|
+
* `state` viaja igual y vuelve igual: los docs lo contemplan explícitamente
|
|
174
|
+
* para esta URL, que es lo que permite seguir atando la vuelta con lo que
|
|
175
|
+
* guardamos en Redis.
|
|
176
|
+
*
|
|
177
|
+
* Sigue sin llevar `scope`, por lo de siempre: los permisos de una GitHub App
|
|
178
|
+
* se declaran al registrarla, y lo que el usuario elige son repos.
|
|
179
|
+
*/
|
|
180
|
+
export declare function buildGithubInstallUrl(params: {
|
|
181
|
+
appSlug: string;
|
|
182
|
+
state: string;
|
|
183
|
+
}): string;
|
|
184
|
+
/**
|
|
185
|
+
* El nombre que se ve en la lista de credenciales.
|
|
186
|
+
*
|
|
187
|
+
* Prefiere la cuenta donde se instaló, que es lo que de verdad alcanza el
|
|
188
|
+
* token y lo que necesita alguien que tenga dos instalaciones. El id suelto es
|
|
189
|
+
* el último recurso: es feo, pero es cierto y único, y gana a una fila vacía.
|
|
190
|
+
*/
|
|
191
|
+
export declare function githubDisplayName(inst: GithubInstallation): string;
|
|
192
|
+
/**
|
|
193
|
+
* Decodifica la private key desde la variable en base64.
|
|
194
|
+
*
|
|
195
|
+
* Devuelve `null` en vez de reventar cuando falta o viene mal, para que quien
|
|
196
|
+
* llama pueda dar un mensaje que diga QUÉ variable falta. Un stack trace de
|
|
197
|
+
* `crypto` no le sirve a nadie a las tres de la mañana.
|
|
198
|
+
*/
|
|
199
|
+
export declare function decodeGithubPrivateKey(raw: string | undefined): string | null;
|