@elding/cli 0.8.0 → 0.9.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.
@@ -35,28 +35,28 @@ async function init() {
35
35
  console.log(chalk.yellow("Aucun set trouvé. Créez-en un sur le vault."));
36
36
  process.exit(0);
37
37
  }
38
- // Étape 1 : choisir l'organisation (sauf s'il n'y en a qu'une)
38
+ // Étape 1 : choisir l'organisation (toujours, pour une approche projet cohérente)
39
39
  const orgs = [...new Map(sets.map((s) => [s.workspaceId, s.workspaceName])).entries()];
40
- let workspaceId = orgs[0][0];
41
- if (orgs.length > 1) {
42
- const ans = await inquirer.prompt([
43
- {
44
- type: "list",
45
- name: "workspaceId",
46
- message: "Quelle organisation ?",
47
- choices: orgs.map(([id, name]) => ({ name: (0, terminal_js_1.safeText)(name), value: id })),
48
- },
49
- ]);
50
- workspaceId = ans.workspaceId;
51
- }
52
- // Étape 2 : choisir le set parmi ceux de l'org
40
+ const { workspaceId } = await inquirer.prompt([
41
+ {
42
+ type: "select",
43
+ name: "workspaceId",
44
+ message: "Quelle organisation ?",
45
+ choices: orgs.map(([id, name]) => ({ name: (0, terminal_js_1.safeText)(name), value: id })),
46
+ },
47
+ ]);
48
+ // Étape 2 : choisir le set parmi ceux de l'org (env affiché en français)
49
+ const ENV_LABELS = { DEV: "Dev", STAGING: "Staging", PROD: "Prod" };
53
50
  const orgSets = sets.filter((s) => s.workspaceId === workspaceId);
54
51
  const { setId } = await inquirer.prompt([
55
52
  {
56
- type: "list",
53
+ type: "select",
57
54
  name: "setId",
58
55
  message: "Quel set utiliser pour ce projet ?",
59
- choices: orgSets.map((s) => ({ name: (0, terminal_js_1.safeText)(s.name), value: s.id })),
56
+ choices: orgSets.map((s) => {
57
+ const env = s.environment ? ENV_LABELS[s.environment] : undefined;
58
+ return { name: env ? `${(0, terminal_js_1.safeText)(s.name)} (${env})` : (0, terminal_js_1.safeText)(s.name), value: s.id };
59
+ }),
60
60
  },
61
61
  ]);
62
62
  const selected = orgSets.find((s) => s.id === setId);
@@ -69,5 +69,4 @@ async function init() {
69
69
  (0, config_js_1.writeProject)(project);
70
70
  (0, config_js_1.trustProject)(project);
71
71
  console.log(chalk.green(`✓ Set "${(0, terminal_js_1.safeText)(selected.name)}" (${(0, terminal_js_1.safeText)(selected.workspaceName)}) configuré dans .elding.json`));
72
- console.log(chalk.dim("Ajoutez .elding.json à votre .gitignore si besoin."));
73
72
  }
@@ -42,8 +42,19 @@ async function login() {
42
42
  "Referrer-Policy": "no-referrer",
43
43
  "Cache-Control": "no-store",
44
44
  });
45
- res.end(`<!DOCTYPE html><html><head><meta charset="utf-8"><script>history.replaceState(null,"","/")</script></head><body style="font-family:sans-serif;padding:2rem">
46
- <p><strong>Authentification réussie.</strong> Vous pouvez fermer cet onglet.</p>
45
+ res.end(`<!DOCTYPE html><html lang="fr"><head><meta charset="utf-8"><title>Elding</title><script>history.replaceState(null,"","/")</script><style>
46
+ *{margin:0;padding:0;box-sizing:border-box}
47
+ html,body{height:100%}
48
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#fff;color:#000;display:flex;align-items:center;justify-content:center;text-align:center}
49
+ .wrap{display:flex;flex-direction:column;align-items:center;gap:.75rem;padding:2rem}
50
+ h1{font-size:2.25rem;font-weight:600;letter-spacing:-.02em}
51
+ p{font-size:1rem;color:#6b7280}
52
+ </style></head><body>
53
+ <div class="wrap">
54
+ <h1>Tout est prêt, codez l'esprit tranquille</h1>
55
+ <p>Vous êtes connecté à Elding.</p>
56
+ <p>Vous pouvez maintenant fermer cette fenêtre.</p>
57
+ </div>
47
58
  </body></html>`);
48
59
  clearTimeout(timeout);
49
60
  server.close();
@@ -43,10 +43,14 @@ async function proxy(cmd, args, options = {}) {
43
43
  spinner.succeed(chalk.green(`Proxy actif sur ${server.url} — ${Object.keys(secrets).length} secret(s) pour ${(0, terminal_js_1.safeText)(project.setName)}`));
44
44
  console.log(chalk.dim("Les clés restent dans le proxy, jamais dans la mémoire de l'app."));
45
45
  if (!options.reportLogs)
46
- console.log(chalk.dim("Logs cloud proxy désactivés. Ajoutez --report-logs pour les envoyer."));
46
+ console.log(chalk.dim("Logs cloud proxy désactivés (--no-report-logs)."));
47
+ else
48
+ console.log(chalk.dim("Métadonnées de requêtes envoyées au vault (jamais les valeurs). Couper avec --no-report-logs."));
47
49
  const child = (0, child_process_1.spawn)(cmd, args, {
48
50
  env: {
49
51
  ...process.env,
52
+ // Résout les binaires locaux (next, vite...) comme le ferait npm.
53
+ PATH: `${process.cwd()}/node_modules/.bin:${process.env.PATH ?? ""}`,
50
54
  ELDING_PROXY_URL: server.url,
51
55
  ELDING_PROXY_TOKEN: server.token,
52
56
  },
@@ -35,11 +35,18 @@ async function run(cmd, args, options = {}) {
35
35
  spinner.fail(chalk.red((0, terminal_js_1.safeError)(err)));
36
36
  process.exit(1);
37
37
  }
38
+ console.error(chalk.yellow("⚠ Mode run : les clés sont injectées en clair dans process.env (lisibles par votre IA et vos dépendances)."));
39
+ console.error(chalk.dim(" Pour que les clés n'entrent jamais dans votre app, préférez `elding proxy`."));
38
40
  const { env: safeSecrets, rejected } = (0, env_js_1.filterSecretsForEnv)(secrets);
39
41
  if (rejected.length > 0) {
40
42
  throw new Error(`Secrets refuses car leurs noms sont dangereux pour l'environnement: ${rejected.map((name) => (0, terminal_js_1.safeText)(name, 80)).join(", ")}`);
41
43
  }
42
- const env = { ...process.env, ...safeSecrets };
44
+ const env = {
45
+ ...process.env,
46
+ // Résout les binaires locaux (next, vite...) comme le ferait npm.
47
+ PATH: `${process.cwd()}/node_modules/.bin:${process.env.PATH ?? ""}`,
48
+ ...safeSecrets,
49
+ };
43
50
  const result = (0, child_process_1.spawnSync)(cmd, args, {
44
51
  env,
45
52
  stdio: "inherit",
package/dist/index.js CHANGED
@@ -18,7 +18,9 @@ const program = new commander_1.Command();
18
18
  program
19
19
  .name("elding")
20
20
  .description("Elding CLI — secrets depuis le vault, zéro .env")
21
- .version("0.3.0");
21
+ .version("0.8.2")
22
+ // Permet aux sous-commandes de passer les flags de la commande wrappée (next dev --turbopack...).
23
+ .enablePositionalOptions();
22
24
  program
23
25
  .command("login")
24
26
  .description("Authentifier le CLI via le navigateur")
@@ -41,6 +43,7 @@ program
41
43
  .command("run <cmd> [args...]")
42
44
  .description("Lancer une commande avec les secrets injectés en variables d'environnement")
43
45
  .option("--shell", "Executer via le shell systeme (a utiliser seulement si necessaire)")
46
+ .passThroughOptions()
44
47
  .allowUnknownOption()
45
48
  .action(async (cmd, args = [], options) => {
46
49
  await (0, run_js_1.run)(cmd, args, { shell: !!options.shell }).catch((err) => {
@@ -52,8 +55,9 @@ program
52
55
  .command("proxy <cmd> [args...]")
53
56
  .description("Lancer une commande derrière un proxy local qui injecte les clés — jamais en mémoire de l'app")
54
57
  .option("-v, --verbose", "Logger chaque requête (méthode/host/status/latence)")
55
- .option("--report-logs", "Envoyer les metadonnees de requetes proxy au vault")
58
+ .option("--no-report-logs", "Ne pas envoyer les metadonnees de requetes proxy au vault")
56
59
  .option("--shell", "Executer via le shell systeme (a utiliser seulement si necessaire)")
60
+ .passThroughOptions()
57
61
  .allowUnknownOption()
58
62
  .action(async (cmd, args = [], options) => {
59
63
  await (0, proxy_js_1.proxy)(cmd, args, {
package/dist/lib/api.d.ts CHANGED
@@ -2,6 +2,7 @@ export declare const BASE_URL: string;
2
2
  export type ApiKeySetItem = {
3
3
  id: string;
4
4
  name: string;
5
+ environment?: string;
5
6
  workspaceId: string;
6
7
  workspaceName: string;
7
8
  };
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.resolveBaseUrl = resolveBaseUrl;
7
7
  const net_1 = __importDefault(require("net"));
8
- const DEFAULT_BASE_URL = "https://app.elding.io";
8
+ const DEFAULT_BASE_URL = "https://elding-dev.vercel.app";
9
9
  function stripBrackets(hostname) {
10
10
  return hostname.startsWith("[") && hostname.endsWith("]")
11
11
  ? hostname.slice(1, -1)
@@ -34,7 +34,7 @@ function resolveBaseUrl(raw = process.env.ELDING_API_URL) {
34
34
  throw new Error("ELDING_API_URL ne doit pas contenir d'identifiants.");
35
35
  }
36
36
  if (url.pathname !== "/" || url.search || url.hash) {
37
- throw new Error("ELDING_API_URL doit etre une origine seule, par exemple https://app.elding.io.");
37
+ throw new Error("ELDING_API_URL doit etre une origine seule, par exemple https://elding-dev.vercel.app.");
38
38
  }
39
39
  if (url.protocol === "https:")
40
40
  return url.origin;
@@ -7,8 +7,11 @@ exports.startProxy = startProxy;
7
7
  const http_1 = __importDefault(require("http"));
8
8
  const https_1 = __importDefault(require("https"));
9
9
  const net_1 = __importDefault(require("net"));
10
+ const stream_1 = require("stream");
10
11
  const dns_1 = require("dns");
11
12
  const crypto_1 = require("crypto");
13
+ const zlib_1 = require("zlib");
14
+ const redact_js_1 = require("./redact.js");
12
15
  const PLACEHOLDER = /\{\{([A-Z0-9_]+)\}\}/g;
13
16
  const PROXY_TIMEOUT_MS = 60_000;
14
17
  const MAX_HOST_LENGTH = 253;
@@ -152,6 +155,8 @@ function startProxy(secrets, hosts = {}, verbose = false, onLog) {
152
155
  return null;
153
156
  };
154
157
  const substitute = (val) => val.replace(PLACEHOLDER, (_, name) => secrets[name] ?? "");
158
+ // Valeurs de secret a effacer des reponses (anti-echo).
159
+ const secretValues = Object.values(secrets).filter((v) => typeof v === "string" && v.length >= 8);
155
160
  const server = http_1.default.createServer(async (req, res) => {
156
161
  const started = Date.now();
157
162
  try {
@@ -218,6 +223,8 @@ function startProxy(secrets, hosts = {}, verbose = false, onLog) {
218
223
  continue;
219
224
  if (HOP_BY_HOP_HEADERS.has(lk))
220
225
  continue;
226
+ if (lk === "accept-encoding")
227
+ continue; // force identite : reponse en clair pour la redaction
221
228
  if (typeof v === "string") {
222
229
  rawHeaders[k] = v;
223
230
  headers[k] = substitute(v);
@@ -227,10 +234,13 @@ function startProxy(secrets, hosts = {}, verbose = false, onLog) {
227
234
  headers[k] = v.map(substitute);
228
235
  }
229
236
  }
237
+ // Empeche l'upstream de compresser : sinon impossible de rediger les secrets reflechis.
238
+ if (secretValues.length > 0)
239
+ headers["accept-encoding"] = "identity";
230
240
  // Streame le corps vers l'upstream sans le bufferiser en mémoire (évite un DoS sur gros upload)
231
241
  const hasBody = req.method !== "GET" && req.method !== "HEAD";
232
242
  const used = placeholdersIn(rawHeaders);
233
- const status = await forwardHttps(req, res, upstream, headers, pinned, hasBody);
243
+ const status = await forwardHttps(req, res, upstream, headers, pinned, hasBody, secretValues);
234
244
  log(req.method ?? "?", targetHost, upstream.pathname, status, Date.now() - started, used, used);
235
245
  }
236
246
  catch {
@@ -261,16 +271,53 @@ function normalizeAllowedHost(value) {
261
271
  return null;
262
272
  }
263
273
  }
264
- function filterResponseHeaders(headers) {
274
+ function filterResponseHeaders(headers, secretValues, decodedBody) {
265
275
  const out = {};
276
+ const redacting = secretValues.length > 0;
266
277
  for (const [name, value] of Object.entries(headers)) {
267
- if (HOP_BY_HOP_HEADERS.has(name.toLowerCase()))
278
+ const lower = name.toLowerCase();
279
+ if (HOP_BY_HOP_HEADERS.has(lower))
268
280
  continue;
269
- out[name] = value;
281
+ if (decodedBody && lower === "content-encoding")
282
+ continue;
283
+ if ((redacting || decodedBody) && (lower === "content-md5" || lower === "digest" || lower === "etag"))
284
+ continue;
285
+ // La redaction modifie la taille du corps : on laisse Node re-calculer (chunked).
286
+ if (redacting && lower === "content-length")
287
+ continue;
288
+ if (typeof value === "string")
289
+ out[name] = (0, redact_js_1.redactString)(value, secretValues);
290
+ else if (Array.isArray(value))
291
+ out[name] = value.map((v) => (0, redact_js_1.redactString)(v, secretValues));
292
+ else
293
+ out[name] = value;
270
294
  }
271
295
  return out;
272
296
  }
273
- function forwardHttps(req, res, upstream, headers, pinned, hasBody) {
297
+ function responseBodyForRedaction(upstreamRes, shouldRedact) {
298
+ const raw = upstreamRes.headers["content-encoding"];
299
+ const encodings = (Array.isArray(raw) ? raw.join(",") : raw ?? "")
300
+ .toLowerCase()
301
+ .split(",")
302
+ .map((v) => v.trim())
303
+ .filter((v) => v && v !== "identity");
304
+ if (!shouldRedact || encodings.length === 0)
305
+ return { stream: upstreamRes, decoded: false };
306
+ if (encodings.length !== 1)
307
+ return null;
308
+ switch (encodings[0]) {
309
+ case "gzip":
310
+ case "x-gzip":
311
+ return { stream: upstreamRes.pipe((0, zlib_1.createGunzip)()), decoded: true };
312
+ case "deflate":
313
+ return { stream: upstreamRes.pipe((0, zlib_1.createInflate)()), decoded: true };
314
+ case "br":
315
+ return { stream: upstreamRes.pipe((0, zlib_1.createBrotliDecompress)()), decoded: true };
316
+ default:
317
+ return null;
318
+ }
319
+ }
320
+ function forwardHttps(req, res, upstream, headers, pinned, hasBody, secretValues) {
274
321
  return new Promise((resolve, reject) => {
275
322
  const upstreamHostname = normalizeHostname(upstream.hostname);
276
323
  const upstreamReq = https_1.default.request({
@@ -281,16 +328,36 @@ function forwardHttps(req, res, upstream, headers, pinned, hasBody) {
281
328
  method: req.method,
282
329
  headers: { ...headers, host: upstream.host },
283
330
  servername: net_1.default.isIP(upstreamHostname) ? undefined : upstreamHostname,
331
+ // Connexion fraiche par requete : evite qu'un socket pinne reutilise
332
+ // soit rejete par le CDN de la cible (faux 401/erreurs intermittentes).
333
+ agent: false,
284
334
  timeout: PROXY_TIMEOUT_MS,
285
- lookup: (_hostname, _options, callback) => {
286
- callback(null, pinned.address, pinned.family);
335
+ // Node >=20 (autoSelectFamily) appelle lookup avec { all: true } et
336
+ // attend un tableau ; sinon la forme simple (address, family).
337
+ lookup: (_hostname, options, callback) => {
338
+ if (options && options.all)
339
+ callback(null, [{ address: pinned.address, family: pinned.family }]);
340
+ else
341
+ callback(null, pinned.address, pinned.family);
287
342
  },
288
343
  }, (upstreamRes) => {
289
344
  const status = upstreamRes.statusCode ?? 502;
290
- res.writeHead(status, filterResponseHeaders(upstreamRes.headers));
291
- upstreamRes.pipe(res);
292
- upstreamRes.on("end", () => resolve(status));
293
- upstreamRes.on("error", reject);
345
+ const body = responseBodyForRedaction(upstreamRes, secretValues.length > 0);
346
+ if (!body) {
347
+ upstreamRes.resume();
348
+ res.writeHead(502);
349
+ res.end("unsupported compressed response");
350
+ resolve(502);
351
+ return;
352
+ }
353
+ res.writeHead(status, filterResponseHeaders(upstreamRes.headers, secretValues, body.decoded));
354
+ const redactor = (0, redact_js_1.createRedactor)(secretValues);
355
+ (0, stream_1.pipeline)(body.stream, redactor, res, (err) => {
356
+ if (err)
357
+ reject(err);
358
+ else
359
+ resolve(status);
360
+ });
294
361
  });
295
362
  upstreamReq.on("timeout", () => upstreamReq.destroy(new Error("upstream timeout")));
296
363
  upstreamReq.on("error", reject);
@@ -0,0 +1,3 @@
1
+ import { Transform } from "stream";
2
+ export declare function createRedactor(secretValues: string[]): Transform;
3
+ export declare function redactString(value: string, secretValues: string[]): string;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createRedactor = createRedactor;
4
+ exports.redactString = redactString;
5
+ const stream_1 = require("stream");
6
+ const REDACTED = Buffer.from("[REDACTED]");
7
+ // Remplace toutes les occurrences de `search` dans `buf` (recherche binaire, ne
8
+ // corrompt pas les corps non-texte : sans correspondance, passthrough inchange).
9
+ function replaceAll(buf, search) {
10
+ const parts = [];
11
+ let idx = 0;
12
+ let found = buf.indexOf(search, idx);
13
+ if (found === -1)
14
+ return buf;
15
+ while (found !== -1) {
16
+ parts.push(buf.subarray(idx, found), REDACTED);
17
+ idx = found + search.length;
18
+ found = buf.indexOf(search, idx);
19
+ }
20
+ parts.push(buf.subarray(idx));
21
+ return Buffer.concat(parts);
22
+ }
23
+ // Transform qui efface les valeurs de secret du flux de reponse avant qu'il
24
+ // atteigne l'app. Tue le cas "domaine qui echo la cle". Gere les coupures de
25
+ // chunk en retenant les (maxLen-1) derniers octets entre deux passes.
26
+ function createRedactor(secretValues) {
27
+ const values = [...new Set(secretValues)]
28
+ .filter((v) => v.length >= 8)
29
+ .map((v) => Buffer.from(v, "utf8"));
30
+ const maxLen = values.reduce((m, v) => Math.max(m, v.length), 0);
31
+ let carry = Buffer.alloc(0);
32
+ const scrub = (b) => {
33
+ for (const v of values)
34
+ b = replaceAll(b, v);
35
+ return b;
36
+ };
37
+ return new stream_1.Transform({
38
+ transform(chunk, _enc, cb) {
39
+ if (values.length === 0)
40
+ return cb(null, chunk);
41
+ const buf = scrub(Buffer.concat([carry, chunk]));
42
+ const hold = Math.min(buf.length, maxLen - 1);
43
+ carry = Buffer.from(buf.subarray(buf.length - hold));
44
+ cb(null, buf.subarray(0, buf.length - hold));
45
+ },
46
+ flush(cb) {
47
+ cb(null, carry);
48
+ },
49
+ });
50
+ }
51
+ // Rediger aussi les valeurs de secret dans une chaine (entetes de reponse).
52
+ function redactString(value, secretValues) {
53
+ let out = value;
54
+ for (const v of secretValues)
55
+ if (v.length >= 8)
56
+ out = out.split(v).join("[REDACTED]");
57
+ return out;
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elding/cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Elding CLI — zero .env, secrets from vault",
5
5
  "bin": {
6
6
  "elding": "dist/index.js"