@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,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_EXIT_CODE = exports.DEFAULT_ENV_PATH = void 0;
4
+ exports.readEnvSettings = readEnvSettings;
5
+ exports.DEFAULT_ENV_PATH = 'growtickets/env';
6
+ /** 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`). */
7
+ exports.DEFAULT_EXIT_CODE = 75;
8
+ const DEFAULT_MAX_STALE_HOURS = 168;
9
+ const PATH_SHAPE = /^[A-Za-z0-9][A-Za-z0-9/_-]{0,118}$/;
10
+ /**
11
+ * 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
12
+ * 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.
13
+ *
14
+ * 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
15
+ * (`env/transport`), que es la única pieza que cambiaría con otra versión del protocolo.
16
+ */
17
+ function readEnvSettings(env) {
18
+ const horas = Number.parseInt((env.GROWTICKETS_ENV_MAX_STALE_HOURS ?? '').trim(), 10);
19
+ const dir = (env.GROWTICKETS_ENV_DIR ?? '').trim();
20
+ return {
21
+ path: cleanPath(env.GROWTICKETS_ENV_PATH),
22
+ onFailure: (env.GROWTICKETS_ENV_ON_FAILURE ?? '').trim().toLowerCase() === 'fail' ? 'fail' : 'keep',
23
+ maxStaleHours: Number.isFinite(horas) && horas >= 1 ? horas : DEFAULT_MAX_STALE_HOURS,
24
+ onChange: (env.GROWTICKETS_ENV_ON_CHANGE ?? '').trim().toLowerCase() === 'reload' ? 'reload' : 'restart',
25
+ dir: dir === '' ? null : dir,
26
+ exitCode: cleanExitCode(env.GROWTICKETS_ENV_EXIT_CODE),
27
+ };
28
+ }
29
+ /** Un entero de 0 a 255 (lo que cabe en un código de salida); cualquier otra cosa vuelve al de serie. */
30
+ function cleanExitCode(value) {
31
+ const texto = (value ?? '').trim();
32
+ return /^\d{1,3}$/.test(texto) && Number(texto) <= 255 ? Number(texto) : exports.DEFAULT_EXIT_CODE;
33
+ }
34
+ /**
35
+ * Por dónde avisa la central. Esta cadena acaba comparada con la ruta de cada petición, así que sólo letras, números, `/`, `_` y `-`: lo
36
+ * que no tenga esa forma vuelve a la de serie.
37
+ */
38
+ function cleanPath(value) {
39
+ const ruta = (value ?? exports.DEFAULT_ENV_PATH).trim().replace(/^[/\s]+|[/\s]+$/g, '');
40
+ return PATH_SHAPE.test(ruta) && !ruta.includes('//') ? ruta : exports.DEFAULT_ENV_PATH;
41
+ }
@@ -0,0 +1,32 @@
1
+ import { type EnvPayload } from './policy';
2
+ import type { EnvStorage } from './storage';
3
+ export declare const SNAPSHOT_FILE = "env.snapshot";
4
+ export interface SnapshotCopy {
5
+ payload: EnvPayload;
6
+ /** Cuándo se bajó o se confirmó por última vez (segundos Unix). */
7
+ fetchedAt: number;
8
+ }
9
+ /**
10
+ * La COPIA LOCAL de la última versión bajada, CIFRADA, en `env.snapshot` con permisos 0600. Sirve para tres cosas: preguntar a la central
11
+ * con `If-None-Match` (si nada cambió contesta 304 y se reconstruye con esto), saber desde qué versión no se puede retroceder, y arrancar
12
+ * si la central no contesta (`GROWTICKETS_ENV_ON_FAILURE=keep`, hasta `max_stale_hours`).
13
+ *
14
+ * AES-256-GCM con una llave que le da el transporte (`snapshotKey()`; el transporte decide de qué semilla sale y la deriva aparte de lo que
15
+ * firma los avisos): sin la llave el archivo no dice nada, y con otra (se rotó) no se lee —la etiqueta de GCM no casa— y se baja de nuevo. El formato es `gtenv1:` y base64 de
16
+ * `iv(12) | etiqueta(16) | cifrado`; el nombre de la librería viaja como dato autenticado, así que el archivo no vale para otro uso.
17
+ */
18
+ export declare class EnvSnapshot {
19
+ private readonly storage;
20
+ private readonly key;
21
+ private readonly now;
22
+ constructor(storage: EnvStorage, key: () => Buffer | null, now: () => number);
23
+ /** La copia, o `null` si no hay, no se puede leer o es de otra clave. */
24
+ read(): SnapshotCopy | null;
25
+ /** Guarda la versión bajada. Nunca lanza: sin copia se sigue funcionando. */
26
+ write(payload: EnvPayload, fetchedAt?: number): boolean;
27
+ /** Marca la copia como recién confirmada (la central contestó 304: sigue valiendo). */
28
+ touch(payload: EnvPayload): boolean;
29
+ forget(): void;
30
+ /** Horas desde que se bajó, o `null` sin copia. */
31
+ ageHours(copia: SnapshotCopy | null): number | null;
32
+ }
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EnvSnapshot = exports.SNAPSHOT_FILE = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const policy_1 = require("./policy");
6
+ exports.SNAPSHOT_FILE = 'env.snapshot';
7
+ const FORMAT = 'gtenv1:';
8
+ const AAD = Buffer.from('growtickets-env-snapshot', 'utf8');
9
+ const IV_END = 12;
10
+ const TAG_END = 28;
11
+ /**
12
+ * La COPIA LOCAL de la última versión bajada, CIFRADA, en `env.snapshot` con permisos 0600. Sirve para tres cosas: preguntar a la central
13
+ * con `If-None-Match` (si nada cambió contesta 304 y se reconstruye con esto), saber desde qué versión no se puede retroceder, y arrancar
14
+ * si la central no contesta (`GROWTICKETS_ENV_ON_FAILURE=keep`, hasta `max_stale_hours`).
15
+ *
16
+ * AES-256-GCM con una llave que le da el transporte (`snapshotKey()`; el transporte decide de qué semilla sale y la deriva aparte de lo que
17
+ * firma los avisos): sin la llave el archivo no dice nada, y con otra (se rotó) no se lee —la etiqueta de GCM no casa— y se baja de nuevo. El formato es `gtenv1:` y base64 de
18
+ * `iv(12) | etiqueta(16) | cifrado`; el nombre de la librería viaja como dato autenticado, así que el archivo no vale para otro uso.
19
+ */
20
+ class EnvSnapshot {
21
+ constructor(storage, key, now) {
22
+ this.storage = storage;
23
+ this.key = key;
24
+ this.now = now;
25
+ }
26
+ /** La copia, o `null` si no hay, no se puede leer o es de otra clave. */
27
+ read() {
28
+ const crudo = this.storage.read(exports.SNAPSHOT_FILE);
29
+ const llave = this.key();
30
+ if (crudo === null || llave === null || !crudo.startsWith(FORMAT))
31
+ return null;
32
+ try {
33
+ const bytes = Buffer.from(crudo.slice(FORMAT.length).trim(), 'base64');
34
+ if (bytes.length <= TAG_END)
35
+ return null;
36
+ const decipher = (0, node_crypto_1.createDecipheriv)('aes-256-gcm', llave, bytes.subarray(0, IV_END), { authTagLength: TAG_END - IV_END });
37
+ decipher.setAAD(AAD);
38
+ decipher.setAuthTag(bytes.subarray(IV_END, TAG_END));
39
+ const texto = Buffer.concat([decipher.update(bytes.subarray(TAG_END)), decipher.final()]).toString('utf8');
40
+ const datos = JSON.parse(texto);
41
+ const payload = (0, policy_1.parseEnvPayload)({ schema: 1, ...datos.payload }, typeof datos.payload?.etag === 'string' ? datos.payload.etag : null);
42
+ return { payload, fetchedAt: Number.isFinite(Number(datos.fetched_at)) ? Number(datos.fetched_at) : 0 };
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ /** Guarda la versión bajada. Nunca lanza: sin copia se sigue funcionando. */
49
+ write(payload, fetchedAt = this.now()) {
50
+ const llave = this.key();
51
+ if (llave === null)
52
+ return false;
53
+ try {
54
+ const iv = (0, node_crypto_1.randomBytes)(IV_END);
55
+ const cipher = (0, node_crypto_1.createCipheriv)('aes-256-gcm', llave, iv, { authTagLength: TAG_END - IV_END });
56
+ cipher.setAAD(AAD);
57
+ const contenido = JSON.stringify({
58
+ payload: {
59
+ version: payload.version,
60
+ environment: { id: payload.environmentId, name: payload.environmentName, kind: payload.environmentKind },
61
+ variables: payload.variables,
62
+ etag: payload.etag,
63
+ },
64
+ fetched_at: fetchedAt,
65
+ });
66
+ const cifrado = Buffer.concat([cipher.update(contenido, 'utf8'), cipher.final()]);
67
+ return this.storage.write(exports.SNAPSHOT_FILE, FORMAT + Buffer.concat([iv, cipher.getAuthTag(), cifrado]).toString('base64'));
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ }
73
+ /** Marca la copia como recién confirmada (la central contestó 304: sigue valiendo). */
74
+ touch(payload) {
75
+ return this.write(payload);
76
+ }
77
+ forget() {
78
+ this.storage.remove(exports.SNAPSHOT_FILE);
79
+ }
80
+ /** Horas desde que se bajó, o `null` sin copia. */
81
+ ageHours(copia) {
82
+ return copia === null ? null : Math.max(0, this.now() - copia.fetchedAt) / 3600;
83
+ }
84
+ }
85
+ exports.EnvSnapshot = EnvSnapshot;
@@ -0,0 +1,27 @@
1
+ import type { EnvPayload } from './policy';
2
+ import type { EnvStorage } from './storage';
3
+ export declare const STATE_FILE = "env.state";
4
+ export interface EnvStateData {
5
+ /** «entorno:versión» de lo último aplicado, o `null` si nunca. */
6
+ applied: string | null;
7
+ version: number;
8
+ environment: number;
9
+ /** Segundos Unix. */
10
+ appliedAt: number;
11
+ /** Cuántas variables se pusieron en la última aplicación. Nunca sus nombres. */
12
+ count: number;
13
+ /** Los nombres que la política descartó la última vez (sólo nombres, hasta 20). */
14
+ discarded: string[];
15
+ }
16
+ /**
17
+ * Qué tiene aplicado este servidor, en disco (`env.state`): el entorno y la versión de lo último que se puso, y cuándo. No lleva ningún valor
18
+ * ni ningún nombre de variable aplicada. Es lo que deja a `growtickets-nest env status`, que es OTRO proceso, decir qué versión corre, y lo
19
+ * que sostiene el «no se retrocede» entre un arranque y el siguiente.
20
+ */
21
+ export declare class EnvState {
22
+ private readonly storage;
23
+ private readonly now;
24
+ constructor(storage: EnvStorage, now: () => number);
25
+ read(): EnvStateData;
26
+ applied(payload: EnvPayload): void;
27
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EnvState = exports.STATE_FILE = void 0;
4
+ const policy_1 = require("./policy");
5
+ exports.STATE_FILE = 'env.state';
6
+ /**
7
+ * Qué tiene aplicado este servidor, en disco (`env.state`): el entorno y la versión de lo último que se puso, y cuándo. No lleva ningún valor
8
+ * ni ningún nombre de variable aplicada. Es lo que deja a `growtickets-nest env status`, que es OTRO proceso, decir qué versión corre, y lo
9
+ * que sostiene el «no se retrocede» entre un arranque y el siguiente.
10
+ */
11
+ class EnvState {
12
+ constructor(storage, now) {
13
+ this.storage = storage;
14
+ this.now = now;
15
+ }
16
+ read() {
17
+ const vacio = { applied: null, version: 0, environment: 0, appliedAt: 0, count: 0, discarded: [] };
18
+ const crudo = this.storage.read(exports.STATE_FILE);
19
+ if (crudo === null)
20
+ return vacio;
21
+ try {
22
+ const datos = JSON.parse(crudo);
23
+ return {
24
+ applied: typeof datos.applied === 'string' ? datos.applied : null,
25
+ version: int(datos.version),
26
+ environment: int(datos.environment),
27
+ appliedAt: int(datos.applied_at),
28
+ count: int(datos.count),
29
+ discarded: Array.isArray(datos.discarded) ? datos.discarded.filter((n) => typeof n === 'string').slice(0, 20) : [],
30
+ };
31
+ }
32
+ catch {
33
+ return vacio;
34
+ }
35
+ }
36
+ applied(payload) {
37
+ this.storage.write(exports.STATE_FILE, JSON.stringify({
38
+ applied: (0, policy_1.markerOf)(payload),
39
+ version: payload.version,
40
+ environment: payload.environmentId,
41
+ applied_at: this.now(),
42
+ count: Object.keys(payload.variables).length,
43
+ discarded: payload.discarded.slice(0, 20),
44
+ }));
45
+ }
46
+ }
47
+ exports.EnvState = EnvState;
48
+ function int(value) {
49
+ const n = Number(value);
50
+ return Number.isSafeInteger(n) && n > 0 ? n : 0;
51
+ }
@@ -0,0 +1,33 @@
1
+ export interface StorageOptions {
2
+ /** `GROWTICKETS_ENV_DIR`: si se fija, es la única carpeta que se usa. Se pregunta en cada uso: el entorno puede cambiar. */
3
+ dir: () => string | null;
4
+ cwd: () => string;
5
+ }
6
+ /**
7
+ * Dónde guarda la pieza lo suyo (la copia cifrada y el estado): una carpeta propia con permisos 0700 y archivos 0600.
8
+ *
9
+ * De serie es `.growtickets/` junto al proceso. Si no se puede escribir ahí (un contenedor con el sistema de archivos de sólo lectura), cae
10
+ * a una carpeta del directorio temporal con el usuario en el nombre, y sólo la usa si ya es un directorio de verdad (no un enlace), del
11
+ * mismo usuario y sin permisos para los demás: en un `/tmp` compartido cualquiera pudo crear antes ese nombre. Una carpeta fijada con
12
+ * `GROWTICKETS_ENV_DIR` no tiene alternativa: es decisión de quien administra el servidor.
13
+ *
14
+ * Escribir nunca lanza: sin copia el servidor sigue funcionando, sólo pregunta más a la central. Se escribe en un temporal y se
15
+ * renombra, así que un corte a medias deja el archivo anterior entero, nunca la mitad.
16
+ */
17
+ export declare class EnvStorage {
18
+ private readonly options;
19
+ private resolved;
20
+ constructor(options: StorageOptions);
21
+ /** La carpeta de trabajo, creada. `null` si ninguna sirve. */
22
+ directory(): string | null;
23
+ /** Todas las carpetas donde pudo quedar algo, la preferida primero: la lectura mira en las dos. */
24
+ readCandidates(): string[];
25
+ read(name: string): string | null;
26
+ /** Escribe `contenido` en `name` con permisos 0600, de forma atómica. `false` si no se pudo. */
27
+ write(name: string, contenido: string): boolean;
28
+ /** Borra el archivo de TODAS las carpetas posibles (una clave revocada no deja copias). */
29
+ remove(name: string): void;
30
+ private candidates;
31
+ private prepare;
32
+ private writable;
33
+ }
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EnvStorage = void 0;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_os_1 = require("node:os");
6
+ const node_path_1 = require("node:path");
7
+ const node_crypto_1 = require("node:crypto");
8
+ const MAX_FILE_BYTES = 16 * 1024 * 1024;
9
+ /**
10
+ * Dónde guarda la pieza lo suyo (la copia cifrada y el estado): una carpeta propia con permisos 0700 y archivos 0600.
11
+ *
12
+ * De serie es `.growtickets/` junto al proceso. Si no se puede escribir ahí (un contenedor con el sistema de archivos de sólo lectura), cae
13
+ * a una carpeta del directorio temporal con el usuario en el nombre, y sólo la usa si ya es un directorio de verdad (no un enlace), del
14
+ * mismo usuario y sin permisos para los demás: en un `/tmp` compartido cualquiera pudo crear antes ese nombre. Una carpeta fijada con
15
+ * `GROWTICKETS_ENV_DIR` no tiene alternativa: es decisión de quien administra el servidor.
16
+ *
17
+ * Escribir nunca lanza: sin copia el servidor sigue funcionando, sólo pregunta más a la central. Se escribe en un temporal y se
18
+ * renombra, así que un corte a medias deja el archivo anterior entero, nunca la mitad.
19
+ */
20
+ class EnvStorage {
21
+ constructor(options) {
22
+ this.options = options;
23
+ this.resolved = null;
24
+ }
25
+ /** La carpeta de trabajo, creada. `null` si ninguna sirve. */
26
+ directory() {
27
+ if (this.resolved !== null && (0, node_fs_1.existsSync)(this.resolved))
28
+ return this.resolved;
29
+ for (const candidata of this.candidates()) {
30
+ if (this.prepare(candidata.path, candidata.strict)) {
31
+ this.resolved = candidata.path;
32
+ return candidata.path;
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+ /** Todas las carpetas donde pudo quedar algo, la preferida primero: la lectura mira en las dos. */
38
+ readCandidates() {
39
+ return this.candidates().map((c) => c.path);
40
+ }
41
+ read(name) {
42
+ for (const dir of this.readCandidates()) {
43
+ const ruta = (0, node_path_1.join)(dir, name);
44
+ try {
45
+ if ((0, node_fs_1.existsSync)(ruta) && (0, node_fs_1.statSync)(ruta).size <= MAX_FILE_BYTES)
46
+ return (0, node_fs_1.readFileSync)(ruta, 'utf8');
47
+ }
48
+ catch {
49
+ continue;
50
+ }
51
+ }
52
+ return null;
53
+ }
54
+ /** Escribe `contenido` en `name` con permisos 0600, de forma atómica. `false` si no se pudo. */
55
+ write(name, contenido) {
56
+ const dir = this.directory();
57
+ if (dir === null)
58
+ return false;
59
+ const destino = (0, node_path_1.join)(dir, name);
60
+ const temporal = `${destino}.${(0, node_crypto_1.randomBytes)(6).toString('hex')}.tmp`;
61
+ try {
62
+ const fd = (0, node_fs_1.openSync)(temporal, 'wx', 0o600);
63
+ try {
64
+ (0, node_fs_1.writeSync)(fd, contenido);
65
+ }
66
+ finally {
67
+ (0, node_fs_1.closeSync)(fd);
68
+ }
69
+ (0, node_fs_1.chmodSync)(temporal, 0o600);
70
+ (0, node_fs_1.renameSync)(temporal, destino);
71
+ return true;
72
+ }
73
+ catch {
74
+ try {
75
+ (0, node_fs_1.unlinkSync)(temporal);
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ return false;
81
+ }
82
+ }
83
+ /** Borra el archivo de TODAS las carpetas posibles (una clave revocada no deja copias). */
84
+ remove(name) {
85
+ for (const dir of this.readCandidates()) {
86
+ try {
87
+ (0, node_fs_1.unlinkSync)((0, node_path_1.join)(dir, name));
88
+ }
89
+ catch {
90
+ continue;
91
+ }
92
+ }
93
+ }
94
+ candidates() {
95
+ const fijada = this.options.dir();
96
+ if (fijada !== null)
97
+ return [{ path: fijada, strict: false }];
98
+ const uid = typeof process.getuid === 'function' ? String(process.getuid()) : 'user';
99
+ return [
100
+ { path: (0, node_path_1.join)(this.options.cwd(), '.growtickets'), strict: false },
101
+ { path: (0, node_path_1.join)((0, node_os_1.tmpdir)(), `growtickets-${uid}`), strict: true },
102
+ ];
103
+ }
104
+ prepare(path, strict) {
105
+ try {
106
+ (0, node_fs_1.mkdirSync)(path, { recursive: true, mode: 0o700 });
107
+ const info = (0, node_fs_1.lstatSync)(path);
108
+ if (!info.isDirectory())
109
+ return false;
110
+ if (strict) {
111
+ const mio = typeof process.getuid !== 'function' || info.uid === process.getuid();
112
+ if (!mio || (info.mode & 0o077) !== 0)
113
+ return false;
114
+ }
115
+ return this.writable(path);
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ }
121
+ writable(path) {
122
+ const prueba = (0, node_path_1.join)(path, `.write-test-${(0, node_crypto_1.randomBytes)(4).toString('hex')}`);
123
+ try {
124
+ (0, node_fs_1.closeSync)((0, node_fs_1.openSync)(prueba, 'wx', 0o600));
125
+ (0, node_fs_1.unlinkSync)(prueba);
126
+ return true;
127
+ }
128
+ catch {
129
+ return false;
130
+ }
131
+ }
132
+ }
133
+ exports.EnvStorage = EnvStorage;
@@ -0,0 +1,58 @@
1
+ import { type EnvPayload } from '../policy';
2
+ import type { EnvCredential } from './credential';
3
+ import { type TransportConfig } from './config';
4
+ export interface EnvClientDeps {
5
+ /** La credencial del servidor; lanza `EnvError` `invalid_key` si falta o no tiene la forma. */
6
+ credential: () => EnvCredential;
7
+ config: () => TransportConfig;
8
+ env: () => Record<string, string | undefined>;
9
+ fetchImpl?: typeof fetch;
10
+ /** La última dirección pública que vio la seguridad, para cuando `GROWTICKETS_ORIGIN` no la fija. */
11
+ learnedOrigin?: () => string | null;
12
+ /** Para las pruebas: la espera entre intentos. */
13
+ sleep?: (ms: number) => Promise<void>;
14
+ /** Segundos, época Unix. Para las pruebas: el reloj que firma. */
15
+ now?: () => number;
16
+ /** Para las pruebas: el azar de los nonce y las efímeras, que el vector común fija byte a byte. */
17
+ random?: (bytes: number) => Buffer;
18
+ }
19
+ /**
20
+ * Lo que habla con la central (protocolo GT-ENV2): bajar las variables de ESTE servidor (`GET api/ingest/env`) y confirmar que se aplicaron
21
+ * (`POST api/ingest/env/ack`). Ninguna clave viaja: cada petición va FIRMADA con la llave del servidor (`EnvCredential`) y cada respuesta se
22
+ * cree sólo si la firmó la central FIJADA en la credencial.
23
+ *
24
+ * - **Cifrado de punta a punta**: al bajar se manda una efímera X25519 nueva (firmada dentro de la petición) y la central cifra para ella. Sólo
25
+ * este proceso puede abrir la respuesta, y la efímera se borra al salir: capturar el tráfico —o robar luego la credencial— no descifra nada.
26
+ * - **Sólo lo firmado cambia el estado**: un 304, un «revocada» o un error sin la firma de la central son `unsigned_response` (un fallo
27
+ * pasajero: se sigue con la copia), y un 200 sin cifrar no se acepta aunque venga firmado.
28
+ * - **Sólo https** (salvo `localhost`, `127.0.0.1` y nombres `.test`/`.localhost`), **sin redirecciones** (`redirect: 'error'`), **tiempos y
29
+ * tamaño con tope** (3 s hasta la respuesta, 10 s en total, 8 MB) y hasta tres intentos si la central no contestó o dio 5xx: cada uno con su
30
+ * hora, su nonce y su efímera, porque repetir la misma petición sería un `replayed`.
31
+ * - **Errores con CÓDIGO** (`invalid_key`, `revoked_key`, `expired_key`, `no_environment`, `not_published`, `clock_skew`, `replayed`,
32
+ * `network_not_allowed`, `unsigned_response`, `unreachable`, `timeout`, `malformed`, `incompatible_schema`), nunca con un valor ni con la
33
+ * credencial.
34
+ *
35
+ * Cada petición dice quién pregunta —la réplica, la dirección del producto, la ruta por la que escucha avisos, la librería y el runtime— y
36
+ * esas cabeceras van DENTRO de la firma: la central las guarda y las usa.
37
+ */
38
+ export declare class EnvClient {
39
+ private readonly deps;
40
+ constructor(deps: EnvClientDeps);
41
+ /** La versión vigente, o `null` si la central dice (firmado) que no cambió: 304 con el `etag` de la copia. */
42
+ fetch(etag?: string | null): Promise<EnvPayload | null>;
43
+ /** Confirma a la central que esta réplica aplicó (o por qué no). Nunca lanza: un acuse perdido sólo deja el panel atrás. */
44
+ ack(version: number, ok: boolean, errorCode?: string | null): Promise<boolean>;
45
+ /**
46
+ * Lo que entregó la central, si lo FIRMÓ ella para esta petición.
47
+ *
48
+ * @throws EnvError
49
+ */
50
+ private delivered;
51
+ /** ¿La firmó la central fijada, para ESTA petición (su hash), con este estado, este cuerpo y este ETag? */
52
+ private signed;
53
+ private call;
54
+ /** Las cabeceras de UNA petición firmada y el hash de su cadena. Cada intento llama otra vez: hora, nonce y efímera nuevos. */
55
+ private sign;
56
+ private once;
57
+ private random;
58
+ }