titan-vault-node 1.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,66 +1,168 @@
1
- # 🛡️ Titan Vault (Node.js SDK)
1
+ # Titan Vault (Node.js SDK)
2
2
 
3
- > **Zero-trust, E2E encrypted, drop-in replacement for dotenv.**
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 is the easiest way to secure your application secrets. Forget about hardcoded `.env` files that get leaked. Titan Vault acts as a highly secure, encrypted environment variable injector that requires **zero friction** to use.
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.
6
9
 
7
- ## 🚀 The Problem with `.env`
8
- If a hacker gets read access to your server (LFI, directory traversal, etc.), they can read your `.env` file and steal all your database passwords and API keys.
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`.
9
11
 
10
- ## 💡 The Titan Vault Solution
11
- Titan Vault stores your secrets in a centralized, encrypted server. Your app authenticates using a lightweight AppToken, and the Vault dynamically injects the secrets directly into Node.js memory (`process.env`) using **Hybrid E2E Encryption (RSA-OAEP + AES-CFB)**. If the app stops, the secrets disappear.
12
+ ---
13
+
14
+ ## ⚡ ¿Por qué `dotenv` y `process.env` son inseguros?
15
+
16
+ 1. **Memory Dumps:** Cualquier vulnerabilidad que permita un volcado de memoria (heapdump, coredump, inspector v8) expone todas las variables de entorno en texto plano.
17
+ 2. **Inmutabilidad V8:** En Node.js, `process.env` almacena strings inmutables. El recolector de basura (GC) no puede sobreescribir con ceros esas cadenas en memoria.
18
+ 3. **Peligro de Logs:** Un simple `console.log(process.env)` o reporte de error APM filtra todas tus credenciales.
19
+
20
+ ---
21
+
22
+ ## Qué hace el modo paranoia
12
23
 
13
- ### 🔥 Killer Features
14
- - **Zero-Friction SDK:** Replaces `dotenv` with exactly 2 lines of code.
15
- - **E2E Encryption out-of-the-box:** Secrets are never sent in plain text over the network.
16
- - **Honeytokens & Webhooks:** Create fake "Honeytoken" secrets. If a hacker tries to use them, the Vault instantly locks down and sends an alert to your **Telegram/Slack**.
17
- - **Burn-on-read Tokens:** For maximum paranoia, use one-time wrap tokens that self-destruct after your app boots.
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.
18
29
 
19
- ## 📦 Installation
30
+ ---
31
+
32
+ ## 💻 Guía de Integración
33
+
34
+ ### 1. El Patrón Bootstrap (`bootstrap.js` o `src/index.ts`)
35
+
36
+ Crea un archivo de arranque que cargue el SDK antes de importar el servidor:
37
+
38
+ ```typescript
39
+ // bootstrap.ts
40
+ import { TitanVault } from 'titan-vault-node';
41
+
42
+ async function main() {
43
+ // Lanza VaultConnectionError si la bóveda no responde.
44
+ await TitanVault.armParanoiaMode({
45
+ vaultUrl: process.env.VAULT_URL || 'http://127.0.0.1:8200',
46
+ vaultToken: process.env.VAULT_APP_TOKEN,
47
+ path: 'mi_aplicacion_env',
48
+ });
49
+
50
+ // 2. Importa el servidor dinámicamente UNA VEZ que la memoria está blindada
51
+ await import('./server.js');
52
+ }
20
53
 
21
- ```bash
22
- npm install titan-vault-node
54
+ main();
23
55
  ```
24
56
 
25
- ## 💻 Usage
57
+ ---
58
+
59
+ ### 2. Firma y Validación de Tokens JWT (C++ `KeyObject`)
60
+
61
+ Compatible de forma nativa con `jsonwebtoken`, `jose` y el módulo `crypto` de Node.js:
62
+
63
+ ```typescript
64
+ import jwt from 'jsonwebtoken';
65
+ import { TitanVault } from 'titan-vault-node';
26
66
 
27
- Replace your standard `dotenv.config()` with `initVault()`.
67
+ const jwtKey = TitanVault.secrets.get('JWT_SECRET');
28
68
 
29
- ### 1. Set your App Token
30
- In your deployment environment (or a local `.env`), just provide your vault's URL and your App Token:
69
+ // Firmar
70
+ const token = jwt.sign({ userId: 123, role: 'admin' }, jwtKey, {
71
+ algorithm: 'HS256',
72
+ expiresIn: '2h',
73
+ });
31
74
 
32
- ```env
33
- VAULT_URL=http://localhost:8200
34
- VAULT_APP_TOKEN=my-super-secret-app-role-token
75
+ // Verificar
76
+ const payload = jwt.verify(token, jwtKey);
35
77
  ```
36
78
 
37
- ### 2. Initialize in your code
79
+ ---
80
+
81
+ ### 3. Conexión Segura a Base de Datos (JIT)
38
82
 
83
+ #### Con Sequelize:
39
84
  ```typescript
40
- import { initVault } from 'titan-vault-node';
85
+ import { Sequelize } from 'sequelize';
86
+ import { TitanVault } from 'titan-vault-node';
87
+
88
+ export const sequelize = new Sequelize({
89
+ dialect: 'postgres',
90
+ host: TitanVault.config.get('DB_HOST'),
91
+ database: TitanVault.config.get('DB_NAME'),
92
+ username: TitanVault.config.get('DB_USER'),
93
+ // No pongas password aquí
94
+ });
95
+
96
+ // Registra el inyector JIT
97
+ TitanVault.databases.hookSequelize(sequelize, {
98
+ credentialKey: 'DB_PASSWORD',
99
+ });
100
+ ```
41
101
 
42
- async function startServer() {
43
- // Magically connects to Titan Vault, decrypts the E2E payload,
44
- // and injects everything into process.env!
45
- await initVault();
102
+ #### Con `pg` (node-postgres):
103
+ ```typescript
104
+ import pg from 'pg';
105
+ import { TitanVault } from 'titan-vault-node';
106
+
107
+ export const pool = new pg.Pool({
108
+ host: TitanVault.config.get('DB_HOST'),
109
+ user: TitanVault.config.get('DB_USER'),
110
+ database: TitanVault.config.get('DB_NAME'),
111
+ // Inyección JIT asíncrona por conexión
112
+ password: TitanVault.databases.getPgPasswordProvider('DB_PASSWORD'),
113
+ });
114
+ ```
46
115
 
47
- console.log("My secure DB password is:", process.env.DB_PASSWORD);
48
-
49
- // Start your Express/NestJS/HTTP server...
50
- }
116
+ ---
117
+
118
+ ### 4. Criptografía como Servicio (Transit Engine / CaaS)
119
+
120
+ Para máxima seguridad, puedes delegar el cifrado y la firma directamente a Titan Vault (cero llaves en tu app):
121
+
122
+ ```typescript
123
+ import { TitanVault } from 'titan-vault-node';
124
+
125
+ // Cifrar datos con AES-256-GCM en el Vault
126
+ const ciphertext = await TitanVault.transit.encrypt('mi-llave', 'Texto Ultra Confidencial');
51
127
 
52
- startServer();
128
+ // Descifrar en el Vault
129
+ const plaintext = await TitanVault.transit.decrypt('mi-llave', ciphertext);
130
+
131
+ // Firmar datos con HMAC-SHA256 en el Vault
132
+ const signature = await TitanVault.transit.sign('mi-firma-jwt', 'payload.a.firmar');
133
+
134
+ // Verificar firma en el Vault
135
+ const isValid = await TitanVault.transit.verify('mi-firma-jwt', 'payload.a.firmar', signature);
53
136
  ```
54
137
 
55
- ## 🚨 Telegram Webhook Setup (Server-side)
56
- Want to be notified if someone touches your Honeytoken? Run your Titan Vault server with Telegram variables:
138
+ ---
139
+
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`.
57
143
 
58
- ```bash
59
- export TELEGRAM_BOT_TOKEN="your_bot_token"
60
- export TELEGRAM_CHAT_ID="your_chat_id"
61
- # Start your vault...
144
+ ```typescript
145
+ const databaseUrl = TitanVault.config.get('DATABASE_URL');
146
+ const port = TitanVault.config.get('PORT', '3000');
147
+ const host = TitanVault.config.get('HOST', '0.0.0.0');
62
148
  ```
63
- If your Honeytoken is triggered, the Vault seals itself and sends a red alert to your phone instantly.
64
149
 
65
150
  ---
66
- **Built for the paranoid, designed for the developer.**
151
+
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.
155
+
156
+ ```typescript
157
+ TitanVault.verifyRuntimeHardening();
158
+ ```
159
+
160
+ ---
161
+
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.
167
+
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
+ }
@@ -0,0 +1,18 @@
1
+ import crypto from 'crypto';
2
+ export declare class CryptoStore {
3
+ private keyObjects;
4
+ /**
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
+ */
9
+ register(name: string, rawSecret: string): void;
10
+ get(name: string): crypto.KeyObject;
11
+ /**
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.
14
+ */
15
+ getRaw(name: string): string;
16
+ has(name: string): boolean;
17
+ keys(): string[];
18
+ }
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CryptoStore = void 0;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ class CryptoStore {
9
+ keyObjects = new Map();
10
+ /**
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
+ */
15
+ register(name, rawSecret) {
16
+ if (!rawSecret)
17
+ return;
18
+ const buffer = Buffer.from(rawSecret, 'utf-8');
19
+ try {
20
+ if (rawSecret.includes('-----BEGIN PRIVATE KEY-----') || rawSecret.includes('-----BEGIN RSA PRIVATE KEY-----')) {
21
+ this.keyObjects.set(name, crypto_1.default.createPrivateKey({ key: rawSecret, format: 'pem' }));
22
+ }
23
+ else if (rawSecret.includes('-----BEGIN PUBLIC KEY-----')) {
24
+ this.keyObjects.set(name, crypto_1.default.createPublicKey({ key: rawSecret, format: 'pem' }));
25
+ }
26
+ else {
27
+ this.keyObjects.set(name, crypto_1.default.createSecretKey(buffer));
28
+ }
29
+ }
30
+ finally {
31
+ buffer.fill(0);
32
+ }
33
+ }
34
+ get(name) {
35
+ const key = this.keyObjects.get(name);
36
+ if (!key) {
37
+ throw new Error(`[Titan Vault] Llave criptográfica '${name}' no encontrada en el almacén seguro.`);
38
+ }
39
+ return key;
40
+ }
41
+ /**
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.
44
+ */
45
+ getRaw(name) {
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');
53
+ }
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');
59
+ }
60
+ has(name) {
61
+ return this.keyObjects.has(name);
62
+ }
63
+ keys() {
64
+ return Array.from(this.keyObjects.keys());
65
+ }
66
+ }
67
+ exports.CryptoStore = CryptoStore;
@@ -0,0 +1,17 @@
1
+ export declare class DatabaseHooks {
2
+ private credentials;
3
+ register(key: string, value: string): void;
4
+ getJitCredential(key: string): string;
5
+ /**
6
+ * Inject the password on `beforeConnect` and clear `config.password` after.
7
+ * The driver still receives a JavaScript string for the handshake.
8
+ */
9
+ hookSequelize(sequelize: any, options?: {
10
+ credentialKey?: string;
11
+ }): void;
12
+ /**
13
+ * Password function for `pg.Pool`. node-postgres calls it per connection.
14
+ */
15
+ getPgPasswordProvider(keyName?: string): () => Promise<string>;
16
+ private materialize;
17
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DatabaseHooks = void 0;
4
+ class DatabaseHooks {
5
+ credentials = new Map();
6
+ register(key, value) {
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'));
13
+ }
14
+ getJitCredential(key) {
15
+ return this.materialize(key);
16
+ }
17
+ /**
18
+ * Inject the password on `beforeConnect` and clear `config.password` after.
19
+ * The driver still receives a JavaScript string for the handshake.
20
+ */
21
+ hookSequelize(sequelize, options = {}) {
22
+ const credKey = options.credentialKey || 'DB_PASSWORD';
23
+ if (!sequelize || typeof sequelize.addHook !== 'function') {
24
+ console.warn('⚠️ [Titan Vault] La instancia provista no parece ser un objeto Sequelize válido.');
25
+ return;
26
+ }
27
+ sequelize.addHook('beforeConnect', async (config) => {
28
+ config.password = this.materialize(credKey);
29
+ });
30
+ sequelize.addHook('afterConnect', async (_connection, config) => {
31
+ if (config && 'password' in config) {
32
+ config.password = null;
33
+ }
34
+ });
35
+ console.log(`🛡️ [Titan Vault] Hook Sequelize JIT activado para '${credKey}'.`);
36
+ }
37
+ /**
38
+ * Password function for `pg.Pool`. node-postgres calls it per connection.
39
+ */
40
+ getPgPasswordProvider(keyName = 'DB_PASSWORD') {
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');
49
+ }
50
+ }
51
+ exports.DatabaseHooks = DatabaseHooks;
@@ -0,0 +1,2 @@
1
+ import { HardeningReport } from '../types';
2
+ export declare function verifyRuntimeHardening(paranoiaArmed: boolean): HardeningReport;
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.verifyRuntimeHardening = 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) {
24
+ const recommendations = [];
25
+ const nodeEnv = process.env.NODE_ENV || 'development';
26
+ let runningAsRoot = false;
27
+ try {
28
+ if (typeof process.getuid === 'function') {
29
+ runningAsRoot = process.getuid() === 0;
30
+ }
31
+ }
32
+ catch {
33
+ // Windows or a restricted environment.
34
+ }
35
+ const coreDumpsDisabled = readCoreDumpSoftLimit();
36
+ if (runningAsRoot) {
37
+ recommendations.push('El proceso corre como root (UID 0). En producción usa un usuario sin privilegios.');
38
+ }
39
+ if (nodeEnv !== 'production') {
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.');
50
+ }
51
+ const isHardened = !runningAsRoot && nodeEnv === 'production' && coreDumpsDisabled === true && paranoiaArmed;
52
+ console.log('\n🛡️ [Titan Vault] Reporte de hardening:');
53
+ console.log(` - Entorno: ${nodeEnv}`);
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'}`);
57
+ for (const rec of recommendations) {
58
+ console.log(` ${rec}`);
59
+ }
60
+ console.log('');
61
+ return {
62
+ isHardened,
63
+ nodeEnv,
64
+ coreDumpsDisabled: coreDumpsDisabled === true,
65
+ coreDumpLimitKnown: coreDumpsDisabled !== null,
66
+ runningAsRoot,
67
+ paranoiaArmed,
68
+ recommendations,
69
+ };
70
+ }
@@ -0,0 +1,5 @@
1
+ export declare class ParanoiaBlockedError extends Error {
2
+ readonly key: string;
3
+ constructor(key: string, reason?: string);
4
+ }
5
+ export declare function poisonProcessEnv(secretKeys: string[], honeytokenKey?: string, onHoneytokenDetonated?: (key: string) => void): void;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ParanoiaBlockedError = void 0;
4
+ exports.poisonProcessEnv = poisonProcessEnv;
5
+ class ParanoiaBlockedError extends Error {
6
+ key;
7
+ constructor(key, reason) {
8
+ super(`🚨 [Titan Vault - Modo Paranoia] Acceso bloqueado a 'process.env.${key}'. ` +
9
+ (reason ||
10
+ `Esta variable fue clasificada como secreto crítico y ha sido envenenada para prevenir Memory Dumps / Heap Dumps. ` +
11
+ `Usa 'TitanVault.secrets.get("${key}")' (KeyObject) o hooks JIT de Base de Datos.`));
12
+ this.name = 'ParanoiaBlockedError';
13
+ this.key = key;
14
+ Object.setPrototypeOf(this, ParanoiaBlockedError.prototype);
15
+ }
16
+ }
17
+ exports.ParanoiaBlockedError = ParanoiaBlockedError;
18
+ function poisonProcessEnv(secretKeys, honeytokenKey, onHoneytokenDetonated) {
19
+ const poisonedSet = new Set(secretKeys);
20
+ const honeyKey = honeytokenKey || 'VAULT_EMERGENCY_RECOVERY_TOKEN';
21
+ // 1. Delete existing raw values from underlying process.env
22
+ for (const key of secretKeys) {
23
+ try {
24
+ delete process.env[key];
25
+ }
26
+ catch {
27
+ // ignore
28
+ }
29
+ }
30
+ // 2. Wrap process.env in a Proxy
31
+ const targetEnv = process.env;
32
+ const envProxy = new Proxy(targetEnv, {
33
+ get(target, prop, receiver) {
34
+ if (typeof prop === 'string') {
35
+ if (prop === honeyKey) {
36
+ if (onHoneytokenDetonated) {
37
+ try {
38
+ onHoneytokenDetonated(prop);
39
+ }
40
+ catch {
41
+ // ignore
42
+ }
43
+ }
44
+ throw new ParanoiaBlockedError(prop, `HONEYTOKEN DETONADO. Intento de acceso a '${prop}' detectado. Alerta de intrusión emitida.`);
45
+ }
46
+ if (poisonedSet.has(prop)) {
47
+ throw new ParanoiaBlockedError(prop);
48
+ }
49
+ }
50
+ return Reflect.get(target, prop, receiver);
51
+ },
52
+ set(target, prop, value, receiver) {
53
+ if (typeof prop === 'string' && (prop === honeyKey || poisonedSet.has(prop))) {
54
+ throw new ParanoiaBlockedError(prop, `No se permite mutar 'process.env.${prop}' en Modo Paranoia.`);
55
+ }
56
+ return Reflect.set(target, prop, value, receiver);
57
+ },
58
+ has(target, prop) {
59
+ if (typeof prop === 'string' && (prop === honeyKey || poisonedSet.has(prop))) {
60
+ return false;
61
+ }
62
+ return Reflect.has(target, prop);
63
+ },
64
+ ownKeys(target) {
65
+ return Reflect.ownKeys(target).filter((k) => typeof k !== 'string' || (k !== honeyKey && !poisonedSet.has(k)));
66
+ },
67
+ getOwnPropertyDescriptor(target, prop) {
68
+ if (typeof prop === 'string' && (prop === honeyKey || poisonedSet.has(prop))) {
69
+ return undefined;
70
+ }
71
+ return Reflect.getOwnPropertyDescriptor(target, prop);
72
+ },
73
+ });
74
+ process.env = envProxy;
75
+ }
@@ -0,0 +1,13 @@
1
+ import { TransitKeyInfo } from '../types';
2
+ export declare class TransitClient {
3
+ private getBaseUrl;
4
+ private getToken;
5
+ constructor(getBaseUrl: () => string, getToken: () => string | undefined);
6
+ private getHeaders;
7
+ createKey(name: string, keyType?: 'aes-gcm' | 'hmac-sha256'): Promise<TransitKeyInfo>;
8
+ listKeys(): Promise<TransitKeyInfo[]>;
9
+ encrypt(name: string, plaintext: string): Promise<string>;
10
+ decrypt(name: string, ciphertext: string): Promise<string>;
11
+ sign(name: string, data: string): Promise<string>;
12
+ verify(name: string, data: string, signature: string): Promise<boolean>;
13
+ }
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TransitClient = void 0;
4
+ class TransitClient {
5
+ getBaseUrl;
6
+ getToken;
7
+ constructor(getBaseUrl, getToken) {
8
+ this.getBaseUrl = getBaseUrl;
9
+ this.getToken = getToken;
10
+ }
11
+ getHeaders() {
12
+ const token = this.getToken();
13
+ if (!token) {
14
+ throw new Error('[Titan Vault - Transit] Token de Vault no configurado.');
15
+ }
16
+ return {
17
+ 'Content-Type': 'application/json',
18
+ 'x-vault-token': token,
19
+ };
20
+ }
21
+ async createKey(name, keyType = 'aes-gcm') {
22
+ const url = `${this.getBaseUrl()}/transit/keys/${encodeURIComponent(name)}`;
23
+ const res = await fetch(url, {
24
+ method: 'POST',
25
+ headers: this.getHeaders(),
26
+ body: JSON.stringify({ key_type: keyType }),
27
+ });
28
+ if (!res.ok) {
29
+ const err = await res.text();
30
+ throw new Error(`[Titan Vault - Transit] Error creando llave '${name}': ${err}`);
31
+ }
32
+ return res.json();
33
+ }
34
+ async listKeys() {
35
+ const url = `${this.getBaseUrl()}/transit/keys`;
36
+ const res = await fetch(url, {
37
+ method: 'GET',
38
+ headers: this.getHeaders(),
39
+ });
40
+ if (!res.ok) {
41
+ const err = await res.text();
42
+ throw new Error(`[Titan Vault - Transit] Error listando llaves: ${err}`);
43
+ }
44
+ return res.json();
45
+ }
46
+ async encrypt(name, plaintext) {
47
+ const url = `${this.getBaseUrl()}/transit/encrypt/${encodeURIComponent(name)}`;
48
+ const res = await fetch(url, {
49
+ method: 'POST',
50
+ headers: this.getHeaders(),
51
+ body: JSON.stringify({ plaintext }),
52
+ });
53
+ if (!res.ok) {
54
+ const err = await res.text();
55
+ throw new Error(`[Titan Vault - Transit] Error cifrando con '${name}': ${err}`);
56
+ }
57
+ const json = (await res.json());
58
+ return json.ciphertext;
59
+ }
60
+ async decrypt(name, ciphertext) {
61
+ const url = `${this.getBaseUrl()}/transit/decrypt/${encodeURIComponent(name)}`;
62
+ const res = await fetch(url, {
63
+ method: 'POST',
64
+ headers: this.getHeaders(),
65
+ body: JSON.stringify({ ciphertext }),
66
+ });
67
+ if (!res.ok) {
68
+ const err = await res.text();
69
+ throw new Error(`[Titan Vault - Transit] Error descifrando con '${name}': ${err}`);
70
+ }
71
+ const json = (await res.json());
72
+ return json.plaintext;
73
+ }
74
+ async sign(name, data) {
75
+ const url = `${this.getBaseUrl()}/transit/sign/${encodeURIComponent(name)}`;
76
+ const res = await fetch(url, {
77
+ method: 'POST',
78
+ headers: this.getHeaders(),
79
+ body: JSON.stringify({ data }),
80
+ });
81
+ if (!res.ok) {
82
+ const err = await res.text();
83
+ throw new Error(`[Titan Vault - Transit] Error firmando con '${name}': ${err}`);
84
+ }
85
+ const json = (await res.json());
86
+ return json.signature;
87
+ }
88
+ async verify(name, data, signature) {
89
+ const url = `${this.getBaseUrl()}/transit/verify/${encodeURIComponent(name)}`;
90
+ const res = await fetch(url, {
91
+ method: 'POST',
92
+ headers: this.getHeaders(),
93
+ body: JSON.stringify({ data, signature }),
94
+ });
95
+ if (!res.ok) {
96
+ const err = await res.text();
97
+ throw new Error(`[Titan Vault - Transit] Error verificando firma con '${name}': ${err}`);
98
+ }
99
+ const json = (await res.json());
100
+ return json.valid;
101
+ }
102
+ }
103
+ exports.TransitClient = TransitClient;
package/dist/index.d.ts CHANGED
@@ -1,9 +1,47 @@
1
- interface VaultOptions {
2
- vaultUrl?: string;
3
- vaultToken?: string;
4
- path?: string;
5
- maxRetries?: number;
6
- retryDelayMs?: number;
1
+ import { VaultOptions, HardeningReport } from './types';
2
+ import { CryptoStore } from './core/crypto-store';
3
+ import { DatabaseHooks } from './core/database-hooks';
4
+ import { TransitClient } from './core/transit-client';
5
+ export * from './types';
6
+ export * from './core/poison';
7
+ export * from './core/crypto-store';
8
+ export * from './core/database-hooks';
9
+ export * from './core/transit-client';
10
+ export * from './core/hardening';
11
+ export * from './core/classify';
12
+ export declare class VaultConnectionError extends Error {
13
+ constructor(message: string);
7
14
  }
15
+ export declare class TitanVault {
16
+ private static instance;
17
+ static readonly secrets: CryptoStore;
18
+ static readonly databases: DatabaseHooks;
19
+ private static rawConfig;
20
+ private static vaultUrl;
21
+ private static vaultToken?;
22
+ private static isArmed;
23
+ private static baitValues;
24
+ private static tripwireInflight;
25
+ static readonly transit: TransitClient;
26
+ static readonly config: {
27
+ get(key: string, defaultValue?: string): string;
28
+ has(key: string): boolean;
29
+ getAll(): Record<string, string>;
30
+ };
31
+ /**
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.
35
+ */
36
+ static armParanoiaMode(options?: VaultOptions): Promise<void>;
37
+ /** Resolves when the in-flight honeytoken alert has finished or failed. */
38
+ static flushTripwire(): Promise<void>;
39
+ static bootstrap(serverFn: () => Promise<any>, options?: VaultOptions): Promise<void>;
40
+ static verifyRuntimeHardening(): HardeningReport;
41
+ }
42
+ /**
43
+ * Classic mode (`paranoiaMode: false`) copies string values into process.env.
44
+ * The default calls armParanoiaMode and throws if the vault is unreachable.
45
+ */
8
46
  export declare function initVault(options?: VaultOptions): Promise<void>;
9
- export {};
47
+ export default TitanVault;
package/dist/index.js CHANGED
@@ -1,84 +1,292 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
2
16
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
18
  };
5
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.TitanVault = exports.VaultConnectionError = void 0;
6
21
  exports.initVault = initVault;
7
22
  const dotenv_1 = __importDefault(require("dotenv"));
8
23
  const crypto_1 = __importDefault(require("crypto"));
9
- async function initVault(options = {}) {
10
- // Load local .env first to get base configs (like VAULT_URL or VAULT_TOKEN)
11
- dotenv_1.default.config();
12
- const VAULT_URL = options.vaultUrl || process.env.VAULT_URL || 'http://127.0.0.1:8200';
13
- const VAULT_TOKEN = options.vaultToken || process.env.VAULT_TOKEN;
14
- const SECRET_PATH = options.path || 'backend_env';
15
- const MAX_RETRIES = options.maxRetries || 5;
16
- const RETRY_DELAY = options.retryDelayMs || 5000;
17
- if (!VAULT_TOKEN) {
18
- console.warn("⚠️ [Titan Vault] No VAULT_TOKEN found. Using local .env fallback.");
19
- return;
24
+ const poison_1 = require("./core/poison");
25
+ const crypto_store_1 = require("./core/crypto-store");
26
+ const database_hooks_1 = require("./core/database-hooks");
27
+ const transit_client_1 = require("./core/transit-client");
28
+ const hardening_1 = require("./core/hardening");
29
+ const classify_1 = require("./core/classify");
30
+ __exportStar(require("./types"), exports);
31
+ __exportStar(require("./core/poison"), exports);
32
+ __exportStar(require("./core/crypto-store"), exports);
33
+ __exportStar(require("./core/database-hooks"), exports);
34
+ __exportStar(require("./core/transit-client"), exports);
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);
20
42
  }
21
- console.log(`\n🔒 [Titan Vault] Conectando a ${VAULT_URL}...`);
22
- // E2E Encryption: Generate ephemeral RSA keypair
23
- const { publicKey, privateKey } = crypto_1.default.generateKeyPairSync('rsa', {
24
- modulusLength: 2048,
25
- publicKeyEncoding: { type: 'spki', format: 'pem' },
26
- privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
27
- });
28
- const pubBase64 = Buffer.from(publicKey).toString('base64');
29
- let attempt = 0;
30
- while (attempt < MAX_RETRIES) {
31
- attempt++;
32
- try {
33
- const res = await fetch(`${VAULT_URL}/secret/data/${SECRET_PATH}`, {
34
- headers: {
35
- "x-vault-token": VAULT_TOKEN,
36
- "x-vault-public-key": pubBase64
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
+ }
78
+ class TitanVault {
79
+ static instance;
80
+ static secrets = new crypto_store_1.CryptoStore();
81
+ static databases = new database_hooks_1.DatabaseHooks();
82
+ static rawConfig = new Map();
83
+ static vaultUrl = 'http://127.0.0.1:8200';
84
+ static vaultToken;
85
+ static isArmed = false;
86
+ static baitValues = new Map();
87
+ static tripwireInflight = Promise.resolve();
88
+ static transit = new transit_client_1.TransitClient(() => TitanVault.vaultUrl, () => TitanVault.vaultToken);
89
+ static config = {
90
+ get(key, defaultValue) {
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.`);
37
101
  }
38
- });
39
- if (res.ok) {
40
- const json = await res.json();
41
- let finalSecretStr = "";
42
- if (json.data && json.data.is_encrypted) {
43
- console.log("🔐 [Titan Vault] Descifrando payload E2E híbrido (RSA-OAEP + AES-CFB)...");
44
- const buffer = Buffer.from(json.data.value, 'base64');
45
- const encAesKey = buffer.subarray(0, 256);
46
- const iv = buffer.subarray(256, 256 + 16);
47
- const ciphertext = buffer.subarray(256 + 16);
48
- const aesKey = crypto_1.default.privateDecrypt({
49
- key: privateKey,
50
- padding: crypto_1.default.constants.RSA_PKCS1_OAEP_PADDING,
51
- oaepHash: "sha256"
52
- }, encAesKey);
53
- const decipher = crypto_1.default.createDecipheriv('aes-256-cfb', aesKey, iv);
54
- let decrypted = decipher.update(ciphertext, undefined, 'utf8');
55
- decrypted += decipher.final('utf8');
56
- finalSecretStr = decrypted;
102
+ throw err;
103
+ }
104
+ if (envVal !== undefined)
105
+ return envVal;
106
+ if (defaultValue !== undefined)
107
+ return defaultValue;
108
+ throw new Error(`[Titan Vault] Configuración '${key}' no encontrada.`);
109
+ },
110
+ has(key) {
111
+ return TitanVault.rawConfig.has(key) || key in process.env;
112
+ },
113
+ getAll() {
114
+ const res = {};
115
+ for (const [k, v] of TitanVault.rawConfig.entries()) {
116
+ res[k] = v;
117
+ }
118
+ return res;
119
+ }
120
+ };
121
+ /**
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.
125
+ */
126
+ static async armParanoiaMode(options = {}) {
127
+ if (this.isArmed) {
128
+ console.log('🛡️ [Titan Vault] Modo paranoia ya está armado.');
129
+ return;
130
+ }
131
+ dotenv_1.default.config();
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;
138
+ if (!this.vaultToken) {
139
+ throw new VaultConnectionError('Falta el token. Pasa vaultToken o define VAULT_TOKEN / VAULT_APP_TOKEN. El proceso no arranca sin la bóveda.');
140
+ }
141
+ console.log(`\n🛡️ [Titan Vault] Conectando a ${this.vaultUrl}...`);
142
+ const { publicKey, privateKey } = crypto_1.default.generateKeyPairSync('rsa', {
143
+ modulusLength: 2048,
144
+ });
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++) {
150
+ try {
151
+ const res = await fetch(`${this.vaultUrl}/secret/data/${secretPath}`, {
152
+ headers: {
153
+ 'x-vault-token': this.vaultToken,
154
+ 'x-vault-public-key': pubBase64,
155
+ },
156
+ });
157
+ if (!res.ok) {
158
+ lastError = `HTTP ${res.status}: ${await res.text()}`;
159
+ console.warn(`[Titan Vault] ${lastError}. Reintento ${attempt}/${maxRetries}.`);
57
160
  }
58
161
  else {
59
- finalSecretStr = json.data.value;
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;
60
169
  }
61
- const secrets = JSON.parse(finalSecretStr);
62
- // Inject into process.env
63
- let injectedCount = 0;
64
- for (const [key, value] of Object.entries(secrets)) {
65
- process.env[key] = value;
66
- injectedCount++;
67
- }
68
- console.log(`✅ [Titan Vault] ${injectedCount} secretos inyectados en memoria (Zero-Trust).`);
69
- return; // Success!
70
170
  }
71
- else {
72
- const errText = await res.text();
73
- console.warn(`[Titan Vault] Vault sellado o token inválido (HTTP ${res.status}): ${errText}. Reintentando en ${RETRY_DELAY / 1000}s...`);
171
+ catch (err) {
172
+ if (err instanceof VaultConnectionError)
173
+ throw err;
174
+ lastError = err?.message || String(err);
175
+ console.warn(`❌ [Titan Vault] ${lastError}. Reintento ${attempt}/${maxRetries}.`);
74
176
  }
177
+ if (attempt < maxRetries) {
178
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
179
+ }
180
+ }
181
+ if (!rawSecrets) {
182
+ throw new VaultConnectionError(`No se pudieron leer los secretos de '${secretPath}' en ${this.vaultUrl}. ${lastError}`);
75
183
  }
76
- catch (err) {
77
- console.warn(`❌ [Titan Vault] Error de conexión (${err.message}). Reintentando en ${RETRY_DELAY / 1000}s...`);
184
+ const classification = rawSecrets['__titan_classification__'] || {};
185
+ delete rawSecrets['__titan_classification__'];
186
+ const allowedEnvList = new Set(options.allowedEnvKeys || ['NODE_ENV', 'PORT', 'HOST', 'FRONTEND_PORT', 'BACKEND_PORT']);
187
+ const poisonedKeys = [];
188
+ this.baitValues = new Map();
189
+ let cryptoCount = 0;
190
+ let dbCount = 0;
191
+ let configCount = 0;
192
+ for (const [key, value] of Object.entries(rawSecrets)) {
193
+ if (typeof value !== 'string')
194
+ continue;
195
+ const kind = (0, classify_1.classifyKey)(key, classification[key], options);
196
+ if (kind === 'honeytoken') {
197
+ this.baitValues.set(key, value);
198
+ poisonedKeys.push(key);
199
+ }
200
+ else if (kind === 'crypto') {
201
+ this.secrets.register(key, value);
202
+ poisonedKeys.push(key);
203
+ cryptoCount++;
204
+ }
205
+ else if (kind === 'database') {
206
+ this.databases.register(key, value);
207
+ poisonedKeys.push(key);
208
+ dbCount++;
209
+ }
210
+ else {
211
+ this.rawConfig.set(key, value);
212
+ if (allowedEnvList.has(key)) {
213
+ process.env[key] = value;
214
+ }
215
+ configCount++;
216
+ }
78
217
  }
79
- if (attempt < MAX_RETRIES) {
80
- await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
218
+ if (!poisonedKeys.includes(honeyField)) {
219
+ poisonedKeys.push(honeyField);
81
220
  }
221
+ const onDetonate = (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);
228
+ }
229
+ options.onHoneytokenDetonated?.(tokenKey);
230
+ };
231
+ (0, poison_1.poisonProcessEnv)(poisonedKeys, honeyField, onDetonate);
232
+ this.isArmed = true;
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;
245
+ }
246
+ static async bootstrap(serverFn, options = {}) {
247
+ await this.armParanoiaMode(options);
248
+ console.log('🚀 [Titan Vault] Secretos cargados. Arrancando servidor...');
249
+ await serverFn();
250
+ }
251
+ static verifyRuntimeHardening() {
252
+ return (0, hardening_1.verifyRuntimeHardening)(this.isArmed);
253
+ }
254
+ }
255
+ exports.TitanVault = TitanVault;
256
+ /**
257
+ * Classic mode (`paranoiaMode: false`) copies string values into process.env.
258
+ * The default calls armParanoiaMode and throws if the vault is unreachable.
259
+ */
260
+ async function initVault(options = {}) {
261
+ if (options.paranoiaMode !== false) {
262
+ await TitanVault.armParanoiaMode(options);
263
+ return;
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;
82
289
  }
83
- console.error("🚨 [Titan Vault] No se pudo conectar al vault tras múltiples intentos. Iniciando con secretos locales si existen.");
290
+ console.log('✅ [Titan Vault] Secretos copiados a process.env (modo clásico).');
84
291
  }
292
+ exports.default = TitanVault;
@@ -0,0 +1,67 @@
1
+ export type SecretType = 'crypto' | 'database' | 'config' | 'honeytoken';
2
+ export interface VaultOptions {
3
+ vaultUrl?: string;
4
+ vaultToken?: string;
5
+ path?: string;
6
+ maxRetries?: number;
7
+ retryDelayMs?: number;
8
+ /**
9
+ * If true, activates Paranoia Mode (process.env poisoning, C++ KeyObject storage, JIT DB credentials).
10
+ * Defaults to true in armParanoiaMode().
11
+ */
12
+ paranoiaMode?: boolean;
13
+ /**
14
+ * Keys explicitly marked as cryptographic secrets (instantiated as C++ KeyObjects).
15
+ */
16
+ cryptoKeys?: string[];
17
+ /**
18
+ * Keys explicitly marked as database credentials (accessible only via JIT DB hooks).
19
+ */
20
+ dbKeys?: string[];
21
+ /**
22
+ * Keys that are safe to expose in process.env (e.g. PORT, NODE_ENV).
23
+ */
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;
30
+ /**
31
+ * Custom callback triggered if a honeytoken is accessed in process.env.
32
+ */
33
+ onHoneytokenDetonated?: (tokenName: string) => void;
34
+ }
35
+ export interface TransitKeyInfo {
36
+ name: string;
37
+ key_type: 'aes-gcm' | 'hmac-sha256' | string;
38
+ created_at: string | null;
39
+ status?: string;
40
+ }
41
+ export interface TransitEncryptResult {
42
+ ciphertext: string;
43
+ name: string;
44
+ }
45
+ export interface TransitDecryptResult {
46
+ plaintext: string;
47
+ name: string;
48
+ }
49
+ export interface TransitSignResult {
50
+ signature: string;
51
+ name: string;
52
+ }
53
+ export interface TransitVerifyResult {
54
+ valid: boolean;
55
+ name: string;
56
+ }
57
+ export interface HardeningReport {
58
+ isHardened: boolean;
59
+ nodeEnv: string;
60
+ /** True only when the soft core limit was read and is 0. */
61
+ coreDumpsDisabled: boolean;
62
+ /** False when `/proc/self/limits` could not be read. */
63
+ coreDumpLimitKnown: boolean;
64
+ runningAsRoot: boolean;
65
+ paranoiaArmed: boolean;
66
+ recommendations: string[];
67
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "name": "titan-vault-node",
3
- "version": "1.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
+ "files": [
8
+ "dist"
9
+ ],
7
10
  "scripts": {
8
- "build": "tsc"
11
+ "build": "tsc",
12
+ "prepare": "npm run build",
13
+ "test": "npm run build && node --test test/harmony.test.mjs"
9
14
  },
10
15
  "keywords": [
11
16
  "vault",
package/src/index.ts DELETED
@@ -1,101 +0,0 @@
1
- import dotenv from 'dotenv';
2
- import crypto from 'crypto';
3
-
4
- interface VaultOptions {
5
- vaultUrl?: string;
6
- vaultToken?: string;
7
- path?: string;
8
- maxRetries?: number;
9
- retryDelayMs?: number;
10
- }
11
-
12
- export async function initVault(options: VaultOptions = {}) {
13
- // Load local .env first to get base configs (like VAULT_URL or VAULT_TOKEN)
14
- dotenv.config();
15
-
16
- const VAULT_URL = options.vaultUrl || process.env.VAULT_URL || 'http://127.0.0.1:8200';
17
- const VAULT_TOKEN = options.vaultToken || process.env.VAULT_TOKEN;
18
- const SECRET_PATH = options.path || 'backend_env';
19
- const MAX_RETRIES = options.maxRetries || 5;
20
- const RETRY_DELAY = options.retryDelayMs || 5000;
21
-
22
- if (!VAULT_TOKEN) {
23
- console.warn("⚠️ [Titan Vault] No VAULT_TOKEN found. Using local .env fallback.");
24
- return;
25
- }
26
-
27
- console.log(`\n🔒 [Titan Vault] Conectando a ${VAULT_URL}...`);
28
-
29
- // E2E Encryption: Generate ephemeral RSA keypair
30
- const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
31
- modulusLength: 2048,
32
- publicKeyEncoding: { type: 'spki', format: 'pem' },
33
- privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
34
- });
35
-
36
- const pubBase64 = Buffer.from(publicKey).toString('base64');
37
- let attempt = 0;
38
-
39
- while (attempt < MAX_RETRIES) {
40
- attempt++;
41
- try {
42
- const res = await fetch(`${VAULT_URL}/secret/data/${SECRET_PATH}`, {
43
- headers: {
44
- "x-vault-token": VAULT_TOKEN,
45
- "x-vault-public-key": pubBase64
46
- }
47
- });
48
-
49
- if (res.ok) {
50
- const json = await res.json();
51
- let finalSecretStr = "";
52
-
53
- if (json.data && json.data.is_encrypted) {
54
- console.log("🔐 [Titan Vault] Descifrando payload E2E híbrido (RSA-OAEP + AES-CFB)...");
55
- const buffer = Buffer.from(json.data.value, 'base64');
56
-
57
- const encAesKey = buffer.subarray(0, 256);
58
- const iv = buffer.subarray(256, 256 + 16);
59
- const ciphertext = buffer.subarray(256 + 16);
60
-
61
- const aesKey = crypto.privateDecrypt({
62
- key: privateKey,
63
- padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
64
- oaepHash: "sha256"
65
- }, encAesKey);
66
-
67
- const decipher = crypto.createDecipheriv('aes-256-cfb', aesKey, iv);
68
- let decrypted = decipher.update(ciphertext, undefined, 'utf8');
69
- decrypted += decipher.final('utf8');
70
-
71
- finalSecretStr = decrypted;
72
- } else {
73
- finalSecretStr = json.data.value;
74
- }
75
-
76
- const secrets = JSON.parse(finalSecretStr);
77
-
78
- // Inject into process.env
79
- let injectedCount = 0;
80
- for (const [key, value] of Object.entries(secrets)) {
81
- process.env[key] = value as string;
82
- injectedCount++;
83
- }
84
-
85
- console.log(`✅ [Titan Vault] ${injectedCount} secretos inyectados en memoria (Zero-Trust).`);
86
- return; // Success!
87
- } else {
88
- const errText = await res.text();
89
- console.warn(`[Titan Vault] Vault sellado o token inválido (HTTP ${res.status}): ${errText}. Reintentando en ${RETRY_DELAY/1000}s...`);
90
- }
91
- } catch (err: any) {
92
- console.warn(`❌ [Titan Vault] Error de conexión (${err.message}). Reintentando en ${RETRY_DELAY/1000}s...`);
93
- }
94
-
95
- if (attempt < MAX_RETRIES) {
96
- await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
97
- }
98
- }
99
-
100
- console.error("🚨 [Titan Vault] No se pudo conectar al vault tras múltiples intentos. Iniciando con secretos locales si existen.");
101
- }
package/tsconfig.json DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "CommonJS",
5
- "moduleResolution": "node",
6
- "declaration": true,
7
- "outDir": "./dist",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true
12
- },
13
- "include": ["src/**/*"]
14
- }