titan-vault-node 2.0.0 → 2.1.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/README.md CHANGED
@@ -1,8 +1,13 @@
1
- # 🛡️ Titan Vault (Node.js SDK) — Modo Paranoia & Zero-Trust
1
+ # Titan Vault (Node.js SDK)
2
2
 
3
- > **SDK de seguridad Zero-Trust, Criptografía E2E, Envenenamiento de `process.env` y Protección de Memoria RAM.**
3
+ Cliente de la bóveda local. Pide `GET /secret/data/{path}` con una llave RSA-2048 efímera, descifra la respuesta híbrida (RSA-OAEP-SHA256 + AES-256-CFB) y reparte cada campo:
4
4
 
5
- `titan-vault-node` resuelve la vulnerabilidad fundamental de Node.js: **la exposición de secretos en texto plano en la memoria RAM (Heap Dumps) y `process.env`.**
5
+ - `JWT_SECRET` y el resto de llaves quedan como `crypto.KeyObject`. El SDK no guarda otra copia en string. `getRaw()` sí crea un string, y el JSON descifrado vive en el heap hasta que V8 lo recolecta.
6
+ - `DB_PASSWORD` y las demás contraseñas de base de datos se entregan solo cuando Sequelize o `pg` piden la conexión.
7
+ - `DATABASE_URL`, `REDIS_URL`, `PORT` y el resto se leen con `TitanVault.config.get`. No entran en `process.env`, salvo `NODE_ENV`, `PORT`, `HOST`, `FRONTEND_PORT` y `BACKEND_PORT`.
8
+ - `VAULT_EMERGENCY_RECOVERY_TOKEN` no se puede leer. Si el payload trae el cebo, esa lectura llama a `GET /sys/recovery` con el valor que inyectó la bóveda.
9
+
10
+ Si la bóveda no responde, o devuelve el secreto en claro después de recibir `x-vault-public-key`, `armParanoiaMode` lanza `VaultConnectionError` y el proceso no debe seguir. El token se lee de `vaultToken`, `VAULT_TOKEN` o `VAULT_APP_TOKEN`.
6
11
 
7
12
  ---
8
13
 
@@ -14,17 +19,13 @@
14
19
 
15
20
  ---
16
21
 
17
- ## 🛡️ La Solución: Modo Paranoia (Los 4 Pilares)
18
-
19
- `titan-vault-node` implementa una arquitectura de grado militar:
22
+ ## Qué hace el modo paranoia
20
23
 
21
- 1. **Patrón Bootstrap (Arranque Seguro):** Pausa el arranque del runtime para negociar llaves efímeras (RSA-2048 + AES-256-CFB) y proteger la memoria antes de evaluar el código de negocio.
22
- 2. **Envenenamiento de `process.env`:** Borra los secretos del entorno global y coloca un `Proxy` reactivo. Si alguien o una librería de terceros intenta hacer `process.env.JWT_SECRET`, la ejecución se interrumpe con `ParanoiaBlockedError`. Además, las variables secretas no son enumerables (no aparecen en `Object.keys()` ni `console.log(process.env)`).
23
- 3. **Criptografía Nativa C++ (`KeyObject`):** Las llaves simétricas (JWT, cookies, AES) se cargan directamente como instancias nativas `crypto.KeyObject` de C++, invisibles a búsquedas de strings en el heap de V8.
24
- 4. **Inyección JIT en Bases de Datos:**
25
- - **Sequelize:** Gancho `beforeConnect` para inyectar contraseñas microsegundos antes del handshake, y `afterConnect` para nulificar (`password = null`) de inmediato.
26
- - **node-postgres (`pg`):** Función asíncrona de contraseña bajo demanda (`async () => password`).
27
- 5. **CaaS / Transit Engine:** Criptografía como Servicio. Permite firmar tokens o cifrar datos directamente en el servidor Titan Vault sin mantener ninguna llave en memoria.
24
+ 1. **Arranque.** `bootstrap` espera a `armParanoiaMode`. Si esa llamada lanza, no importa el servidor.
25
+ 2. **`process.env`.** Un proxy lanza `ParanoiaBlockedError` al leer o escribir un secreto, y lo oculta de `Object.keys` y `console.log(process.env)`.
26
+ 3. **`KeyObject`.** `TitanVault.secrets.get('JWT_SECRET')` sirve para `jsonwebtoken` y `crypto`. La búsqueda de strings en el heap de V8 todavía puede encontrar el valor que produjo `JSON.parse` hasta que el recolector lo suelte.
27
+ 4. **Base de datos.** `hookSequelize` pone la contraseña en `beforeConnect` y la quita del config en `afterConnect`. `getPgPasswordProvider` la devuelve en cada conexión. El driver conserva el string que necesita el handshake.
28
+ 5. **Transit.** `encrypt`, `decrypt`, `sign` y `verify` llaman a `/transit/*` en la bóveda. La llave no sale del servidor.
28
29
 
29
30
  ---
30
31
 
@@ -39,7 +40,7 @@ Crea un archivo de arranque que cargue el SDK antes de importar el servidor:
39
40
  import { TitanVault } from 'titan-vault-node';
40
41
 
41
42
  async function main() {
42
- // 1. Arma el Modo Paranoia y asegura la memoria
43
+ // Lanza VaultConnectionError si la bóveda no responde.
43
44
  await TitanVault.armParanoiaMode({
44
45
  vaultUrl: process.env.VAULT_URL || 'http://127.0.0.1:8200',
45
46
  vaultToken: process.env.VAULT_APP_TOKEN,
@@ -63,7 +64,6 @@ Compatible de forma nativa con `jsonwebtoken`, `jose` y el módulo `crypto` de N
63
64
  import jwt from 'jsonwebtoken';
64
65
  import { TitanVault } from 'titan-vault-node';
65
66
 
66
- // Obtiene el KeyObject de C++ (Invisible en volcados de RAM)
67
67
  const jwtKey = TitanVault.secrets.get('JWT_SECRET');
68
68
 
69
69
  // Firmar
@@ -137,16 +137,21 @@ const isValid = await TitanVault.transit.verify('mi-firma-jwt', 'payload.a.firma
137
137
 
138
138
  ---
139
139
 
140
- ### 5. Parámetros de Configuración No Sensibles
140
+ ### 5. Configuración
141
+
142
+ `DATABASE_URL` y `REDIS_URL` salen de aquí. No son contraseñas y no se copian a `process.env`.
141
143
 
142
144
  ```typescript
145
+ const databaseUrl = TitanVault.config.get('DATABASE_URL');
143
146
  const port = TitanVault.config.get('PORT', '3000');
144
147
  const host = TitanVault.config.get('HOST', '0.0.0.0');
145
148
  ```
146
149
 
147
150
  ---
148
151
 
149
- ### 6. Verificación de Hardening en Contenedores
152
+ ### 6. Reporte de runtime
153
+
154
+ `verifyRuntimeHardening()` dice si el proceso es root, si `NODE_ENV` es `production`, si el límite blando de core dumps es 0 (`/proc/self/limits`) y si el modo paranoia llegó a armarse. No cambia esos límites.
150
155
 
151
156
  ```typescript
152
157
  TitanVault.verifyRuntimeHardening();
@@ -154,6 +159,10 @@ TitanVault.verifyRuntimeHardening();
154
159
 
155
160
  ---
156
161
 
157
- ## 🍯 Tripwires y Honeytokens
162
+ ## Honeytoken
163
+
164
+ En la bóveda, la trampa está apagada hasta definir `TITAN_INJECT_HONEYTOKEN=1` y `TITAN_HONEYTOKEN_VALUE`. El campo por defecto es `VAULT_EMERGENCY_RECOVERY_TOKEN` (`TITAN_HONEYTOKEN_FIELD` lo cambia en los dos lados).
165
+
166
+ Leer `process.env.VAULT_EMERGENCY_RECOVERY_TOKEN` lanza `ParanoiaBlockedError`. Si el payload traía el cebo, el SDK llama a `GET /sys/recovery` con ese valor en `x-vault-token`. La bóveda sella cuando el header coincide. Sin cebo en el payload, la lectura se bloquea y la bóveda sigue abierta.
158
167
 
159
- Si un atacante inspecciona `process.env` e intenta leer la variable trampa `VAULT_EMERGENCY_RECOVERY_TOKEN`, Titan Vault detona inmediatamente una alerta de intrusión hacia el servidor central y sella la bóveda.
168
+ `TitanVault.flushTripwire()` espera a que esa petición termine.
@@ -0,0 +1,9 @@
1
+ import { VaultOptions } from '../types';
2
+ /** Same default field the vault injects (`TITAN_HONEYTOKEN_FIELD`). */
3
+ export declare const HONEYTOKEN_FIELD = "VAULT_EMERGENCY_RECOVERY_TOKEN";
4
+ export type SecretClass = 'honeytoken' | 'crypto' | 'database' | 'config';
5
+ /**
6
+ * Classification shared with the vault.
7
+ * `DATABASE_URL` is configuration the app reads. Only password fields go to the JIT store.
8
+ */
9
+ export declare function classifyKey(key: string, tag: string | undefined, options?: VaultOptions): SecretClass;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HONEYTOKEN_FIELD = void 0;
4
+ exports.classifyKey = classifyKey;
5
+ /** Same default field the vault injects (`TITAN_HONEYTOKEN_FIELD`). */
6
+ exports.HONEYTOKEN_FIELD = 'VAULT_EMERGENCY_RECOVERY_TOKEN';
7
+ // Keep in sync with titan_vault/core.py `_classify_key`.
8
+ const DB_PASSWORD_RE = /(DB_PASS|POSTGRES_PASS|MYSQL_PASS|DB_PASSWORD|POSTGRES_PASSWORD|MYSQL_PASSWORD)/i;
9
+ const CRYPTO_RE = /(JWT|SECRET|KEY|PRIVATE|TOKEN|SALT)/i;
10
+ /**
11
+ * Classification shared with the vault.
12
+ * `DATABASE_URL` is configuration the app reads. Only password fields go to the JIT store.
13
+ */
14
+ function classifyKey(key, tag, options = {}) {
15
+ const honeyField = options.honeytokenField || exports.HONEYTOKEN_FIELD;
16
+ if (tag === 'honeytoken' || key === honeyField)
17
+ return 'honeytoken';
18
+ if ((options.cryptoKeys || []).includes(key) || tag === 'crypto')
19
+ return 'crypto';
20
+ if ((options.dbKeys || []).includes(key) || tag === 'database' || DB_PASSWORD_RE.test(key)) {
21
+ return 'database';
22
+ }
23
+ if (CRYPTO_RE.test(key))
24
+ return 'crypto';
25
+ return 'config';
26
+ }
@@ -1,20 +1,16 @@
1
1
  import crypto from 'crypto';
2
2
  export declare class CryptoStore {
3
3
  private keyObjects;
4
- private rawFallback;
5
4
  /**
6
- * Securely store a secret key by converting it to a native C++ KeyObject
7
- * and immediately zeroing out the JavaScript buffer.
5
+ * Store the secret as a KeyObject and drop the temporary byte buffer.
6
+ * The original JavaScript string from JSON.parse is immutable and stays
7
+ * until V8 collects it. This store does not keep a second copy.
8
8
  */
9
9
  register(name: string, rawSecret: string): void;
10
- /**
11
- * Retrieves the native C++ KeyObject.
12
- * This object is invisible to JavaScript heap string inspection and safe from RAM dumps.
13
- */
14
10
  get(name: string): crypto.KeyObject;
15
11
  /**
16
- * Retrieves the raw string representation.
17
- * WARNING: Accessing secrets as raw strings reintroduces them to the V8 Heap.
12
+ * Materialize the secret as a string. That string lives on the V8 heap
13
+ * until it is collected. Prefer `get()` when the library accepts a KeyObject.
18
14
  */
19
15
  getRaw(name: string): string;
20
16
  has(name: string): boolean;
@@ -7,49 +7,30 @@ exports.CryptoStore = void 0;
7
7
  const crypto_1 = __importDefault(require("crypto"));
8
8
  class CryptoStore {
9
9
  keyObjects = new Map();
10
- rawFallback = new Map();
11
10
  /**
12
- * Securely store a secret key by converting it to a native C++ KeyObject
13
- * and immediately zeroing out the JavaScript buffer.
11
+ * Store the secret as a KeyObject and drop the temporary byte buffer.
12
+ * The original JavaScript string from JSON.parse is immutable and stays
13
+ * until V8 collects it. This store does not keep a second copy.
14
14
  */
15
15
  register(name, rawSecret) {
16
16
  if (!rawSecret)
17
17
  return;
18
- // Allocate buffer for the raw secret
19
18
  const buffer = Buffer.from(rawSecret, 'utf-8');
20
19
  try {
21
- // Check if it's PEM formatted private key
22
20
  if (rawSecret.includes('-----BEGIN PRIVATE KEY-----') || rawSecret.includes('-----BEGIN RSA PRIVATE KEY-----')) {
23
- const privKey = crypto_1.default.createPrivateKey({
24
- key: rawSecret,
25
- format: 'pem',
26
- });
27
- this.keyObjects.set(name, privKey);
21
+ this.keyObjects.set(name, crypto_1.default.createPrivateKey({ key: rawSecret, format: 'pem' }));
28
22
  }
29
23
  else if (rawSecret.includes('-----BEGIN PUBLIC KEY-----')) {
30
- const pubKey = crypto_1.default.createPublicKey({
31
- key: rawSecret,
32
- format: 'pem',
33
- });
34
- this.keyObjects.set(name, pubKey);
24
+ this.keyObjects.set(name, crypto_1.default.createPublicKey({ key: rawSecret, format: 'pem' }));
35
25
  }
36
26
  else {
37
- // Symmetric secret key (HMAC, AES, JWT, cookies)
38
- const secretKey = crypto_1.default.createSecretKey(buffer);
39
- this.keyObjects.set(name, secretKey);
27
+ this.keyObjects.set(name, crypto_1.default.createSecretKey(buffer));
40
28
  }
41
29
  }
42
30
  finally {
43
- // ZERO OUT BUFFER IN MEMORY immediately to prevent RAM heap dump recovery
44
31
  buffer.fill(0);
45
32
  }
46
- // Keep an internal raw fallback for libraries that strictly require raw strings (e.g. legacy argon2)
47
- this.rawFallback.set(name, rawSecret);
48
33
  }
49
- /**
50
- * Retrieves the native C++ KeyObject.
51
- * This object is invisible to JavaScript heap string inspection and safe from RAM dumps.
52
- */
53
34
  get(name) {
54
35
  const key = this.keyObjects.get(name);
55
36
  if (!key) {
@@ -58,17 +39,23 @@ class CryptoStore {
58
39
  return key;
59
40
  }
60
41
  /**
61
- * Retrieves the raw string representation.
62
- * WARNING: Accessing secrets as raw strings reintroduces them to the V8 Heap.
42
+ * Materialize the secret as a string. That string lives on the V8 heap
43
+ * until it is collected. Prefer `get()` when the library accepts a KeyObject.
63
44
  */
64
45
  getRaw(name) {
65
- const raw = this.rawFallback.get(name);
66
- if (!raw) {
67
- throw new Error(`[Titan Vault] Llave '${name}' no encontrada.`);
46
+ const key = this.get(name);
47
+ console.warn(`⚠️ [Titan Vault] '${name}' se materializó como texto. ` +
48
+ `Ese string queda en el heap de V8 hasta que el recolector lo libere. ` +
49
+ `Usa get() cuando la librería acepte un KeyObject.`);
50
+ if (key.type === 'secret') {
51
+ const exported = key.export();
52
+ return Buffer.isBuffer(exported) ? exported.toString('utf8') : Buffer.from(exported).toString('utf8');
68
53
  }
69
- console.warn(`⚠️ [Titan Vault - Seguridad] Se accedió a la llave '${name}' como texto plano. ` +
70
- `Esto expone el secreto en el Heap de V8. Considera usar 'get("${name}")' devolviendo KeyObject.`);
71
- return raw;
54
+ const exported = key.export({
55
+ type: key.type === 'private' ? 'pkcs8' : 'spki',
56
+ format: 'pem',
57
+ });
58
+ return typeof exported === 'string' ? exported : exported.toString('utf8');
72
59
  }
73
60
  has(name) {
74
61
  return this.keyObjects.has(name);
@@ -3,17 +3,15 @@ export declare class DatabaseHooks {
3
3
  register(key: string, value: string): void;
4
4
  getJitCredential(key: string): string;
5
5
  /**
6
- * Hook for Sequelize ORM.
7
- * Injects the database password micro-seconds before connection handshake,
8
- * and immediately wipes it (sets config.password = null) after connection succeeds,
9
- * minimizing RAM heap exposure.
6
+ * Inject the password on `beforeConnect` and clear `config.password` after.
7
+ * The driver still receives a JavaScript string for the handshake.
10
8
  */
11
9
  hookSequelize(sequelize: any, options?: {
12
10
  credentialKey?: string;
13
11
  }): void;
14
12
  /**
15
- * Generates an async password provider for node-postgres (pg.Pool).
16
- * pg.Pool supports `password: async () => string` dynamically per socket connection.
13
+ * Password function for `pg.Pool`. node-postgres calls it per connection.
17
14
  */
18
15
  getPgPasswordProvider(keyName?: string): () => Promise<string>;
16
+ private materialize;
19
17
  }
@@ -4,22 +4,19 @@ exports.DatabaseHooks = void 0;
4
4
  class DatabaseHooks {
5
5
  credentials = new Map();
6
6
  register(key, value) {
7
- if (key && value) {
8
- this.credentials.set(key, value);
9
- }
7
+ if (!key || !value)
8
+ return;
9
+ const existing = this.credentials.get(key);
10
+ if (existing)
11
+ existing.fill(0);
12
+ this.credentials.set(key, Buffer.from(value, 'utf8'));
10
13
  }
11
14
  getJitCredential(key) {
12
- const cred = this.credentials.get(key);
13
- if (!cred) {
14
- throw new Error(`[Titan Vault - JIT DB] Credencial '${key}' no encontrada en el almacén JIT.`);
15
- }
16
- return cred;
15
+ return this.materialize(key);
17
16
  }
18
17
  /**
19
- * Hook for Sequelize ORM.
20
- * Injects the database password micro-seconds before connection handshake,
21
- * and immediately wipes it (sets config.password = null) after connection succeeds,
22
- * minimizing RAM heap exposure.
18
+ * Inject the password on `beforeConnect` and clear `config.password` after.
19
+ * The driver still receives a JavaScript string for the handshake.
23
20
  */
24
21
  hookSequelize(sequelize, options = {}) {
25
22
  const credKey = options.credentialKey || 'DB_PASSWORD';
@@ -27,43 +24,28 @@ class DatabaseHooks {
27
24
  console.warn('⚠️ [Titan Vault] La instancia provista no parece ser un objeto Sequelize válido.');
28
25
  return;
29
26
  }
30
- // 1. Before connection handshake: Inject credential JIT
31
27
  sequelize.addHook('beforeConnect', async (config) => {
32
- try {
33
- const password = this.credentials.get(credKey) || this.credentials.get('DATABASE_URL');
34
- if (password) {
35
- config.password = password;
36
- }
37
- }
38
- catch (err) {
39
- console.error('🚨 [Titan Vault] Error inyectando contraseña JIT en Sequelize:', err);
40
- }
28
+ config.password = this.materialize(credKey);
41
29
  });
42
- // 2. After connection handshake: Nullify immediately to force Garbage Collection
43
30
  sequelize.addHook('afterConnect', async (_connection, config) => {
44
- try {
45
- if (config && 'password' in config) {
46
- config.password = null;
47
- }
48
- }
49
- catch (err) {
50
- // ignore
31
+ if (config && 'password' in config) {
32
+ config.password = null;
51
33
  }
52
34
  });
53
- console.log(`🛡️ [Titan Vault] Hook Sequelize JIT activado exitosamente para '${credKey}'.`);
35
+ console.log(`🛡️ [Titan Vault] Hook Sequelize JIT activado para '${credKey}'.`);
54
36
  }
55
37
  /**
56
- * Generates an async password provider for node-postgres (pg.Pool).
57
- * pg.Pool supports `password: async () => string` dynamically per socket connection.
38
+ * Password function for `pg.Pool`. node-postgres calls it per connection.
58
39
  */
59
40
  getPgPasswordProvider(keyName = 'DB_PASSWORD') {
60
- return async () => {
61
- const password = this.credentials.get(keyName) || this.credentials.get('DATABASE_URL');
62
- if (!password) {
63
- throw new Error(`[Titan Vault - JIT pg] Credencial de BD '${keyName}' no disponible.`);
64
- }
65
- return password;
66
- };
41
+ return async () => this.materialize(keyName);
42
+ }
43
+ materialize(key) {
44
+ const buf = this.credentials.get(key);
45
+ if (!buf) {
46
+ throw new Error(`[Titan Vault - JIT DB] Credencial '${key}' no encontrada en el almacén JIT.`);
47
+ }
48
+ return buf.toString('utf8');
67
49
  }
68
50
  }
69
51
  exports.DatabaseHooks = DatabaseHooks;
@@ -1,2 +1,2 @@
1
1
  import { HardeningReport } from '../types';
2
- export declare function verifyRuntimeHardening(): HardeningReport;
2
+ export declare function verifyRuntimeHardening(paranoiaArmed: boolean): HardeningReport;
@@ -1,7 +1,26 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.verifyRuntimeHardening = verifyRuntimeHardening;
4
- function verifyRuntimeHardening() {
7
+ const fs_1 = __importDefault(require("fs"));
8
+ function readCoreDumpSoftLimit() {
9
+ try {
10
+ const text = fs_1.default.readFileSync('/proc/self/limits', 'utf8');
11
+ const line = text.split('\n').find((entry) => entry.toLowerCase().startsWith('max core file size'));
12
+ if (!line)
13
+ return null;
14
+ const cols = line.split(/\s{2,}/).map((part) => part.trim());
15
+ if (!cols[1])
16
+ return null;
17
+ return cols[1] === '0';
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ function verifyRuntimeHardening(paranoiaArmed) {
5
24
  const recommendations = [];
6
25
  const nodeEnv = process.env.NODE_ENV || 'development';
7
26
  let runningAsRoot = false;
@@ -11,20 +30,30 @@ function verifyRuntimeHardening() {
11
30
  }
12
31
  }
13
32
  catch {
14
- // Windows or restricted environment
33
+ // Windows or a restricted environment.
15
34
  }
35
+ const coreDumpsDisabled = readCoreDumpSoftLimit();
16
36
  if (runningAsRoot) {
17
- recommendations.push('⚠️ El proceso corre como root (UID 0). En producción, corre como un usuario no privilegiado (ej. USER node).');
37
+ recommendations.push('El proceso corre como root (UID 0). En producción usa un usuario sin privilegios.');
18
38
  }
19
39
  if (nodeEnv !== 'production') {
20
- recommendations.push('ℹ️ NODE_ENV no es "production". Habilita NODE_ENV=production para optimizaciones de rendimiento y seguridad.');
40
+ recommendations.push('NODE_ENV no es "production".');
41
+ }
42
+ if (coreDumpsDisabled === null) {
43
+ recommendations.push('No se pudo leer el límite de core dumps. En Linux revisa /proc/self/limits (Max core file size).');
44
+ }
45
+ else if (!coreDumpsDisabled) {
46
+ recommendations.push('Los core dumps están permitidos. Un ulimit -c 0 evita que un volcado escriba la memoria a disco.');
47
+ }
48
+ if (!paranoiaArmed) {
49
+ recommendations.push('El modo paranoia no está armado. Los secretos no pasaron por el proxy de process.env.');
21
50
  }
22
- recommendations.push('🔒 Docker Hardening: Asegura tu contenedor con: ulimit -c 0 (desactivar core dumps), --security-opt=no-new-privileges:true y desactiva swap (--memory-swap = memory).');
23
- const isHardened = !runningAsRoot && nodeEnv === 'production';
24
- console.log('\n🛡️ [Titan Vault] Reporte de Hardening en Runtime:');
51
+ const isHardened = !runningAsRoot && nodeEnv === 'production' && coreDumpsDisabled === true && paranoiaArmed;
52
+ console.log('\n🛡️ [Titan Vault] Reporte de hardening:');
25
53
  console.log(` - Entorno: ${nodeEnv}`);
26
- console.log(` - Usuario Root: ${runningAsRoot ? '⚠️ SÍ (No recomendado)' : '✅ NO (Seguro)'}`);
27
- console.log(` - Protección Heap V8: ✅ Paranoia Mode Activo`);
54
+ console.log(` - Usuario root: ${runningAsRoot ? 'sí' : 'no'}`);
55
+ console.log(` - Core dumps desactivados: ${coreDumpsDisabled === null ? 'desconocido' : coreDumpsDisabled ? 'sí' : 'no'}`);
56
+ console.log(` - Modo paranoia armado: ${paranoiaArmed ? 'sí' : 'no'}`);
28
57
  for (const rec of recommendations) {
29
58
  console.log(` ${rec}`);
30
59
  }
@@ -32,8 +61,10 @@ function verifyRuntimeHardening() {
32
61
  return {
33
62
  isHardened,
34
63
  nodeEnv,
35
- coreDumpsDisabled: true,
64
+ coreDumpsDisabled: coreDumpsDisabled === true,
65
+ coreDumpLimitKnown: coreDumpsDisabled !== null,
36
66
  runningAsRoot,
67
+ paranoiaArmed,
37
68
  recommendations,
38
69
  };
39
70
  }
@@ -50,22 +50,22 @@ function poisonProcessEnv(secretKeys, honeytokenKey, onHoneytokenDetonated) {
50
50
  return Reflect.get(target, prop, receiver);
51
51
  },
52
52
  set(target, prop, value, receiver) {
53
- if (typeof prop === 'string' && poisonedSet.has(prop)) {
53
+ if (typeof prop === 'string' && (prop === honeyKey || poisonedSet.has(prop))) {
54
54
  throw new ParanoiaBlockedError(prop, `No se permite mutar 'process.env.${prop}' en Modo Paranoia.`);
55
55
  }
56
56
  return Reflect.set(target, prop, value, receiver);
57
57
  },
58
58
  has(target, prop) {
59
- if (typeof prop === 'string' && poisonedSet.has(prop)) {
59
+ if (typeof prop === 'string' && (prop === honeyKey || poisonedSet.has(prop))) {
60
60
  return false;
61
61
  }
62
62
  return Reflect.has(target, prop);
63
63
  },
64
64
  ownKeys(target) {
65
- return Reflect.ownKeys(target).filter(k => typeof k !== 'string' || !poisonedSet.has(k));
65
+ return Reflect.ownKeys(target).filter((k) => typeof k !== 'string' || (k !== honeyKey && !poisonedSet.has(k)));
66
66
  },
67
67
  getOwnPropertyDescriptor(target, prop) {
68
- if (typeof prop === 'string' && poisonedSet.has(prop)) {
68
+ if (typeof prop === 'string' && (prop === honeyKey || poisonedSet.has(prop))) {
69
69
  return undefined;
70
70
  }
71
71
  return Reflect.getOwnPropertyDescriptor(target, prop);
package/dist/index.d.ts CHANGED
@@ -8,6 +8,10 @@ export * from './core/crypto-store';
8
8
  export * from './core/database-hooks';
9
9
  export * from './core/transit-client';
10
10
  export * from './core/hardening';
11
+ export * from './core/classify';
12
+ export declare class VaultConnectionError extends Error {
13
+ constructor(message: string);
14
+ }
11
15
  export declare class TitanVault {
12
16
  private static instance;
13
17
  static readonly secrets: CryptoStore;
@@ -16,6 +20,8 @@ export declare class TitanVault {
16
20
  private static vaultUrl;
17
21
  private static vaultToken?;
18
22
  private static isArmed;
23
+ private static baitValues;
24
+ private static tripwireInflight;
19
25
  static readonly transit: TransitClient;
20
26
  static readonly config: {
21
27
  get(key: string, defaultValue?: string): string;
@@ -23,24 +29,19 @@ export declare class TitanVault {
23
29
  getAll(): Record<string, string>;
24
30
  };
25
31
  /**
26
- * Arm Paranoia Mode (Zero-RAM Leak Protection):
27
- * 1. Fetches secrets with E2E Ephemeral RSA-2048 hybrid encryption.
28
- * 2. Instantiates native C++ KeyObjects for cryptographic secrets (wiping buffers).
29
- * 3. Isolates database passwords into JIT hooks.
30
- * 4. Envenoms process.env for all secret keys, throwing on unauthorized reads.
31
- * 5. Arms Honeytoken Tripwire alarms.
32
+ * Fetch the app path with an ephemeral RSA-2048 key, decrypt the hybrid
33
+ * payload, and block secret reads on process.env.
34
+ * Throws if the token is missing or the vault does not return ciphertext.
32
35
  */
33
36
  static armParanoiaMode(options?: VaultOptions): Promise<void>;
34
- /**
35
- * The 10-line Bootstrap Runner:
36
- * Initializes Paranoia Mode asynchronously, and once memory is hardened,
37
- * dynamically executes the app server import.
38
- */
37
+ /** Resolves when the in-flight honeytoken alert has finished or failed. */
38
+ static flushTripwire(): Promise<void>;
39
39
  static bootstrap(serverFn: () => Promise<any>, options?: VaultOptions): Promise<void>;
40
40
  static verifyRuntimeHardening(): HardeningReport;
41
41
  }
42
42
  /**
43
- * Backward-compatible helper for drop-in replacement
43
+ * Classic mode (`paranoiaMode: false`) copies string values into process.env.
44
+ * The default calls armParanoiaMode and throws if the vault is unreachable.
44
45
  */
45
46
  export declare function initVault(options?: VaultOptions): Promise<void>;
46
47
  export default TitanVault;
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.TitanVault = void 0;
20
+ exports.TitanVault = exports.VaultConnectionError = void 0;
21
21
  exports.initVault = initVault;
22
22
  const dotenv_1 = __importDefault(require("dotenv"));
23
23
  const crypto_1 = __importDefault(require("crypto"));
@@ -26,12 +26,55 @@ const crypto_store_1 = require("./core/crypto-store");
26
26
  const database_hooks_1 = require("./core/database-hooks");
27
27
  const transit_client_1 = require("./core/transit-client");
28
28
  const hardening_1 = require("./core/hardening");
29
+ const classify_1 = require("./core/classify");
29
30
  __exportStar(require("./types"), exports);
30
31
  __exportStar(require("./core/poison"), exports);
31
32
  __exportStar(require("./core/crypto-store"), exports);
32
33
  __exportStar(require("./core/database-hooks"), exports);
33
34
  __exportStar(require("./core/transit-client"), exports);
34
35
  __exportStar(require("./core/hardening"), exports);
36
+ __exportStar(require("./core/classify"), exports);
37
+ class VaultConnectionError extends Error {
38
+ constructor(message) {
39
+ super(message);
40
+ this.name = 'VaultConnectionError';
41
+ Object.setPrototypeOf(this, VaultConnectionError.prototype);
42
+ }
43
+ }
44
+ exports.VaultConnectionError = VaultConnectionError;
45
+ function resolveToken(explicit) {
46
+ return explicit || process.env.VAULT_TOKEN || process.env.VAULT_APP_TOKEN;
47
+ }
48
+ function resolveUrl(explicit) {
49
+ return explicit || process.env.VAULT_URL || 'http://127.0.0.1:8200';
50
+ }
51
+ function decryptHybrid(privateKey, payloadB64) {
52
+ const buffer = Buffer.from(payloadB64, 'base64');
53
+ const modulusLength = privateKey.asymmetricKeyDetails?.modulusLength;
54
+ if (!modulusLength) {
55
+ throw new VaultConnectionError('No se pudo leer el tamaño de la llave RSA efímera.');
56
+ }
57
+ const keyBytes = modulusLength / 8;
58
+ if (buffer.length < keyBytes + 16) {
59
+ throw new VaultConnectionError('El payload cifrado es más corto que una respuesta RSA-OAEP + AES-256-CFB.');
60
+ }
61
+ const encAesKey = buffer.subarray(0, keyBytes);
62
+ const iv = buffer.subarray(keyBytes, keyBytes + 16);
63
+ const ciphertext = buffer.subarray(keyBytes + 16);
64
+ const aesKey = crypto_1.default.privateDecrypt({
65
+ key: privateKey,
66
+ padding: crypto_1.default.constants.RSA_PKCS1_OAEP_PADDING,
67
+ oaepHash: 'sha256',
68
+ }, encAesKey);
69
+ try {
70
+ const decipher = crypto_1.default.createDecipheriv('aes-256-cfb', aesKey, iv);
71
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
72
+ return decrypted.toString('utf8');
73
+ }
74
+ finally {
75
+ aesKey.fill(0);
76
+ }
77
+ }
35
78
  class TitanVault {
36
79
  static instance;
37
80
  static secrets = new crypto_store_1.CryptoStore();
@@ -40,12 +83,26 @@ class TitanVault {
40
83
  static vaultUrl = 'http://127.0.0.1:8200';
41
84
  static vaultToken;
42
85
  static isArmed = false;
86
+ static baitValues = new Map();
87
+ static tripwireInflight = Promise.resolve();
43
88
  static transit = new transit_client_1.TransitClient(() => TitanVault.vaultUrl, () => TitanVault.vaultToken);
44
89
  static config = {
45
90
  get(key, defaultValue) {
46
- const val = TitanVault.rawConfig.get(key) || process.env[key];
47
- if (val !== undefined)
48
- return val;
91
+ if (TitanVault.rawConfig.has(key)) {
92
+ return TitanVault.rawConfig.get(key);
93
+ }
94
+ let envVal;
95
+ try {
96
+ envVal = process.env[key];
97
+ }
98
+ catch (err) {
99
+ if (err instanceof Error && err.name === 'ParanoiaBlockedError') {
100
+ throw new Error(`[Titan Vault] '${key}' es un secreto. Usa TitanVault.secrets, TitanVault.databases o el campo de configuración correspondiente.`);
101
+ }
102
+ throw err;
103
+ }
104
+ if (envVal !== undefined)
105
+ return envVal;
49
106
  if (defaultValue !== undefined)
50
107
  return defaultValue;
51
108
  throw new Error(`[Titan Vault] Configuración '${key}' no encontrada.`);
@@ -62,126 +119,95 @@ class TitanVault {
62
119
  }
63
120
  };
64
121
  /**
65
- * Arm Paranoia Mode (Zero-RAM Leak Protection):
66
- * 1. Fetches secrets with E2E Ephemeral RSA-2048 hybrid encryption.
67
- * 2. Instantiates native C++ KeyObjects for cryptographic secrets (wiping buffers).
68
- * 3. Isolates database passwords into JIT hooks.
69
- * 4. Envenoms process.env for all secret keys, throwing on unauthorized reads.
70
- * 5. Arms Honeytoken Tripwire alarms.
122
+ * Fetch the app path with an ephemeral RSA-2048 key, decrypt the hybrid
123
+ * payload, and block secret reads on process.env.
124
+ * Throws if the token is missing or the vault does not return ciphertext.
71
125
  */
72
126
  static async armParanoiaMode(options = {}) {
73
127
  if (this.isArmed) {
74
- console.log('🛡️ [Titan Vault] Modo Paranoia ya está armado.');
128
+ console.log('🛡️ [Titan Vault] Modo paranoia ya está armado.');
75
129
  return;
76
130
  }
77
131
  dotenv_1.default.config();
78
- this.vaultUrl = options.vaultUrl || process.env.VAULT_URL || 'http://127.0.0.1:8200';
79
- this.vaultToken = options.vaultToken || process.env.VAULT_TOKEN;
80
- const SECRET_PATH = options.path || 'backend_env';
81
- const MAX_RETRIES = options.maxRetries || 5;
82
- const RETRY_DELAY = options.retryDelayMs || 5000;
132
+ this.vaultUrl = resolveUrl(options.vaultUrl);
133
+ this.vaultToken = resolveToken(options.vaultToken);
134
+ const secretPath = options.path || 'backend_env';
135
+ const maxRetries = options.maxRetries ?? 5;
136
+ const retryDelay = options.retryDelayMs ?? 5000;
137
+ const honeyField = options.honeytokenField || classify_1.HONEYTOKEN_FIELD;
83
138
  if (!this.vaultToken) {
84
- console.warn('⚠️ [Titan Vault] No se encontró VAULT_TOKEN. Modo Paranoia operando con variables locales.');
85
- return;
139
+ throw new VaultConnectionError('Falta el token. Pasa vaultToken o define VAULT_TOKEN / VAULT_APP_TOKEN. El proceso no arranca sin la bóveda.');
86
140
  }
87
- console.log(`\n🛡️ [Titan Vault - Modo Paranoia] Conectando a ${this.vaultUrl}...`);
88
- // Ephemeral RSA keypair for E2E zero-trust transit
141
+ console.log(`\n🛡️ [Titan Vault] Conectando a ${this.vaultUrl}...`);
89
142
  const { publicKey, privateKey } = crypto_1.default.generateKeyPairSync('rsa', {
90
143
  modulusLength: 2048,
91
- publicKeyEncoding: { type: 'spki', format: 'pem' },
92
- privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
93
144
  });
94
- const pubBase64 = Buffer.from(publicKey).toString('base64');
95
- let attempt = 0;
96
- let rawSecrets = {};
97
- while (attempt < MAX_RETRIES) {
98
- attempt++;
145
+ const pubPem = publicKey.export({ type: 'spki', format: 'pem' });
146
+ const pubBase64 = Buffer.from(pubPem).toString('base64');
147
+ let rawSecrets = null;
148
+ let lastError = 'sin respuesta';
149
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
99
150
  try {
100
- const res = await fetch(`${this.vaultUrl}/secret/data/${SECRET_PATH}`, {
151
+ const res = await fetch(`${this.vaultUrl}/secret/data/${secretPath}`, {
101
152
  headers: {
102
153
  'x-vault-token': this.vaultToken,
103
154
  'x-vault-public-key': pubBase64,
104
155
  },
105
156
  });
106
- if (res.ok) {
107
- const json = await res.json();
108
- let finalSecretStr = '';
109
- if (json.data && json.data.is_encrypted) {
110
- console.log('🔐 [Titan Vault] Descifrando payload E2E híbrido (RSA-OAEP-2048 + AES-256-CFB)...');
111
- const buffer = Buffer.from(json.data.value, 'base64');
112
- const encAesKey = buffer.subarray(0, 256);
113
- const iv = buffer.subarray(256, 256 + 16);
114
- const ciphertext = buffer.subarray(256 + 16);
115
- const aesKey = crypto_1.default.privateDecrypt({
116
- key: privateKey,
117
- padding: crypto_1.default.constants.RSA_PKCS1_OAEP_PADDING,
118
- oaepHash: 'sha256',
119
- }, encAesKey);
120
- const decipher = crypto_1.default.createDecipheriv('aes-256-cfb', aesKey, iv);
121
- let decrypted = decipher.update(ciphertext, undefined, 'utf8');
122
- decrypted += decipher.final('utf8');
123
- finalSecretStr = decrypted;
124
- }
125
- else {
126
- finalSecretStr = json.data?.value || '';
127
- }
128
- rawSecrets = JSON.parse(finalSecretStr);
129
- break; // Success!
157
+ if (!res.ok) {
158
+ lastError = `HTTP ${res.status}: ${await res.text()}`;
159
+ console.warn(`[Titan Vault] ${lastError}. Reintento ${attempt}/${maxRetries}.`);
130
160
  }
131
161
  else {
132
- const errText = await res.text();
133
- console.warn(`[Titan Vault] Vault sellado o token inválido (HTTP ${res.status}): ${errText}. Reintentando en ${RETRY_DELAY / 1000}s...`);
162
+ const json = await res.json();
163
+ if (!json.data?.is_encrypted || typeof json.data.value !== 'string') {
164
+ throw new VaultConnectionError('La bóveda devolvió el secreto en claro después de recibir x-vault-public-key.');
165
+ }
166
+ console.log('🔐 [Titan Vault] Descifrando payload híbrido (RSA-OAEP-SHA256 + AES-256-CFB)...');
167
+ rawSecrets = JSON.parse(decryptHybrid(privateKey, json.data.value));
168
+ break;
134
169
  }
135
170
  }
136
171
  catch (err) {
137
- console.warn(`❌ [Titan Vault] Error de conexión (${err.message}). Reintentando en ${RETRY_DELAY / 1000}s...`);
172
+ if (err instanceof VaultConnectionError)
173
+ throw err;
174
+ lastError = err?.message || String(err);
175
+ console.warn(`❌ [Titan Vault] ${lastError}. Reintento ${attempt}/${maxRetries}.`);
138
176
  }
139
- if (attempt < MAX_RETRIES) {
140
- await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
177
+ if (attempt < maxRetries) {
178
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
141
179
  }
142
180
  }
143
- if (Object.keys(rawSecrets).length === 0) {
144
- console.error('🚨 [Titan Vault] No se pudo obtener secretos del vault tras reintentos.');
145
- return;
181
+ if (!rawSecrets) {
182
+ throw new VaultConnectionError(`No se pudieron leer los secretos de '${secretPath}' en ${this.vaultUrl}. ${lastError}`);
146
183
  }
147
- // Unpack classification or run auto-classification
148
184
  const classification = rawSecrets['__titan_classification__'] || {};
149
185
  delete rawSecrets['__titan_classification__'];
150
- const cryptoKeyList = options.cryptoKeys || [];
151
- const dbKeyList = options.dbKeys || [];
152
186
  const allowedEnvList = new Set(options.allowedEnvKeys || ['NODE_ENV', 'PORT', 'HOST', 'FRONTEND_PORT', 'BACKEND_PORT']);
153
187
  const poisonedKeys = [];
188
+ this.baitValues = new Map();
154
189
  let cryptoCount = 0;
155
190
  let dbCount = 0;
156
191
  let configCount = 0;
157
192
  for (const [key, value] of Object.entries(rawSecrets)) {
158
193
  if (typeof value !== 'string')
159
194
  continue;
160
- const tag = classification[key];
161
- const isCrypto = cryptoKeyList.includes(key) ||
162
- tag === 'crypto' ||
163
- (!dbKeyList.includes(key) && /(JWT|SECRET|KEY|PRIVATE|TOKEN|SALT)/i.test(key));
164
- const isDb = dbKeyList.includes(key) ||
165
- tag === 'database' ||
166
- /(DB_PASS|DATABASE_URL|POSTGRES_PASS|MYSQL_PASS|DB_PASSWORD)/i.test(key);
167
- const isHoney = tag === 'honeytoken' || key === 'VAULT_EMERGENCY_RECOVERY_TOKEN';
168
- if (isHoney) {
195
+ const kind = (0, classify_1.classifyKey)(key, classification[key], options);
196
+ if (kind === 'honeytoken') {
197
+ this.baitValues.set(key, value);
169
198
  poisonedKeys.push(key);
170
199
  }
171
- else if (isCrypto) {
172
- // Securely store in C++ KeyObject store & poison process.env
200
+ else if (kind === 'crypto') {
173
201
  this.secrets.register(key, value);
174
202
  poisonedKeys.push(key);
175
203
  cryptoCount++;
176
204
  }
177
- else if (isDb) {
178
- // Securely store in JIT DB store & poison process.env
205
+ else if (kind === 'database') {
179
206
  this.databases.register(key, value);
180
207
  poisonedKeys.push(key);
181
208
  dbCount++;
182
209
  }
183
210
  else {
184
- // General configuration
185
211
  this.rawConfig.set(key, value);
186
212
  if (allowedEnvList.has(key)) {
187
213
  process.env[key] = value;
@@ -189,79 +215,78 @@ class TitanVault {
189
215
  configCount++;
190
216
  }
191
217
  }
192
- // Detonate honeytoken callback if someone accesses it
218
+ if (!poisonedKeys.includes(honeyField)) {
219
+ poisonedKeys.push(honeyField);
220
+ }
193
221
  const onDetonate = (tokenKey) => {
194
- console.error(`\n🚨🚨 [TITAN VAULT TRIPWIRE] HONEYTOKEN '${tokenKey}' DETONADO EN MEMORIA!`);
195
- // Alert server
196
- if (this.vaultToken) {
197
- fetch(`${this.vaultUrl}/sys/recovery`, {
198
- headers: { 'x-vault-token': 'DOOMSDAY_12345_BYPASS' },
199
- }).catch(() => { });
200
- }
201
- if (options.onHoneytokenDetonated) {
202
- options.onHoneytokenDetonated(tokenKey);
222
+ console.error(`\n🚨 [Titan Vault] Honeytoken '${tokenKey}' leído desde process.env.`);
223
+ const bait = this.baitValues.get(tokenKey);
224
+ if (bait) {
225
+ this.tripwireInflight = fetch(`${this.vaultUrl}/sys/recovery`, {
226
+ headers: { 'x-vault-token': bait },
227
+ }).then(() => undefined, () => undefined);
203
228
  }
229
+ options.onHoneytokenDetonated?.(tokenKey);
204
230
  };
205
- // POISON process.env FOR ALL SECRETS
206
- (0, poison_1.poisonProcessEnv)(poisonedKeys, 'VAULT_EMERGENCY_RECOVERY_TOKEN', onDetonate);
231
+ (0, poison_1.poisonProcessEnv)(poisonedKeys, honeyField, onDetonate);
207
232
  this.isArmed = true;
208
- console.log(`🛡️ [Titan Vault - Modo Paranoia] Armado con éxito:`);
209
- console.log(` - 🔑 ${cryptoCount} Llaves criptográficas (C++ KeyObjects nativos, fuera del Heap V8)`);
210
- console.log(` - 🗄️ ${dbCount} Credenciales de BD aisladas (JIT Hooks para Sequelize / pg)`);
211
- console.log(` - ⚙️ ${configCount} Parámetros de configuración seguros`);
212
- console.log(` - ☠️ ${poisonedKeys.length} Variables de entorno envenenadas contra RAM Dumps`);
213
- console.log(` - 🍯 Honeytoken Tripwire activo\n`);
233
+ console.log('🛡️ [Titan Vault] Modo paranoia armado:');
234
+ console.log(` - ${cryptoCount} llaves en KeyObject (sin copia string en el SDK)`);
235
+ console.log(` - ${dbCount} contraseñas de base de datos para hooks JIT`);
236
+ console.log(` - ${configCount} valores de configuración, fuera de process.env salvo la lista permitida`);
237
+ console.log(` - ${poisonedKeys.length} nombres bloqueados en process.env`);
238
+ console.log(this.baitValues.size
239
+ ? ' - trampa armada: leerla llama a GET /sys/recovery con el valor inyectado por la bóveda'
240
+ : ' - trampa local activa, sin cebo en el payload (la bóveda no se sella)');
241
+ }
242
+ /** Resolves when the in-flight honeytoken alert has finished or failed. */
243
+ static flushTripwire() {
244
+ return this.tripwireInflight;
214
245
  }
215
- /**
216
- * The 10-line Bootstrap Runner:
217
- * Initializes Paranoia Mode asynchronously, and once memory is hardened,
218
- * dynamically executes the app server import.
219
- */
220
246
  static async bootstrap(serverFn, options = {}) {
221
247
  await this.armParanoiaMode(options);
222
- console.log('🚀 [Titan Vault] Memoria blindada. Arrancando servidor...');
248
+ console.log('🚀 [Titan Vault] Secretos cargados. Arrancando servidor...');
223
249
  await serverFn();
224
250
  }
225
251
  static verifyRuntimeHardening() {
226
- return (0, hardening_1.verifyRuntimeHardening)();
252
+ return (0, hardening_1.verifyRuntimeHardening)(this.isArmed);
227
253
  }
228
254
  }
229
255
  exports.TitanVault = TitanVault;
230
256
  /**
231
- * Backward-compatible helper for drop-in replacement
257
+ * Classic mode (`paranoiaMode: false`) copies string values into process.env.
258
+ * The default calls armParanoiaMode and throws if the vault is unreachable.
232
259
  */
233
260
  async function initVault(options = {}) {
234
261
  if (options.paranoiaMode !== false) {
235
262
  await TitanVault.armParanoiaMode(options);
263
+ return;
236
264
  }
237
- else {
238
- // Legacy plaintext injection
239
- dotenv_1.default.config();
240
- const VAULT_URL = options.vaultUrl || process.env.VAULT_URL || 'http://127.0.0.1:8200';
241
- const VAULT_TOKEN = options.vaultToken || process.env.VAULT_TOKEN;
242
- const SECRET_PATH = options.path || 'backend_env';
243
- if (!VAULT_TOKEN) {
244
- console.warn('⚠️ [Titan Vault] No VAULT_TOKEN found. Using local .env fallback.');
245
- return;
246
- }
247
- try {
248
- const res = await fetch(`${VAULT_URL}/secret/data/${SECRET_PATH}`, {
249
- headers: { 'x-vault-token': VAULT_TOKEN },
250
- });
251
- if (res.ok) {
252
- const json = await res.json();
253
- const secrets = typeof json.data?.value === 'string' ? JSON.parse(json.data.value) : json.data?.value || {};
254
- for (const [k, v] of Object.entries(secrets)) {
255
- if (k !== '__titan_classification__' && typeof v === 'string') {
256
- process.env[k] = v;
257
- }
258
- }
259
- console.log('✅ [Titan Vault] Secretos inyectados en process.env (Modo Clásico).');
260
- }
261
- }
262
- catch (err) {
263
- console.error(`❌ [Titan Vault] Error: ${err.message}`);
264
- }
265
+ dotenv_1.default.config();
266
+ const vaultUrl = resolveUrl(options.vaultUrl);
267
+ const vaultToken = resolveToken(options.vaultToken);
268
+ const secretPath = options.path || 'backend_env';
269
+ const honeyField = options.honeytokenField || classify_1.HONEYTOKEN_FIELD;
270
+ if (!vaultToken) {
271
+ console.warn('⚠️ [Titan Vault] Sin token. Se mantienen las variables de .env.');
272
+ return;
273
+ }
274
+ const res = await fetch(`${vaultUrl}/secret/data/${secretPath}`, {
275
+ headers: { 'x-vault-token': vaultToken },
276
+ });
277
+ if (!res.ok) {
278
+ throw new VaultConnectionError(`Modo clásico: la bóveda respondió HTTP ${res.status} al leer '${secretPath}'.`);
279
+ }
280
+ const json = await res.json();
281
+ const secrets = typeof json.data?.value === 'string' ? JSON.parse(json.data.value) : json.data?.value || {};
282
+ const classification = secrets['__titan_classification__'] || {};
283
+ for (const [key, value] of Object.entries(secrets)) {
284
+ if (key === '__titan_classification__' || typeof value !== 'string')
285
+ continue;
286
+ if ((0, classify_1.classifyKey)(key, classification[key], options) === 'honeytoken' || key === honeyField)
287
+ continue;
288
+ process.env[key] = value;
265
289
  }
290
+ console.log('✅ [Titan Vault] Secretos copiados a process.env (modo clásico).');
266
291
  }
267
292
  exports.default = TitanVault;
package/dist/types.d.ts CHANGED
@@ -22,6 +22,11 @@ export interface VaultOptions {
22
22
  * Keys that are safe to expose in process.env (e.g. PORT, NODE_ENV).
23
23
  */
24
24
  allowedEnvKeys?: string[];
25
+ /**
26
+ * Field the vault injects when `TITAN_HONEYTOKEN_FIELD` is set.
27
+ * Defaults to `VAULT_EMERGENCY_RECOVERY_TOKEN`.
28
+ */
29
+ honeytokenField?: string;
25
30
  /**
26
31
  * Custom callback triggered if a honeytoken is accessed in process.env.
27
32
  */
@@ -52,7 +57,11 @@ export interface TransitVerifyResult {
52
57
  export interface HardeningReport {
53
58
  isHardened: boolean;
54
59
  nodeEnv: string;
60
+ /** True only when the soft core limit was read and is 0. */
55
61
  coreDumpsDisabled: boolean;
62
+ /** False when `/proc/self/limits` could not be read. */
63
+ coreDumpLimitKnown: boolean;
56
64
  runningAsRoot: boolean;
65
+ paranoiaArmed: boolean;
57
66
  recommendations: string[];
58
67
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "titan-vault-node",
3
- "version": "2.0.0",
4
- "description": "Zero-trust, E2E encrypted, drop-in replacement for dotenv.",
3
+ "version": "2.1.0",
4
+ "description": "Client for Titan Vault. Decrypts the hybrid secret payload, blocks secret reads on process.env, and calls transit on the vault.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [
@@ -9,7 +9,8 @@
9
9
  ],
10
10
  "scripts": {
11
11
  "build": "tsc",
12
- "prepare": "npm run build"
12
+ "prepare": "npm run build",
13
+ "test": "npm run build && node --test test/harmony.test.mjs"
13
14
  },
14
15
  "keywords": [
15
16
  "vault",