@hostwebhook/platform-node 0.2.0 → 0.4.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/index.d.ts CHANGED
@@ -4,3 +4,10 @@ export * from './mongo-databases';
4
4
  export * from './postgres-databases';
5
5
  export * from './busqueda-literal';
6
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';
11
+ export * from './reference-id';
12
+ export * from './rate-limiter';
13
+ export * from './un-hueco-se-toma-de-una-vez';
package/dist/index.js CHANGED
@@ -23,3 +23,19 @@ __exportStar(require("./mongo-databases"), exports);
23
23
  __exportStar(require("./postgres-databases"), exports);
24
24
  __exportStar(require("./busqueda-literal"), exports);
25
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);
33
+ /* `reference-id` necesita `mongoose` de verdad (`Types.ObjectId` en ejecución):
34
+ es la ÚNICA dependencia real del paquete, y va como `peerDependency` porque
35
+ todo consumidor ya la tiene.
36
+
37
+ `rate-limiter` y su ayudante sólo usan `ioredis` como TIPO, así que no pesan
38
+ nada en ejecución — el `Redis` se lo pasa quien llama. */
39
+ __exportStar(require("./reference-id"), exports);
40
+ __exportStar(require("./rate-limiter"), exports);
41
+ __exportStar(require("./un-hueco-se-toma-de-una-vez"), exports);
@@ -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;
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ /**
3
+ * Constantes y formas del OAuth de HostWebhook contra GitHub.
4
+ *
5
+ * ── Qué se guarda, y por qué es distinto a los demás proveedores ──
6
+ *
7
+ * Casi todos los proveedores de esta carpeta guardan un `access_token` del
8
+ * usuario y lo refrescan. Aquí NO. Lo que se guarda es un `installationId`, y
9
+ * **ningún secreto**: cada llamada mina un token de instalación firmando un
10
+ * JWT con la private key de la app, que vive en el entorno.
11
+ *
12
+ * La razón no es estética. Un token de usuario muere cuando esa persona deja
13
+ * la organización, y una automatización que lleva meses corriendo se cae con
14
+ * ella. El token de instalación pertenece a la instalación, no a quien la hizo.
15
+ *
16
+ * Consecuencia práctica: aquí no hay nada que caduque en la base de datos, y
17
+ * el flujo de OAuth existe para dos cosas —comprobar que el usuario autorizó y
18
+ * quedarse con el `installation_id` que vuelve en el callback—, no para
19
+ * quedarse con su token.
20
+ *
21
+ * ── Contra qué docs está escrito (verificado 2026-08-25) ──
22
+ *
23
+ * - JWT: RS256; `iss` es el App ID (los docs admiten también el client id);
24
+ * `iat` **60 segundos en el pasado** por deriva de reloj; `exp` como mucho
25
+ * 10 minutos en el futuro.
26
+ * - Token de instalación: `POST /app/installations/{id}/access_tokens` con
27
+ * el JWT en `Authorization: Bearer`. Dura **1 hora**.
28
+ * - Token de usuario: 8 h (28800 s), refresh de 6 meses. No se usa más allá
29
+ * del intercambio inicial — ver arriba.
30
+ *
31
+ * ── ⚠️ Las variables de entorno ──
32
+ *
33
+ * Son `GITHUB_APP_*`. `GITHUB_OAUTH_CLIENT_ID` y `GITHUB_OAUTH_CLIENT_SECRET`
34
+ * ya existían y son de OTRA app: la OAuth App que usa `MCP_OAUTH_PRESETS.github`
35
+ * para llegar al servidor MCP de GitHub. Reusar ese par rompería las dos cosas.
36
+ */
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.GITHUB_PAT_CREDENTIAL_TYPE = exports.GITHUB_APP_CREDENTIAL_TYPE = exports.GITHUB_APP_OAUTH_CONFIG = void 0;
39
+ exports.buildGithubInstallUrl = buildGithubInstallUrl;
40
+ exports.githubDisplayName = githubDisplayName;
41
+ exports.decodeGithubPrivateKey = decodeGithubPrivateKey;
42
+ exports.GITHUB_APP_OAUTH_CONFIG = {
43
+ authorizationEndpoint: 'https://github.com/login/oauth/authorize',
44
+ tokenEndpoint: 'https://github.com/login/oauth/access_token',
45
+ apiBase: 'https://api.github.com',
46
+ /** La versión de la API REST que fijan todas las llamadas. Fijarla es lo que
47
+ * evita que un cambio de GitHub llegue sin avisar. */
48
+ apiVersion: '2026-03-10',
49
+ /** Nombres de las variables con las credenciales registradas de HW. */
50
+ clientIdEnv: 'GITHUB_APP_CLIENT_ID',
51
+ clientSecretEnv: 'GITHUB_APP_CLIENT_SECRET',
52
+ appIdEnv: 'GITHUB_APP_ID',
53
+ /** La private key va en base64: un PEM en crudo dentro de una variable de
54
+ * entorno se parte por los saltos de línea y sólo se descubre en runtime. */
55
+ privateKeyBase64Env: 'GITHUB_APP_PRIVATE_KEY_BASE64',
56
+ /**
57
+ * El secreto con el que GitHub firma cada entrega del webhook.
58
+ *
59
+ * ⚠️ Es el mismo valor que hay escrito en **Webhook → Secret** del registro
60
+ * de la app en github.com. Si los dos no dicen exactamente lo mismo, TODAS
61
+ * las entregas se rechazan con 401 y en GitHub se ven en rojo — el síntoma
62
+ * es un trigger activo que no dispara nunca.
63
+ *
64
+ * Sin la variable no se deja pasar nada: un despliegue al que le falte no
65
+ * debe convertirse en un endpoint abierto por el que cualquiera pueda
66
+ * inyectar eventos. Ver `verificarFirmaGithub`.
67
+ */
68
+ webhookSecretEnv: 'GITHUB_APP_WEBHOOK_SECRET',
69
+ /** El JWT no puede pedir más de 10 minutos. Se usan 9 para dejar margen. */
70
+ jwtLifetimeSeconds: 9 * 60,
71
+ /** Los docs piden retrasar `iat` para absorber la deriva de reloj. */
72
+ jwtClockSkewSeconds: 60,
73
+ /** Un token de instalación dura 1 h; se re-mina un minuto antes. */
74
+ installationTokenTtlSeconds: 60 * 60,
75
+ installationTokenSafetySeconds: 60,
76
+ };
77
+ /** El `type` de la credencial que crea este flujo. */
78
+ exports.GITHUB_APP_CREDENTIAL_TYPE = 'github_app';
79
+ /** El `type` de la credencial de token pegado. Mismas operaciones, otra vía. */
80
+ exports.GITHUB_PAT_CREDENTIAL_TYPE = 'github_pat';
81
+ /**
82
+ * A dónde se manda al usuario: a **INSTALAR**, no a autorizar.
83
+ *
84
+ * En GitHub son dos gestos distintos y sólo uno sirve para lo que hace este
85
+ * nodo. `/login/oauth/authorize` le da a HostWebhook permiso para actuar en su
86
+ * nombre; **no instala nada**, así que después no hay ninguna instalación de la
87
+ * que sacar un token y la conexión muere con "no lo has instalado en ninguna
88
+ * cuenta". Pasó en producción, con el mensaje correcto y la puerta equivocada.
89
+ *
90
+ * `/apps/{slug}/installations/new` es la buena: lleva por elegir cuenta y
91
+ * elegir repositorios, que es justo la promesa que le hacemos al usuario en el
92
+ * texto de al lado. Y con "Request user authorization (OAuth) during
93
+ * installation" encendido en el registro de la app, al terminar vuelve al
94
+ * Callback URL con el `code` — así que el resto del flujo no cambia.
95
+ *
96
+ * `state` viaja igual y vuelve igual: los docs lo contemplan explícitamente
97
+ * para esta URL, que es lo que permite seguir atando la vuelta con lo que
98
+ * guardamos en Redis.
99
+ *
100
+ * Sigue sin llevar `scope`, por lo de siempre: los permisos de una GitHub App
101
+ * se declaran al registrarla, y lo que el usuario elige son repos.
102
+ */
103
+ function buildGithubInstallUrl(params) {
104
+ const qs = new URLSearchParams({ state: params.state });
105
+ return `https://github.com/apps/${encodeURIComponent(params.appSlug)}/installations/new?${qs.toString()}`;
106
+ }
107
+ /**
108
+ * El nombre que se ve en la lista de credenciales.
109
+ *
110
+ * Prefiere la cuenta donde se instaló, que es lo que de verdad alcanza el
111
+ * token y lo que necesita alguien que tenga dos instalaciones. El id suelto es
112
+ * el último recurso: es feo, pero es cierto y único, y gana a una fila vacía.
113
+ */
114
+ function githubDisplayName(inst) {
115
+ const login = inst.account?.login?.trim();
116
+ if (login)
117
+ return `GitHub (${login})`;
118
+ return inst.id ? `GitHub (installation ${inst.id})` : 'GitHub';
119
+ }
120
+ /**
121
+ * Decodifica la private key desde la variable en base64.
122
+ *
123
+ * Devuelve `null` en vez de reventar cuando falta o viene mal, para que quien
124
+ * llama pueda dar un mensaje que diga QUÉ variable falta. Un stack trace de
125
+ * `crypto` no le sirve a nadie a las tres de la mañana.
126
+ */
127
+ function decodeGithubPrivateKey(raw) {
128
+ if (!raw || !raw.trim())
129
+ return null;
130
+ try {
131
+ const pem = Buffer.from(raw.trim(), 'base64').toString('utf8');
132
+ // Comprobar que de verdad salió un PEM y no cualquier cosa que decodifique.
133
+ // Sin esto, una variable con un valor viejo daría un error de firma
134
+ // ilegible varias capas más abajo.
135
+ return pem.includes('-----BEGIN') && pem.includes('PRIVATE KEY')
136
+ ? pem
137
+ : null;
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ }