
- Ajout d'un système de chargement robuste des variables d'environnement - Recherche automatique des fichiers .env dans plusieurs emplacements - Rechargement des variables à chaque appel de syncLocalFiles() - Ajout de logs pour indiquer quel fichier .env a été chargé - Documentation complète avec section de dépannage - Solutions pour les problèmes courants avec VAULT_CONFS_DIR Résout le problème où VAULT_CONFS_DIR n'était pas pris en compte quand le fichier .env n'était pas dans le répertoire courant du projet.
749 lines
22 KiB
TypeScript
749 lines
22 KiB
TypeScript
import fetch from 'node-fetch';
|
|
import https from 'https';
|
|
import dotenv from 'dotenv';
|
|
const { chacha20poly1305 } = require('@noble/ciphers/chacha.js');
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
// Types pour l'API sécurisée
|
|
export interface VaultConfig {
|
|
baseUrl: string;
|
|
verifySsl?: boolean;
|
|
timeout?: number;
|
|
userId: string; // ID utilisateur obligatoire
|
|
}
|
|
|
|
export interface VaultFile {
|
|
content: string;
|
|
filename: string;
|
|
size: number;
|
|
encrypted: boolean;
|
|
algorithm?: string | undefined;
|
|
user_id?: string | undefined;
|
|
key_version?: string | undefined;
|
|
timestamp?: string | undefined;
|
|
}
|
|
|
|
export interface VaultHealth {
|
|
status: string;
|
|
service: string;
|
|
encryption: string;
|
|
algorithm: string;
|
|
authentication?: string;
|
|
key_rotation?: string;
|
|
timestamp?: string;
|
|
}
|
|
|
|
export interface VaultInfo {
|
|
name: string;
|
|
version: string;
|
|
domain: string;
|
|
port: number;
|
|
protocol: string;
|
|
encryption: string;
|
|
authentication?: string;
|
|
key_rotation?: string;
|
|
endpoints?: Record<string, string>;
|
|
}
|
|
|
|
export interface VaultRoute {
|
|
method: string;
|
|
path: string;
|
|
description: string;
|
|
authentication: string;
|
|
headers_required: string[];
|
|
response_type: string;
|
|
parameters?: Record<string, string>;
|
|
examples?: string[];
|
|
}
|
|
|
|
export interface VaultRoutes {
|
|
routes: VaultRoute[];
|
|
total_routes: number;
|
|
authentication: {
|
|
type: string;
|
|
header: string;
|
|
description: string;
|
|
};
|
|
user_id: string;
|
|
timestamp: string;
|
|
}
|
|
|
|
export interface SyncOptions {
|
|
environment: string;
|
|
localDir?: string;
|
|
verbose?: boolean;
|
|
}
|
|
|
|
export interface SyncResult {
|
|
synced: number;
|
|
skipped: number;
|
|
errors: number;
|
|
details: Array<{
|
|
file: string;
|
|
status: 'synced' | 'skipped' | 'error';
|
|
message?: string;
|
|
}>;
|
|
}
|
|
|
|
// Classes d'erreurs personnalisées
|
|
export interface VaultError {
|
|
message: string;
|
|
code?: string | undefined;
|
|
statusCode?: number | undefined;
|
|
}
|
|
|
|
export class VaultApiError extends Error implements VaultError {
|
|
public readonly code?: string | undefined;
|
|
public readonly statusCode?: number | undefined;
|
|
|
|
constructor(message: string, code?: string, statusCode?: number) {
|
|
super(message);
|
|
this.name = 'VaultApiError';
|
|
this.code = code;
|
|
this.statusCode = statusCode;
|
|
}
|
|
}
|
|
|
|
export class VaultDecryptionError extends Error implements VaultError {
|
|
public readonly code?: string | undefined;
|
|
|
|
constructor(message: string, code?: string) {
|
|
super(message);
|
|
this.name = 'VaultDecryptionError';
|
|
this.code = code;
|
|
}
|
|
}
|
|
|
|
export class VaultAuthenticationError extends Error implements VaultError {
|
|
public readonly code?: string | undefined;
|
|
public readonly statusCode?: number | undefined;
|
|
|
|
constructor(message: string, code?: string, statusCode?: number) {
|
|
super(message);
|
|
this.name = 'VaultAuthenticationError';
|
|
this.code = code;
|
|
this.statusCode = statusCode;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Client sécurisé pour l'API Vault avec authentification par clés utilisateur
|
|
* Les clés sont gérées côté serveur avec rotation automatique
|
|
*/
|
|
export class SecureVaultClient {
|
|
private config: VaultConfig;
|
|
private vaultKey: string | null = null;
|
|
|
|
constructor(config?: VaultConfig) {
|
|
// Charger les variables d'environnement depuis .env
|
|
// Essayer plusieurs emplacements pour le fichier .env
|
|
this._loadEnvironmentVariables();
|
|
|
|
// Si pas de config fournie, utiliser les variables d'environnement
|
|
if (!config) {
|
|
const envUser = process.env['VAULT_USER'];
|
|
const envKey = process.env['VAULT_KEY'];
|
|
const envEnv = process.env['VAULT_ENV'];
|
|
|
|
if (!envUser || !envKey || !envEnv) {
|
|
throw new Error('Variables d\'environnement requises: VAULT_USER, VAULT_KEY, VAULT_ENV');
|
|
}
|
|
|
|
config = {
|
|
baseUrl: 'https://vault.4nkweb.com:6666',
|
|
userId: envUser,
|
|
timeout: 30000,
|
|
verifySsl: false
|
|
};
|
|
|
|
this.vaultKey = envKey;
|
|
}
|
|
|
|
this.config = {
|
|
verifySsl: false,
|
|
timeout: 30000,
|
|
...config,
|
|
};
|
|
|
|
// Validation de l'ID utilisateur
|
|
if (!this.config.userId || this.config.userId.length < 3 || this.config.userId.length > 128) {
|
|
throw new Error('ID utilisateur requis (3-128 caractères)');
|
|
}
|
|
|
|
if (!/^[a-zA-Z0-9_-]+$/.test(this.config.userId)) {
|
|
throw new Error('ID utilisateur invalide - caractères autorisés: a-z, A-Z, 0-9, _, -');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Récupère un fichier depuis l'API Vault
|
|
*/
|
|
async getFile(env: string, filePath: string): Promise<VaultFile> {
|
|
const url = `${this.config.baseUrl}/${env}/${filePath}`;
|
|
|
|
try {
|
|
const response = await this._fetchApi(url);
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
throw new VaultAuthenticationError(
|
|
'Authentification échouée - vérifiez votre ID utilisateur',
|
|
'AUTH_FAILED',
|
|
response.status
|
|
);
|
|
}
|
|
if (response.status === 403) {
|
|
throw new VaultApiError(
|
|
'Accès non autorisé à ce fichier',
|
|
'ACCESS_DENIED',
|
|
response.status
|
|
);
|
|
}
|
|
throw new VaultApiError(
|
|
`Erreur API: ${response.status} ${response.statusText}`,
|
|
'API_ERROR',
|
|
response.status
|
|
);
|
|
}
|
|
|
|
const encryptedData = await response.arrayBuffer();
|
|
|
|
// Extraction des métadonnées depuis les headers
|
|
const user_id = response.headers.get('X-User-ID') || undefined;
|
|
const keyRotation = response.headers.get('X-Key-Rotation') || undefined;
|
|
const algorithm = response.headers.get('X-Algorithm') || undefined;
|
|
|
|
// Déchiffrement du contenu avec les headers pour la prochaine clé
|
|
const decryptedContent = this.decryptContent(Buffer.from(encryptedData), response.headers);
|
|
|
|
return {
|
|
content: decryptedContent,
|
|
filename: filePath.split('/').pop() || filePath,
|
|
size: decryptedContent.length,
|
|
encrypted: true,
|
|
algorithm,
|
|
user_id,
|
|
key_version: keyRotation,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
} catch (error) {
|
|
if (error instanceof VaultApiError || error instanceof VaultAuthenticationError) {
|
|
throw error;
|
|
}
|
|
throw new VaultApiError(
|
|
`Erreur lors de la récupération du fichier: ${error instanceof Error ? error.message : 'Inconnue'}`
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Récupère plusieurs fichiers en parallèle
|
|
*/
|
|
async getFiles(env: string, filePaths: string[]): Promise<VaultFile[]> {
|
|
const promises = filePaths.map(filePath => this.getFile(env, filePath));
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
/**
|
|
* Recherche des fichiers correspondant à un pattern
|
|
*/
|
|
async searchFiles(_env: string, _pattern: string): Promise<VaultFile[]> {
|
|
// Cette fonctionnalité nécessiterait une implémentation côté serveur
|
|
// Pour l'instant, retour d'une erreur explicative
|
|
throw new VaultApiError(
|
|
'Recherche de fichiers non implémentée - contactez l\'administrateur',
|
|
'NOT_IMPLEMENTED'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Vérifie l'état de santé de l'API
|
|
*/
|
|
async health(): Promise<VaultHealth> {
|
|
const url = `${this.config.baseUrl}/health`;
|
|
|
|
try {
|
|
const response = await this._fetchApi(url);
|
|
|
|
if (!response.ok) {
|
|
throw new VaultApiError(
|
|
`Erreur de santé API: ${response.status}`,
|
|
'HEALTH_CHECK_FAILED',
|
|
response.status
|
|
);
|
|
}
|
|
|
|
return await response.json() as VaultHealth;
|
|
|
|
} catch (error) {
|
|
if (error instanceof VaultApiError) {
|
|
throw error;
|
|
}
|
|
throw new VaultApiError(
|
|
`Erreur lors du contrôle de santé: ${error instanceof Error ? error.message : 'Inconnue'}`
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Récupère les informations sur l'API
|
|
*/
|
|
async info(): Promise<VaultInfo> {
|
|
const url = `${this.config.baseUrl}/info`;
|
|
|
|
try {
|
|
const response = await this._fetchApi(url);
|
|
|
|
if (!response.ok) {
|
|
throw new VaultApiError(
|
|
`Erreur info API: ${response.status}`,
|
|
'INFO_ERROR',
|
|
response.status
|
|
);
|
|
}
|
|
|
|
return await response.json() as VaultInfo;
|
|
|
|
} catch (error) {
|
|
if (error instanceof VaultApiError) {
|
|
throw error;
|
|
}
|
|
throw new VaultApiError(
|
|
`Erreur lors de la récupération des informations: ${error instanceof Error ? error.message : 'Inconnue'}`
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test de connectivité simple
|
|
*/
|
|
async ping(): Promise<boolean> {
|
|
try {
|
|
await this.health();
|
|
return true;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Récupère toutes les routes disponibles de l'API
|
|
*/
|
|
async getRoutes(): Promise<VaultRoutes> {
|
|
const url = `${this.config.baseUrl}/routes`;
|
|
|
|
try {
|
|
const response = await this._fetchApi(url);
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
throw new VaultAuthenticationError(
|
|
'Authentification échouée - vérifiez votre ID utilisateur',
|
|
'AUTH_FAILED',
|
|
response.status
|
|
);
|
|
}
|
|
throw new VaultApiError(
|
|
`Erreur API routes: ${response.status} ${response.statusText}`,
|
|
'ROUTES_API_ERROR',
|
|
response.status
|
|
);
|
|
}
|
|
|
|
return await response.json() as VaultRoutes;
|
|
|
|
} catch (error) {
|
|
if (error instanceof VaultApiError || error instanceof VaultAuthenticationError) {
|
|
throw error;
|
|
}
|
|
throw new VaultApiError(
|
|
`Erreur lors de la récupération des routes: ${error instanceof Error ? error.message : 'Inconnue'}`
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Synchronise les fichiers déchiffrés localement
|
|
* Route vault /<env>/<project>/<file_name> -> ../confs/<project>/<file_name>
|
|
* Les fichiers existants dans confs/ sont toujours écrasés pour avoir le contenu le plus récent
|
|
*/
|
|
async syncLocalFiles(options: SyncOptions): Promise<SyncResult> {
|
|
// Recharger les variables d'environnement au cas où elles auraient changé
|
|
this._loadEnvironmentVariables();
|
|
|
|
// Récupérer le dossier de destination depuis les variables d'environnement
|
|
const defaultConfsDir = process.env['VAULT_CONFS_DIR'] || '../confs';
|
|
|
|
const {
|
|
environment,
|
|
localDir = defaultConfsDir,
|
|
verbose = false
|
|
} = options;
|
|
|
|
const result: SyncResult = {
|
|
synced: 0,
|
|
skipped: 0,
|
|
errors: 0,
|
|
details: []
|
|
};
|
|
|
|
try {
|
|
// 1. Créer le dossier de destination s'il n'existe pas
|
|
const targetDir = path.resolve(localDir);
|
|
if (!fs.existsSync(targetDir)) {
|
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
if (verbose) {
|
|
console.log(`📁 Dossier créé: ${targetDir}`);
|
|
}
|
|
}
|
|
|
|
// 2. Récupérer la liste des routes pour identifier les fichiers
|
|
const routes = await this.getRoutes();
|
|
|
|
// 3. Extraire les fichiers disponibles depuis les exemples de routes
|
|
const fileRoute = routes.routes.find(route => route.path.includes('<env>') && route.path.includes('<file_path>'));
|
|
|
|
if (!fileRoute || !fileRoute.examples) {
|
|
throw new VaultApiError('Impossible de déterminer les fichiers disponibles');
|
|
}
|
|
|
|
// 4. Parser les exemples pour extraire les projets et fichiers
|
|
const filesToSync: Array<{ project: string; fileName: string; vaultPath: string }> = [];
|
|
|
|
for (const example of fileRoute.examples) {
|
|
// Format: /dev/bitcoin/bitcoin.conf -> project: bitcoin, fileName: bitcoin.conf
|
|
const pathParts = example.split('/').filter(part => part && part !== environment);
|
|
|
|
if (pathParts.length >= 2) {
|
|
const project = pathParts[0] || 'unknown';
|
|
const fileName = pathParts[pathParts.length - 1] || 'unknown';
|
|
const vaultPath = pathParts.join('/');
|
|
|
|
filesToSync.push({
|
|
project,
|
|
fileName,
|
|
vaultPath
|
|
});
|
|
}
|
|
}
|
|
|
|
// 5. Synchroniser chaque fichier
|
|
for (const file of filesToSync) {
|
|
try {
|
|
const localProjectDir = path.join(targetDir, file.project);
|
|
const localFilePath = path.join(localProjectDir, file.fileName);
|
|
|
|
// Créer le dossier du projet s'il n'existe pas
|
|
if (!fs.existsSync(localProjectDir)) {
|
|
fs.mkdirSync(localProjectDir, { recursive: true });
|
|
if (verbose) {
|
|
console.log(`📁 Dossier projet créé: ${localProjectDir}`);
|
|
}
|
|
}
|
|
|
|
// Toujours écraser les fichiers dans confs/ pour avoir le contenu le plus récent
|
|
// La vérification d'existence est conservée pour le logging uniquement
|
|
const fileExists = fs.existsSync(localFilePath);
|
|
if (fileExists && verbose) {
|
|
console.log(`🔄 Écrasement du fichier existant: ${file.vaultPath}`);
|
|
}
|
|
|
|
// Récupérer le fichier depuis le vault
|
|
const vaultFile = await this.getFile(environment, file.vaultPath);
|
|
|
|
// Extraire le contenu déchiffré (simulation pour le format de démonstration)
|
|
let content = vaultFile.content;
|
|
|
|
// Le contenu est maintenant déchiffré automatiquement par decryptContent
|
|
// Pas besoin de récupérer depuis le storage, le contenu vient de l'API déchiffrée
|
|
|
|
// Écrire le fichier local
|
|
fs.writeFileSync(localFilePath, content, 'utf8');
|
|
|
|
result.synced++;
|
|
result.details.push({
|
|
file: file.vaultPath,
|
|
status: 'synced',
|
|
message: `Synchronisé vers ${localFilePath}`
|
|
});
|
|
|
|
if (verbose) {
|
|
console.log(`✅ Synchronisé: ${file.vaultPath} -> ${localFilePath}`);
|
|
}
|
|
|
|
} catch (error) {
|
|
result.errors++;
|
|
const errorMessage = error instanceof Error ? error.message : 'Erreur inconnue';
|
|
result.details.push({
|
|
file: file.vaultPath,
|
|
status: 'error',
|
|
message: errorMessage
|
|
});
|
|
|
|
if (verbose) {
|
|
console.log(`❌ Erreur: ${file.vaultPath} - ${errorMessage}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (verbose) {
|
|
console.log(`\n📊 Résumé de synchronisation:`);
|
|
console.log(` ✅ Synchronisés: ${result.synced}`);
|
|
console.log(` ⏭️ Ignorés: ${result.skipped}`);
|
|
console.log(` ❌ Erreurs: ${result.errors}`);
|
|
}
|
|
|
|
return result;
|
|
|
|
} catch (error) {
|
|
throw new VaultApiError(
|
|
`Erreur lors de la synchronisation: ${error instanceof Error ? error.message : 'Inconnue'}`
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Déchiffre le contenu avec les métadonnées utilisateur et gère la prochaine clé
|
|
*/
|
|
private decryptContent(encryptedData: Buffer, responseHeaders?: Headers): string {
|
|
try {
|
|
// Décoder le base64
|
|
const decoded = Buffer.from(encryptedData.toString(), 'base64');
|
|
|
|
// Nouveau format: nonce (12 bytes) + taille_métadonnées (4 bytes) + métadonnées + contenu chiffré
|
|
if (decoded.length < 16) {
|
|
throw new Error('Données chiffrées invalides - format incorrect');
|
|
}
|
|
|
|
const nonce = decoded.subarray(0, 12);
|
|
const metadataSize = decoded.readUInt32BE(12);
|
|
const metadataJson = decoded.subarray(16, 16 + metadataSize);
|
|
const ciphertext = decoded.subarray(16 + metadataSize);
|
|
|
|
// Parse des métadonnées
|
|
const metadata = JSON.parse(metadataJson.toString('utf-8'));
|
|
|
|
// Vérification de l'utilisateur
|
|
if (metadata.user_id !== this.config.userId) {
|
|
throw new VaultAuthenticationError(
|
|
'Métadonnées utilisateur ne correspondent pas',
|
|
'USER_MISMATCH'
|
|
);
|
|
}
|
|
|
|
// Récupération de la prochaine clé depuis les headers ou les métadonnées
|
|
const nextKey = responseHeaders?.get('X-Next-Key') || metadata.next_key;
|
|
|
|
// Pour le déchiffrement, utiliser la clé courante d'abord
|
|
const keyToUse = this.vaultKey;
|
|
if (!keyToUse) {
|
|
throw new VaultDecryptionError('Clé de déchiffrement non disponible');
|
|
}
|
|
|
|
console.log(`🔑 Utilisation de la clé courante pour déchiffrement: ${keyToUse.substring(0, 20)}...`);
|
|
|
|
// Ne pas mettre à jour la clé immédiatement, attendre un déchiffrement réussi
|
|
|
|
try {
|
|
// Convertir la clé base64 en Uint8Array
|
|
const key = Buffer.from(keyToUse, 'base64');
|
|
|
|
// Déchiffrement ChaCha20-Poly1305 avec @noble/ciphers
|
|
const cipher = chacha20poly1305(key, nonce);
|
|
const decrypted = cipher.decrypt(ciphertext);
|
|
|
|
// Déchiffrement réussi, mettre à jour la clé pour la prochaine requête
|
|
if (nextKey) {
|
|
this.updateNextKey(nextKey);
|
|
}
|
|
|
|
// Retourner le contenu déchiffré
|
|
return Buffer.from(decrypted).toString('utf-8');
|
|
|
|
} catch (decryptError) {
|
|
// Si le déchiffrement échoue, essayer avec la prochaine clé si elle est différente
|
|
if (nextKey && nextKey !== keyToUse) {
|
|
console.log(`🔄 Tentative de déchiffrement avec la prochaine clé...`);
|
|
try {
|
|
const nextKeyBuffer = Buffer.from(nextKey, 'base64');
|
|
const cipherNext = chacha20poly1305(nextKeyBuffer, nonce);
|
|
const decryptedNext = cipherNext.decrypt(ciphertext);
|
|
|
|
console.log(`✅ Déchiffrement réussi avec la prochaine clé !`);
|
|
// Déchiffrement réussi avec la prochaine clé, mettre à jour
|
|
this.updateNextKey(nextKey);
|
|
|
|
return Buffer.from(decryptedNext).toString('utf-8');
|
|
} catch (nextDecryptError) {
|
|
console.warn(`⚠️ Déchiffrement avec la prochaine clé échoué: ${nextDecryptError instanceof Error ? nextDecryptError.message : 'Erreur inconnue'}`);
|
|
}
|
|
}
|
|
|
|
throw new VaultDecryptionError(
|
|
`Erreur de déchiffrement ChaCha20-Poly1305: ${decryptError instanceof Error ? decryptError.message : 'Inconnue'}`
|
|
);
|
|
}
|
|
|
|
} catch (error) {
|
|
if (error instanceof VaultAuthenticationError || error instanceof VaultDecryptionError) {
|
|
throw error;
|
|
}
|
|
throw new VaultDecryptionError(
|
|
`Erreur de déchiffrement: ${error instanceof Error ? error.message : 'Inconnue'}`
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Met à jour la prochaine clé pour les requêtes suivantes et le fichier .env
|
|
*/
|
|
private updateNextKey(nextKey: string): void {
|
|
if (nextKey) {
|
|
this.vaultKey = nextKey; // Mettre à jour la clé courante
|
|
|
|
// Mettre à jour le fichier .env avec la nouvelle clé
|
|
this.updateEnvFile(nextKey);
|
|
|
|
console.log(`🔑 Prochaine clé mise à jour: ${nextKey.substring(0, 20)}...`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Charge les variables d'environnement depuis plusieurs emplacements possibles
|
|
*/
|
|
private _loadEnvironmentVariables(): void {
|
|
const possibleEnvPaths = [
|
|
'.env', // Répertoire courant
|
|
'../.env', // Répertoire parent
|
|
'../../.env', // Répertoire grand-parent
|
|
path.join(__dirname, '.env'), // Répertoire du SDK
|
|
path.join(__dirname, '../.env'), // Parent du SDK
|
|
path.join(__dirname, '../../.env'), // Grand-parent du SDK
|
|
path.join(process.cwd(), '.env'), // Répertoire de travail
|
|
];
|
|
|
|
let envLoaded = false;
|
|
for (const envPath of possibleEnvPaths) {
|
|
try {
|
|
if (fs.existsSync(envPath)) {
|
|
const result = dotenv.config({ path: envPath });
|
|
if (!result.error) {
|
|
console.log(`📄 Variables d'environnement chargées depuis: ${envPath}`);
|
|
envLoaded = true;
|
|
break;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Ignorer les erreurs et continuer avec le chemin suivant
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Si aucun fichier .env n'a été trouvé, essayer le chargement par défaut
|
|
if (!envLoaded) {
|
|
try {
|
|
dotenv.config();
|
|
console.log('📄 Variables d\'environnement chargées depuis .env par défaut');
|
|
} catch (error) {
|
|
console.log('⚠️ Aucun fichier .env trouvé, utilisation des variables système uniquement');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Met à jour le fichier .env avec la nouvelle clé
|
|
*/
|
|
private updateEnvFile(newKey: string): void {
|
|
try {
|
|
const envPath = path.join(__dirname, '../../.env');
|
|
|
|
// Lire le fichier .env actuel
|
|
let envContent = '';
|
|
if (fs.existsSync(envPath)) {
|
|
envContent = fs.readFileSync(envPath, 'utf8');
|
|
}
|
|
|
|
// Mettre à jour ou ajouter la clé VAULT_KEY
|
|
const keyRegex = /^VAULT_KEY=.*$/m;
|
|
if (keyRegex.test(envContent)) {
|
|
envContent = envContent.replace(keyRegex, `VAULT_KEY="${newKey}"`);
|
|
} else {
|
|
envContent += `\nVAULT_KEY="${newKey}"\n`;
|
|
}
|
|
|
|
// Écrire le fichier .env mis à jour
|
|
fs.writeFileSync(envPath, envContent, 'utf8');
|
|
|
|
console.log(`📝 Fichier .env mis à jour avec la nouvelle clé`);
|
|
|
|
} catch (error) {
|
|
console.warn(`⚠️ Impossible de mettre à jour le fichier .env: ${error instanceof Error ? error.message : 'Erreur inconnue'}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Effectue une requête vers l'API avec authentification
|
|
*/
|
|
private async _fetchApi(url: string): Promise<Response> {
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
|
|
try {
|
|
const mergedOptions: any = {
|
|
method: 'GET',
|
|
headers: {
|
|
'User-Agent': 'SecureVaultClient/2.0.0',
|
|
'X-User-ID': this.config.userId,
|
|
'Accept': 'application/octet-stream'
|
|
},
|
|
signal: controller.signal
|
|
};
|
|
|
|
// Configuration SSL pour les certificats auto-signés
|
|
if (this.config.verifySsl === false) {
|
|
mergedOptions.agent = new https.Agent({
|
|
rejectUnauthorized: false
|
|
});
|
|
}
|
|
|
|
const response = await fetch(url, mergedOptions) as any;
|
|
clearTimeout(timeoutId);
|
|
return response;
|
|
|
|
} catch (error) {
|
|
clearTimeout(timeoutId);
|
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
throw new VaultApiError(
|
|
`Timeout de la requête (${this.config.timeout}ms)`,
|
|
'TIMEOUT'
|
|
);
|
|
}
|
|
throw new VaultApiError(
|
|
`Erreur de connexion: ${error instanceof Error ? error.message : 'Inconnue'}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fonction utilitaire pour créer un client sécurisé
|
|
*/
|
|
export function createSecureVaultClient(baseUrl: string, userId: string): SecureVaultClient {
|
|
return new SecureVaultClient({
|
|
baseUrl,
|
|
userId,
|
|
verifySsl: false, // Pour les certificats auto-signés en développement
|
|
timeout: 15000
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fonction utilitaire pour créer un client sécurisé avec configuration complète
|
|
*/
|
|
export function createSecureVaultClientWithConfig(config: VaultConfig): SecureVaultClient {
|
|
return new SecureVaultClient(config);
|
|
}
|