@envsafes-org/cli 0.0.6 → 0.0.8
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 +2 -2
- package/dist/index.js +2 -2
- package/package.json +4 -1
- package/STORAGE_AGENT_README.md +0 -328
- package/src/commands/login.ts +0 -58
- package/src/commands/projects.ts +0 -40
- package/src/commands/pull.ts +0 -53
- package/src/commands/push.ts +0 -64
- package/src/commands/run.ts +0 -55
- package/src/commands/whoami.ts +0 -33
- package/src/index.ts +0 -105
- package/src/utils/api.ts +0 -36
- package/tsconfig.json +0 -22
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ EnvSafe CLI vous permet de gérer vos variables d'environnement de manière séc
|
|
|
9
9
|
|
|
10
10
|
## 🛑 Prérequis
|
|
11
11
|
|
|
12
|
-
Avant d'utiliser la CLI, vous devez créer un compte et un projet sur **[https://envsafe.
|
|
12
|
+
Avant d'utiliser la CLI, vous devez créer un compte et un projet sur **[https://envsafe.dev](https://envsafe.dev)**.
|
|
13
13
|
|
|
14
14
|
## 📦 Installation
|
|
15
15
|
|
|
@@ -318,7 +318,7 @@ EnvSafe utilise une architecture de chiffrement de bout en bout :
|
|
|
318
318
|
|
|
319
319
|
## 📚 Documentation complète
|
|
320
320
|
|
|
321
|
-
Pour plus d'informations, consultez la [documentation officielle](https://envsafe.
|
|
321
|
+
Pour plus d'informations, consultez la [documentation officielle](https://envsafe.dev/docs).
|
|
322
322
|
|
|
323
323
|
---
|
|
324
324
|
|
package/dist/index.js
CHANGED
|
@@ -20,8 +20,8 @@ exports.config = new conf_1.default({
|
|
|
20
20
|
projectName: "envsafe-cli",
|
|
21
21
|
schema: {
|
|
22
22
|
token: { type: "string", default: "" },
|
|
23
|
-
apiUrl: { type: "string", default: "https://envsafe.
|
|
24
|
-
dashboardUrl: { type: "string", default: "https://envsafe.
|
|
23
|
+
apiUrl: { type: "string", default: "https://envsafe.dev" },
|
|
24
|
+
dashboardUrl: { type: "string", default: "https://envsafe.dev" },
|
|
25
25
|
},
|
|
26
26
|
});
|
|
27
27
|
const program = new commander_1.Command();
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@envsafes-org/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "CLI pour EnvSafe - Gestionnaire sécurisé de variables d'environnement",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"envsafe": "./dist/index.js"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
9
12
|
"repository": {
|
|
10
13
|
"type": "git",
|
|
11
14
|
"url": "git+https://github.com/Ifiboys/envsafe-cli.git"
|
package/STORAGE_AGENT_README.md
DELETED
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
# EnvSafe Storage Agent (BYOS)
|
|
2
|
-
|
|
3
|
-
Agent simple pour héberger vos secrets chiffrés EnvSafe sur votre propre infrastructure.
|
|
4
|
-
|
|
5
|
-
## Qu'est-ce que c'est ?
|
|
6
|
-
|
|
7
|
-
Cet agent vous permet de stocker vos secrets chiffrés sur **votre propre serveur** au lieu du cloud EnvSafe, tout en continuant à utiliser l'interface EnvSafe et la CLI.
|
|
8
|
-
|
|
9
|
-
## ⚠ Rappel Important de Sécurité
|
|
10
|
-
|
|
11
|
-
**Même avec cet agent sur votre serveur, vos données sont DÉJÀ chiffrées (AES-256-GCM).**
|
|
12
|
-
|
|
13
|
-
- **Cet agent ne peut PAS lire vos secrets**
|
|
14
|
-
- Il stocke uniquement du texte illisible
|
|
15
|
-
- **Seule votre clé privée locale peut déchiffrer les données**
|
|
16
|
-
- EnvSafe (SaaS) ne peut pas non plus lire vos secrets
|
|
17
|
-
|
|
18
|
-
## Installation
|
|
19
|
-
|
|
20
|
-
### Prérequis
|
|
21
|
-
|
|
22
|
-
- Docker et Docker Compose
|
|
23
|
-
- Node.js 18+ (si vous lancez sans Docker)
|
|
24
|
-
|
|
25
|
-
### Avec Docker (Recommandé)
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
# Cloner ce dépôt
|
|
29
|
-
git clone https://github.com/Ifiboys/envsafe-storage-agent
|
|
30
|
-
cd envsafe-storage-agent
|
|
31
|
-
|
|
32
|
-
# Configurer les variables d'environnement
|
|
33
|
-
cp .env.example .env
|
|
34
|
-
# Éditez .env et définissez AUTH_SECRET avec une valeur très sécurisée
|
|
35
|
-
|
|
36
|
-
# Lancer l'agent
|
|
37
|
-
docker-compose up -d
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### Sans Docker
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
npm install
|
|
44
|
-
npm run build
|
|
45
|
-
npm start
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Configuration
|
|
49
|
-
|
|
50
|
-
### Variables d'environnement
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
# SECRET PARTAGÉ - Utilisez un générateur de mots de passe sécurisé
|
|
54
|
-
AUTH_SECRET=your-very-long-and-secure-secret-here
|
|
55
|
-
|
|
56
|
-
# Port d'écoute
|
|
57
|
-
PORT=3001
|
|
58
|
-
|
|
59
|
-
# Base de données (SQLite par défaut, PostgreSQL recommandé en production)
|
|
60
|
-
DATABASE_URL=file:./envsafe-storage.db
|
|
61
|
-
# Exemple PostgreSQL:
|
|
62
|
-
# DATABASE_URL=postgresql://user:password@localhost:5432/envsafe_storage
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### Génération du secret
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
# Utilisez cette commande pour générer un secret sécurisé:
|
|
69
|
-
openssl rand -hex 32
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
## Configuration dans EnvSafe
|
|
73
|
-
|
|
74
|
-
1. Allez dans votre projet EnvSafe
|
|
75
|
-
2. Paramètres → Stockage
|
|
76
|
-
3. Activez **"Stockage Externe (BYOS)"**
|
|
77
|
-
4. Renseignez:
|
|
78
|
-
- **URL**: `https://votre-serveur.com` (l'URL où votre agent est accessible)
|
|
79
|
-
- **Secret**: Le même `AUTH_SECRET` que vous avez défini dans `.env`
|
|
80
|
-
5. Cliquez sur **"Tester la connexion"**
|
|
81
|
-
6. Si le test est réussi, sauvegardez
|
|
82
|
-
|
|
83
|
-
## API Endpoints
|
|
84
|
-
|
|
85
|
-
L'agent expose ces routes (toutes protégées par authentification):
|
|
86
|
-
|
|
87
|
-
- `GET /health` - Vérification de la santé de l'agent
|
|
88
|
-
- `POST /store-secret` - Stocke un secret chiffré
|
|
89
|
-
- `GET /get-secret` - Récupère un secret chiffré
|
|
90
|
-
- `GET /get-all-secrets` - Récupère tous les secrets d'un environnement
|
|
91
|
-
- `DELETE /delete-secret` - Supprime un secret
|
|
92
|
-
|
|
93
|
-
## Exemple de Code de l'Agent
|
|
94
|
-
|
|
95
|
-
Voici un exemple complet d'agent de stockage (Node.js + Express):
|
|
96
|
-
|
|
97
|
-
```typescript
|
|
98
|
-
import express, { Request, Response } from "express";
|
|
99
|
-
import crypto from "crypto";
|
|
100
|
-
import { PrismaClient } from "@prisma/client";
|
|
101
|
-
|
|
102
|
-
const app = express();
|
|
103
|
-
const prisma = new PrismaClient();
|
|
104
|
-
|
|
105
|
-
app.use(express.json());
|
|
106
|
-
|
|
107
|
-
// ⚠️ IMPORTANT: Cet agent stocke UNIQUEMENT des données CHIFFRÉES
|
|
108
|
-
// Il ne peut PAS lire vos secrets
|
|
109
|
-
|
|
110
|
-
// Middleware d'authentification
|
|
111
|
-
const authenticate = (req: Request, res: Response, next: Function) => {
|
|
112
|
-
const authHeader = req.headers.authorization;
|
|
113
|
-
const timestamp = req.headers["x-envsafe-timestamp"] as string;
|
|
114
|
-
const signature = req.headers["x-envsafe-signature"] as string;
|
|
115
|
-
|
|
116
|
-
if (!authHeader || !timestamp || !signature) {
|
|
117
|
-
return res.status(401).json({ error: "Missing authentication headers" });
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const token = authHeader.replace("Bearer ", "");
|
|
121
|
-
const expectedSecret = process.env.AUTH_SECRET;
|
|
122
|
-
|
|
123
|
-
if (!expectedSecret) {
|
|
124
|
-
console.error("AUTH_SECRET not configured");
|
|
125
|
-
return res.status(500).json({ error: "Server misconfiguration" });
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Vérifier le token
|
|
129
|
-
if (token !== expectedSecret) {
|
|
130
|
-
console.warn(`Failed authentication attempt from ${req.ip}`);
|
|
131
|
-
return res.status(401).json({ error: "Invalid token" });
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Vérifier la signature HMAC
|
|
135
|
-
const expectedSignature = crypto
|
|
136
|
-
.createHmac("sha256", expectedSecret)
|
|
137
|
-
.update(timestamp)
|
|
138
|
-
.digest("hex");
|
|
139
|
-
|
|
140
|
-
if (signature !== expectedSignature) {
|
|
141
|
-
console.warn(`Invalid HMAC signature from ${req.ip}`);
|
|
142
|
-
return res.status(401).json({ error: "Invalid signature" });
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Vérifier que le timestamp n'est pas trop ancien (5 minutes max)
|
|
146
|
-
const now = Date.now();
|
|
147
|
-
const requestTime = parseInt(timestamp);
|
|
148
|
-
if (Math.abs(now - requestTime) > 5 * 60 * 1000) {
|
|
149
|
-
return res.status(401).json({ error: "Request expired" });
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
next();
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
// Health check
|
|
156
|
-
app.get("/health", authenticate, (req: Request, res: Response) => {
|
|
157
|
-
res.json({
|
|
158
|
-
status: "healthy",
|
|
159
|
-
timestamp: new Date().toISOString(),
|
|
160
|
-
version: "1.0.0"
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
// Stocker un secret chiffré
|
|
165
|
-
app.post("/store-secret", authenticate, async (req: Request, res: Response) => {
|
|
166
|
-
const { key, encryptedValue, environmentId } = req.body;
|
|
167
|
-
|
|
168
|
-
if (!key || !encryptedValue || !environmentId) {
|
|
169
|
-
return res.status(400).json({ error: "Missing required fields" });
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
// Note: encryptedValue est déjà chiffré par le client
|
|
174
|
-
// L'agent ne fait que le stocker, il ne peut pas le lire
|
|
175
|
-
await prisma.encryptedVariable.upsert({
|
|
176
|
-
where: {
|
|
177
|
-
environmentId_key: { environmentId, key }
|
|
178
|
-
},
|
|
179
|
-
create: { key, encryptedValue, environmentId },
|
|
180
|
-
update: { encryptedValue }
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
console.log(`✅ Stored encrypted variable: ${key} for env: ${environmentId}`);
|
|
184
|
-
res.json({ success: true });
|
|
185
|
-
} catch (error) {
|
|
186
|
-
console.error("Error storing variable:", error);
|
|
187
|
-
res.status(500).json({ error: "Failed to store variable" });
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
// Récupérer un secret chiffré
|
|
192
|
-
app.get("/get-secret", authenticate, async (req: Request, res: Response) => {
|
|
193
|
-
const { key, environmentId } = req.query;
|
|
194
|
-
|
|
195
|
-
if (!key || !environmentId) {
|
|
196
|
-
return res.status(400).json({ error: "Missing required parameters" });
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
try {
|
|
200
|
-
const variable = await prisma.encryptedVariable.findUnique({
|
|
201
|
-
where: {
|
|
202
|
-
environmentId_key: {
|
|
203
|
-
environmentId: environmentId as string,
|
|
204
|
-
key: key as string
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
if (!variable) {
|
|
210
|
-
return res.status(404).json({ error: "Variable not found" });
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Retourne la valeur chiffrée (toujours illisible)
|
|
214
|
-
res.json({ encryptedValue: variable.encryptedValue });
|
|
215
|
-
} catch (error) {
|
|
216
|
-
console.error("Error getting variable:", error);
|
|
217
|
-
res.status(500).json({ error: "Failed to get variable" });
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
const PORT = process.env.PORT || 3001;
|
|
222
|
-
|
|
223
|
-
app.listen(PORT, () => {
|
|
224
|
-
console.log(`🔐 EnvSafe Storage Agent running on port ${PORT}`);
|
|
225
|
-
console.log(`⚠️ Reminder: This agent stores ENCRYPTED data only`);
|
|
226
|
-
console.log(`⚠️ Even with database access, secrets remain unreadable`);
|
|
227
|
-
});
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
### Schéma Prisma pour l'agent
|
|
231
|
-
|
|
232
|
-
```prisma
|
|
233
|
-
// prisma/schema.prisma (pour l'agent de stockage)
|
|
234
|
-
|
|
235
|
-
datasource db {
|
|
236
|
-
provider = "postgresql"
|
|
237
|
-
url = env("DATABASE_URL")
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
generator client {
|
|
241
|
-
provider = "prisma-client-js"
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
model EncryptedVariable {
|
|
245
|
-
id String @id @default(cuid())
|
|
246
|
-
key String
|
|
247
|
-
encryptedValue String @map("encrypted_value") // Toujours chiffré (AES-256-GCM)
|
|
248
|
-
environmentId String @map("environment_id")
|
|
249
|
-
|
|
250
|
-
createdAt DateTime @default(now()) @map("created_at")
|
|
251
|
-
updatedAt DateTime @updatedAt @map("updated_at")
|
|
252
|
-
|
|
253
|
-
@@unique([environmentId, key])
|
|
254
|
-
@@map("encrypted_variables")
|
|
255
|
-
}
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
## Sécurité
|
|
259
|
-
|
|
260
|
-
### Authentification
|
|
261
|
-
|
|
262
|
-
Toutes les requêtes doivent inclure:
|
|
263
|
-
- `Authorization: Bearer <AUTH_SECRET>`
|
|
264
|
-
- `X-EnvSafe-Timestamp: <timestamp>`
|
|
265
|
-
- `X-EnvSafe-Signature: <HMAC-SHA256>`
|
|
266
|
-
|
|
267
|
-
### Ce que l'agent NE FAIT PAS
|
|
268
|
-
|
|
269
|
-
- ❌ Il ne stocke PAS de secrets en clair
|
|
270
|
-
- ❌ Il ne peut PAS déchiffrer vos données
|
|
271
|
-
- ❌ Il n'a PAS accès à vos clés de déchiffrement
|
|
272
|
-
|
|
273
|
-
### Ce qu'il FAIT
|
|
274
|
-
|
|
275
|
-
- ✅ Il stocke des blobs chiffrés (texte illisible)
|
|
276
|
-
- ✅ Il vérifie l'authentification HMAC des requêtes
|
|
277
|
-
- ✅ Il refuse les requêtes non autorisées
|
|
278
|
-
|
|
279
|
-
## Production
|
|
280
|
-
|
|
281
|
-
### Recommandations
|
|
282
|
-
|
|
283
|
-
1. **HTTPS Obligatoire** : Utilisez un reverse proxy (Nginx, Caddy) avec certificat SSL
|
|
284
|
-
2. **Base de données externe** : Utilisez PostgreSQL au lieu de SQLite
|
|
285
|
-
3. **Firewall** : Limitez l'accès à l'IP du serveur EnvSafe
|
|
286
|
-
4. **Backups** : Configurez des sauvegardes régulières de votre base de données
|
|
287
|
-
5. **Monitoring** : Surveillez les logs et les tentatives d'accès
|
|
288
|
-
|
|
289
|
-
### Exemple Nginx
|
|
290
|
-
|
|
291
|
-
```nginx
|
|
292
|
-
server {
|
|
293
|
-
listen 443 ssl http2;
|
|
294
|
-
server_name envsafe-storage.votre-domaine.com;
|
|
295
|
-
|
|
296
|
-
ssl_certificate /etc/letsencrypt/live/votre-domaine.com/fullchain.pem;
|
|
297
|
-
ssl_certificate_key /etc/letsencrypt/live/votre-domaine.com/privkey.pem;
|
|
298
|
-
|
|
299
|
-
location / {
|
|
300
|
-
proxy_pass http://localhost:3001;
|
|
301
|
-
proxy_set_header Host $host;
|
|
302
|
-
proxy_set_header X-Real-IP $remote_addr;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
## Logs
|
|
308
|
-
|
|
309
|
-
Les logs de l'agent incluent:
|
|
310
|
-
- Tentatives d'authentification (succès/échecs)
|
|
311
|
-
- Opérations de stockage/récupération
|
|
312
|
-
- Erreurs système
|
|
313
|
-
|
|
314
|
-
```bash
|
|
315
|
-
# Voir les logs en temps réel
|
|
316
|
-
docker-compose logs -f
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
## Support
|
|
320
|
-
|
|
321
|
-
Pour toute question ou problème:
|
|
322
|
-
- Documentation: https://envsafe.vercel.app/docs
|
|
323
|
-
- Issues: https://github.com/Ifiboys/envsafe-storage-agent/issues
|
|
324
|
-
- Email: oladokunefi123@gmail.com
|
|
325
|
-
|
|
326
|
-
## Licence
|
|
327
|
-
|
|
328
|
-
MIT - Voir LICENSE
|
package/src/commands/login.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import inquirer from "inquirer";
|
|
3
|
-
import { config } from "../index.js";
|
|
4
|
-
import { apiRequest } from "../utils/api.js";
|
|
5
|
-
|
|
6
|
-
import open from "open";
|
|
7
|
-
|
|
8
|
-
export async function login(options: { token?: string }) {
|
|
9
|
-
let token = options.token;
|
|
10
|
-
|
|
11
|
-
if (!token) {
|
|
12
|
-
const dashboardUrl = config.get("dashboardUrl") as string;
|
|
13
|
-
const loginUrl = `${dashboardUrl}/dashboard?redirect=tokens`;
|
|
14
|
-
|
|
15
|
-
console.log(chalk.cyan(`\nOuverture du navigateur pour générer un token...\n`));
|
|
16
|
-
console.log(chalk.dim(`Si le navigateur ne s'ouvre pas, visitez : ${loginUrl}`));
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
await open(loginUrl);
|
|
20
|
-
} catch (err) {
|
|
21
|
-
// Ignore error if browser fails to open
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const answers = await inquirer.prompt([
|
|
25
|
-
{
|
|
26
|
-
type: "password",
|
|
27
|
-
name: "token",
|
|
28
|
-
message: "Collez votre token API EnvSafe ici:",
|
|
29
|
-
mask: "*",
|
|
30
|
-
validate: (input) => input.length > 0 || "Le token est requis",
|
|
31
|
-
},
|
|
32
|
-
]);
|
|
33
|
-
token = answers.token;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
console.log(chalk.dim("Vérification du token..."));
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
// Test the token
|
|
40
|
-
const response = await apiRequest("/api/v1/projects", {
|
|
41
|
-
method: "GET",
|
|
42
|
-
token,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
if (response.error) {
|
|
46
|
-
console.log(chalk.red(`✗ Erreur: ${response.error}`));
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Save token
|
|
51
|
-
config.set("token", token);
|
|
52
|
-
|
|
53
|
-
console.log(chalk.green("✓ Connexion réussie!"));
|
|
54
|
-
console.log(chalk.dim(` ${response.count} projet(s) accessible(s)`));
|
|
55
|
-
} catch (error: any) {
|
|
56
|
-
console.log(chalk.red(`✗ Erreur de connexion: ${error.message}`));
|
|
57
|
-
}
|
|
58
|
-
}
|
package/src/commands/projects.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import { config } from "../index.js";
|
|
3
|
-
import { apiRequest } from "../utils/api.js";
|
|
4
|
-
|
|
5
|
-
export async function projects() {
|
|
6
|
-
const token = config.get("token") as string;
|
|
7
|
-
|
|
8
|
-
if (!token) {
|
|
9
|
-
console.log(chalk.red("✗ Non connecté. Utilisez 'envsafe login' d'abord."));
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
const response = await apiRequest("/api/v1/projects", {
|
|
15
|
-
method: "GET",
|
|
16
|
-
token,
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
if (response.error) {
|
|
20
|
-
console.log(chalk.red(`✗ ${response.error}`));
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
console.log(chalk.cyan("\n📁 Projets accessibles\n"));
|
|
25
|
-
|
|
26
|
-
if (response.projects.length === 0) {
|
|
27
|
-
console.log(chalk.dim(" Aucun projet trouvé."));
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
for (const project of response.projects) {
|
|
32
|
-
console.log(chalk.bold(` ${project.name}`));
|
|
33
|
-
console.log(chalk.dim(` slug: ${project.slug}`));
|
|
34
|
-
console.log(chalk.dim(` envs: ${project.environments.join(", ")}`));
|
|
35
|
-
console.log("");
|
|
36
|
-
}
|
|
37
|
-
} catch (error: any) {
|
|
38
|
-
console.log(chalk.red(`✗ Erreur: ${error.message}`));
|
|
39
|
-
}
|
|
40
|
-
}
|
package/src/commands/pull.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import ora from "ora";
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import { config } from "../index.js";
|
|
5
|
-
import { apiRequest } from "../utils/api.js";
|
|
6
|
-
|
|
7
|
-
export async function pull(
|
|
8
|
-
project: string,
|
|
9
|
-
options: { env: string; output: string }
|
|
10
|
-
) {
|
|
11
|
-
const token = config.get("token") as string;
|
|
12
|
-
|
|
13
|
-
if (!token) {
|
|
14
|
-
console.log(chalk.red("✗ Non connecté. Utilisez 'envsafe login' d'abord."));
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const spinner = ora(`Téléchargement des variables pour ${project} (${options.env})...`).start();
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
const response = await apiRequest(`/api/v1/projects/${project}/${options.env}`, {
|
|
22
|
-
method: "GET",
|
|
23
|
-
token,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
if (response.error) {
|
|
27
|
-
spinner.fail(chalk.red(response.error));
|
|
28
|
-
|
|
29
|
-
// Suggestion intelligente si l'utilisateur a confondu projet et environnement
|
|
30
|
-
const commonEnvs = ["development", "dev", "staging", "stg", "production", "prod", "test"];
|
|
31
|
-
if (commonEnvs.includes(project.toLowerCase()) || response.error.includes("trouvé")) {
|
|
32
|
-
console.log(chalk.yellow("\n💡 Astuce : La syntaxe est 'envsafe pull <MON_PROJET>'"));
|
|
33
|
-
console.log(chalk.yellow(` Vous avez essayé de pull le projet "${project}".`));
|
|
34
|
-
console.log(chalk.yellow(` Utilisez 'envsafe projects' pour voir la liste de vos projets/slugs.`));
|
|
35
|
-
}
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Format as .env content
|
|
40
|
-
const envContent = Object.entries(response.variables as Record<string, string>)
|
|
41
|
-
.map(([key, value]) => `${key}="${value}"`)
|
|
42
|
-
.join("\n");
|
|
43
|
-
|
|
44
|
-
// Write to file
|
|
45
|
-
fs.writeFileSync(options.output, envContent + "\n");
|
|
46
|
-
|
|
47
|
-
spinner.succeed(
|
|
48
|
-
chalk.green(`✓ ${response.count} variable(s) téléchargée(s) vers ${options.output}`)
|
|
49
|
-
);
|
|
50
|
-
} catch (error: any) {
|
|
51
|
-
spinner.fail(chalk.red(`Erreur: ${error.message}`));
|
|
52
|
-
}
|
|
53
|
-
}
|
package/src/commands/push.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import ora from "ora";
|
|
3
|
-
import fs from "fs";
|
|
4
|
-
import dotenv from "dotenv";
|
|
5
|
-
import { config } from "../index.js";
|
|
6
|
-
import { apiRequest } from "../utils/api.js";
|
|
7
|
-
|
|
8
|
-
export async function push(
|
|
9
|
-
project: string,
|
|
10
|
-
options: { env: string; file: string }
|
|
11
|
-
) {
|
|
12
|
-
const token = config.get("token") as string;
|
|
13
|
-
|
|
14
|
-
if (!token) {
|
|
15
|
-
console.log(chalk.red("✗ Non connecté. Utilisez 'envsafe login' d'abord."));
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Check if file exists
|
|
20
|
-
if (!fs.existsSync(options.file)) {
|
|
21
|
-
console.log(chalk.red(`✗ Fichier non trouvé: ${options.file}`));
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const spinner = ora(`Envoi des variables vers ${project} (${options.env})...`).start();
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
// Parse .env file
|
|
29
|
-
const envContent = fs.readFileSync(options.file, "utf-8");
|
|
30
|
-
const parsed = dotenv.parse(envContent);
|
|
31
|
-
|
|
32
|
-
const response = await apiRequest(`/api/v1/projects/${project}/${options.env}`, {
|
|
33
|
-
method: "POST",
|
|
34
|
-
token,
|
|
35
|
-
body: { variables: parsed },
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
if (response.error) {
|
|
39
|
-
spinner.fail(chalk.red(response.error));
|
|
40
|
-
|
|
41
|
-
// Suggestion intelligente
|
|
42
|
-
const commonEnvs = ["development", "dev", "staging", "stg", "production", "prod", "test"];
|
|
43
|
-
if (commonEnvs.includes(project.toLowerCase()) || response.error.includes("trouvé")) {
|
|
44
|
-
console.log(chalk.yellow("\n💡 Astuce : La syntaxe est 'envsafe push <MON_PROJET>'"));
|
|
45
|
-
console.log(chalk.yellow(` Vous avez essayé de push vers le projet "${project}".`));
|
|
46
|
-
console.log(chalk.yellow(` Utilisez 'envsafe projects' pour voir la liste de vos projets/slugs.`));
|
|
47
|
-
}
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
spinner.succeed(
|
|
52
|
-
chalk.green(`✓ ${response.created} variable(s) créée(s)`)
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
if (response.errors?.length > 0) {
|
|
56
|
-
console.log(chalk.yellow("\nAvertissements:"));
|
|
57
|
-
response.errors.forEach((err: string) => {
|
|
58
|
-
console.log(chalk.dim(` - ${err}`));
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
} catch (error: any) {
|
|
62
|
-
spinner.fail(chalk.red(`Erreur: ${error.message}`));
|
|
63
|
-
}
|
|
64
|
-
}
|
package/src/commands/run.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import { spawn } from "child_process";
|
|
3
|
-
import { config } from "../index.js";
|
|
4
|
-
import { apiRequest } from "../utils/api.js";
|
|
5
|
-
|
|
6
|
-
export async function run(
|
|
7
|
-
project: string,
|
|
8
|
-
command: string[],
|
|
9
|
-
options: { env: string }
|
|
10
|
-
) {
|
|
11
|
-
const token = config.get("token") as string;
|
|
12
|
-
|
|
13
|
-
if (!token) {
|
|
14
|
-
console.log(chalk.red("✗ Non connecté. Utilisez 'envsafe login' d'abord."));
|
|
15
|
-
process.exit(1);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
console.log(chalk.dim(`Récupération des variables pour ${project} (${options.env})...`));
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
const response = await apiRequest(`/api/v1/projects/${project}/${options.env}`, {
|
|
22
|
-
method: "GET",
|
|
23
|
-
token,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
if (response.error) {
|
|
27
|
-
console.log(chalk.red(`✗ ${response.error}`));
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const variables = response.variables as Record<string, string>;
|
|
32
|
-
console.log(chalk.green(`✓ ${Object.keys(variables).length} variable(s) chargée(s)`));
|
|
33
|
-
console.log(chalk.dim(`Exécution: ${command.join(" ")}\n`));
|
|
34
|
-
|
|
35
|
-
// Spawn the command with injected env vars
|
|
36
|
-
const [cmd, ...args] = command;
|
|
37
|
-
const child = spawn(cmd, args, {
|
|
38
|
-
env: { ...process.env, ...variables },
|
|
39
|
-
stdio: "inherit",
|
|
40
|
-
shell: true,
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
child.on("close", (code) => {
|
|
44
|
-
process.exit(code ?? 0);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
child.on("error", (err) => {
|
|
48
|
-
console.error(chalk.red(`Erreur d'exécution: ${err.message}`));
|
|
49
|
-
process.exit(1);
|
|
50
|
-
});
|
|
51
|
-
} catch (error: any) {
|
|
52
|
-
console.log(chalk.red(`✗ Erreur: ${error.message}`));
|
|
53
|
-
process.exit(1);
|
|
54
|
-
}
|
|
55
|
-
}
|
package/src/commands/whoami.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import { config } from "../index.js";
|
|
3
|
-
import { apiRequest } from "../utils/api.js";
|
|
4
|
-
|
|
5
|
-
export async function whoami() {
|
|
6
|
-
const token = config.get("token") as string;
|
|
7
|
-
const apiUrl = config.get("apiUrl") as string;
|
|
8
|
-
|
|
9
|
-
if (!token) {
|
|
10
|
-
console.log(chalk.red("✗ Non connecté. Utilisez 'envsafe login' d'abord."));
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
const user = await apiRequest("/api/v1/user/me", {
|
|
16
|
-
method: "GET",
|
|
17
|
-
token
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
console.log(chalk.cyan("\n🔐 EnvSafe CLI\n"));
|
|
21
|
-
console.log(` Utilisateur: ${chalk.white(user.name)} (${chalk.dim(user.email)})`);
|
|
22
|
-
|
|
23
|
-
if (user.lastLoginAt) {
|
|
24
|
-
const date = new Date(user.lastLoginAt).toLocaleString();
|
|
25
|
-
console.log(` Dernière connexion: ${chalk.dim(date)}`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
console.log(` API: ${chalk.dim(apiUrl)}`);
|
|
29
|
-
console.log(` Token: ${chalk.dim(token.substring(0, 12) + "...")}\n`);
|
|
30
|
-
} catch (error: any) {
|
|
31
|
-
console.log(chalk.red(`✗ Impossible de récupérer les infos utilisateur: ${error.message}`));
|
|
32
|
-
}
|
|
33
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Command } from "commander";
|
|
4
|
-
import chalk from "chalk";
|
|
5
|
-
import Conf from "conf";
|
|
6
|
-
import { login } from "./commands/login.js";
|
|
7
|
-
import { pull } from "./commands/pull.js";
|
|
8
|
-
import { push } from "./commands/push.js";
|
|
9
|
-
import { run } from "./commands/run.js";
|
|
10
|
-
import { projects } from "./commands/projects.js";
|
|
11
|
-
import { whoami } from "./commands/whoami.js";
|
|
12
|
-
import pkg from "../package.json";
|
|
13
|
-
|
|
14
|
-
// Config store for token
|
|
15
|
-
export const config = new Conf({
|
|
16
|
-
projectName: "envsafe-cli",
|
|
17
|
-
schema: {
|
|
18
|
-
token: { type: "string", default: "" },
|
|
19
|
-
apiUrl: { type: "string", default: "https://envsafe.vercel.app" },
|
|
20
|
-
dashboardUrl: { type: "string", default: "https://envsafe.vercel.app" },
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const program = new Command();
|
|
25
|
-
|
|
26
|
-
program
|
|
27
|
-
.name("envsafe")
|
|
28
|
-
.description(chalk.cyan("🔐 EnvSafe CLI - Gestionnaire sécurisé de variables d'environnement"))
|
|
29
|
-
.version(pkg.version);
|
|
30
|
-
|
|
31
|
-
// Login command
|
|
32
|
-
program
|
|
33
|
-
.command("login")
|
|
34
|
-
.description("Se connecter avec un token API")
|
|
35
|
-
.option("-t, --token <token>", "Token API")
|
|
36
|
-
.action(login);
|
|
37
|
-
|
|
38
|
-
// Whoami command
|
|
39
|
-
program
|
|
40
|
-
.command("whoami")
|
|
41
|
-
.description("Afficher l'utilisateur connecté")
|
|
42
|
-
.action(whoami);
|
|
43
|
-
|
|
44
|
-
// Projects command
|
|
45
|
-
program
|
|
46
|
-
.command("projects")
|
|
47
|
-
.alias("ls")
|
|
48
|
-
.description("Lister les projets accessibles")
|
|
49
|
-
.action(projects);
|
|
50
|
-
|
|
51
|
-
// Pull command
|
|
52
|
-
program
|
|
53
|
-
.command("pull")
|
|
54
|
-
.description("Télécharger les variables d'environnement")
|
|
55
|
-
.argument("<project>", "Slug du projet")
|
|
56
|
-
.option("-e, --env <environment>", "Environnement (development, staging, production)", "development")
|
|
57
|
-
.option("-o, --output <file>", "Fichier de sortie", ".env")
|
|
58
|
-
.action(pull);
|
|
59
|
-
|
|
60
|
-
// Push command
|
|
61
|
-
program
|
|
62
|
-
.command("push")
|
|
63
|
-
.description("Envoyer les variables d'environnement")
|
|
64
|
-
.argument("<project>", "Slug du projet")
|
|
65
|
-
.option("-e, --env <environment>", "Environnement", "development")
|
|
66
|
-
.option("-f, --file <file>", "Fichier source", ".env")
|
|
67
|
-
.action(push);
|
|
68
|
-
|
|
69
|
-
// Run command
|
|
70
|
-
program
|
|
71
|
-
.command("run")
|
|
72
|
-
.description("Exécuter une commande avec les variables d'environnement injectées")
|
|
73
|
-
.argument("<project>", "Slug du projet")
|
|
74
|
-
.option("-e, --env <environment>", "Environnement", "development")
|
|
75
|
-
.argument("<command...>", "Commande à exécuter")
|
|
76
|
-
.action(run);
|
|
77
|
-
|
|
78
|
-
// Config command
|
|
79
|
-
program
|
|
80
|
-
.command("config")
|
|
81
|
-
.description("Gérer la configuration")
|
|
82
|
-
.option("--api-url <url>", "Définir l'URL de l'API EnvSafe")
|
|
83
|
-
.option("--show", "Afficher la configuration actuelle")
|
|
84
|
-
.action((options) => {
|
|
85
|
-
if (options.apiUrl) {
|
|
86
|
-
config.set("apiUrl", options.apiUrl);
|
|
87
|
-
console.log(chalk.green(`✓ URL de l'API définie: ${options.apiUrl}`));
|
|
88
|
-
}
|
|
89
|
-
if (options.show) {
|
|
90
|
-
console.log(chalk.cyan("Configuration actuelle:"));
|
|
91
|
-
console.log(` API URL: ${config.get("apiUrl")}`);
|
|
92
|
-
console.log(` Token: ${config.get("token") ? "••••••••" : "(non défini)"}`);
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// Logout command
|
|
97
|
-
program
|
|
98
|
-
.command("logout")
|
|
99
|
-
.description("Se déconnecter")
|
|
100
|
-
.action(() => {
|
|
101
|
-
config.set("token", "");
|
|
102
|
-
console.log(chalk.green("✓ Déconnexion réussie"));
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
program.parse();
|
package/src/utils/api.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { config } from "../index.js";
|
|
2
|
-
|
|
3
|
-
interface ApiRequestOptions {
|
|
4
|
-
method: "GET" | "POST" | "PUT" | "DELETE";
|
|
5
|
-
token?: string;
|
|
6
|
-
body?: any;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export async function apiRequest(endpoint: string, options: ApiRequestOptions) {
|
|
10
|
-
const apiUrl = config.get("apiUrl") as string;
|
|
11
|
-
const token = options.token || (config.get("token") as string);
|
|
12
|
-
|
|
13
|
-
const url = `${apiUrl}${endpoint}`;
|
|
14
|
-
|
|
15
|
-
const headers: Record<string, string> = {
|
|
16
|
-
"Content-Type": "application/json",
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
if (token) {
|
|
20
|
-
headers["Authorization"] = `Bearer ${token}`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const response = await fetch(url, {
|
|
24
|
-
method: options.method,
|
|
25
|
-
headers,
|
|
26
|
-
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
const data = await response.json();
|
|
30
|
-
|
|
31
|
-
if (!response.ok && !data.error) {
|
|
32
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return data;
|
|
36
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"outDir": "./dist",
|
|
7
|
-
"rootDir": "./src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true,
|
|
12
|
-
"declaration": true,
|
|
13
|
-
"resolveJsonModule": true
|
|
14
|
-
},
|
|
15
|
-
"include": [
|
|
16
|
-
"src/**/*"
|
|
17
|
-
],
|
|
18
|
-
"exclude": [
|
|
19
|
-
"node_modules",
|
|
20
|
-
"dist"
|
|
21
|
-
]
|
|
22
|
-
}
|