@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
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EnvOverlay = void 0;
4
+ /**
5
+ * Pone las variables de la central ENCIMA del entorno del proceso y recuerda qué había debajo. La central gana sobre la plataforma
6
+ * (Docker, Railway) y sobre un `.env` que lea el producto después, porque `dotenv` no pisa lo que ya está en `process.env`.
7
+ *
8
+ * Al recibir una versión nueva en caliente (`reload`) lo que la central dejó de mandar NO se queda: vuelve al valor que tenía la
9
+ * plataforma, o se borra si no tenía ninguno. Sin esto, quitar una variable de la bóveda no la quitaría hasta el siguiente reinicio.
10
+ */
11
+ class EnvOverlay {
12
+ constructor(target) {
13
+ this.target = target;
14
+ this.baseline = new Map();
15
+ this.current = new Set();
16
+ }
17
+ apply(variables) {
18
+ const removed = [];
19
+ const changed = [];
20
+ for (const nombre of [...this.current]) {
21
+ if (nombre in variables)
22
+ continue;
23
+ this.restore(nombre);
24
+ removed.push(nombre);
25
+ }
26
+ for (const [nombre, valor] of Object.entries(variables)) {
27
+ if (!this.current.has(nombre)) {
28
+ this.baseline.set(nombre, this.target[nombre]);
29
+ this.current.add(nombre);
30
+ }
31
+ if (this.target[nombre] !== valor) {
32
+ this.target[nombre] = valor;
33
+ changed.push(nombre);
34
+ }
35
+ }
36
+ return { changed: changed.sort(), removed: removed.sort() };
37
+ }
38
+ restore(nombre) {
39
+ const antes = this.baseline.get(nombre);
40
+ if (antes === undefined)
41
+ delete this.target[nombre];
42
+ else
43
+ this.target[nombre] = antes;
44
+ this.baseline.delete(nombre);
45
+ this.current.delete(nombre);
46
+ }
47
+ }
48
+ exports.EnvOverlay = EnvOverlay;
@@ -0,0 +1,30 @@
1
+ export declare const ENV_SCHEMA = 1;
2
+ export declare const MAX_VARIABLES = 1000;
3
+ export declare const MAX_VALUE_BYTES = 32768;
4
+ /** Lo que la central dio por bueno para este servidor: la versión, de qué entorno, y las variables ya validadas. */
5
+ export interface EnvPayload {
6
+ version: number;
7
+ environmentId: number;
8
+ environmentName: string;
9
+ environmentKind: string;
10
+ variables: Record<string, string>;
11
+ /** El `ETag` de la respuesta: la siguiente pregunta lo manda y, si nada cambió, la central contesta 304 sin cuerpo. */
12
+ etag: string | null;
13
+ /** Los NOMBRES (nunca los valores) que la política descartó. No se guardan en la copia: se calculan al validar. */
14
+ discarded: string[];
15
+ }
16
+ /** «7:12»: el entorno y la versión. Lo que se compara para saber si algo ya está aplicado. */
17
+ export declare function markerOf(payload: Pick<EnvPayload, 'environmentId' | 'version'>): string;
18
+ /** ¿El nombre se puede poner en el entorno del proceso? */
19
+ export declare function isAllowedName(name: string): boolean;
20
+ /**
21
+ * La respuesta de `GET api/ingest/env` ya decodificada, convertida en una carga útil en la que se puede confiar. La central ya valida al
22
+ * guardar; aquí se vuelve a validar porque lo recibido acaba en `process.env`, y eso es más que configuración.
23
+ *
24
+ * - **Esquema**: `schema` 1; la versión y el entorno son enteros positivos. Otro esquema es `incompatible_schema`: la librería es vieja.
25
+ * - **Nombres**: los no permitidos se DESCARTAN —sólo su nombre queda en `discarded`— en vez de tumbar la versión entera: el resto de la
26
+ * configuración sigue llegando.
27
+ * - **Valores**: texto, sin NUL, hasta 32 KB; como mucho 1000 variables. Un valor que no cumple es `malformed` aunque su nombre se
28
+ * fuera a descartar, para no aceptar una respuesta a medias.
29
+ */
30
+ export declare function parseEnvPayload(json: unknown, etag?: string | null): EnvPayload;
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_VALUE_BYTES = exports.MAX_VARIABLES = exports.ENV_SCHEMA = void 0;
4
+ exports.markerOf = markerOf;
5
+ exports.isAllowedName = isAllowedName;
6
+ exports.parseEnvPayload = parseEnvPayload;
7
+ const errors_1 = require("./errors");
8
+ exports.ENV_SCHEMA = 1;
9
+ exports.MAX_VARIABLES = 1000;
10
+ exports.MAX_VALUE_BYTES = 32768;
11
+ /** Sin la bandera `m` y con `$` al final: en JavaScript `$` no acepta un salto de línea final, así que `"PATH\n"` no pasa por nombre válido. */
12
+ const NAME_SHAPE = /^[A-Z_][A-Z0-9_]{0,127}$/;
13
+ /** Todo `GROWTICKETS_*` es de la librería: la bóveda no puede tocar sus claves, sus logs remotos ni su seguridad. */
14
+ const RESERVED_PREFIX = 'GROWTICKETS_';
15
+ const SYSTEM_PREFIXES = ['LD_', 'DYLD_', 'GIT_'];
16
+ /**
17
+ * Las que gobiernan el PROCESO y no la aplicación, o cargan código o certificados por otra puerta. Es la lista de la central
18
+ * (`EnvVariableRules::SYSTEM`, espejo de `EnvPolicy` de growtickets-laravel) entera: aquí lo recibido acaba en `process.env`, así que
19
+ * `NODE_OPTIONS`, `NODE_PATH` y `NODE_EXTRA_CA_CERTS` cambian qué código y qué certificados cargan los procesos hijos,
20
+ * `NODE_TLS_REJECT_UNAUTHORIZED=0` apagaría la verificación de TLS de todo el proceso —la de la propia llamada a la central incluida—,
21
+ * `OPENSSL_CONF`/`SSL_CERT_FILE`/`SSLKEYLOGFILE` tocan el TLS de lo que el producto lanza, y `PYTHONWARNINGS`, `BROWSER`, `PAGER`, `EDITOR`,
22
+ * `PS4`, `SHELLOPTS`, `JAVA_TOOL_OPTIONS`… ejecutan código al arrancar un intérprete o una herramienta.
23
+ */
24
+ const SYSTEM = new Set([
25
+ 'GROWTICKETS_ENV_VERSION', 'GROWTICKETS_ENV_BUILDING', 'GROWTICKETS_ENV_PATH',
26
+ 'APP_CONFIG_CACHE', 'APP_SERVICES_CACHE', 'APP_PACKAGES_CACHE', 'APP_EVENTS_CACHE', 'APP_ROUTES_CACHE', 'APP_BASE_PATH',
27
+ 'PATH', 'HOME', 'SHELL', 'IFS', 'PHPRC', 'PHP_INI_SCAN_DIR', 'PHP_BINARY', 'BASH_ENV', 'ENV', 'PERL5OPT', 'PERL5LIB',
28
+ 'NODE_OPTIONS', 'PYTHONPATH', 'PYTHONSTARTUP', 'GCONV_PATH', 'TESTBENCH_WORKING_PATH',
29
+ 'PYTHONHOME', 'PYTHONBREAKPOINT', 'PYTHONINSPECT', 'PYTHONUSERBASE', 'PYTHONEXECUTABLE',
30
+ 'NODE_PATH', 'NODE_TLS_REJECT_UNAUTHORIZED', 'NODE_EXTRA_CA_CERTS', 'NEXT_RUNTIME',
31
+ 'OPENSSL_CONF', 'OPENSSL_MODULES', 'OPENSSL_ENGINES', 'SSLKEYLOGFILE', 'SSL_CERT_FILE', 'SSL_CERT_DIR',
32
+ 'REQUESTS_CA_BUNDLE', 'CURL_CA_BUNDLE', 'PYTHONWARNINGS', 'PYTHONPLATLIBDIR', 'BROWSER', 'PAGER', 'EDITOR', 'VISUAL',
33
+ 'PS4', 'SHELLOPTS', 'BASHOPTS', 'PROMPT_COMMAND', 'JAVA_TOOL_OPTIONS', '_JAVA_OPTIONS', 'JDK_JAVA_OPTIONS',
34
+ 'RUBYOPT', 'RUBYLIB', 'PERLLIB', 'PERL5DB', 'GLIBC_TUNABLES', 'HOSTALIASES', 'LOCALDOMAIN', 'RES_OPTIONS',
35
+ 'NLSPATH', 'LOCPATH', 'NODE_REPL_EXTERNAL_MODULE',
36
+ ]);
37
+ /** «7:12»: el entorno y la versión. Lo que se compara para saber si algo ya está aplicado. */
38
+ function markerOf(payload) {
39
+ return `${payload.environmentId}:${payload.version}`;
40
+ }
41
+ /** ¿El nombre se puede poner en el entorno del proceso? */
42
+ function isAllowedName(name) {
43
+ if (!NAME_SHAPE.test(name) || name.startsWith(RESERVED_PREFIX) || SYSTEM.has(name))
44
+ return false;
45
+ return !SYSTEM_PREFIXES.some((prefijo) => name.startsWith(prefijo));
46
+ }
47
+ /**
48
+ * La respuesta de `GET api/ingest/env` ya decodificada, convertida en una carga útil en la que se puede confiar. La central ya valida al
49
+ * guardar; aquí se vuelve a validar porque lo recibido acaba en `process.env`, y eso es más que configuración.
50
+ *
51
+ * - **Esquema**: `schema` 1; la versión y el entorno son enteros positivos. Otro esquema es `incompatible_schema`: la librería es vieja.
52
+ * - **Nombres**: los no permitidos se DESCARTAN —sólo su nombre queda en `discarded`— en vez de tumbar la versión entera: el resto de la
53
+ * configuración sigue llegando.
54
+ * - **Valores**: texto, sin NUL, hasta 32 KB; como mucho 1000 variables. Un valor que no cumple es `malformed` aunque su nombre se
55
+ * fuera a descartar, para no aceptar una respuesta a medias.
56
+ */
57
+ function parseEnvPayload(json, etag = null) {
58
+ if (!isRecord(json) || !('schema' in json))
59
+ throw new errors_1.EnvError('malformed', 'La central contestó algo que no son variables de entorno.');
60
+ if (positiveInt(json.schema) !== exports.ENV_SCHEMA) {
61
+ throw new errors_1.EnvError('incompatible_schema', 'La central manda un formato que esta versión de la librería no conoce: actualízala.');
62
+ }
63
+ const entorno = isRecord(json.environment) ? json.environment : {};
64
+ const version = positiveInt(json.version);
65
+ const environmentId = positiveInt(entorno.id);
66
+ const variables = json.variables;
67
+ if (version === null || environmentId === null || !isRecord(variables) || Object.keys(variables).length > exports.MAX_VARIABLES) {
68
+ throw new errors_1.EnvError('malformed', 'La respuesta de la central no tiene la forma esperada.');
69
+ }
70
+ const limpias = {};
71
+ const descartadas = [];
72
+ for (const [nombre, valor] of Object.entries(variables)) {
73
+ if (typeof valor !== 'string' || valor.includes('\0') || Buffer.byteLength(valor, 'utf8') > exports.MAX_VALUE_BYTES) {
74
+ throw new errors_1.EnvError('malformed', `La variable ${printable(nombre)} no tiene un valor válido.`);
75
+ }
76
+ if (isAllowedName(nombre))
77
+ limpias[nombre] = valor;
78
+ else
79
+ descartadas.push(printable(nombre));
80
+ }
81
+ return {
82
+ version,
83
+ environmentId,
84
+ environmentName: String(entorno.name ?? '').slice(0, 80),
85
+ environmentKind: /^[a-z]{1,20}$/.test(String(entorno.kind ?? '')) ? String(entorno.kind) : 'other',
86
+ variables: Object.fromEntries(Object.entries(limpias).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))),
87
+ etag,
88
+ discarded: descartadas,
89
+ };
90
+ }
91
+ /** Un nombre recibido de la red, listo para ir a un mensaje: sin caracteres de control y acotado. */
92
+ function printable(name) {
93
+ return name.replace(/[^\x20-\x7E]/g, '?').slice(0, 130);
94
+ }
95
+ function isRecord(value) {
96
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
97
+ }
98
+ function positiveInt(value) {
99
+ const n = typeof value === 'string' && /^\d{1,15}$/.test(value) ? Number(value) : value;
100
+ return typeof n === 'number' && Number.isSafeInteger(n) && n >= 1 ? n : null;
101
+ }
@@ -0,0 +1,27 @@
1
+ /** Lo que el temporizador hace tras cada revisión: seguir al ritmo de siempre, espaciarla (la central pidió esperar) o no volver a revisar. */
2
+ export type PollHint = 'continue' | 'slow' | 'stop';
3
+ export interface PollerOptions {
4
+ tick: () => Promise<PollHint>;
5
+ intervalMs: number;
6
+ jitterMs: number;
7
+ slowMs: number;
8
+ random?: () => number;
9
+ }
10
+ /**
11
+ * La revisión periódica que hace cada PROCESO: el aviso de «Vincular» le llega a UNA réplica, y las demás (y la propia, si el aviso se
12
+ * perdió) se enteran preguntando a la central con `If-None-Match` cada minuto, con algo de jitter para que varias réplicas arrancadas a la
13
+ * vez no pregunten a la vez. Un temporizador por vez (no un `setInterval`): la siguiente espera se decide cuando termina la anterior, así dos
14
+ * revisiones nunca se pisan, y el temporizador no retiene el proceso (`unref`).
15
+ */
16
+ export declare class EnvPoller {
17
+ private readonly options;
18
+ private timer;
19
+ private stopped;
20
+ constructor(options: PollerOptions);
21
+ running(): boolean;
22
+ start(): void;
23
+ stop(): void;
24
+ private schedule;
25
+ private run;
26
+ private delay;
27
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EnvPoller = void 0;
4
+ /**
5
+ * La revisión periódica que hace cada PROCESO: el aviso de «Vincular» le llega a UNA réplica, y las demás (y la propia, si el aviso se
6
+ * perdió) se enteran preguntando a la central con `If-None-Match` cada minuto, con algo de jitter para que varias réplicas arrancadas a la
7
+ * vez no pregunten a la vez. Un temporizador por vez (no un `setInterval`): la siguiente espera se decide cuando termina la anterior, así dos
8
+ * revisiones nunca se pisan, y el temporizador no retiene el proceso (`unref`).
9
+ */
10
+ class EnvPoller {
11
+ constructor(options) {
12
+ this.options = options;
13
+ this.timer = null;
14
+ this.stopped = false;
15
+ }
16
+ running() {
17
+ return this.timer !== null;
18
+ }
19
+ start() {
20
+ if (this.timer !== null || this.stopped)
21
+ return;
22
+ this.schedule(this.delay(this.options.intervalMs));
23
+ }
24
+ stop() {
25
+ this.stopped = true;
26
+ if (this.timer !== null)
27
+ clearTimeout(this.timer);
28
+ this.timer = null;
29
+ }
30
+ schedule(ms) {
31
+ const timer = setTimeout(() => void this.run(), ms);
32
+ timer.unref?.();
33
+ this.timer = timer;
34
+ }
35
+ async run() {
36
+ this.timer = null;
37
+ let pista = 'continue';
38
+ try {
39
+ pista = await this.options.tick();
40
+ }
41
+ catch {
42
+ pista = 'continue';
43
+ }
44
+ if (pista === 'stop' || this.stopped) {
45
+ this.stopped = true;
46
+ return;
47
+ }
48
+ this.schedule(this.delay(pista === 'slow' ? this.options.slowMs : this.options.intervalMs));
49
+ }
50
+ delay(base) {
51
+ return base + Math.floor((this.options.random ?? Math.random)() * this.options.jitterMs);
52
+ }
53
+ }
54
+ exports.EnvPoller = EnvPoller;
@@ -0,0 +1,122 @@
1
+ import { EnvApplier, type EnvChange } from './applier';
2
+ import { type PollHint } from './poller';
3
+ import { type EnvSettings } from './settings';
4
+ import { EnvSnapshot } from './snapshot';
5
+ import { EnvState } from './state';
6
+ import { EnvStorage } from './storage';
7
+ import { type EnvTransport } from './transport';
8
+ type Env = Record<string, string | undefined>;
9
+ export type EnvChangeListener = (change: EnvChange) => void | Promise<void>;
10
+ /** Lo único que el reinicio necesita de la app de Nest: cerrarla en orden (`app.close()`). */
11
+ export interface EnvAppHandle {
12
+ close?: () => Promise<void>;
13
+ }
14
+ export interface EnvRuntimeDeps {
15
+ env: () => Env;
16
+ fetchImpl?: typeof fetch;
17
+ /** Segundos, época Unix. */
18
+ now: () => number;
19
+ cwd: () => string;
20
+ /** Dónde se escribe un aviso de la propia librería: el error estándar y nunca la consola, que otros adaptadores capturan. */
21
+ warn: (message: string) => void;
22
+ /** La revisión de cada minuto. Las pruebas la apagan para mover el reloj a mano. */
23
+ timer: boolean;
24
+ random?: () => number;
25
+ exit?: (code: number) => void;
26
+ sleep?: (ms: number) => Promise<void>;
27
+ learnedOrigin?: () => string | null;
28
+ /** Otro protocolo con la central. De serie, `createEnvTransport`. */
29
+ transport?: EnvTransport;
30
+ }
31
+ export interface LoadEnvOptions {
32
+ /** Con `GROWTICKETS_ENV_ON_CHANGE=reload`: se llama tras poner en `process.env` una versión nueva. Sólo recibe NOMBRES, nunca valores. */
33
+ onEnvChange?: EnvChangeListener;
34
+ /**
35
+ * Con `GROWTICKETS_ENV_ON_CHANGE=restart` (de serie): el código de salida del cierre ordenado. Si no se pasa, el de `GROWTICKETS_ENV_EXIT_CODE`
36
+ * y, sin él, 75 (`EX_TEMPFAIL`): con 0 una política «sólo si falla» no reinicia el proceso y el producto se queda caído.
37
+ */
38
+ exitCode?: number;
39
+ /** `false` no arranca la revisión de cada minuto: sólo el arranque y los avisos. */
40
+ poll?: boolean;
41
+ }
42
+ export type LoadEnvStatus = 'off' | 'applied' | 'current' | 'offline' | 'empty';
43
+ export interface LoadEnvResult {
44
+ status: LoadEnvStatus;
45
+ version: number;
46
+ environment: string | null;
47
+ /** Cuántas variables se pusieron. Nunca sus nombres: este resultado acaba en el registro de arranque de alguien. */
48
+ count: number;
49
+ /** Los nombres (sólo nombres) que la política descartó. */
50
+ discarded: string[];
51
+ }
52
+ /**
53
+ * Todo el estado de las variables desde la central de UN proceso: lo que dice el entorno, la copia cifrada, lo aplicado, la revisión de cada
54
+ * minuto, los nonce de los avisos y los oyentes de `reload`. El arranque (`loadGrowTicketsEnv`), el middleware (la ruta del aviso) y la CLI
55
+ * hablan con esta clase y no entre ellos.
56
+ *
57
+ * ## Por qué hay UNA instancia y vive en `globalThis`
58
+ *
59
+ * El arranque corre en `main.ts`, fuera del contenedor de Nest, y la ruta del aviso en el middleware: tienen que compartir lo aplicado y el
60
+ * candado. `globalThis[Symbol.for(…)]` también resiste a que el producto acabe con dos copias de esta librería en `node_modules`.
61
+ *
62
+ * ## Apagada, no hace nada
63
+ *
64
+ * Sin credencial para la central (`transport.configured()`) no pregunta, no escucha avisos y no arranca ningún temporizador.
65
+ */
66
+ export declare class EnvRuntime {
67
+ private readonly deps;
68
+ readonly storage: EnvStorage;
69
+ readonly snapshot: EnvSnapshot;
70
+ readonly state: EnvState;
71
+ readonly transport: EnvTransport;
72
+ readonly applier: EnvApplier;
73
+ private readonly listeners;
74
+ private readonly poller;
75
+ private readonly warned;
76
+ private app;
77
+ private exitCode;
78
+ private booted;
79
+ constructor(deps: EnvRuntimeDeps);
80
+ settings(): EnvSettings;
81
+ enabled(): boolean;
82
+ now(): number;
83
+ warn(message: string): void;
84
+ /** La app de Nest, para cerrarla en orden al reiniciar. La deja el middleware de `main.ts`, que es quien la recibe. */
85
+ attachApp(app: EnvAppHandle | undefined | null): void;
86
+ /** Suscribe un oyente de `reload`. Devuelve la función que lo quita. */
87
+ onChange(listener: EnvChangeListener): () => void;
88
+ /**
89
+ * El arranque: baja las variables, las pone en `process.env` y deja en marcha la revisión de cada minuto. Si no se puede (la central no
90
+ * contesta y no hay una copia que sirva, o la clave está revocada) LANZA `EnvError` y el producto no arranca: es lo que `fail` y una
91
+ * clave muerta piden, y arrancar sin su configuración es peor que no arrancar. Sin credencial no hace nada.
92
+ */
93
+ load(options?: LoadEnvOptions): Promise<LoadEnvResult>;
94
+ /**
95
+ * Un aviso firmado de la central ya contestado: baja lo último y lo aplica. Nunca lanza. Si el producto no llamó a `loadGrowTicketsEnv()` en el
96
+ * arranque este proceso no tiene las variables de la central puestas, y reiniciarlo (o ponerlas ahora) no arreglaría nada: se ignora el aviso, se
97
+ * dice una vez por hora y la central ve que esta réplica no confirma.
98
+ */
99
+ pushed(): Promise<void>;
100
+ stopPolling(): void;
101
+ pollingActive(): boolean;
102
+ /** Una revisión: la misma que hace el temporizador cada minuto. Pública para las pruebas. */
103
+ poll(): Promise<PollHint>;
104
+ private emit;
105
+ /**
106
+ * El cierre ordenado de `restart`: con la app a mano, `app.close()` (que termina lo pendiente y manda los logs); sin ella, manda lo último
107
+ * de los logs y espera un instante a que se vacíen las respuestas. Después sale con el código pedido —`loadGrowTicketsEnv({ exitCode })`, o
108
+ * `GROWTICKETS_ENV_EXIT_CODE`, o 75 de serie—, y un tope de diez segundos impide que una conexión colgada deje al proceso vivo con las variables
109
+ * viejas. ⚠️ Es 75 y no 0 a propósito: Railway (política «On Failure» de serie), Docker `restart: on-failure` y systemd `Restart=on-failure` no
110
+ * reinician una salida con 0, y el producto se quedaría caído tras cada «Vincular». Con `restart: always`, PM2 o Kubernetes da igual; con un
111
+ * gestor al que un código distinto de 0 le parezca un fallo que alerta, se pone `GROWTICKETS_ENV_EXIT_CODE=0`.
112
+ */
113
+ private restart;
114
+ private warnOnce;
115
+ }
116
+ /** La instancia de este proceso, creada al primer uso. */
117
+ export declare function envRuntime(): EnvRuntime;
118
+ /** Sólo pruebas: instala (o quita, con `null`) la instancia del proceso. */
119
+ export declare function installEnvRuntime(runtime: EnvRuntime | null): void;
120
+ /** ¿Hay ya una instancia? El middleware lo pregunta antes de crear una: sin clave en el entorno no hay nada que crear. */
121
+ export declare function envRuntimeExists(): boolean;
122
+ export {};
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EnvRuntime = void 0;
4
+ exports.envRuntime = envRuntime;
5
+ exports.installEnvRuntime = installEnvRuntime;
6
+ exports.envRuntimeExists = envRuntimeExists;
7
+ const monitor_1 = require("../logs/monitor");
8
+ const runtime_1 = require("../security/runtime");
9
+ const applier_1 = require("./applier");
10
+ const errors_1 = require("./errors");
11
+ const overlay_1 = require("./overlay");
12
+ const poller_1 = require("./poller");
13
+ const settings_1 = require("./settings");
14
+ const snapshot_1 = require("./snapshot");
15
+ const state_1 = require("./state");
16
+ const storage_1 = require("./storage");
17
+ const transport_1 = require("./transport");
18
+ const POLL_INTERVAL_MS = 60_000;
19
+ const POLL_JITTER_MS = 15_000;
20
+ const POLL_SLOW_MS = 5 * 60_000;
21
+ const WARN_EVERY_SECONDS = 3600;
22
+ const RESTART_GRACE_MS = 250;
23
+ const RESTART_LIMIT_MS = 10_000;
24
+ const STOP_POLLING = new Set(['invalid_key', 'revoked_key', 'expired_key']);
25
+ /**
26
+ * Todo el estado de las variables desde la central de UN proceso: lo que dice el entorno, la copia cifrada, lo aplicado, la revisión de cada
27
+ * minuto, los nonce de los avisos y los oyentes de `reload`. El arranque (`loadGrowTicketsEnv`), el middleware (la ruta del aviso) y la CLI
28
+ * hablan con esta clase y no entre ellos.
29
+ *
30
+ * ## Por qué hay UNA instancia y vive en `globalThis`
31
+ *
32
+ * El arranque corre en `main.ts`, fuera del contenedor de Nest, y la ruta del aviso en el middleware: tienen que compartir lo aplicado y el
33
+ * candado. `globalThis[Symbol.for(…)]` también resiste a que el producto acabe con dos copias de esta librería en `node_modules`.
34
+ *
35
+ * ## Apagada, no hace nada
36
+ *
37
+ * Sin credencial para la central (`transport.configured()`) no pregunta, no escucha avisos y no arranca ningún temporizador.
38
+ */
39
+ class EnvRuntime {
40
+ constructor(deps) {
41
+ this.deps = deps;
42
+ this.listeners = new Set();
43
+ this.warned = new Map();
44
+ this.app = null;
45
+ this.exitCode = null;
46
+ this.booted = false;
47
+ this.transport = deps.transport ?? (0, transport_1.createEnvTransport)({ env: deps.env, fetchImpl: deps.fetchImpl, learnedOrigin: deps.learnedOrigin, sleep: deps.sleep, now: deps.now });
48
+ this.storage = new storage_1.EnvStorage({ dir: () => this.settings().dir, cwd: deps.cwd });
49
+ this.snapshot = new snapshot_1.EnvSnapshot(this.storage, () => this.transport.snapshotKey(), deps.now);
50
+ this.state = new state_1.EnvState(this.storage, deps.now);
51
+ this.applier = new applier_1.EnvApplier({
52
+ settings: () => this.settings(),
53
+ transport: this.transport,
54
+ snapshot: this.snapshot,
55
+ state: this.state,
56
+ overlay: new overlay_1.EnvOverlay(deps.env()),
57
+ now: deps.now,
58
+ onReload: (change) => this.emit(change),
59
+ onRestart: () => this.restart(),
60
+ warn: deps.warn,
61
+ });
62
+ this.poller = new poller_1.EnvPoller({ tick: () => this.poll(), intervalMs: POLL_INTERVAL_MS, jitterMs: POLL_JITTER_MS, slowMs: POLL_SLOW_MS, random: deps.random });
63
+ }
64
+ settings() {
65
+ return (0, settings_1.readEnvSettings)(this.deps.env());
66
+ }
67
+ enabled() {
68
+ return this.transport.configured();
69
+ }
70
+ now() {
71
+ return this.deps.now();
72
+ }
73
+ warn(message) {
74
+ this.deps.warn(message);
75
+ }
76
+ /** La app de Nest, para cerrarla en orden al reiniciar. La deja el middleware de `main.ts`, que es quien la recibe. */
77
+ attachApp(app) {
78
+ if (app && typeof app.close === 'function')
79
+ this.app = app;
80
+ }
81
+ /** Suscribe un oyente de `reload`. Devuelve la función que lo quita. */
82
+ onChange(listener) {
83
+ this.listeners.add(listener);
84
+ return () => {
85
+ this.listeners.delete(listener);
86
+ };
87
+ }
88
+ /**
89
+ * El arranque: baja las variables, las pone en `process.env` y deja en marcha la revisión de cada minuto. Si no se puede (la central no
90
+ * contesta y no hay una copia que sirva, o la clave está revocada) LANZA `EnvError` y el producto no arranca: es lo que `fail` y una
91
+ * clave muerta piden, y arrancar sin su configuración es peor que no arrancar. Sin credencial no hace nada.
92
+ */
93
+ async load(options = {}) {
94
+ if (!this.enabled())
95
+ return { status: 'off', version: 0, environment: null, count: 0, discarded: [] };
96
+ this.booted = true;
97
+ if (options.onEnvChange)
98
+ this.onChange(options.onEnvChange);
99
+ if (Number.isInteger(options.exitCode))
100
+ this.exitCode = options.exitCode;
101
+ for (const comprobacion of this.transport.diagnose())
102
+ if (comprobacion.level === 'fail')
103
+ this.deps.warn(`GrowTickets: ${comprobacion.text}`);
104
+ const resultado = await this.applier.apply({ phase: 'boot', network: true, wait: true, ackCurrent: true });
105
+ if (resultado.status === 'failed') {
106
+ throw new errors_1.EnvError(resultado.reason ?? 'apply_failed', `GrowTickets no pudo cargar las variables de entorno (${resultado.reason ?? 'apply_failed'}): ${resultado.message}`);
107
+ }
108
+ if (options.poll !== false && this.deps.timer)
109
+ this.poller.start();
110
+ const payload = resultado.payload;
111
+ const estado = resultado.status === 'applied' || resultado.status === 'offline' || resultado.status === 'empty' ? resultado.status : 'current';
112
+ return {
113
+ status: estado,
114
+ version: payload?.version ?? 0,
115
+ environment: payload?.environmentName ?? null,
116
+ count: payload ? Object.keys(payload.variables).length : 0,
117
+ discarded: payload?.discarded ?? [],
118
+ };
119
+ }
120
+ /**
121
+ * Un aviso firmado de la central ya contestado: baja lo último y lo aplica. Nunca lanza. Si el producto no llamó a `loadGrowTicketsEnv()` en el
122
+ * arranque este proceso no tiene las variables de la central puestas, y reiniciarlo (o ponerlas ahora) no arreglaría nada: se ignora el aviso, se
123
+ * dice una vez por hora y la central ve que esta réplica no confirma.
124
+ */
125
+ async pushed() {
126
+ if (!this.booted) {
127
+ this.warnOnce('push:not-booted', 'GrowTickets: llegó un aviso de variables de entorno pero loadGrowTicketsEnv() no se llamó en main.ts: se ignora.');
128
+ return;
129
+ }
130
+ try {
131
+ const resultado = await this.applier.apply({ phase: 'live', network: true, wait: true, ackCurrent: true });
132
+ if (resultado.status === 'failed')
133
+ this.warnOnce(`push:${resultado.reason}`, `GrowTickets: no se pudo aplicar el aviso de variables de entorno (${resultado.reason}): ${resultado.message}`);
134
+ }
135
+ catch {
136
+ this.warnOnce('push:crash', 'GrowTickets: falló la aplicación de un aviso de variables de entorno.');
137
+ }
138
+ }
139
+ stopPolling() {
140
+ this.poller.stop();
141
+ }
142
+ pollingActive() {
143
+ return this.poller.running();
144
+ }
145
+ /** Una revisión: la misma que hace el temporizador cada minuto. Pública para las pruebas. */
146
+ async poll() {
147
+ if (!this.enabled())
148
+ return 'stop';
149
+ try {
150
+ const resultado = await this.applier.apply({ phase: 'live', network: true, wait: false, ackCurrent: false });
151
+ if (resultado.reason && resultado.status !== 'empty') {
152
+ this.warnOnce(`poll:${resultado.reason}`, `GrowTickets: la revisión de variables de entorno falló (${resultado.reason}): ${resultado.message}`);
153
+ }
154
+ if (resultado.reason === 'rate_limited')
155
+ return 'slow';
156
+ if (resultado.status !== 'failed')
157
+ return 'continue';
158
+ return resultado.reason !== null && STOP_POLLING.has(resultado.reason) ? 'stop' : 'continue';
159
+ }
160
+ catch {
161
+ return 'continue';
162
+ }
163
+ }
164
+ async emit(change) {
165
+ for (const oyente of [...this.listeners]) {
166
+ try {
167
+ await oyente(change);
168
+ }
169
+ catch {
170
+ this.deps.warn('GrowTickets: un oyente de onEnvChange lanzó una excepción; las variables ya están puestas.');
171
+ }
172
+ }
173
+ }
174
+ /**
175
+ * El cierre ordenado de `restart`: con la app a mano, `app.close()` (que termina lo pendiente y manda los logs); sin ella, manda lo último
176
+ * de los logs y espera un instante a que se vacíen las respuestas. Después sale con el código pedido —`loadGrowTicketsEnv({ exitCode })`, o
177
+ * `GROWTICKETS_ENV_EXIT_CODE`, o 75 de serie—, y un tope de diez segundos impide que una conexión colgada deje al proceso vivo con las variables
178
+ * viejas. ⚠️ Es 75 y no 0 a propósito: Railway (política «On Failure» de serie), Docker `restart: on-failure` y systemd `Restart=on-failure` no
179
+ * reinician una salida con 0, y el producto se quedaría caído tras cada «Vincular». Con `restart: always`, PM2 o Kubernetes da igual; con un
180
+ * gestor al que un código distinto de 0 le parezca un fallo que alerta, se pone `GROWTICKETS_ENV_EXIT_CODE=0`.
181
+ */
182
+ async restart() {
183
+ const salir = this.deps.exit ?? ((code) => process.exit(code));
184
+ const codigo = this.exitCode ?? this.settings().exitCode;
185
+ const tope = setTimeout(() => salir(codigo), RESTART_LIMIT_MS);
186
+ try {
187
+ if (this.app?.close) {
188
+ await this.app.close();
189
+ }
190
+ else {
191
+ await (0, monitor_1.logMonitor)()?.shutdown();
192
+ await (this.deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))))(RESTART_GRACE_MS);
193
+ }
194
+ }
195
+ catch {
196
+ this.deps.warn('GrowTickets: el cierre ordenado falló; se sale igualmente para tomar las variables nuevas.');
197
+ }
198
+ salir(codigo);
199
+ clearTimeout(tope);
200
+ }
201
+ warnOnce(clave, mensaje) {
202
+ const ahora = this.deps.now();
203
+ const ultimo = this.warned.get(clave);
204
+ if (ultimo !== undefined && ahora - ultimo < WARN_EVERY_SECONDS)
205
+ return;
206
+ this.warned.set(clave, ahora);
207
+ this.deps.warn(mensaje);
208
+ }
209
+ }
210
+ exports.EnvRuntime = EnvRuntime;
211
+ const KEY = Symbol.for('growtickets.nest.env');
212
+ /** La instancia de este proceso, creada al primer uso. */
213
+ function envRuntime() {
214
+ const global = globalThis;
215
+ global[KEY] ??= new EnvRuntime({
216
+ env: () => process.env,
217
+ now: () => Math.floor(Date.now() / 1000),
218
+ cwd: () => process.cwd(),
219
+ warn: (message) => {
220
+ process.stderr.write(`${message}\n`);
221
+ },
222
+ timer: true,
223
+ learnedOrigin: () => (0, runtime_1.securityRuntime)().origin(),
224
+ });
225
+ return global[KEY];
226
+ }
227
+ /** Sólo pruebas: instala (o quita, con `null`) la instancia del proceso. */
228
+ function installEnvRuntime(runtime) {
229
+ const global = globalThis;
230
+ if (runtime)
231
+ global[KEY] = runtime;
232
+ else
233
+ delete global[KEY];
234
+ }
235
+ /** ¿Hay ya una instancia? El middleware lo pregunta antes de crear una: sin clave en el entorno no hay nada que crear. */
236
+ function envRuntimeExists() {
237
+ return globalThis[KEY] !== undefined;
238
+ }
@@ -0,0 +1,26 @@
1
+ type Env = Record<string, string | undefined>;
2
+ export declare const DEFAULT_ENV_PATH = "growtickets/env";
3
+ export type EnvOnFailure = 'keep' | 'fail';
4
+ export type EnvOnChange = 'restart' | 'reload';
5
+ export interface EnvSettings {
6
+ /** `GROWTICKETS_ENV_PATH`: por dónde avisa la central (`POST {path}/{operación}`), sin barras en los extremos. */
7
+ path: string;
8
+ onFailure: EnvOnFailure;
9
+ maxStaleHours: number;
10
+ onChange: EnvOnChange;
11
+ /** `GROWTICKETS_ENV_DIR`: dónde guarda lo suyo (copia cifrada y estado); `null` = la carpeta de serie. */
12
+ dir: string | null;
13
+ /** `GROWTICKETS_ENV_EXIT_CODE`: el código con que sale el proceso al reiniciar para tomar una versión nueva (`restart`). */
14
+ exitCode: number;
15
+ }
16
+ /** 75 = `EX_TEMPFAIL` de `sysexits.h`: «vuelve a intentarlo». Una salida con 0 no la reinicia una política «sólo si falla» (Railway de serie, Docker `on-failure`). */
17
+ export declare const DEFAULT_EXIT_CODE = 75;
18
+ /**
19
+ * Lo que el entorno dice de CÓMO se comporta la pieza, con los valores de serie en un solo sitio. NUNCA lanza: una variable mal escrita cae a
20
+ * su valor por defecto. Se lee en cada llamada (es un objeto en memoria) para que un cambio de entorno —y las pruebas— se noten.
21
+ *
22
+ * Nada de aquí dice cómo se autentica ni se cifra la conversación con la central: la credencial, la dirección y la firma son del transporte
23
+ * (`env/transport`), que es la única pieza que cambiaría con otra versión del protocolo.
24
+ */
25
+ export declare function readEnvSettings(env: Env): EnvSettings;
26
+ export {};