@growconnect/growtickets-nest 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.
Files changed (146) hide show
  1. package/.env.example +8 -0
  2. package/README.md +319 -6
  3. package/dist/cli/args.d.ts +9 -1
  4. package/dist/cli/args.js +13 -4
  5. package/dist/cli/check.d.ts +4 -1
  6. package/dist/cli/check.js +47 -2
  7. package/dist/cli/env-example.js +17 -0
  8. package/dist/cli/env.d.ts +34 -0
  9. package/dist/cli/env.js +125 -0
  10. package/dist/cli/index.js +35 -1
  11. package/dist/cli/init.js +5 -1
  12. package/dist/client/ingest-client.js +6 -1
  13. package/dist/env/applier.d.ts +83 -0
  14. package/dist/env/applier.js +162 -0
  15. package/dist/env/errors.d.ts +12 -0
  16. package/dist/env/errors.js +18 -0
  17. package/dist/env/load.d.ts +35 -0
  18. package/dist/env/load.js +43 -0
  19. package/dist/env/overlay.d.ts +23 -0
  20. package/dist/env/overlay.js +48 -0
  21. package/dist/env/policy.d.ts +30 -0
  22. package/dist/env/policy.js +101 -0
  23. package/dist/env/poller.d.ts +27 -0
  24. package/dist/env/poller.js +54 -0
  25. package/dist/env/runtime.d.ts +122 -0
  26. package/dist/env/runtime.js +238 -0
  27. package/dist/env/settings.d.ts +26 -0
  28. package/dist/env/settings.js +41 -0
  29. package/dist/env/snapshot.d.ts +32 -0
  30. package/dist/env/snapshot.js +85 -0
  31. package/dist/env/state.d.ts +27 -0
  32. package/dist/env/state.js +51 -0
  33. package/dist/env/storage.d.ts +33 -0
  34. package/dist/env/storage.js +133 -0
  35. package/dist/env/transport/client.d.ts +58 -0
  36. package/dist/env/transport/client.js +249 -0
  37. package/dist/env/transport/config.d.ts +20 -0
  38. package/dist/env/transport/config.js +38 -0
  39. package/dist/env/transport/credential.d.ts +33 -0
  40. package/dist/env/transport/credential.js +76 -0
  41. package/dist/env/transport/gt2-transport.d.ts +53 -0
  42. package/dist/env/transport/gt2-transport.js +162 -0
  43. package/dist/env/transport/index.d.ts +10 -0
  44. package/dist/env/transport/index.js +17 -0
  45. package/dist/env/transport/keygen.d.ts +32 -0
  46. package/dist/env/transport/keygen.js +77 -0
  47. package/dist/env/transport/nonce-cache.d.ts +18 -0
  48. package/dist/env/transport/nonce-cache.js +37 -0
  49. package/dist/env/transport/protocol.d.ts +100 -0
  50. package/dist/env/transport/protocol.js +292 -0
  51. package/dist/env/transport/types.d.ts +78 -0
  52. package/dist/env/transport/types.js +2 -0
  53. package/dist/index.d.ts +14 -1
  54. package/dist/index.js +18 -1
  55. package/dist/logs/buffer.d.ts +25 -0
  56. package/dist/logs/buffer.js +58 -0
  57. package/dist/logs/doctor.d.ts +17 -0
  58. package/dist/logs/doctor.js +66 -0
  59. package/dist/logs/entry-builder.d.ts +20 -0
  60. package/dist/logs/entry-builder.js +127 -0
  61. package/dist/logs/flusher.d.ts +72 -0
  62. package/dist/logs/flusher.js +189 -0
  63. package/dist/logs/format.d.ts +4 -0
  64. package/dist/logs/format.js +76 -0
  65. package/dist/logs/levels.d.ts +12 -0
  66. package/dist/logs/levels.js +55 -0
  67. package/dist/logs/monitor.d.ts +92 -0
  68. package/dist/logs/monitor.js +224 -0
  69. package/dist/logs/normalize.d.ts +14 -0
  70. package/dist/logs/normalize.js +114 -0
  71. package/dist/logs/pino-line.d.ts +13 -0
  72. package/dist/logs/pino-line.js +52 -0
  73. package/dist/logs/redactor.d.ts +38 -0
  74. package/dist/logs/redactor.js +146 -0
  75. package/dist/logs/remote-config.d.ts +37 -0
  76. package/dist/logs/remote-config.js +89 -0
  77. package/dist/logs/runtime.d.ts +24 -0
  78. package/dist/logs/runtime.js +59 -0
  79. package/dist/logs/sampler.d.ts +15 -0
  80. package/dist/logs/sampler.js +36 -0
  81. package/dist/logs/sender.d.ts +20 -0
  82. package/dist/logs/sender.js +64 -0
  83. package/dist/logs/settings.d.ts +22 -0
  84. package/dist/logs/settings.js +30 -0
  85. package/dist/logs/types.d.ts +74 -0
  86. package/dist/logs/types.js +9 -0
  87. package/dist/logs-nest/api.d.ts +5 -0
  88. package/dist/logs-nest/api.js +13 -0
  89. package/dist/logs-nest/host.d.ts +10 -0
  90. package/dist/logs-nest/host.js +45 -0
  91. package/dist/logs-nest/lifecycle.d.ts +20 -0
  92. package/dist/logs-nest/lifecycle.js +40 -0
  93. package/dist/logs-nest/nest-args.d.ts +7 -0
  94. package/dist/logs-nest/nest-args.js +66 -0
  95. package/dist/logs-nest/service.d.ts +25 -0
  96. package/dist/logs-nest/service.js +61 -0
  97. package/dist/logs-nest/stream.d.ts +21 -0
  98. package/dist/logs-nest/stream.js +32 -0
  99. package/dist/maintenance/feed.d.ts +68 -0
  100. package/dist/maintenance/feed.js +160 -0
  101. package/dist/maintenance/http.d.ts +67 -0
  102. package/dist/maintenance/http.js +177 -0
  103. package/dist/maintenance/page.d.ts +11 -0
  104. package/dist/maintenance/page.js +208 -0
  105. package/dist/maintenance/settings.d.ts +24 -0
  106. package/dist/maintenance/settings.js +42 -0
  107. package/dist/maintenance/windows.d.ts +78 -0
  108. package/dist/maintenance/windows.js +167 -0
  109. package/dist/nest/csp-response.d.ts +19 -0
  110. package/dist/nest/csp-response.js +84 -0
  111. package/dist/nest/env-push.d.ts +35 -0
  112. package/dist/nest/env-push.js +110 -0
  113. package/dist/nest/growtickets.module.js +11 -3
  114. package/dist/nest/maintenance-request.d.ts +12 -0
  115. package/dist/nest/maintenance-request.js +30 -0
  116. package/dist/nest/maintenance.guard.d.ts +28 -0
  117. package/dist/nest/maintenance.guard.js +90 -0
  118. package/dist/nest/options.d.ts +15 -0
  119. package/dist/nest/request.d.ts +1 -1
  120. package/dist/nest/request.js +2 -2
  121. package/dist/nest/route-tag.d.ts +6 -0
  122. package/dist/nest/route-tag.js +13 -0
  123. package/dist/nest/security.middleware.d.ts +10 -4
  124. package/dist/nest/security.middleware.js +77 -18
  125. package/dist/nest/services.js +1 -1
  126. package/dist/proxy/handle-portal-request.js +2 -1
  127. package/dist/proxy/types.d.ts +1 -0
  128. package/dist/proxy/unread.js +1 -1
  129. package/dist/security/client-ip.d.ts +15 -1
  130. package/dist/security/client-ip.js +20 -2
  131. package/dist/security/csp.d.ts +58 -0
  132. package/dist/security/csp.js +109 -0
  133. package/dist/security/flusher.js +4 -2
  134. package/dist/security/proxy-chain.d.ts +35 -0
  135. package/dist/security/proxy-chain.js +58 -0
  136. package/dist/security/remote-config.d.ts +4 -0
  137. package/dist/security/remote-config.js +7 -0
  138. package/dist/security/request-ip.d.ts +3 -2
  139. package/dist/security/request-ip.js +6 -3
  140. package/dist/security/runtime.d.ts +8 -0
  141. package/dist/security/runtime.js +17 -0
  142. package/dist/security/sdk.d.ts +7 -1
  143. package/dist/security/sdk.js +27 -1
  144. package/dist/security/settings.d.ts +3 -1
  145. package/dist/security/settings.js +1 -0
  146. package/package.json +1 -1
package/.env.example CHANGED
@@ -1,2 +1,10 @@
1
1
  GROWTICKETS_URL=
2
2
  GROWTICKETS_API_KEY=
3
+ GROWTICKETS_TRUSTED_PROXY_HOPS=1
4
+ GROWTICKETS_TRUST_REAL_IP=false
5
+ GROWTICKETS_ENV_KEY=
6
+ GROWTICKETS_ENV_PATH=growtickets/env
7
+ GROWTICKETS_ENV_ON_FAILURE=keep
8
+ GROWTICKETS_ENV_MAX_STALE_HOURS=168
9
+ GROWTICKETS_ENV_ON_CHANGE=restart
10
+ GROWTICKETS_ENV_EXIT_CODE=75
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @growconnect/growtickets-nest
2
2
 
3
- GrowTickets para **NestJS** (10, 11 y 12, con **Express o Fastify**), en dos mitades que no se necesitan entre sí:
3
+ GrowTickets para **NestJS** (10, 11 y 12, con **Express o Fastify**), en tres mitades que no se necesitan entre sí:
4
4
 
5
5
  - **El proxy del portal**: guarda `GROWTICKETS_API_KEY`, firma quién reporta con la sesión del producto y reenvía a
6
6
  `api/ingest` por una lista blanca de 9 rutas. Es el mismo contrato que `growtickets-laravel` y
@@ -9,6 +9,10 @@ GrowTickets para **NestJS** (10, 11 y 12, con **Express o Fastify**), en dos mit
9
9
  - **La seguridad del producto** (opcional, apagada por defecto): manda a la mesa de ayuda lo que ve pasar, **corta a
10
10
  las IP que ella bloquee**, contesta la ruta de salud con la que sabe si el producto se cayó y reenvía la lista de
11
11
  paquetes de tu front. Ver [Seguridad y disponibilidad](#seguridad-y-disponibilidad).
12
+ - **Los logs** (0.2.0, opcional): lo que escribe el logger de Nest se manda además a GrowTickets, con los secretos tapados, para buscarlo
13
+ y agrupar lo repetido. Ver [Logs](#logs-020).
14
+ - **Las variables de entorno desde GrowTickets** (0.3.0, opcional): el servidor trae sus variables, cifradas, de la pestaña «Entornos» del
15
+ proyecto, y «Vincular» las aplica en segundos. Ver [Variables de entorno desde GrowTickets](#variables-de-entorno-desde-growtickets-030).
12
16
 
13
17
  No trae pantallas: Nest es tu backend y el portal lo pinta tu front.
14
18
 
@@ -182,9 +186,11 @@ ficha del proyecto.
182
186
  | `GROWTICKETS_SECURITY_BLOCKING` | `true` | `false` sigue capturando pero nunca corta una petición. |
183
187
  | `GROWTICKETS_SECURITY_HEALTH_PATH` | `growtickets/health` | Dónde contesta la ruta de salud (sin barras en los extremos, sin el prefijo global). `off` la apaga. |
184
188
  | `GROWTICKETS_TRUSTED_PROXY_HOPS` | `1` | Cuántos proxies de confianza hay delante (1 = Railway; con Cloudflare delante de Railway, 2). `0` = sin proxy. |
189
+ | `GROWTICKETS_TRUST_REAL_IP` | `false` | Lee `X-Real-IP` cuando `X-Forwarded-For` no da una IP, para un proxy que sólo escribe ésa. Apagado, esa cabecera la puede mandar cualquiera. |
185
190
  | `GROWTICKETS_FRONT_DEPS_TOKEN` | vacío | El token de build del front (ver [Dependencias de un front](#dependencias-de-un-front-que-vive-en-otro-repo)). Vacío = el relevo no existe. |
186
191
  | `GROWTICKETS_FRONT_DEPS_PATH` | `growtickets/front-deps` | Dónde recibe el relevo. `off` lo quita. |
187
192
  | `GROWTICKETS_SECURITY_DEPENDENCIES` | `true` | `false` apaga el relevo aunque tenga token. |
193
+ | `GROWTICKETS_SECURITY_CSP` | `true` | `false` quita la política «sólo informar» de GrowTickets (la de tu `csp` de `forRoot` se queda). Ver [Política de contenido](#política-de-contenido-sólo-informar-030). |
188
194
 
189
195
  Una variable mal escrita cae a su valor por defecto: la seguridad nunca lanza ni tumba una petición. Se leen al
190
196
  arrancar el proceso y en cada petición, no al compilar.
@@ -198,7 +204,9 @@ Corre **antes** que el router, los guards y el parser de cuerpo, en Express y en
198
204
  No revela versiones ni tablas.
199
205
  2. **Una IP bloqueada** recibe 403 (`{ "message": "Acceso bloqueado." }`, o texto si pide HTML) y no llega a tu app.
200
206
  3. **El relevo del front** (`POST /growtickets/front-deps`) se contesta ahí con el token de build.
201
- 4. **Todo lo demás sigue**, y al terminar la respuesta se cuenta con su código real.
207
+ 4. **El mantenimiento**: la ruta del front se contesta ahí, y con una ventana de GrowTickets en curso que toca a este
208
+ servidor se responde 503. Ver [Mantenimiento](#mantenimiento-ventanas-programadas-desde-growtickets-020).
209
+ 5. **Todo lo demás sigue**, y al terminar la respuesta se cuenta con su código real.
202
210
 
203
211
  Lo que se cuenta: el **tráfico** por minuto y por PLANTILLA de ruta (`GET /api/orders/[id]`, nunca la URL ni la query),
204
212
  con su reparto de códigos y su latencia; **`access.unauthorized` / `forbidden` / `rate_limited`** para 401, 403 y 429,
@@ -265,6 +273,35 @@ security: { checks: { database: () => dataSource.query('select 1') } } // con
265
273
 
266
274
  `false` o una excepción son un 503 que la mesa de ayuda cuenta como caída. Sin `checks` no se mira ninguna base.
267
275
 
276
+ ### Política de contenido «sólo informar» (0.3.0)
277
+
278
+ Si el proyecto enciende la política de contenido en GrowTickets (sección Seguridad), la configuración remota trae `csp: { policy, report_uri }` y el
279
+ middleware de `main.ts` la **suma** a lo que ya tenga la respuesta, en UNA `Content-Security-Policy-Report-Only` (el navegador evalúa e informa
280
+ cada política por su lado, así ninguna pierde), y fusiona `Reporting-Endpoints` con `growtickets-csp="…"`:
281
+
282
+ ```
283
+ Content-Security-Policy-Report-Only: <la tuya>, <la de GrowTickets>; report-uri <destino>; report-to growtickets-csp
284
+ Reporting-Endpoints: <los tuyos>, growtickets-csp="<destino>"
285
+ ```
286
+
287
+ - **Sólo páginas**: una respuesta 2xx de tipo HTML a un `GET`/`HEAD` que pide una página (`Sec-Fetch-Dest: document`, o que acepta
288
+ `text/html` si el navegador no lo manda). Ni un JSON, ni una redirección, ni un error, ni un `POST`. La suma se hace al escribir las cabeceras
289
+ (`writeHead`), así que ve lo que tu app —helmet, un interceptor, un controlador— haya puesto.
290
+ - **Nunca toca `Content-Security-Policy`**, la que bloquea. Una política idéntica no se repite; si un grupo de `Reporting-Endpoints` coincide, gana el tuyo.
291
+ - **Tu propia política** se declara en el `forRoot` (se aplica aunque la seguridad esté apagada):
292
+
293
+ ```ts
294
+ GrowTicketsModule.forRoot({
295
+ resolveReporter,
296
+ csp: { reportOnly: ["frame-ancestors 'none'"], reportingEndpoints: { 'mi-csp': '/informes' } },
297
+ });
298
+ ```
299
+
300
+ Una opción inválida (varias líneas, no ASCII, más de 4096 caracteres, un grupo con un destino raro) **lanza `ConfigurationError` al montar el módulo**
301
+ —no se descarta en silencio—, y el mensaje dice qué opción falló sin repetir su valor.
302
+ - **Lo que llega de la central se vuelve a validar**: una línea imprimible de hasta 4096 caracteres, sin comas (la coma separa políticas) y un
303
+ `report_uri` http(s); si no, se ignora. `GROWTICKETS_SECURITY_CSP=false` la quita sin tocar el código.
304
+
268
305
  ### Bloques de red
269
306
 
270
307
  Además de IP sueltas, GrowTickets puede cerrar un **país, una red (ASN) o las salidas de Tor**. Esos bloques son miles de
@@ -308,7 +345,8 @@ Las dependencias del **propio servidor Nest** se mandan desde CI con [`npx growt
308
345
  manda sus lotes, y la mesa de ayuda los suma.
309
346
  - **La IP** sale de `X-Forwarded-For` contando `GROWTICKETS_TRUSTED_PROXY_HOPS` desde la DERECHA: lo que escribe el cliente
310
347
  va a la izquierda y no cuenta. No depende del `trust proxy` de Express. Con un número equivocado todas las peticiones
311
- parecen de la misma IP privada; la librería lo detecta y avisa a la mesa de ayuda (`no_trusted_proxies`).
348
+ parecen de la misma IP privada; la librería lo detecta y avisa a la mesa de ayuda (`no_trusted_proxies`). Sin proxy delante:
349
+ `GROWTICKETS_TRUSTED_PROXY_HOPS=0`; `X-Real-IP` sólo con `GROWTICKETS_TRUST_REAL_IP=true`.
312
350
  - **La dirección pública del producto** se aprende de las respuestas 2xx (con `X-Forwarded-Host`/`-Proto` detrás de un
313
351
  proxy) y viaja con cada lote: de ahí sale la sugerencia de dominio en «Disponibilidad».
314
352
  - **Fastify** contesta 415 a un `POST` sin `Content-Type` antes de que llegue al proxy (el contrato pide 403). Sigue
@@ -316,6 +354,246 @@ Las dependencias del **propio servidor Nest** se mandan desde CI con [`npx growt
316
354
  - **Sin política de contenido (CSP)** en esta versión: Nest sirve JSON y el HTML lo sirve tu front. **Sin integridad de
317
355
  archivos**: los despliegues de un producto Node son inmutables, y comparar archivos no aportaría nada.
318
356
 
357
+ ## Logs (0.2.0)
358
+
359
+ Lo que escribe el logger de Nest llega a GrowTickets, para buscarlo, agrupar lo repetido y ver un archivo por servidor. **No cambia lo que ya
360
+ sale por la consola**: es el `ConsoleLogger` de Nest —mismo formato, mismos colores— al que se le añade el envío.
361
+
362
+ ```ts
363
+ import { GrowTicketsLogService } from '@growconnect/growtickets-nest';
364
+
365
+ const app = await NestFactory.create(AppModule, { logger: new GrowTicketsLogService() });
366
+ app.enableShutdownHooks();
367
+ ```
368
+
369
+ Y la clave de la pestaña «Claves de API» del proyecto (la Principal ya lleva el alcance «Logs»; si usas otra, márcalo allí), con el módulo «Logs»
370
+ encendido en «General». Para que al cerrar la app se mande lo último, importa el módulo de los logs (o el `GrowTicketsModule`, que ya lo trae):
371
+
372
+ ```ts
373
+ @Module({ imports: [GrowTicketsLogsModule] })
374
+ export class AppModule {}
375
+ ```
376
+
377
+ - **Cada línea** llega con su nivel (`log`→`info`, `warn`→`warning`, `verbose`→`debug`, `fatal`→`critical`) y su **contexto como canal**
378
+ (`PagosService`, `NestFactory`, `ExceptionsHandler`).
379
+ - **El error**: un `Error` entre los argumentos, o la pila que Nest recibe aparte (`logger.error('Falló', err.stack, 'PagosService')`), es la
380
+ excepción de la línea, con su clase, el archivo y la línea de donde salió y su traza (rutas relativas a tu carpeta).
381
+ - **Lo que Nest escribe al arrancar** (`NestFactory`, `InstanceLoader`, `RoutesResolver`) también llega; `GROWTICKETS_LOGS_LEVEL` sube el mínimo.
382
+ - **Con `nestjs-pino`** usa la salida de pino: `LoggerModule.forRoot({ pinoHttp: { stream: growTicketsLogStream() } })`.
383
+
384
+ ### Lo que se tapa antes de salir
385
+
386
+ - Por **nombre de campo**: `password`, `token`, `secret`, `api_key`, `authorization`, `cookie`… y las palabras que añadas en
387
+ `GROWTICKETS_LOGS_REDACT_KEYS` (`cpf,documento`).
388
+ - Por **forma**: `Bearer …`, un JWT, una clave de GrowTickets o de AWS, credenciales en una URL, una clave privada y los pares `clave=valor`.
389
+ - Por **valor**: los secretos que tu entorno tiene de verdad (`DATABASE_URL`, `STRIPE_SECRET_KEY`, cualquier `*_TOKEN`, `*_KEY`, `*_DSN`) se
390
+ tapan dondequiera que aparezcan.
391
+ - **Nunca se ejecuta código tuyo**: un objeto con `toJSON()` o con getters en el contexto sale como `[object Clase]`.
392
+
393
+ GrowTickets lo vuelve a tapar al recibirlo y puede además tachar los **datos personales** (correos, cédulas, celulares) con un interruptor del
394
+ proyecto («Tachar datos personales», en «General»).
395
+
396
+ ### Variables
397
+
398
+ | Variable | Qué hace |
399
+ |---|---|
400
+ | `GROWTICKETS_LOGS` | `false` apaga los logs sin tocar el código. |
401
+ | `GROWTICKETS_LOGS_LEVEL` | El nivel más bajo que se manda (`debug` de serie). |
402
+ | `GROWTICKETS_LOGS_ORIGIN` | La dirección pública del producto, si quieres fijarla. |
403
+ | `GROWTICKETS_LOGS_REDACT_KEYS` | Palabras que hacen que se tape el valor de un campo, separadas por comas. |
404
+
405
+ ### Cómo funciona
406
+
407
+ Capturar es síncrono, barato y **nunca lanza**: una línea que no se entiende, un error al tapar o la cola llena se pierden en silencio y
408
+ jamás rompen el log ni la petición. Cada minuto se manda lo acumulado, comprimido, en lotes de hasta 2.000 líneas; **una línea `warning` o
409
+ peor se manda a los cinco segundos**, para que un fallo no se lleve el último minuto. Un lote se reintenta con el mismo identificador, tras
410
+ tres fallos seguidos hay un corte de cinco minutos y lo que lleva más de una hora esperando se descarta y se cuenta como perdido (la central
411
+ lo sabe). Por encima de 5.000 líneas por minuto se muestrea lo leve con peso; lo grave se manda **siempre**.
412
+
413
+ Los archivos de log de un servidor Node **no se pueden gestionar desde GrowTickets** (escriben a la consola): se ven en el explorador, como
414
+ `project.log` y como un archivo por servidor («NestJS»).
415
+
416
+ ```bash
417
+ npx growtickets-nest doctor # entorno, clave, alcance «Logs» y módulo encendido
418
+ npx growtickets-nest doctor --send # además manda una línea de prueba y comprueba que llega
419
+ ```
420
+
421
+ ## Mantenimiento: ventanas programadas desde GrowTickets (0.2.0)
422
+
423
+ Una ventana se programa en la pestaña «Mantenimiento» del proyecto (a qué se aplica, desde cuándo, hasta cuándo, el mensaje y desde
424
+ cuándo se avisa) y el middleware de `main.ts` la cumple. **Viene encendido** y no hace falta tocar nada más: sin ventanas no se nota.
425
+
426
+ | Variable | Por defecto | Qué hace |
427
+ |---|---|---|
428
+ | `GROWTICKETS_MAINTENANCE` | `true` | `false` lo apaga: no pregunta, no cierra y la ruta del front no existe. |
429
+ | `GROWTICKETS_MAINTENANCE_HOST` | la aprendida | La dirección de ESTE servidor (`https://api.empresa.co`), para las ventanas «por dominios». Sin ella vale la que aprende la seguridad. |
430
+ | `GROWTICKETS_MAINTENANCE_PATH` | `api/growtickets/maintenance` | Por dónde pregunta el front (la ruta CRUDA, con el prefijo global si lo hay). |
431
+
432
+ - **Pregunta a GrowTickets cada minuto** (`GET api/ingest/maintenance`) y guarda la respuesta en memoria. Las peticiones nunca esperan esa
433
+ pregunta, salvo las primeras al arrancar (como mucho 2 s): es lo que hace que un servidor desplegado DURANTE la ventana salga ya cerrado.
434
+ Si GrowTickets no contesta sigue con lo último que supo; sin nada, atiende con normalidad. **Nunca tumba tu producto.**
435
+ - **Activa y apaga POR RELOJ** —el de GrowTickets, corregido—: la ventana empieza y termina a su hora aunque la central no conteste.
436
+ - **Mientras una ventana toque a este servidor responde 503** con `Retry-After`: JSON en las rutas `api/*` y en todo lo que no pida HTML
437
+ (`{ success: false, message, data: { maintenance: { title, message, starts_at, ends_at } } }`), y la página del proyecto a un navegador. Una
438
+ ventana por «partes» toca a este servidor si incluye el *backend*; por «dominios», si llegas por una de sus direcciones.
439
+ - **El pase del equipo** (se ve en la ventana) entra sin que se note: cabecera `X-GrowTickets-Bypass`, cookie, o `?growtickets_bypass=…` una vez,
440
+ que deja una cookie de 12 h.
441
+ - **Nunca se cierran** la ruta de salud, `growtickets/*` ni la del front.
442
+ - **Para un front** (`growtickets-ng` ≥ 0.7, `growtickets-next-react` ≥ 0.5, `growtickets_flutter` ≥ 0.5): pregunta a ESTE servidor en
443
+ `api/growtickets/maintenance` y recibe en `windows` las ventanas que le tocan y en `backend_windows` las que cierran este servidor (para
444
+ cerrar la sesión y decirlo en su login). El pase nunca sale hacia el navegador. La ruta trae su propio CORS (`*`, sin credenciales).
445
+ - ⚠️ **Con el front en otro dominio**, `app.enableCors()` va ANTES de `app.use(growTicketsMiddleware(app))`: así el 503 de una ventana lleva
446
+ las cabeceras de CORS y el front puede leer el motivo.
447
+
448
+ ### Zonas: a quién le afecta (0.2.0)
449
+
450
+ Una ventana puede aplicarse a **zonas** en lugar de a todo: el panel administrativo, el portal de clientes… Se crean en la pestaña «Mantenimiento» del
451
+ proyecto: cada una tiene una **clave** (`empresas`), las rutas de sus **pantallas** (`/enterprise`) y las de su **API** (`/api/empresas`). Una ventana de zonas
452
+ no silencia dominios: el producto sigue sirviendo a los demás, y la central no esconde una caída real.
453
+
454
+ - **Por ruta, sin tocar nada**: el middleware de `main.ts` cierra con 503 lo que cae en una zona (`/api/empresas/x`, `/enterprise/inicio`) y deja todo lo demás.
455
+ Las rutas casan por segmento (`/admin` casa `/admin/x`, no `/administrador`) y sin distinguir mayúsculas. Lo que no está en ninguna zona —el login, la
456
+ autenticación, el soporte— nunca se cierra por zona.
457
+ - **Por público, cuando la API es la misma para todos**: si el equipo y las empresas usan `/api/afiliados`, la ruta no distingue a nadie. Dile a la librería
458
+ de qué público es cada petición con tu sesión, devolviendo la clave de la zona, y pon el guard donde ya sabes quién es:
459
+
460
+ ```ts
461
+ GrowTicketsModule.forRoot<Req>({
462
+ resolveReporter,
463
+ maintenance: { audience: (req) => (req.user?.rol === 'empresa' ? 'empresas' : req.user ? 'panel' : null) },
464
+ });
465
+
466
+ @UseGuards(JwtAuthGuard, GrowTicketsMaintenanceGuard) // DESPUÉS del que deja a la persona en la petición
467
+ @Controller('afiliados')
468
+ export class AfiliadosController {}
469
+ ```
470
+
471
+ ⚠️ El guard **no** se registra solo como global: uno global corre antes que los tuyos, no vería a nadie y no cerraría nada en silencio. No se lo pongas al login,
472
+ al logout ni al soporte: lo que no lo lleva nunca se cierra por público. Sin resolutor, o si devuelve nada, deja pasar. Con él, la central sabe que este
473
+ servidor resuelve el público (si una zona depende de eso y no lo resuelve, la pantalla de la central lo avisa).
474
+ - **Para el front**: `zone_windows` en la ruta del front lleva las ventanas de zonas con sus pantallas (nunca la API); es el front quien decide si le tocan.
475
+
476
+ ### La versión de Node
477
+
478
+ Desde la 0.2.0 cada llamada a GrowTickets lleva `X-GrowTickets-Runtime: node/22.11.0 nestjs/11.0.5` (la versión COMPLETA: un aviso de Node se
479
+ decide por el parche). Es lo que deja a la central decir «tu Node está dentro del rango vulnerable» en la sección de seguridad, aunque la
480
+ seguridad esté apagada. Es información: no autoriza nada.
481
+
482
+ ## Variables de entorno desde GrowTickets (0.3.0)
483
+
484
+ Las variables de cada servidor viven **cifradas en la central** (pestaña «Entornos» del proyecto) y el servidor se queda con dos. «Vincular» en la
485
+ central las aplica en **segundos**, sin desplegar. Es el mismo protocolo (**GT-ENV2**) que `growtickets-laravel` 0.11.0.
486
+
487
+ 1. En la pestaña «Entornos», crea el entorno (Producción, Pruebas…), carga sus variables (o pega tu `.env`: entra todo como secreto) y pulsa **Vincular**.
488
+ 2. **Conectar un servidor**: eliges el dominio (de la pestaña «Dominios») al que la central avisará y escribes tu código de dos pasos. La llave del servidor nace
489
+ en tu navegador y a la central sólo va la **pública**; te enseña la credencial **una sola vez**. Si prefieres que la privada no salga nunca del servidor (uno
490
+ propio, con su `.env`): `npx growtickets-nest env keygen`, pega la pública que te da en «Ya tengo una llave pública», y termina con
491
+ `npx growtickets-nest env keygen --central=<la llave de la central> --write`.
492
+ 3. En el servidor, **en las variables de la plataforma** (no en un `.env` versionado):
493
+
494
+ ```bash
495
+ GROWTICKETS_URL=https://tu-growtickets
496
+ GROWTICKETS_ENV_KEY=gtk2....
497
+ ```
498
+
499
+ 4. En `main.ts`, **antes** de `NestFactory.create` (y por tanto antes de que `ConfigModule` lea nada):
500
+
501
+ ```ts
502
+ import { growTicketsMiddleware, loadGrowTicketsEnv } from '@growconnect/growtickets-nest';
503
+
504
+ async function bootstrap() {
505
+ await loadGrowTicketsEnv(); // trae las variables y las pone en process.env
506
+ const app = await NestFactory.create(AppModule);
507
+ app.use(growTicketsMiddleware(app)); // contesta el aviso de «Vincular» y el reto de identidad (y la ruta de salud, y el resto)
508
+ await app.listen(3000);
509
+ }
510
+ ```
511
+
512
+ `loadGrowTicketsEnv()` lee `process.env`: `GROWTICKETS_URL` y `GROWTICKETS_ENV_KEY` tienen que estar ya en el entorno del proceso (la plataforma
513
+ los pone). En local con un `.env`, cárgalo antes: `node --env-file=.env dist/main.js`, o `process.loadEnvFile()` en la primera línea.
514
+ Sin `GROWTICKETS_ENV_KEY` no hace nada y devuelve `{ status: 'off' }`: se puede dejar puesta en todos los productos antes de conectar ninguno.
515
+ Compruébalo con `npx growtickets-nest env status` (dice el `key_id` de la credencial, el mismo que enseña el panel). Hace falta Node 18.18 o superior.
516
+
517
+ ### Seguridad: la credencial nunca viaja
518
+
519
+ `GROWTICKETS_ENV_KEY` (`gtk2.<semilla>.<pública de la central>`) no se manda nunca: con ella la librería **firma** cada petición (Ed25519, con la hora, un nonce de
520
+ un solo uso y las cabeceras que la identifican) y **fija** la llave de la central. Al bajar manda además una llave efímera X25519, y la central le contesta **cifrado
521
+ para esa petición** (ChaCha20-Poly1305): ni quien capture el tráfico, ni quien robe la credencial después, puede leer lo que viajó. Todo lo que contesta la central va
522
+ **firmado** por ella; un 304, un «revocada» o unas variables sin esa firma no cambian nada (`unsigned_response`: se sigue con la copia), y una entrega en claro no se acepta
523
+ aunque venga firmada. La central, además, puede limitar desde qué red se pide (una lista de IPs). El reloj tiene que ir a ±2 minutos del de la central.
524
+
525
+ Lo que esto no cubre: quien controle tu servidor o las variables de tu plataforma ve la credencial. Si un servidor se compromete, rota en la central su credencial por **filtración**
526
+ —y los secretos que tenía—. El protocolo para otras librerías está en el README de `growtickets-laravel` («Protocolo GT-ENV2»), con un vector común que esta librería pasa byte a byte.
527
+
528
+ | Variable | Por defecto | Qué hace |
529
+ |---|---|---|
530
+ | `GROWTICKETS_ENV_KEY` | vacía | La credencial de **servidor** (`gtk2.` y 92 caracteres), no la clave de API. Vacía = la pieza está apagada. Nunca en `.env.example`: `check` falla si lleva valor. |
531
+ | `GROWTICKETS_ENV_PATH` | `growtickets/env` | Por dónde avisa la central: `POST {ruta}/apply` y `GET {ruta}/identity`, **fuera del prefijo global**. Sólo letras, números, `/`, `_` y `-`. |
532
+ | `GROWTICKETS_ENV_ON_FAILURE` | `keep` | Si la central no contesta al arrancar: `keep` usa la copia local; `fail` no arranca. |
533
+ | `GROWTICKETS_ENV_MAX_STALE_HOURS` | `168` | Cuántas horas puede tener la copia local para arrancar con ella. |
534
+ | `GROWTICKETS_ENV_ON_CHANGE` | `restart` | Qué hacer al llegar una versión nueva con el proceso en marcha: `restart` o `reload` (ver abajo). |
535
+ | `GROWTICKETS_ENV_EXIT_CODE` | `75` | Con `restart`: el código con que sale el proceso (0 a 255). Ver «`restart` o `reload`». |
536
+ | `GROWTICKETS_ENV_DIR` | `.growtickets/` junto al proceso | Dónde guarda la copia cifrada y el estado. Si no se puede escribir ahí cae a una carpeta privada del directorio temporal. |
537
+ | `GROWTICKETS_ORIGIN` | vacía | La dirección pública del producto: la central la cruza con el dominio elegido al conectar el servidor. |
538
+
539
+ ### Quién gana
540
+
541
+ **entorno de la plataforma < la central < `GROWTICKETS_URL` y `GROWTICKETS_ENV_KEY` del arranque.** Lo que la central define pisa lo que pusieron Docker
542
+ o Railway y un `.env` que lea tu app después (`dotenv` no pisa lo que ya está en `process.env`); lo que no define se sigue leyendo de donde estuviera.
543
+ La central **nunca** puede tocar nada con el prefijo `GROWTICKETS_` (son las claves de la librería, sus logs remotos y su seguridad), ni las variables que gobiernan
544
+ el proceso (`PATH`, `HOME`, `LD_*`, `DYLD_*`, `NODE_OPTIONS`, `NODE_PATH`, `NODE_EXTRA_CA_CERTS`, `NODE_TLS_REJECT_UNAUTHORIZED`…): se descartan, se avisa
545
+ por el error estándar **sólo su nombre** y `growtickets-nest env status` las lista. Hasta 1000 variables de hasta 32 KB.
546
+
547
+ ### «Vincular» al instante
548
+
549
+ La central avisa con `POST /growtickets/env/apply` (cámbialo con `GROWTICKETS_ENV_PATH`), **firmado** con su llave —la fijada en tu credencial—, para ESTE servidor y de un
550
+ solo uso: `GT-ENV2-PUSH` sobre el `key_id` de este servidor, la hora, un nonce y el SHA-256 del cuerpo. Se tolera un minuto de reloj y cada nonce se recuerda dos. Cualquier otra
551
+ cosa —sin firma, con otra, para otro servidor, vieja, repetida, con query, con otro método, con un cuerpo de más de 256 bytes— recibe un **404 corriente**; no se escribe
552
+ nada (ni el nonce) antes de verificar la firma.
553
+
554
+ El aviso no trae ningún valor, sólo `{"version": N}`. El servidor contesta **202 al instante** y, **después de responder**, pregunta a la central por su cuenta (firmado, por
555
+ https, con `If-None-Match`), aplica y confirma. Un aviso falsificado, en el peor caso, le hace preguntar. La ruta la contesta el middleware de `main.ts` —igual que la de
556
+ salud—, por eso queda fuera del prefijo global y de los guards de tu app. Regístralo **antes** de cualquier parser de cuerpo (así lo hace `app.use(growTicketsMiddleware(app))`).
557
+
558
+ La central también reta a tu dominio a demostrar que tiene esta credencial (`GET /growtickets/env/identity?challenge=…`, una vez al día y con «Confirmar el dominio»): sólo contesta
559
+ a un reto firmado por la central, y si el dominio responde con otra credencial, la central avisa. Lo que contesta no lleva ningún secreto: el `key_id`, la pública y la firma del reto.
560
+
561
+ ### `restart` o `reload`
562
+
563
+ - **`restart`** (de serie): tras confirmar, la librería cierra la app en orden (`app.close()`, que también manda los últimos logs) y **sale con código 75**
564
+ (`EX_TEMPFAIL`, «vuelve a intentarlo»); el gestor de procesos tiene que levantarla otra vez, ya con las variables nuevas. **Es 75 y no 0 a propósito**: Railway
565
+ (su política de serie es «On Failure»), Docker con `restart: on-failure` y systemd con `Restart=on-failure` **no reinician una salida con 0**, y el producto se quedaría
566
+ caído tras cada «Vincular». Con `restart: always`, `unless-stopped`, PM2 o Kubernetes da igual el código. Si tu gestor trata un código distinto de 0 como un fallo que
567
+ alerta, pon `GROWTICKETS_ENV_EXIT_CODE=0` (y que el gestor reinicie ante una salida limpia: en Railway, la política «Always»), o `loadGrowTicketsEnv({ exitCode: 0 })`, que
568
+ gana sobre la variable. Si no quieres reinicios, usa `reload`.
569
+ - **`reload`**: actualiza `process.env` en caliente (lo que la central deja de mandar vuelve al valor de la plataforma, o se borra) y avisa a quien escuche. **Sólo ve
570
+ la nueva configuración lo que la lee después**: un pool de conexiones o un cliente ya creado con la vieja no cambia solo.
571
+
572
+ ```ts
573
+ loadGrowTicketsEnv({ onEnvChange: (c) => pool.reconnect() }); // o, desde cualquier provider: onGrowTicketsEnvChange(listener)
574
+ // c = { version, environmentId, environmentName, changed: ['DATABASE_URL'], removed: [] } — sólo NOMBRES, nunca valores
575
+ ```
576
+
577
+ ### Varias réplicas
578
+
579
+ El aviso entra por el dominio y, detrás de un balanceador, le llega a **una**. Cada proceso, además, **pregunta a la central cada ~60 s** (con un jitter de hasta 15 s y un
580
+ temporizador que no retiene el proceso) con `If-None-Match`: si nada cambió, un 304 sin cuerpo; si cambió, aplica. La central también reintenta el aviso hasta que todas las
581
+ réplicas confirman. Los nonce se recuerdan **en la memoria de cada proceso** (en varias réplicas, una petición firmada capturada podría repetirse una vez por réplica dentro
582
+ del minuto de tolerancia; lo único que consigue es que vuelva a preguntar). Con PM2 en modo cluster cada proceso aplica por su cuenta.
583
+
584
+ ### Si la central no contesta
585
+
586
+ Cada versión bajada queda en `.growtickets/env.snapshot`, **cifrada** con AES-256-GCM y una llave derivada de tu credencial (permisos 0600, en una carpeta 0700; añade `.growtickets/` al
587
+ `.gitignore`, `check` lo avisa). Con `keep` (de serie) el arranque sigue con esa copia si no tiene más de `GROWTICKETS_ENV_MAX_STALE_HOURS`; con `fail`, o sin copia que sirva,
588
+ `loadGrowTicketsEnv()` **lanza** `EnvError` (con un `reason` corto: `unreachable`, `timeout`, `revoked_key`…) y el producto no arranca: arrancar sin su configuración es peor.
589
+ Una credencial **revocada** o vencida (dicho y firmado por la central) nunca cae a la copia: la borra; una respuesta sin la firma de la central, o desde una red no permitida,
590
+ no: es un fallo pasajero y se sigue con la copia. **No se retrocede**: en el mismo entorno, una versión menor que la aplicada o que la de la copia se rechaza
591
+ (`rollback`, se avisa a la central) y la igual no se vuelve a aplicar; cambiar de entorno sí puede bajar el número. Si la central se restauró y volvió a numerar las
592
+ versiones, borra `.growtickets/` para que este servidor acepte la numeración nueva.
593
+
594
+ La llamada a la central va siempre por **https** (salvo `localhost`, `127.0.0.1`, `::1`, `*.test`, `*.localhost`), **sin seguir redirecciones**,
595
+ con 3 s hasta la respuesta y 10 s en total, y nunca registra ni un valor ni la credencial.
596
+
319
597
  ## CLI: `npx growtickets-nest`
320
598
 
321
599
  Sin dependencias propias.
@@ -330,9 +608,17 @@ deja ahí.
330
608
  ### `check`
331
609
 
332
610
  De sólo lectura, para el CI del producto. Sale con 1 si `.gitignore` no cubre `.env` o si `.env.example` lleva en una
333
- `GROWTICKETS_*` un valor que parece un secreto (la clave o el token de build). Además **avisa** —sin cambiar el código de
611
+ `GROWTICKETS_*` un valor que parece un secreto (la clave, el token de build o `GROWTICKETS_ENV_KEY`). Además **avisa** —sin cambiar el código de
334
612
  salida— de un montaje que no hará lo que se espera: el módulo sin `growTicketsMiddleware` en `main.ts`, Express sin
335
- subir el tope de JSON, o un prefijo global con la ruta de serie del portal.
613
+ subir el tope de JSON, un prefijo global con la ruta de serie del portal, y, si usas las variables desde la central, `GROWTICKETS_ENV_KEY` sin
614
+ `loadGrowTicketsEnv()`, llamada después de `NestFactory.create`, sin el middleware o sin `.growtickets/` en el `.gitignore`.
615
+
616
+ ### `doctor [--send]` (0.2.0)
617
+
618
+ Comprueba los logs de punta a punta: que el entorno traiga la dirección y la clave, que la mesa de ayuda las reconozca, que la clave lleve el
619
+ alcance «Logs» y que el módulo esté encendido en el proyecto. **Los fallos de esta parte no dan error, sólo un explorador vacío.** Con
620
+ `--send` manda además una línea de prueba y comprueba que llega. Sale con 1 si algo impide que las líneas lleguen. Si hay `GROWTICKETS_ENV_KEY`, suma el informe de
621
+ [`env status`](#env-pull-env-status-y-env-keygen-030) (y su código de salida).
336
622
 
337
623
  ### `deps`
338
624
 
@@ -342,6 +628,18 @@ viaja el archivo.** Pensado para CI, después de instalar; la clave necesita el
342
628
  `GROWTICKETS_ORIGIN`) distingue dos servidores con el mismo nombre. Sale con 1 si no puede enviar y con 0 si la mesa de
343
629
  ayuda aún no recibe dependencias o tiene la seguridad apagada: eso no debe frenar un despliegue.
344
630
 
631
+ ### `env pull`, `env status` y `env keygen` (0.3.0)
632
+
633
+ `env pull` baja la versión vigente de las variables de este servidor y escribe la **copia local cifrada**, sin poner nada en el entorno de ningún proceso. Dice la versión y
634
+ **cuántas** variables: nunca sus nombres ni sus valores. Sirve para sembrar la copia en un despliegue y para comprobar que la clave funciona; no pisa una copia de una versión posterior.
635
+ `env status` es el doctor y **no usa la red**: la versión aplicada, la edad de la copia, el `key_id` de la credencial (nunca ella), la ruta del aviso y del reto, qué pasa si la central no
636
+ contesta y qué pasa al cambiar una versión (con qué código sale). Ambos leen `GROWTICKETS_URL`, `GROWTICKETS_ENV_KEY` y `GROWTICKETS_ENV_DIR` del entorno; `status` sale con 1 si algo impide aplicar.
637
+
638
+ `env keygen` genera el par de este servidor **aquí mismo**, para que la privada no pase por un navegador ni un portapapeles: sin opciones guarda la semilla en
639
+ `.growtickets/env.pending` (0600) y enseña sólo la pública y su `key_id` (es lo que va a «Ya tengo una llave pública»); con `--central=<la llave de la central>` arma la credencial y,
640
+ con `--write`, la escribe en el `.env` como `GROWTICKETS_ENV_KEY` si todavía no hay una (nunca la pisa). Sin `--write` la enseña, y entonces queda en el historial de la terminal.
641
+ `--force` genera una semilla nueva aunque haya una pendiente.
642
+
345
643
  ## Cliente servidor a servidor
346
644
 
347
645
  ```ts
@@ -375,6 +673,8 @@ pudo saber: nunca devuelve un cero que no es).
375
673
  | 12 | La clave hacia un proxy de la red en claro | `https` sin depender de `NODE_ENV`, que en Nest casi nunca está |
376
674
  | 13 | El ticket de otra persona por el recorte de `max_input_vars` | `reporter_id` primero, tope de 100 valores, y la mesa de ayuda rechaza la query recortada |
377
675
  | 14 | Una línea falsa en tus registros (un `%0A` en la URL) | Los mensajes salen con los caracteres de control escapados y cortados a 500 |
676
+ | 15 | Las variables de la central (valores o la clave de servidor) en un registro, un acuse o la salida de la CLI | Sólo versión, cuenta y NOMBRES descartados; la copia local va cifrada (AES-256-GCM, 0600); `env status` enseña un prefijo de la clave |
677
+ | 16 | La clave de servidor hacia otro host | `https` obligatorio, sin seguir redirecciones, y el aviso de la central sólo se atiende con la firma v2 de un solo uso |
378
678
 
379
679
  ## Cadena de suministro: prácticas recomendadas
380
680
 
@@ -396,8 +696,15 @@ npm run test:fixtures # empaqueta el .tgz, lo instala en Nest 10 + Express, Ne
396
696
  Las pruebas de `test/nest` levantan una app de Nest de verdad con **cada plataforma**; las de `test/proxy` y
397
697
  `test/security` son las del núcleo.
398
698
 
699
+ **El transporte con la central** de las variables de entorno vive aparte, en `src/env/transport/` (cliente HTTP de pull/ack, cabeceras de
700
+ autenticación, verificación del aviso y la derivación de las llaves). El resto de `src/env` —ajustes, política, copia cifrada, aplicación,
701
+ réplicas, arranque, restart/reload— habla con él por la interfaz `EnvTransport` (pedir la versión con ETag, acusar, verificar un aviso, dar la
702
+ llave de la copia) y una prueba (`test/env/transport.spec.ts`) comprueba que no nombra la credencial ni la firma. Otro protocolo se enchufa
703
+ cambiando `createEnvTransport` o pasando otro `transport` a `EnvRuntime`.
704
+
399
705
  **El núcleo es una COPIA** del de `@growconnect/growtickets-next-server` (`packages/server/src` de `growtickets-next`,
400
- commit `8abdcaa`): `proxy/`, `security/`, `client/`, `errors/`, `logging/`, `config/` y `cli/deps.ts`, sin la CSP, sin el
706
+ commit `8abdcaa`): `proxy/`, `security/`, `client/`, `errors/`, `logging/`, `config/` y `cli/deps.ts`, con la CSP de `security/csp.ts` (la de
707
+ `-next-server`, pero la suma se hace al escribir las cabeceras de Node: `nest/csp-response.ts`), sin el
401
708
  runtime edge y sin la comprobación de `NEXT_PUBLIC_*`. Lo de Nest vive en `src/nest/`. Tras la revisión de seguridad del
402
709
  2026-10-19 el núcleo además lleva aquí: `reporter_id` primero con `limitQuery` (100 valores) y `phpVariableName` en
403
710
  `handle-portal-request.ts`, la valla de origen en todos los métodos (`assertTrustedOrigin`), `safeLogText` en los
@@ -409,6 +716,12 @@ hacerlo también allá** (y al revés): es el mismo contrato con la mesa de ayud
409
716
 
410
717
  ## Versiones
411
718
 
719
+ - **0.3.0** — Las variables de entorno desde GrowTickets (`loadGrowTicketsEnv()`, `onGrowTicketsEnvChange()`, la ruta firmada `POST growtickets/env/apply`, la copia local cifrada, la revisión de
720
+ cada minuto y `env pull` / `env status`), y la **política de contenido «sólo informar»** que la central define para el proyecto (`GROWTICKETS_SECURITY_CSP`, `csp` en `forRoot`).
721
+ Ver [Variables de entorno desde GrowTickets](#variables-de-entorno-desde-growtickets-030) y [Política de contenido](#política-de-contenido-sólo-informar-030).
722
+ - **0.2.0** — Los logs (`GrowTicketsLogService`, `GrowTicketsLogsModule`, `growTicketsLogStream` y `doctor`), el mantenimiento y el lenguaje: el middleware de `main.ts` cumple las ventanas de GrowTickets (503 por reloj, pase del equipo,
723
+ por zonas —por ruta o por público con `GrowTicketsMaintenanceGuard`—, la ruta del front `api/growtickets/maintenance` con `windows`, `backend_windows` y `zone_windows`), y cada llamada lleva `X-GrowTickets-Runtime`
724
+ (`node/… nestjs/…`) para los avisos de Node. `GROWTICKETS_MAINTENANCE=false` lo apaga.
412
725
  - **0.1.0** — Primera versión: el proxy del portal (con `throttle`) y `GET unread` para el lanzador del componente web, la
413
726
  seguridad (bloqueo de IP y de redes, sondeos, logins por decorador, ruta de salud firmada), el relevo de dependencias del
414
727
  front, `GrowTicketsClient` (con `sinLeer()`) y el CLI (`init`, `check`, `deps`). Nest 10, 11 y 12, Express y Fastify.
@@ -1,14 +1,22 @@
1
1
  export interface ParsedArgs {
2
2
  command: string | null;
3
3
  dryRun: boolean;
4
+ /** `--send`: en `doctor`, manda además una línea de prueba y comprueba que llega. */
5
+ send: boolean;
4
6
  /** `--lockfile <ruta>`: el archivo de bloqueo a leer en `deps`, en vez de buscarlo. */
5
7
  lockfile: string | null;
6
8
  /** `--app <nombre>`: cómo aparece el proyecto en el informe de `deps`. */
7
9
  app: string | null;
8
10
  /** `--origin <url>`: la dirección pública del producto, para que `deps` distinga este servidor de otro con el mismo nombre. */
9
11
  origin: string | null;
12
+ /** `--central <llave>` (o `--central=<llave>`; sólo con `env`): en `env keygen`, la llave pública de la central que devolvió el panel al conectar. */
13
+ central: string | null;
14
+ /** `--write`: en `env keygen --central`, escribe `GROWTICKETS_ENV_KEY` en el `.env`. */
15
+ write: boolean;
16
+ /** `--force`: en `env keygen`, genera una llave nueva aunque ya haya una pendiente. */
17
+ force: boolean;
10
18
  /** Banderas que no se reconocen — `runCli` decide qué hacer con ellas, este archivo sólo las señala. */
11
19
  unknown: string[];
12
20
  }
13
- /** Analizador de argumentos propio: son cuatro banderas, y así el CLI no arrastra ninguna dependencia. */
21
+ /** Analizador de argumentos propio: son pocas banderas, y así el CLI no arrastra ninguna dependencia. */
14
22
  export declare function parseArgs(argv: readonly string[]): ParsedArgs;
package/dist/cli/args.js CHANGED
@@ -1,18 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseArgs = parseArgs;
4
- /** Analizador de argumentos propio: son cuatro banderas, y así el CLI no arrastra ninguna dependencia. */
4
+ /** Analizador de argumentos propio: son pocas banderas, y así el CLI no arrastra ninguna dependencia. */
5
5
  function parseArgs(argv) {
6
6
  const [command = null, ...rest] = argv;
7
- const result = { command, dryRun: false, lockfile: null, app: null, origin: null, unknown: [] };
7
+ const esEnv = command === 'env';
8
+ const result = { command, dryRun: false, send: false, lockfile: null, app: null, origin: null, central: null, write: false, force: false, unknown: [] };
8
9
  for (let i = 0; i < rest.length; i++) {
9
10
  const arg = rest[i];
10
11
  if (arg === '--dry-run')
11
12
  result.dryRun = true;
12
- else if (arg === '--lockfile' || arg === '--app' || arg === '--origin') {
13
+ else if (arg === '--send')
14
+ result.send = true;
15
+ else if (esEnv && arg === '--write')
16
+ result.write = true;
17
+ else if (esEnv && arg === '--force')
18
+ result.force = true;
19
+ else if (esEnv && arg?.startsWith('--central='))
20
+ result.central = arg.slice('--central='.length) || null;
21
+ else if (arg === '--lockfile' || arg === '--app' || arg === '--origin' || (esEnv && arg === '--central')) {
13
22
  const value = rest[++i];
14
23
  if (value && !value.startsWith('--'))
15
- result[arg === '--lockfile' ? 'lockfile' : arg === '--app' ? 'app' : 'origin'] = value;
24
+ result[arg === '--lockfile' ? 'lockfile' : arg === '--app' ? 'app' : arg === '--central' ? 'central' : 'origin'] = value;
16
25
  else
17
26
  result.unknown.push(`${arg} ${value ?? ''}`.trim());
18
27
  }
@@ -15,6 +15,9 @@ export interface CheckResult {
15
15
  * - el módulo sin `app.use(growTicketsMiddleware(app))` en `main.ts`: sin él no se corta ninguna IP, no hay
16
16
  * ruta de salud y no se cuenta lo que no casó con ninguna ruta;
17
17
  * - Express sin subir el tope de JSON: su parser corta en 100 KB y el contrato del portal es de 256 KB;
18
- * - un prefijo global con la ruta de serie: el portal quedaría en `/api/api/soporte`.
18
+ * - un prefijo global con la ruta de serie: el portal quedaría en `/api/api/soporte`;
19
+ * - las variables de entorno desde la central (`GROWTICKETS_ENV_KEY`): sin `loadGrowTicketsEnv()` en `main.ts`, o llamada después de
20
+ * `NestFactory.create`, o sin el middleware, o sin `.growtickets/` en el `.gitignore`, no llegan, no llegan a tiempo, no se avisan al
21
+ * instante o dejan la copia cifrada a un `git add` de distancia.
19
22
  */
20
23
  export declare function runCheck(cwd: string): CheckResult;
package/dist/cli/check.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runCheck = runCheck;
4
4
  const node_fs_1 = require("node:fs");
5
5
  const node_path_1 = require("node:path");
6
+ const transport_1 = require("../env/transport");
6
7
  const env_example_1 = require("./env-example");
7
8
  const SOURCE_ROOTS = ['src', 'apps', 'libs'];
8
9
  const SCAN_EXTENSIONS = new Set(['.ts', '.js', '.mts', '.cts', '.mjs', '.cjs']);
@@ -14,6 +15,9 @@ const USES_MIDDLEWARE = /\bgrowTicketsMiddleware\s*\(/;
14
15
  const USES_FASTIFY = /\bFastifyAdapter\b/;
15
16
  const RAISES_JSON_LIMIT = /useBodyParser\s*\(\s*['"]json['"][^)]*limit/;
16
17
  const GLOBAL_PREFIX = /\bsetGlobalPrefix\s*\(\s*['"`]\/?([^'"`]+)['"`]/;
18
+ const LOADS_ENV = /\bloadGrowTicketsEnv\s*\(/;
19
+ const MENTIONS_ENV_KEY = new RegExp(`\\b(${transport_1.ENV_CREDENTIAL_VARIABLES.join('|')})\\b`);
20
+ const CREATES_APP = /\bNestFactory\s*\.\s*create\w*\s*[(<]/;
17
21
  /**
18
22
  * De sólo lectura: nunca escribe nada, y por eso sirve para el CI del propio producto. Sale con 1 si
19
23
  * encuentra una vía por la que la clave acabe versionada (un `.gitignore` que no cubre `.env`, un
@@ -23,11 +27,14 @@ const GLOBAL_PREFIX = /\bsetGlobalPrefix\s*\(\s*['"`]\/?([^'"`]+)['"`]/;
23
27
  * - el módulo sin `app.use(growTicketsMiddleware(app))` en `main.ts`: sin él no se corta ninguna IP, no hay
24
28
  * ruta de salud y no se cuenta lo que no casó con ninguna ruta;
25
29
  * - Express sin subir el tope de JSON: su parser corta en 100 KB y el contrato del portal es de 256 KB;
26
- * - un prefijo global con la ruta de serie: el portal quedaría en `/api/api/soporte`.
30
+ * - un prefijo global con la ruta de serie: el portal quedaría en `/api/api/soporte`;
31
+ * - las variables de entorno desde la central (`GROWTICKETS_ENV_KEY`): sin `loadGrowTicketsEnv()` en `main.ts`, o llamada después de
32
+ * `NestFactory.create`, o sin el middleware, o sin `.growtickets/` en el `.gitignore`, no llegan, no llegan a tiempo, no se avisan al
33
+ * instante o dejan la copia cifrada a un `git add` de distancia.
27
34
  */
28
35
  function runCheck(cwd) {
29
36
  const issues = [...checkGitignore(cwd), ...checkEnvExample(cwd)];
30
- const warnings = checkWiring(cwd);
37
+ const warnings = [...checkWiring(cwd), ...checkEnvWiring(cwd)];
31
38
  return {
32
39
  exitCode: issues.length > 0 ? 1 : 0,
33
40
  issues,
@@ -71,6 +78,44 @@ function checkWiring(cwd) {
71
78
  }
72
79
  return warnings;
73
80
  }
81
+ /**
82
+ * Las variables de entorno desde la central. Sólo avisa si el producto dice usarlas (nombra `GROWTICKETS_ENV_KEY` en su `.env.example` o en
83
+ * el código, o ya llama a `loadGrowTicketsEnv`): sin ello no hay nada que revisar.
84
+ */
85
+ function checkEnvWiring(cwd) {
86
+ let carga = false;
87
+ let menciona = false;
88
+ let usaMiddleware = false;
89
+ let tardia = false;
90
+ for (const root of SOURCE_ROOTS) {
91
+ walk((0, node_path_1.join)(cwd, root), 0, (_path, content) => {
92
+ const llamada = content.search(LOADS_ENV);
93
+ const app = content.search(CREATES_APP);
94
+ carga ||= llamada >= 0;
95
+ menciona ||= MENTIONS_ENV_KEY.test(content);
96
+ usaMiddleware ||= USES_MIDDLEWARE.test(content);
97
+ tardia ||= llamada >= 0 && app >= 0 && llamada > app;
98
+ });
99
+ }
100
+ const ejemplo = (0, node_path_1.join)(cwd, '.env.example');
101
+ const declarada = menciona || ((0, node_fs_1.existsSync)(ejemplo) && MENTIONS_ENV_KEY.test((0, node_fs_1.readFileSync)(ejemplo, 'utf8')));
102
+ if (!declarada && !carga)
103
+ return [];
104
+ const warnings = [];
105
+ if (!carga) {
106
+ warnings.push(`${transport_1.ENV_CREDENTIAL_VARIABLES.join(', ')} está declarada pero ningún archivo llama a loadGrowTicketsEnv(): las variables de la central no llegan al proceso. Añade await loadGrowTicketsEnv() en main.ts, antes de NestFactory.create.`);
107
+ return warnings;
108
+ }
109
+ if (tardia)
110
+ warnings.push('loadGrowTicketsEnv() se llama DESPUÉS de NestFactory.create: ConfigModule y los providers ya leyeron el entorno. Pásala antes.');
111
+ if (!usaMiddleware)
112
+ warnings.push('loadGrowTicketsEnv() sin app.use(growTicketsMiddleware(app)): el aviso de «Vincular» no tiene dónde llegar y cada réplica se entera hasta un minuto después, por su revisión.');
113
+ const gitignore = (0, node_path_1.join)(cwd, '.gitignore');
114
+ if (!(0, node_fs_1.existsSync)(gitignore) || !(0, node_fs_1.readFileSync)(gitignore, 'utf8').split('\n').some((linea) => /^\/?\.growtickets\/?\*?$/.test(linea.trim()))) {
115
+ warnings.push('.gitignore no cubre .growtickets/: ahí queda la copia local cifrada de las variables. Cifrada no es pública: no la versiones.');
116
+ }
117
+ return warnings;
118
+ }
74
119
  /** ¿La llamada a `forRoot`/`forRootAsync` le da su propia `path`? Se mira el trozo de texto justo después de la llamada. */
75
120
  function passesPath(content) {
76
121
  const inicio = content.search(USES_MODULE);
@@ -22,7 +22,24 @@ exports.PUBLIC_KEYS = [
22
22
  'GROWTICKETS_SECURITY_HEALTH_PATH',
23
23
  'GROWTICKETS_SECURITY_DEPENDENCIES',
24
24
  'GROWTICKETS_TRUSTED_PROXY_HOPS',
25
+ 'GROWTICKETS_TRUST_REAL_IP',
25
26
  'GROWTICKETS_FRONT_DEPS_PATH',
27
+ 'GROWTICKETS_SECURITY_CSP',
28
+ 'GROWTICKETS_ORIGIN',
29
+ 'GROWTICKETS_ENV_PATH',
30
+ 'GROWTICKETS_ENV_ON_FAILURE',
31
+ 'GROWTICKETS_ENV_MAX_STALE_HOURS',
32
+ 'GROWTICKETS_ENV_ON_CHANGE',
33
+ 'GROWTICKETS_ENV_EXIT_CODE',
34
+ 'GROWTICKETS_ENV_DIR',
35
+ 'GROWTICKETS_MAINTENANCE',
36
+ 'GROWTICKETS_MAINTENANCE_HOST',
37
+ 'GROWTICKETS_MAINTENANCE_PATH',
38
+ 'GROWTICKETS_LOGS',
39
+ 'GROWTICKETS_LOGS_LEVEL',
40
+ 'GROWTICKETS_LOGS_ORIGIN',
41
+ 'GROWTICKETS_LOGS_REDACT_KEYS',
42
+ 'GROWTICKETS_LOGS_REQUESTS',
26
43
  ];
27
44
  /**
28
45
  * Sólo AÑADE lo que falte, y con valor VACÍO — nunca sobrescribe una clave que