@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.
- package/.env.example +8 -0
- package/README.md +319 -6
- package/dist/cli/args.d.ts +9 -1
- package/dist/cli/args.js +13 -4
- package/dist/cli/check.d.ts +4 -1
- package/dist/cli/check.js +47 -2
- package/dist/cli/env-example.js +17 -0
- package/dist/cli/env.d.ts +34 -0
- package/dist/cli/env.js +125 -0
- package/dist/cli/index.js +35 -1
- package/dist/cli/init.js +5 -1
- package/dist/client/ingest-client.js +6 -1
- package/dist/env/applier.d.ts +83 -0
- package/dist/env/applier.js +162 -0
- package/dist/env/errors.d.ts +12 -0
- package/dist/env/errors.js +18 -0
- package/dist/env/load.d.ts +35 -0
- package/dist/env/load.js +43 -0
- package/dist/env/overlay.d.ts +23 -0
- package/dist/env/overlay.js +48 -0
- package/dist/env/policy.d.ts +30 -0
- package/dist/env/policy.js +101 -0
- package/dist/env/poller.d.ts +27 -0
- package/dist/env/poller.js +54 -0
- package/dist/env/runtime.d.ts +122 -0
- package/dist/env/runtime.js +238 -0
- package/dist/env/settings.d.ts +26 -0
- package/dist/env/settings.js +41 -0
- package/dist/env/snapshot.d.ts +32 -0
- package/dist/env/snapshot.js +85 -0
- package/dist/env/state.d.ts +27 -0
- package/dist/env/state.js +51 -0
- package/dist/env/storage.d.ts +33 -0
- package/dist/env/storage.js +133 -0
- package/dist/env/transport/client.d.ts +58 -0
- package/dist/env/transport/client.js +249 -0
- package/dist/env/transport/config.d.ts +20 -0
- package/dist/env/transport/config.js +38 -0
- package/dist/env/transport/credential.d.ts +33 -0
- package/dist/env/transport/credential.js +76 -0
- package/dist/env/transport/gt2-transport.d.ts +53 -0
- package/dist/env/transport/gt2-transport.js +162 -0
- package/dist/env/transport/index.d.ts +10 -0
- package/dist/env/transport/index.js +17 -0
- package/dist/env/transport/keygen.d.ts +32 -0
- package/dist/env/transport/keygen.js +77 -0
- package/dist/env/transport/nonce-cache.d.ts +18 -0
- package/dist/env/transport/nonce-cache.js +37 -0
- package/dist/env/transport/protocol.d.ts +100 -0
- package/dist/env/transport/protocol.js +292 -0
- package/dist/env/transport/types.d.ts +78 -0
- package/dist/env/transport/types.js +2 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.js +18 -1
- package/dist/logs/buffer.d.ts +25 -0
- package/dist/logs/buffer.js +58 -0
- package/dist/logs/doctor.d.ts +17 -0
- package/dist/logs/doctor.js +66 -0
- package/dist/logs/entry-builder.d.ts +20 -0
- package/dist/logs/entry-builder.js +127 -0
- package/dist/logs/flusher.d.ts +72 -0
- package/dist/logs/flusher.js +189 -0
- package/dist/logs/format.d.ts +4 -0
- package/dist/logs/format.js +76 -0
- package/dist/logs/levels.d.ts +12 -0
- package/dist/logs/levels.js +55 -0
- package/dist/logs/monitor.d.ts +92 -0
- package/dist/logs/monitor.js +224 -0
- package/dist/logs/normalize.d.ts +14 -0
- package/dist/logs/normalize.js +114 -0
- package/dist/logs/pino-line.d.ts +13 -0
- package/dist/logs/pino-line.js +52 -0
- package/dist/logs/redactor.d.ts +38 -0
- package/dist/logs/redactor.js +146 -0
- package/dist/logs/remote-config.d.ts +37 -0
- package/dist/logs/remote-config.js +89 -0
- package/dist/logs/runtime.d.ts +24 -0
- package/dist/logs/runtime.js +59 -0
- package/dist/logs/sampler.d.ts +15 -0
- package/dist/logs/sampler.js +36 -0
- package/dist/logs/sender.d.ts +20 -0
- package/dist/logs/sender.js +64 -0
- package/dist/logs/settings.d.ts +22 -0
- package/dist/logs/settings.js +30 -0
- package/dist/logs/types.d.ts +74 -0
- package/dist/logs/types.js +9 -0
- package/dist/logs-nest/api.d.ts +5 -0
- package/dist/logs-nest/api.js +13 -0
- package/dist/logs-nest/host.d.ts +10 -0
- package/dist/logs-nest/host.js +45 -0
- package/dist/logs-nest/lifecycle.d.ts +20 -0
- package/dist/logs-nest/lifecycle.js +40 -0
- package/dist/logs-nest/nest-args.d.ts +7 -0
- package/dist/logs-nest/nest-args.js +66 -0
- package/dist/logs-nest/service.d.ts +25 -0
- package/dist/logs-nest/service.js +61 -0
- package/dist/logs-nest/stream.d.ts +21 -0
- package/dist/logs-nest/stream.js +32 -0
- package/dist/maintenance/feed.d.ts +68 -0
- package/dist/maintenance/feed.js +160 -0
- package/dist/maintenance/http.d.ts +67 -0
- package/dist/maintenance/http.js +177 -0
- package/dist/maintenance/page.d.ts +11 -0
- package/dist/maintenance/page.js +208 -0
- package/dist/maintenance/settings.d.ts +24 -0
- package/dist/maintenance/settings.js +42 -0
- package/dist/maintenance/windows.d.ts +78 -0
- package/dist/maintenance/windows.js +167 -0
- package/dist/nest/csp-response.d.ts +19 -0
- package/dist/nest/csp-response.js +84 -0
- package/dist/nest/env-push.d.ts +35 -0
- package/dist/nest/env-push.js +110 -0
- package/dist/nest/growtickets.module.js +11 -3
- package/dist/nest/maintenance-request.d.ts +12 -0
- package/dist/nest/maintenance-request.js +30 -0
- package/dist/nest/maintenance.guard.d.ts +28 -0
- package/dist/nest/maintenance.guard.js +90 -0
- package/dist/nest/options.d.ts +15 -0
- package/dist/nest/request.d.ts +1 -1
- package/dist/nest/request.js +2 -2
- package/dist/nest/route-tag.d.ts +6 -0
- package/dist/nest/route-tag.js +13 -0
- package/dist/nest/security.middleware.d.ts +10 -4
- package/dist/nest/security.middleware.js +77 -18
- package/dist/nest/services.js +1 -1
- package/dist/proxy/handle-portal-request.js +2 -1
- package/dist/proxy/types.d.ts +1 -0
- package/dist/proxy/unread.js +1 -1
- package/dist/security/client-ip.d.ts +15 -1
- package/dist/security/client-ip.js +20 -2
- package/dist/security/csp.d.ts +58 -0
- package/dist/security/csp.js +109 -0
- package/dist/security/flusher.js +4 -2
- package/dist/security/proxy-chain.d.ts +35 -0
- package/dist/security/proxy-chain.js +58 -0
- package/dist/security/remote-config.d.ts +4 -0
- package/dist/security/remote-config.js +7 -0
- package/dist/security/request-ip.d.ts +3 -2
- package/dist/security/request-ip.js +6 -3
- package/dist/security/runtime.d.ts +8 -0
- package/dist/security/runtime.js +17 -0
- package/dist/security/sdk.d.ts +7 -1
- package/dist/security/sdk.js +27 -1
- package/dist/security/settings.d.ts +3 -1
- package/dist/security/settings.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type KeygenOptions } from '../env/transport/keygen';
|
|
2
|
+
export interface EnvCliOutcome {
|
|
3
|
+
exitCode: number;
|
|
4
|
+
output: string;
|
|
5
|
+
}
|
|
6
|
+
export interface EnvCliDeps {
|
|
7
|
+
env: Record<string, string | undefined>;
|
|
8
|
+
cwd: string;
|
|
9
|
+
fetchImpl?: typeof fetch;
|
|
10
|
+
/** Segundos, época Unix. */
|
|
11
|
+
now?: () => number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* `growtickets-nest env pull`: baja lo que la central tiene para este servidor y escribe la COPIA LOCAL cifrada (la que sirve para arrancar
|
|
15
|
+
* si la central no contesta), SIN poner nada en el entorno de ningún proceso. Dice la versión y CUÁNTAS variables: nunca sus nombres ni sus
|
|
16
|
+
* valores, que se quedarían en el historial de la terminal y en el registro del despliegue. Útil en el despliegue para dejar sembrada la
|
|
17
|
+
* copia, y para comprobar que la clave funciona. Respeta el «no se retrocede»: no pisa una copia de una versión posterior del mismo entorno.
|
|
18
|
+
*/
|
|
19
|
+
export declare function runEnvPull(deps: EnvCliDeps): Promise<EnvCliOutcome>;
|
|
20
|
+
/**
|
|
21
|
+
* `growtickets-nest env status`: el DOCTOR de las variables desde la central. No habla con la red ni escribe nada: lee el entorno y los
|
|
22
|
+
* archivos de la pieza y dice qué versión tiene aplicada este servidor, la edad de la copia local y si todo lo necesario para que «Vincular» le
|
|
23
|
+
* llegue está en su sitio. Nunca enseña un valor ni un nombre de variable aplicada; de la clave, sólo su prefijo.
|
|
24
|
+
*
|
|
25
|
+
* `✓` está bien, `!` funciona pero conviene mirarlo, `✗` impide aplicar: sale con 1 si hay alguno.
|
|
26
|
+
*/
|
|
27
|
+
export declare function runEnvStatus(deps: EnvCliDeps): EnvCliOutcome;
|
|
28
|
+
export type EnvKeygenOptions = KeygenOptions;
|
|
29
|
+
/**
|
|
30
|
+
* `growtickets-nest env keygen`: genera el par de ESTE servidor aquí mismo, para que su llave privada no pase nunca por un navegador ni por un
|
|
31
|
+
* portapapeles (ver `runKeygen`, que es del transporte: sabe qué es una credencial). Sin `--central` genera la semilla y enseña sólo la
|
|
32
|
+
* pública; con `--central=<llave de la central>` arma la credencial y, con `--write`, la escribe en el `.env`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function runEnvKeygen(deps: EnvCliDeps, options: EnvKeygenOptions): EnvCliOutcome;
|
package/dist/cli/env.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runEnvPull = runEnvPull;
|
|
4
|
+
exports.runEnvStatus = runEnvStatus;
|
|
5
|
+
exports.runEnvKeygen = runEnvKeygen;
|
|
6
|
+
const errors_1 = require("../env/errors");
|
|
7
|
+
const runtime_1 = require("../env/runtime");
|
|
8
|
+
const keygen_1 = require("../env/transport/keygen");
|
|
9
|
+
const sdk_1 = require("../security/sdk");
|
|
10
|
+
/** Un runtime privado para el proceso de la CLI: lee el entorno que se le da y NO se instala en `globalThis`, ni arranca temporizadores. */
|
|
11
|
+
function cliRuntime(deps) {
|
|
12
|
+
return new runtime_1.EnvRuntime({
|
|
13
|
+
env: () => deps.env,
|
|
14
|
+
fetchImpl: deps.fetchImpl,
|
|
15
|
+
now: deps.now ?? (() => Math.floor(Date.now() / 1000)),
|
|
16
|
+
cwd: () => deps.cwd,
|
|
17
|
+
warn: () => undefined,
|
|
18
|
+
timer: false,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* `growtickets-nest env pull`: baja lo que la central tiene para este servidor y escribe la COPIA LOCAL cifrada (la que sirve para arrancar
|
|
23
|
+
* si la central no contesta), SIN poner nada en el entorno de ningún proceso. Dice la versión y CUÁNTAS variables: nunca sus nombres ni sus
|
|
24
|
+
* valores, que se quedarían en el historial de la terminal y en el registro del despliegue. Útil en el despliegue para dejar sembrada la
|
|
25
|
+
* copia, y para comprobar que la clave funciona. Respeta el «no se retrocede»: no pisa una copia de una versión posterior del mismo entorno.
|
|
26
|
+
*/
|
|
27
|
+
async function runEnvPull(deps) {
|
|
28
|
+
const runtime = cliRuntime(deps);
|
|
29
|
+
if (!runtime.enabled())
|
|
30
|
+
return { exitCode: 1, output: `✗ ${runtime.transport.diagnose()[0]?.text ?? 'Está apagada.'}` };
|
|
31
|
+
let payload;
|
|
32
|
+
try {
|
|
33
|
+
payload = await runtime.transport.fetch();
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
const fallo = error instanceof errors_1.EnvError ? error : new errors_1.EnvError('unreachable', 'No se pudo hablar con la central.');
|
|
37
|
+
return { exitCode: 1, output: `✗ ${fallo.message} (${fallo.reason})` };
|
|
38
|
+
}
|
|
39
|
+
if (payload === null)
|
|
40
|
+
return { exitCode: 1, output: '✗ La central no mandó nada.' };
|
|
41
|
+
const copia = runtime.snapshot.read();
|
|
42
|
+
const estado = runtime.state.read();
|
|
43
|
+
const minimo = Math.max(copia && copia.payload.environmentId === payload.environmentId ? copia.payload.version : 0, estado.environment === payload.environmentId ? estado.version : 0);
|
|
44
|
+
if (payload.version < minimo) {
|
|
45
|
+
return { exitCode: 1, output: `✗ La central dio la versión ${payload.version}, anterior a la ${minimo} que ya tiene este servidor: no se retrocede (rollback).` };
|
|
46
|
+
}
|
|
47
|
+
if (!runtime.snapshot.write(payload)) {
|
|
48
|
+
return { exitCode: 1, output: '✗ No se pudo escribir la copia local (¿carpeta sin permisos? GROWTICKETS_ENV_DIR fija otra).' };
|
|
49
|
+
}
|
|
50
|
+
const cuantas = Object.keys(payload.variables).length;
|
|
51
|
+
const lineas = [`✓ «${payload.environmentName}» v${payload.version}: ${cuantas} variables. Copia local cifrada escrita en ${runtime.storage.directory() ?? '(sin carpeta)'}.`];
|
|
52
|
+
if (payload.discarded.length > 0)
|
|
53
|
+
lineas.push(` ${payload.discarded.length} variables se ignoran por política (prefijo GROWTICKETS_, las del sistema…): no se pondrán en el entorno.`);
|
|
54
|
+
return { exitCode: 0, output: lineas.join('\n') };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `growtickets-nest env status`: el DOCTOR de las variables desde la central. No habla con la red ni escribe nada: lee el entorno y los
|
|
58
|
+
* archivos de la pieza y dice qué versión tiene aplicada este servidor, la edad de la copia local y si todo lo necesario para que «Vincular» le
|
|
59
|
+
* llegue está en su sitio. Nunca enseña un valor ni un nombre de variable aplicada; de la clave, sólo su prefijo.
|
|
60
|
+
*
|
|
61
|
+
* `✓` está bien, `!` funciona pero conviene mirarlo, `✗` impide aplicar: sale con 1 si hay alguno.
|
|
62
|
+
*/
|
|
63
|
+
function runEnvStatus(deps) {
|
|
64
|
+
const runtime = cliRuntime(deps);
|
|
65
|
+
const ajustes = runtime.settings();
|
|
66
|
+
const ahora = (deps.now ?? (() => Math.floor(Date.now() / 1000)))();
|
|
67
|
+
const lineas = ['', ' GrowTickets · variables de entorno', ''];
|
|
68
|
+
let fallos = 0;
|
|
69
|
+
const ok = (texto) => lineas.push(` ✓ ${texto}`);
|
|
70
|
+
const aviso = (texto) => lineas.push(` ! ${texto}`);
|
|
71
|
+
const fallo = (texto) => {
|
|
72
|
+
fallos++;
|
|
73
|
+
lineas.push(` ✗ ${texto}`);
|
|
74
|
+
};
|
|
75
|
+
if (!runtime.enabled()) {
|
|
76
|
+
for (const c of runtime.transport.diagnose())
|
|
77
|
+
aviso(c.text);
|
|
78
|
+
return { exitCode: 0, output: [...lineas, ''].join('\n') };
|
|
79
|
+
}
|
|
80
|
+
for (const c of runtime.transport.diagnose())
|
|
81
|
+
(c.level === 'ok' ? ok : c.level === 'warn' ? aviso : fallo)(c.text);
|
|
82
|
+
const aplicada = runtime.state.read();
|
|
83
|
+
if (aplicada.applied === null)
|
|
84
|
+
aviso('Este servidor todavía no ha aplicado ninguna versión (¿falta loadGrowTicketsEnv() en main.ts?).');
|
|
85
|
+
else
|
|
86
|
+
ok(`Aplicada la versión ${aplicada.version} del entorno ${aplicada.environment} (${aplicada.count} variables), ${hace(ahora, aplicada.appliedAt)}.`);
|
|
87
|
+
if (aplicada.discarded.length > 0)
|
|
88
|
+
aviso(`Se ignoran por política: ${aplicada.discarded.join(', ')} (el prefijo GROWTICKETS_ y las del sistema no las puede poner la central).`);
|
|
89
|
+
const copia = runtime.snapshot.read();
|
|
90
|
+
const edad = runtime.snapshot.ageHours(copia);
|
|
91
|
+
if (copia === null) {
|
|
92
|
+
aviso('No hay copia local legible: si la central no contesta al arrancar, no hay con qué seguir. Corre growtickets-nest env pull.');
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
ok(`Copia local cifrada: v${copia.payload.version}, confirmada ${hace(ahora, copia.fetchedAt)}.`);
|
|
96
|
+
if (edad !== null && edad > ajustes.maxStaleHours)
|
|
97
|
+
aviso(`La copia tiene más de ${ajustes.maxStaleHours} h (GROWTICKETS_ENV_MAX_STALE_HOURS): ya no sirve para arrancar sin la central.`);
|
|
98
|
+
}
|
|
99
|
+
lineas.push(` · Esta réplica: ${(0, sdk_1.instanceId)(deps.env)}`);
|
|
100
|
+
lineas.push(` · La central avisa en: POST /${ajustes.path}/apply y confirma el dominio en GET /${ajustes.path}/identity (fuera del prefijo global; los contesta el middleware de main.ts)`);
|
|
101
|
+
lineas.push(` · Si la central no contesta: ${ajustes.onFailure === 'fail' ? 'no se arranca' : `se arranca con la copia local (hasta ${ajustes.maxStaleHours} h)`}`);
|
|
102
|
+
lineas.push(` · Al cambiar una versión: ${ajustes.onChange === 'reload' ? 'se actualiza process.env en caliente y se llama a onEnvChange' : `cierre ordenado del proceso con código ${ajustes.exitCode} (GROWTICKETS_ENV_EXIT_CODE; el gestor tiene que levantarlo otra vez: Railway «On Failure», Docker restart, PM2)`}`);
|
|
103
|
+
lineas.push('');
|
|
104
|
+
return { exitCode: fallos > 0 ? 1 : 0, output: lineas.join('\n') };
|
|
105
|
+
}
|
|
106
|
+
function hace(ahora, momento) {
|
|
107
|
+
if (momento <= 0)
|
|
108
|
+
return 'en un momento desconocido';
|
|
109
|
+
const minutos = Math.floor(Math.max(0, ahora - momento) / 60);
|
|
110
|
+
if (minutos < 1)
|
|
111
|
+
return 'hace un momento';
|
|
112
|
+
if (minutos < 60)
|
|
113
|
+
return `hace ${minutos} min`;
|
|
114
|
+
if (minutos < 1440)
|
|
115
|
+
return `hace ${Math.floor(minutos / 60)} h`;
|
|
116
|
+
return `hace ${Math.floor(minutos / 1440)} d`;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* `growtickets-nest env keygen`: genera el par de ESTE servidor aquí mismo, para que su llave privada no pase nunca por un navegador ni por un
|
|
120
|
+
* portapapeles (ver `runKeygen`, que es del transporte: sabe qué es una credencial). Sin `--central` genera la semilla y enseña sólo la
|
|
121
|
+
* pública; con `--central=<llave de la central>` arma la credencial y, con `--write`, la escribe en el `.env`.
|
|
122
|
+
*/
|
|
123
|
+
function runEnvKeygen(deps, options) {
|
|
124
|
+
return (0, keygen_1.runKeygen)(cliRuntime(deps).storage, deps.cwd, options);
|
|
125
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -5,18 +5,29 @@ exports.runCli = runCli;
|
|
|
5
5
|
const args_1 = require("./args");
|
|
6
6
|
const check_1 = require("./check");
|
|
7
7
|
const deps_1 = require("./deps");
|
|
8
|
+
const env_1 = require("./env");
|
|
8
9
|
const init_1 = require("./init");
|
|
10
|
+
const transport_1 = require("../env/transport");
|
|
11
|
+
const host_1 = require("../logs-nest/host");
|
|
12
|
+
const doctor_1 = require("../logs/doctor");
|
|
9
13
|
const HELP = [
|
|
10
|
-
'growtickets-nest <init|check|deps> [opciones]',
|
|
14
|
+
'growtickets-nest <init|check|deps|doctor|env> [opciones]',
|
|
11
15
|
'',
|
|
12
16
|
' init Crea o completa .env.example y .gitignore, e imprime cómo montar el módulo y el middleware de main.ts.',
|
|
13
17
|
' --dry-run No escribe nada; sólo imprime lo que haría.',
|
|
14
18
|
' check De sólo lectura. Sale con 1 si la clave puede acabar versionada, y avisa de un montaje que no hará lo que se espera.',
|
|
19
|
+
' doctor Comprueba los logs de punta a punta: el entorno, la clave con el alcance «Logs» y el módulo encendido en el proyecto. Los fallos de esta parte no dan error, sólo un explorador vacío. Con GROWTICKETS_ENV_KEY suma el informe de env status.',
|
|
20
|
+
' --send Además manda una línea de prueba y comprueba que llega.',
|
|
15
21
|
' deps Manda a GrowTickets los nombres y versiones de tus paquetes (de package-lock.json, pnpm-lock.yaml o yarn.lock) para vigilar sus vulnerabilidades. Pensado para CI.',
|
|
16
22
|
' --lockfile <ruta> El archivo de bloqueo, en vez de buscarlo en el directorio actual.',
|
|
17
23
|
' --app <nombre> Cómo aparece el proyecto en el informe (por defecto, el name del package.json).',
|
|
18
24
|
' --origin <url> La dirección pública del producto (o GROWTICKETS_ORIGIN): distingue este servidor de otro con el mismo nombre.',
|
|
19
25
|
' --dry-run Cuenta lo que se enviaría, sin enviar nada. Necesita GROWTICKETS_API_KEY para enviar de verdad.',
|
|
26
|
+
' env Las variables de entorno que GROWTICKETS_ENV_KEY trae desde la central (nunca imprime nombres de secretos ni valores).',
|
|
27
|
+
' env pull Baja la versión vigente y escribe la copia local cifrada; dice la versión y cuántas variables.',
|
|
28
|
+
' env status Versión aplicada, edad de la copia y qué falta para que «Vincular» llegue. No usa la red.',
|
|
29
|
+
' env keygen Genera aquí el par de este servidor (la privada no sale de él) y enseña sólo la pública para «Ya tengo una llave pública».',
|
|
30
|
+
' Después: env keygen --central=<llave de la central> --write arma GROWTICKETS_ENV_KEY en el .env (--force: otra llave).',
|
|
20
31
|
].join('\n');
|
|
21
32
|
/**
|
|
22
33
|
* La entrada que usa el binario: igual que `runCli` pero además atiende `deps`, el único comando que habla con la red
|
|
@@ -24,12 +35,35 @@ const HELP = [
|
|
|
24
35
|
*/
|
|
25
36
|
async function runCliAsync(argv, cwd = process.cwd(), env = process.env, fetchImpl) {
|
|
26
37
|
const parsed = (0, args_1.parseArgs)(argv);
|
|
38
|
+
if (parsed.command === 'env')
|
|
39
|
+
return runEnvCommand(argv, cwd, env, fetchImpl);
|
|
40
|
+
if (parsed.command === 'doctor') {
|
|
41
|
+
if (parsed.unknown.length > 0)
|
|
42
|
+
return { exitCode: 1, output: `Opción desconocida: ${parsed.unknown.join(', ')}\n\n${HELP}` };
|
|
43
|
+
const { exitCode, lines } = await (0, doctor_1.runLogsDoctor)({ ...(0, host_1.createLogsHost)(), env: () => env, fetchImpl }, { send: parsed.send });
|
|
44
|
+
const informeEnv = (0, transport_1.envConfigured)(env) ? (0, env_1.runEnvStatus)({ env, cwd }) : null;
|
|
45
|
+
return { exitCode: Math.max(exitCode, informeEnv?.exitCode ?? 0), output: [...lines, ...(informeEnv ? [informeEnv.output] : [])].join('\n') };
|
|
46
|
+
}
|
|
27
47
|
if (parsed.command !== 'deps')
|
|
28
48
|
return runCli(argv, cwd);
|
|
29
49
|
if (parsed.unknown.length > 0)
|
|
30
50
|
return { exitCode: 1, output: `Opción desconocida: ${parsed.unknown.join(', ')}\n\n${HELP}` };
|
|
31
51
|
return (0, deps_1.runDeps)(cwd, { lockfile: parsed.lockfile, app: parsed.app, origin: parsed.origin, dryRun: parsed.dryRun }, env, fetchImpl);
|
|
32
52
|
}
|
|
53
|
+
/** `env pull`, `env status` y `env keygen`: el subcomando va justo después de `env`; sólo `keygen` admite opciones. */
|
|
54
|
+
async function runEnvCommand(argv, cwd, env, fetchImpl) {
|
|
55
|
+
const sub = argv[1];
|
|
56
|
+
const resto = (0, args_1.parseArgs)(['env', ...argv.slice(2)]);
|
|
57
|
+
if (sub !== 'pull' && sub !== 'status' && sub !== 'keygen')
|
|
58
|
+
return { exitCode: 1, output: `Subcomando de env desconocido: ${sub ?? '(falta)'}. Usa env pull, env status o env keygen.\n\n${HELP}` };
|
|
59
|
+
if (resto.unknown.length > 0)
|
|
60
|
+
return { exitCode: 1, output: `Opción desconocida: ${resto.unknown.join(', ')}\n\n${HELP}` };
|
|
61
|
+
if (sub === 'keygen')
|
|
62
|
+
return (0, env_1.runEnvKeygen)({ env, cwd }, { central: resto.central, write: resto.write, force: resto.force });
|
|
63
|
+
if (resto.central !== null || resto.write || resto.force)
|
|
64
|
+
return { exitCode: 1, output: `Esas opciones son de env keygen.\n\n${HELP}` };
|
|
65
|
+
return sub === 'pull' ? (0, env_1.runEnvPull)({ env, cwd, fetchImpl }) : (0, env_1.runEnvStatus)({ env, cwd });
|
|
66
|
+
}
|
|
33
67
|
/**
|
|
34
68
|
* El único punto donde se lee `process.cwd()` de verdad: todo lo demás (`runInit`, `runCheck`) recibe
|
|
35
69
|
* el directorio como parámetro explícito, así que las pruebas pueden apuntar a un `mkdtempSync` sin
|
package/dist/cli/init.js
CHANGED
|
@@ -69,5 +69,9 @@ const NEXT_STEPS = [
|
|
|
69
69
|
" app.useBodyParser('json', { limit: '256kb' }); // sólo con Express: su tope de serie es 100 KB",
|
|
70
70
|
' 4. Seguridad (opcional, apagada por defecto): GROWTICKETS_SECURITY=true y, detrás de un balanceador,',
|
|
71
71
|
' GROWTICKETS_TRUSTED_PROXY_HOPS (1 = Railway). Marca tus rutas de login con @GrowTicketsLogin({ identifier: \'email\' }).',
|
|
72
|
-
' 5.
|
|
72
|
+
' 5. (Opcional) Variables de entorno desde GrowTickets: GROWTICKETS_ENV_KEY (la clave de servidor que da «Conectar un servidor» en la',
|
|
73
|
+
' pestaña «Entornos», NUNCA en .env.example) y, en main.ts, ANTES de NestFactory.create:',
|
|
74
|
+
" await loadGrowTicketsEnv(); // import { loadGrowTicketsEnv } from '@growconnect/growtickets-nest'",
|
|
75
|
+
' Añade .growtickets/ al .gitignore (la copia local cifrada). Se comprueba con: npx growtickets-nest env status',
|
|
76
|
+
' 6. Corre: npx growtickets-nest check',
|
|
73
77
|
].join('\n');
|
|
@@ -55,6 +55,10 @@ function raceConnect(promise, ms, controller) {
|
|
|
55
55
|
});
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* `redirect: 'error'`: una redirección se llevaría `X-Api-Key` a otro sitio (undici sólo quita `Authorization` y las cookies al
|
|
60
|
+
* cambiar de origen). La central nunca redirige `api/ingest`; si alguien lo hace por medio, es un fallo y no un camino.
|
|
61
|
+
*/
|
|
58
62
|
function buildRequestInit(config, init, signal) {
|
|
59
63
|
/*
|
|
60
64
|
* `X-GrowTickets-Proxy` va SIEMPRE: dice qué versión de este paquete habla con la mesa de ayuda
|
|
@@ -65,10 +69,11 @@ function buildRequestInit(config, init, signal) {
|
|
|
65
69
|
const headers = {
|
|
66
70
|
...init.headers,
|
|
67
71
|
'X-GrowTickets-Proxy': `${sdk_1.SDK_NAME}/${sdk_1.SDK_VERSION}`,
|
|
72
|
+
'X-GrowTickets-Runtime': (0, sdk_1.runtimeHeader)(),
|
|
68
73
|
'X-Api-Key': config.apiKey.reveal(),
|
|
69
74
|
Accept: 'application/json',
|
|
70
75
|
};
|
|
71
|
-
const requestInit = { method: init.method, headers, signal };
|
|
76
|
+
const requestInit = { method: init.method, headers, signal, redirect: 'error' };
|
|
72
77
|
if (init.method === 'POST') {
|
|
73
78
|
headers['Content-Type'] = 'application/json';
|
|
74
79
|
requestInit.body = JSON.stringify(init.data ?? {});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { EnvOverlay } from './overlay';
|
|
2
|
+
import { type EnvPayload } from './policy';
|
|
3
|
+
import type { EnvSettings } from './settings';
|
|
4
|
+
import type { EnvSnapshot } from './snapshot';
|
|
5
|
+
import type { EnvState } from './state';
|
|
6
|
+
import type { EnvTransport } from './transport';
|
|
7
|
+
/** `applied` (versión nueva puesta), `current` (ya estaba al día), `offline` (la central no contestó y se aplicó la copia), `empty` (el entorno aún no tiene versión vinculada), `failed` o `busy`. */
|
|
8
|
+
export type EnvStatus = 'applied' | 'current' | 'offline' | 'empty' | 'failed' | 'busy';
|
|
9
|
+
/** Lo que cambió al aplicar en caliente. Sólo nombres: ni un valor viaja a quien escucha. */
|
|
10
|
+
export interface EnvChange {
|
|
11
|
+
version: number;
|
|
12
|
+
environmentId: number;
|
|
13
|
+
environmentName: string;
|
|
14
|
+
changed: string[];
|
|
15
|
+
removed: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface EnvResult {
|
|
18
|
+
status: EnvStatus;
|
|
19
|
+
payload: EnvPayload | null;
|
|
20
|
+
/** El código corto de por qué falló o por qué se usó la copia (`revoked_key`, `unreachable`…). */
|
|
21
|
+
reason: string | null;
|
|
22
|
+
message: string;
|
|
23
|
+
change: EnvChange | null;
|
|
24
|
+
}
|
|
25
|
+
export interface ApplyOptions {
|
|
26
|
+
/** `boot`: el arranque del proceso, que siempre pone las variables. `live`: un aviso o la revisión periódica, que obedece a `GROWTICKETS_ENV_ON_CHANGE`. */
|
|
27
|
+
phase: 'boot' | 'live';
|
|
28
|
+
/** Preguntar a la central; sin red, sólo la copia local. */
|
|
29
|
+
network: boolean;
|
|
30
|
+
/** Esperar a que acabe lo que ya se esté aplicando (arranque y avisos) o rendirse si el candado está tomado (revisión periódica). */
|
|
31
|
+
wait: boolean;
|
|
32
|
+
/** Confirmar también cuando ya estaba al día (el arranque y los avisos: alguien está esperando ver el acuse). */
|
|
33
|
+
ackCurrent: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface ApplierDeps {
|
|
36
|
+
settings: () => EnvSettings;
|
|
37
|
+
transport: Pick<EnvTransport, 'fetch' | 'ack'>;
|
|
38
|
+
snapshot: EnvSnapshot;
|
|
39
|
+
state: EnvState;
|
|
40
|
+
overlay: EnvOverlay;
|
|
41
|
+
now: () => number;
|
|
42
|
+
/** `reload`: las variables ya están en `process.env`; quien escucha decide qué recargar. */
|
|
43
|
+
onReload: (change: EnvChange) => void | Promise<void>;
|
|
44
|
+
/** `restart`: el cierre ordenado que el gestor de procesos convierte en un arranque con las variables nuevas. */
|
|
45
|
+
onRestart: () => Promise<void>;
|
|
46
|
+
warn: (message: string) => void;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* APLICAR una versión: preguntar a la central, validar, guardar la copia, poner las variables y confirmar. Lo usan el arranque
|
|
50
|
+
* (`loadGrowTicketsEnv`), el aviso de «Vincular» y la revisión de cada minuto. Es el equivalente de `EnvApplier` de growtickets-laravel,
|
|
51
|
+
* con una diferencia de fondo: allí se reconstruye UNA caché de configuración que comparten todos los procesos; en Node cada proceso tiene
|
|
52
|
+
* su propio `process.env`, así que cada uno aplica por su cuenta y el candado sólo ordena las aplicaciones DE ESTE proceso.
|
|
53
|
+
*
|
|
54
|
+
* ## Orden y garantías
|
|
55
|
+
*
|
|
56
|
+
* 1. **Una aplicación a la vez.** Un aviso que llega mientras otra aplica espera su turno (y la revisión periódica se rinde): la segunda
|
|
57
|
+
* pregunta con `If-None-Match` y, si nada cambió, no hace nada.
|
|
58
|
+
* 2. **No se retrocede**: en el MISMO entorno, una versión menor que la aplicada o que la de la copia es `rollback` y se avisa a la central;
|
|
59
|
+
* la igual no se vuelve a aplicar. Cambiar de entorno sí puede bajar el número: cada entorno cuenta sus versiones.
|
|
60
|
+
* 3. **Si la central no contesta**, con `keep` se usa la copia local si no es más vieja que `max_stale_hours`; con `fail`, no se arranca.
|
|
61
|
+
* Una clave revocada, vencida o inválida NO cae a la copia: la central dijo que este servidor ya no recibe, y la copia se borra.
|
|
62
|
+
* 4. **Después de poner las variables se confirma, y sólo entonces se reinicia** (`restart`) o se avisa a quien escucha (`reload`).
|
|
63
|
+
*/
|
|
64
|
+
export declare class EnvApplier {
|
|
65
|
+
private readonly deps;
|
|
66
|
+
private applied;
|
|
67
|
+
private appliedVersion;
|
|
68
|
+
private appliedEnvironment;
|
|
69
|
+
private last;
|
|
70
|
+
private pending;
|
|
71
|
+
private tail;
|
|
72
|
+
private closing;
|
|
73
|
+
constructor(deps: ApplierDeps);
|
|
74
|
+
/** «entorno:versión» de lo que este proceso tiene puesto, o `null` si nada. */
|
|
75
|
+
marker(): string | null;
|
|
76
|
+
apply(options: ApplyOptions): Promise<EnvResult>;
|
|
77
|
+
private run;
|
|
78
|
+
private put;
|
|
79
|
+
private afterLive;
|
|
80
|
+
private copy;
|
|
81
|
+
private remember;
|
|
82
|
+
private floor;
|
|
83
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EnvApplier = void 0;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const policy_1 = require("./policy");
|
|
6
|
+
const KEY_ERRORS = new Set(['invalid_key', 'revoked_key', 'expired_key', 'no_environment']);
|
|
7
|
+
const URL_ERRORS = new Set(['insecure_url', 'invalid_url']);
|
|
8
|
+
/**
|
|
9
|
+
* APLICAR una versión: preguntar a la central, validar, guardar la copia, poner las variables y confirmar. Lo usan el arranque
|
|
10
|
+
* (`loadGrowTicketsEnv`), el aviso de «Vincular» y la revisión de cada minuto. Es el equivalente de `EnvApplier` de growtickets-laravel,
|
|
11
|
+
* con una diferencia de fondo: allí se reconstruye UNA caché de configuración que comparten todos los procesos; en Node cada proceso tiene
|
|
12
|
+
* su propio `process.env`, así que cada uno aplica por su cuenta y el candado sólo ordena las aplicaciones DE ESTE proceso.
|
|
13
|
+
*
|
|
14
|
+
* ## Orden y garantías
|
|
15
|
+
*
|
|
16
|
+
* 1. **Una aplicación a la vez.** Un aviso que llega mientras otra aplica espera su turno (y la revisión periódica se rinde): la segunda
|
|
17
|
+
* pregunta con `If-None-Match` y, si nada cambió, no hace nada.
|
|
18
|
+
* 2. **No se retrocede**: en el MISMO entorno, una versión menor que la aplicada o que la de la copia es `rollback` y se avisa a la central;
|
|
19
|
+
* la igual no se vuelve a aplicar. Cambiar de entorno sí puede bajar el número: cada entorno cuenta sus versiones.
|
|
20
|
+
* 3. **Si la central no contesta**, con `keep` se usa la copia local si no es más vieja que `max_stale_hours`; con `fail`, no se arranca.
|
|
21
|
+
* Una clave revocada, vencida o inválida NO cae a la copia: la central dijo que este servidor ya no recibe, y la copia se borra.
|
|
22
|
+
* 4. **Después de poner las variables se confirma, y sólo entonces se reinicia** (`restart`) o se avisa a quien escucha (`reload`).
|
|
23
|
+
*/
|
|
24
|
+
class EnvApplier {
|
|
25
|
+
constructor(deps) {
|
|
26
|
+
this.deps = deps;
|
|
27
|
+
this.applied = null;
|
|
28
|
+
this.appliedVersion = 0;
|
|
29
|
+
this.appliedEnvironment = 0;
|
|
30
|
+
this.last = null;
|
|
31
|
+
this.pending = 0;
|
|
32
|
+
this.tail = Promise.resolve();
|
|
33
|
+
this.closing = false;
|
|
34
|
+
}
|
|
35
|
+
/** «entorno:versión» de lo que este proceso tiene puesto, o `null` si nada. */
|
|
36
|
+
marker() {
|
|
37
|
+
return this.applied;
|
|
38
|
+
}
|
|
39
|
+
async apply(options) {
|
|
40
|
+
if (this.closing || (this.pending > 0 && !options.wait))
|
|
41
|
+
return result('busy', null, null, 'Otro proceso de este servidor ya está aplicando.');
|
|
42
|
+
this.pending++;
|
|
43
|
+
const turno = this.tail.then(() => this.run(options));
|
|
44
|
+
this.tail = turno.catch(() => undefined);
|
|
45
|
+
try {
|
|
46
|
+
return await turno;
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
this.pending--;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async run(options) {
|
|
53
|
+
if (this.closing)
|
|
54
|
+
return result('busy', null, null, 'Este proceso se está reiniciando.');
|
|
55
|
+
const copia = this.copy();
|
|
56
|
+
const anterior = copia?.payload ?? null;
|
|
57
|
+
const estado = this.deps.state.read();
|
|
58
|
+
let offline = null;
|
|
59
|
+
let payload;
|
|
60
|
+
try {
|
|
61
|
+
if (!options.network)
|
|
62
|
+
throw new errors_1.EnvError('offline', 'Sin preguntar a la central.');
|
|
63
|
+
const nuevo = await this.deps.transport.fetch(anterior?.etag);
|
|
64
|
+
if (nuevo === null) {
|
|
65
|
+
if (anterior === null)
|
|
66
|
+
throw new errors_1.EnvError('malformed', 'La central dijo «sin cambios» sin que hubiera copia local.');
|
|
67
|
+
payload = anterior;
|
|
68
|
+
this.remember(anterior);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const minimo = this.floor(nuevo.environmentId, anterior, estado);
|
|
72
|
+
if (nuevo.version < minimo) {
|
|
73
|
+
await this.deps.transport.ack(nuevo.version, false, 'rollback');
|
|
74
|
+
return result('failed', nuevo, 'rollback', `La central dio la versión ${nuevo.version}, anterior a la ${minimo} ya aplicada: no se retrocede.`);
|
|
75
|
+
}
|
|
76
|
+
payload = nuevo;
|
|
77
|
+
this.remember(nuevo);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
const fallo = error instanceof errors_1.EnvError ? error : new errors_1.EnvError('unreachable', 'No se pudo hablar con la central.');
|
|
82
|
+
if (fallo.reason === 'not_published')
|
|
83
|
+
return result('empty', null, fallo.reason, fallo.message);
|
|
84
|
+
if (KEY_ERRORS.has(fallo.reason)) {
|
|
85
|
+
this.deps.snapshot.forget();
|
|
86
|
+
this.last = null;
|
|
87
|
+
return result('failed', null, fallo.reason, fallo.message);
|
|
88
|
+
}
|
|
89
|
+
if (URL_ERRORS.has(fallo.reason))
|
|
90
|
+
return result('failed', null, fallo.reason, fallo.message);
|
|
91
|
+
const edad = this.deps.snapshot.ageHours(copia);
|
|
92
|
+
const sirve = anterior !== null && edad !== null && (fallo.reason === 'offline' || (this.deps.settings().onFailure === 'keep' && edad <= this.deps.settings().maxStaleHours));
|
|
93
|
+
if (!sirve)
|
|
94
|
+
return result('failed', anterior, fallo.reason, fallo.message);
|
|
95
|
+
payload = anterior;
|
|
96
|
+
offline = fallo;
|
|
97
|
+
}
|
|
98
|
+
return this.put(payload, offline, options);
|
|
99
|
+
}
|
|
100
|
+
async put(payload, offline, options) {
|
|
101
|
+
const marcador = (0, policy_1.markerOf)(payload);
|
|
102
|
+
if (this.applied === marcador && options.phase !== 'boot') {
|
|
103
|
+
if (options.ackCurrent && offline === null)
|
|
104
|
+
await this.deps.transport.ack(payload.version, true);
|
|
105
|
+
return result('current', payload, offline?.reason ?? null, offline?.message ?? '');
|
|
106
|
+
}
|
|
107
|
+
const reload = options.phase === 'live' && this.deps.settings().onChange === 'reload';
|
|
108
|
+
const poner = options.phase === 'boot' || reload;
|
|
109
|
+
let cambio = null;
|
|
110
|
+
try {
|
|
111
|
+
cambio = poner ? this.deps.overlay.apply(payload.variables) : null;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
await this.deps.transport.ack(payload.version, false, 'apply_failed');
|
|
115
|
+
return result('failed', payload, 'apply_failed', 'No se pudieron poner las variables en el entorno del proceso.');
|
|
116
|
+
}
|
|
117
|
+
this.applied = marcador;
|
|
118
|
+
this.appliedVersion = payload.version;
|
|
119
|
+
this.appliedEnvironment = payload.environmentId;
|
|
120
|
+
this.deps.state.applied(payload);
|
|
121
|
+
if (payload.discarded.length > 0) {
|
|
122
|
+
this.deps.warn(`GrowTickets: se descartaron ${payload.discarded.length} variables que no puede poner la central: ${payload.discarded.slice(0, 20).join(', ')}.`);
|
|
123
|
+
}
|
|
124
|
+
if (offline === null)
|
|
125
|
+
await this.deps.transport.ack(payload.version, true);
|
|
126
|
+
const aplicada = result(offline === null ? 'applied' : 'offline', payload, offline?.reason ?? null, offline?.message ?? '', cambio ? changeOf(payload, cambio) : null);
|
|
127
|
+
if (options.phase === 'live')
|
|
128
|
+
await this.afterLive(reload, aplicada);
|
|
129
|
+
return aplicada;
|
|
130
|
+
}
|
|
131
|
+
async afterLive(reload, aplicada) {
|
|
132
|
+
if (!reload) {
|
|
133
|
+
this.closing = true;
|
|
134
|
+
await this.deps.onRestart();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
if (aplicada.change)
|
|
139
|
+
await this.deps.onReload(aplicada.change);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
this.deps.warn('GrowTickets: el oyente de onEnvChange lanzó una excepción; las variables ya están puestas.');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
copy() {
|
|
146
|
+
return this.deps.snapshot.read() ?? this.last;
|
|
147
|
+
}
|
|
148
|
+
remember(payload) {
|
|
149
|
+
this.last = { payload, fetchedAt: this.deps.now() };
|
|
150
|
+
this.deps.snapshot.write(payload, this.last.fetchedAt);
|
|
151
|
+
}
|
|
152
|
+
floor(environmentId, anterior, estado) {
|
|
153
|
+
return Math.max(anterior !== null && anterior.environmentId === environmentId ? anterior.version : 0, estado.environment === environmentId ? estado.version : 0, this.appliedEnvironment === environmentId ? this.appliedVersion : 0);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
exports.EnvApplier = EnvApplier;
|
|
157
|
+
function result(status, payload, reason, message, change = null) {
|
|
158
|
+
return { status, payload, reason, message, change };
|
|
159
|
+
}
|
|
160
|
+
function changeOf(payload, cambio) {
|
|
161
|
+
return { version: payload.version, environmentId: payload.environmentId, environmentName: payload.environmentName, changed: cambio.changed, removed: cambio.removed };
|
|
162
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { GrowTicketsError } from '../errors/growtickets-error';
|
|
2
|
+
/**
|
|
3
|
+
* Algo que impidió bajar o aplicar las variables, con un CÓDIGO corto (`reason`: `revoked_key`, `unreachable`, `apply_failed`…) que viaja en
|
|
4
|
+
* el acuse a la central y un mensaje para quien mira el registro. Ninguno de los dos lleva nunca un valor ni la clave.
|
|
5
|
+
*/
|
|
6
|
+
export declare class EnvError extends GrowTicketsError {
|
|
7
|
+
readonly reason: string;
|
|
8
|
+
readonly code = "ENV_ERROR";
|
|
9
|
+
readonly status = 500;
|
|
10
|
+
readonly publicMessage = "Las variables de entorno no est\u00E1n disponibles.";
|
|
11
|
+
constructor(reason: string, message: string);
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EnvError = void 0;
|
|
4
|
+
const growtickets_error_1 = require("../errors/growtickets-error");
|
|
5
|
+
/**
|
|
6
|
+
* Algo que impidió bajar o aplicar las variables, con un CÓDIGO corto (`reason`: `revoked_key`, `unreachable`, `apply_failed`…) que viaja en
|
|
7
|
+
* el acuse a la central y un mensaje para quien mira el registro. Ninguno de los dos lleva nunca un valor ni la clave.
|
|
8
|
+
*/
|
|
9
|
+
class EnvError extends growtickets_error_1.GrowTicketsError {
|
|
10
|
+
constructor(reason, message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.reason = reason;
|
|
13
|
+
this.code = 'ENV_ERROR';
|
|
14
|
+
this.status = 500;
|
|
15
|
+
this.publicMessage = 'Las variables de entorno no están disponibles.';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.EnvError = EnvError;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type EnvChangeListener, type LoadEnvOptions, type LoadEnvResult } from './runtime';
|
|
2
|
+
/**
|
|
3
|
+
* Trae las variables de entorno de ESTE servidor desde GrowTickets (pestaña «Entornos» del proyecto) y las pone en `process.env`.
|
|
4
|
+
* Va en `main.ts` ANTES de `NestFactory.create` —y antes de que `ConfigModule` lea nada—:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { loadGrowTicketsEnv } from '@growconnect/growtickets-nest';
|
|
8
|
+
*
|
|
9
|
+
* async function bootstrap() {
|
|
10
|
+
* await loadGrowTicketsEnv();
|
|
11
|
+
* const app = await NestFactory.create(AppModule);
|
|
12
|
+
* app.use(growTicketsMiddleware(app));
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* ## Precedencia
|
|
17
|
+
*
|
|
18
|
+
* entorno de la plataforma < la central < `GROWTICKETS_URL` y `GROWTICKETS_ENV_KEY` del arranque. La central gana sobre lo que pusieron
|
|
19
|
+
* Docker o Railway y sobre un `.env` que lea el producto después (`dotenv` no pisa lo que ya está en `process.env`), pero nunca puede tocar
|
|
20
|
+
* nada con el prefijo `GROWTICKETS_`: son con las que el servidor vuelve a preguntar.
|
|
21
|
+
*
|
|
22
|
+
* ## Si la central no contesta
|
|
23
|
+
*
|
|
24
|
+
* Con `GROWTICKETS_ENV_ON_FAILURE=keep` (de serie) se arranca con la copia local cifrada si no tiene más de
|
|
25
|
+
* `GROWTICKETS_ENV_MAX_STALE_HOURS` (168); con `fail`, o sin copia que sirva, LANZA y el producto no arranca. Una clave revocada borra la copia.
|
|
26
|
+
*
|
|
27
|
+
* Sin `GROWTICKETS_ENV_KEY` no hace nada y devuelve `{ status: 'off' }`: se puede dejar puesta en todos los productos antes de conectar ninguno.
|
|
28
|
+
*/
|
|
29
|
+
export declare function loadGrowTicketsEnv(options?: LoadEnvOptions): Promise<LoadEnvResult>;
|
|
30
|
+
/**
|
|
31
|
+
* Suscribe un oyente al cambio en caliente (`GROWTICKETS_ENV_ON_CHANGE=reload`): se llama DESPUÉS de poner en `process.env` una versión
|
|
32
|
+
* nueva, con los NOMBRES que cambiaron o se quitaron —nunca los valores—, para que el producto recargue lo que lee una sola vez (un
|
|
33
|
+
* pool de conexiones, un cliente de pagos). Devuelve la función que quita el oyente. Con `restart` no se llama nunca: el proceso se va.
|
|
34
|
+
*/
|
|
35
|
+
export declare function onGrowTicketsEnvChange(listener: EnvChangeListener): () => void;
|
package/dist/env/load.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadGrowTicketsEnv = loadGrowTicketsEnv;
|
|
4
|
+
exports.onGrowTicketsEnvChange = onGrowTicketsEnvChange;
|
|
5
|
+
const runtime_1 = require("./runtime");
|
|
6
|
+
/**
|
|
7
|
+
* Trae las variables de entorno de ESTE servidor desde GrowTickets (pestaña «Entornos» del proyecto) y las pone en `process.env`.
|
|
8
|
+
* Va en `main.ts` ANTES de `NestFactory.create` —y antes de que `ConfigModule` lea nada—:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { loadGrowTicketsEnv } from '@growconnect/growtickets-nest';
|
|
12
|
+
*
|
|
13
|
+
* async function bootstrap() {
|
|
14
|
+
* await loadGrowTicketsEnv();
|
|
15
|
+
* const app = await NestFactory.create(AppModule);
|
|
16
|
+
* app.use(growTicketsMiddleware(app));
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* ## Precedencia
|
|
21
|
+
*
|
|
22
|
+
* entorno de la plataforma < la central < `GROWTICKETS_URL` y `GROWTICKETS_ENV_KEY` del arranque. La central gana sobre lo que pusieron
|
|
23
|
+
* Docker o Railway y sobre un `.env` que lea el producto después (`dotenv` no pisa lo que ya está en `process.env`), pero nunca puede tocar
|
|
24
|
+
* nada con el prefijo `GROWTICKETS_`: son con las que el servidor vuelve a preguntar.
|
|
25
|
+
*
|
|
26
|
+
* ## Si la central no contesta
|
|
27
|
+
*
|
|
28
|
+
* Con `GROWTICKETS_ENV_ON_FAILURE=keep` (de serie) se arranca con la copia local cifrada si no tiene más de
|
|
29
|
+
* `GROWTICKETS_ENV_MAX_STALE_HOURS` (168); con `fail`, o sin copia que sirva, LANZA y el producto no arranca. Una clave revocada borra la copia.
|
|
30
|
+
*
|
|
31
|
+
* Sin `GROWTICKETS_ENV_KEY` no hace nada y devuelve `{ status: 'off' }`: se puede dejar puesta en todos los productos antes de conectar ninguno.
|
|
32
|
+
*/
|
|
33
|
+
function loadGrowTicketsEnv(options = {}) {
|
|
34
|
+
return (0, runtime_1.envRuntime)().load(options);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Suscribe un oyente al cambio en caliente (`GROWTICKETS_ENV_ON_CHANGE=reload`): se llama DESPUÉS de poner en `process.env` una versión
|
|
38
|
+
* nueva, con los NOMBRES que cambiaron o se quitaron —nunca los valores—, para que el producto recargue lo que lee una sola vez (un
|
|
39
|
+
* pool de conexiones, un cliente de pagos). Devuelve la función que quita el oyente. Con `restart` no se llama nunca: el proceso se va.
|
|
40
|
+
*/
|
|
41
|
+
function onGrowTicketsEnvChange(listener) {
|
|
42
|
+
return (0, runtime_1.envRuntime)().onChange(listener);
|
|
43
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
type Target = Record<string, string | undefined>;
|
|
2
|
+
export interface OverlayChange {
|
|
3
|
+
/** Los nombres que se pusieron o cambiaron de valor (nunca los valores). */
|
|
4
|
+
changed: string[];
|
|
5
|
+
/** Los nombres que la central ya no manda y volvieron a lo que tenían antes. */
|
|
6
|
+
removed: string[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Pone las variables de la central ENCIMA del entorno del proceso y recuerda qué había debajo. La central gana sobre la plataforma
|
|
10
|
+
* (Docker, Railway) y sobre un `.env` que lea el producto después, porque `dotenv` no pisa lo que ya está en `process.env`.
|
|
11
|
+
*
|
|
12
|
+
* 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
|
|
13
|
+
* 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.
|
|
14
|
+
*/
|
|
15
|
+
export declare class EnvOverlay {
|
|
16
|
+
private readonly target;
|
|
17
|
+
private readonly baseline;
|
|
18
|
+
private readonly current;
|
|
19
|
+
constructor(target: Target);
|
|
20
|
+
apply(variables: Record<string, string>): OverlayChange;
|
|
21
|
+
private restore;
|
|
22
|
+
}
|
|
23
|
+
export {};
|