@elding/cli 0.3.0 → 0.8.1

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.
@@ -5,59 +5,162 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.startProxy = startProxy;
7
7
  const http_1 = __importDefault(require("http"));
8
+ const https_1 = __importDefault(require("https"));
9
+ const net_1 = __importDefault(require("net"));
10
+ const stream_1 = require("stream");
11
+ const dns_1 = require("dns");
8
12
  const crypto_1 = require("crypto");
13
+ const zlib_1 = require("zlib");
14
+ const redact_js_1 = require("./redact.js");
9
15
  const PLACEHOLDER = /\{\{([A-Z0-9_]+)\}\}/g;
10
- // Bloque loopback + plages privées + métadata cloud — empêche le proxy de servir de pivot SSRF
11
- function isBlockedHost(hostname) {
12
- const h = hostname.replace(/^\[|\]$/g, "");
13
- if (h === "localhost" || h === "::1")
14
- return true;
15
- if (/^127\./.test(h))
16
- return true;
17
- if (/^10\./.test(h))
18
- return true;
19
- if (/^192\.168\./.test(h))
20
- return true;
21
- if (/^172\.(1[6-9]|2\d|3[01])\./.test(h))
22
- return true;
23
- if (/^169\.254\./.test(h))
24
- return true; // link-local + metadata (169.254.169.254)
25
- if (/^fe80:/i.test(h) || /^fc00:/i.test(h) || /^fd/i.test(h))
16
+ const PROXY_TIMEOUT_MS = 60_000;
17
+ const MAX_HOST_LENGTH = 253;
18
+ const HOP_BY_HOP_HEADERS = new Set([
19
+ "connection",
20
+ "content-length",
21
+ "keep-alive",
22
+ "proxy-authenticate",
23
+ "proxy-authorization",
24
+ "proxy-connection",
25
+ "te",
26
+ "trailer",
27
+ "transfer-encoding",
28
+ "upgrade",
29
+ ]);
30
+ // Valide une IP résolue : bloque loopback + plages privées + link-local + ULA + métadata cloud.
31
+ function isBlockedIp(ip) {
32
+ const normalized = stripBrackets(ip).toLowerCase();
33
+ const v4 = ipv4FromMappedIpv6(normalized) ?? normalized;
34
+ if (net_1.default.isIPv4(v4)) {
35
+ const [a, b, c] = v4.split(".").map(Number);
36
+ if (a === 0 || a === 10 || a === 127)
37
+ return true;
38
+ if (a === 100 && b >= 64 && b <= 127)
39
+ return true; // carrier-grade NAT
40
+ if (a === 192 && b === 168)
41
+ return true;
42
+ if (a === 192 && b === 0)
43
+ return true;
44
+ if (a === 172 && b >= 16 && b <= 31)
45
+ return true;
46
+ if (a === 169 && b === 254)
47
+ return true; // link-local + metadata 169.254.169.254
48
+ if (a === 198 && (b === 18 || b === 19))
49
+ return true; // benchmarking
50
+ if (a === 192 && b === 0 && c === 2)
51
+ return true;
52
+ if (a === 198 && b === 51 && c === 100)
53
+ return true;
54
+ if (a === 203 && b === 0 && c === 113)
55
+ return true;
56
+ if (a >= 224)
57
+ return true; // multicast + reserved
58
+ return false;
59
+ }
60
+ const l = normalized;
61
+ if (l === "::1" || l === "::")
26
62
  return true;
63
+ if (l.startsWith("fe8") || l.startsWith("fe9") || l.startsWith("fea") || l.startsWith("feb"))
64
+ return true; // fe80::/10
65
+ if (l.startsWith("fc") || l.startsWith("fd"))
66
+ return true; // ULA
67
+ if (l.startsWith("ff"))
68
+ return true; // multicast
69
+ if (l.startsWith("2001:db8"))
70
+ return true; // documentation
27
71
  return false;
28
72
  }
73
+ function ipv4FromMappedIpv6(ip) {
74
+ if (!ip.startsWith("::ffff:"))
75
+ return null;
76
+ const tail = ip.slice("::ffff:".length);
77
+ if (net_1.default.isIPv4(tail))
78
+ return tail;
79
+ const parts = tail.split(":");
80
+ if (parts.length !== 2)
81
+ return null;
82
+ const high = Number.parseInt(parts[0], 16);
83
+ const low = Number.parseInt(parts[1], 16);
84
+ if (!Number.isInteger(high) || !Number.isInteger(low) || high < 0 || high > 0xffff || low < 0 || low > 0xffff) {
85
+ return null;
86
+ }
87
+ return `${(high >> 8) & 255}.${high & 255}.${(low >> 8) & 255}.${low & 255}`;
88
+ }
89
+ function stripBrackets(hostname) {
90
+ return hostname.startsWith("[") && hostname.endsWith("]")
91
+ ? hostname.slice(1, -1)
92
+ : hostname;
93
+ }
94
+ function normalizeHostname(hostname) {
95
+ return stripBrackets(hostname).toLowerCase().replace(/\.$/, "");
96
+ }
97
+ // Résout le hostname et exige que TOUTES les adresses soient publiques.
98
+ async function resolvePublicAddresses(hostname) {
99
+ const normalized = normalizeHostname(hostname);
100
+ if (!normalized || normalized.length > MAX_HOST_LENGTH || normalized === "localhost")
101
+ return [];
102
+ const directIp = net_1.default.isIP(normalized);
103
+ if (directIp)
104
+ return isBlockedIp(normalized) ? [] : [{ address: normalized, family: directIp }];
105
+ try {
106
+ const addrs = await dns_1.promises.lookup(normalized, { all: true, verbatim: true });
107
+ if (addrs.length === 0 || addrs.some((a) => isBlockedIp(a.address)))
108
+ return [];
109
+ return addrs.map((a) => ({ address: a.address, family: a.family }));
110
+ }
111
+ catch {
112
+ return [];
113
+ }
114
+ }
115
+ // Comparaison constant-time du token de session.
116
+ function tokenMatches(provided, expected) {
117
+ if (typeof provided !== "string" || provided.length !== expected.length)
118
+ return false;
119
+ return (0, crypto_1.timingSafeEqual)(Buffer.from(provided), Buffer.from(expected));
120
+ }
29
121
  // hosts: map nom_secret -> domaine autorisé. Si défini, le secret ne peut être envoyé qu'à ce host.
30
- function startProxy(secrets, hosts = {}, verbose = false) {
122
+ function startProxy(secrets, hosts = {}, verbose = false, onLog) {
31
123
  const token = (0, crypto_1.randomBytes)(24).toString("hex");
32
124
  // Log une ligne par requête — jamais les valeurs, seulement les placeholders référencés
33
- const log = (method, host, path, status, ms, note = "") => {
34
- if (!verbose)
35
- return;
36
- const code = status === "BLOCKED" ? "BLOCKED" : String(status);
37
- console.error(`[elding] ${method} ${host}${path} → ${code} ${ms}ms${note ? " " + note : ""}`);
125
+ const log = (method, host, path, status, ms, note = "", secretNames = "") => {
126
+ const blocked = status === "BLOCKED";
127
+ const code = blocked ? "BLOCKED" : String(status);
128
+ if (verbose)
129
+ console.error(`[elding] ${method} ${host}${path} → ${code} ${ms}ms${note ? " " + note : ""}`);
130
+ onLog?.({ method, host, path, status: blocked ? 403 : status, latencyMs: ms, blocked, secretNames });
38
131
  };
39
132
  const placeholdersIn = (headers) => {
40
133
  const names = new Set();
41
- for (const v of Object.values(headers))
42
- for (const m of v.matchAll(PLACEHOLDER))
43
- names.add(m[1]);
134
+ for (const v of Object.values(headers)) {
135
+ const values = Array.isArray(v) ? v : [v];
136
+ for (const item of values)
137
+ for (const m of item.matchAll(PLACEHOLDER))
138
+ names.add(m[1]);
139
+ }
44
140
  return names.size ? `{{${[...names].join(",")}}}` : "";
45
141
  };
46
- // Vérifie que chaque placeholder utilisé est autorisé pour ce host. Retourne le nom fautif, ou null.
142
+ // Vérifie que chaque placeholder utilisé est autorisé pour ce host. Retourne une raison de blocage, ou null.
47
143
  const findHostViolation = (val, targetHost) => {
48
144
  for (const m of val.matchAll(PLACEHOLDER)) {
49
145
  const name = m[1];
146
+ if (!Object.prototype.hasOwnProperty.call(secrets, name))
147
+ return `${name} inconnu`;
50
148
  const allowed = hosts[name];
51
- if (allowed && allowed !== targetHost)
52
- return name;
149
+ if (!allowed)
150
+ return `${name} sans domaine autorise`;
151
+ const allowedHost = normalizeAllowedHost(allowed);
152
+ if (!allowedHost || allowedHost !== targetHost)
153
+ return `${name} non autorise`;
53
154
  }
54
155
  return null;
55
156
  };
56
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);
57
160
  const server = http_1.default.createServer(async (req, res) => {
58
161
  const started = Date.now();
59
162
  try {
60
- if (req.headers["x-elding-token"] !== token) {
163
+ if (!tokenMatches(req.headers["x-elding-token"], token)) {
61
164
  res.writeHead(401);
62
165
  res.end("unauthorized");
63
166
  return;
@@ -82,24 +185,33 @@ function startProxy(secrets, hosts = {}, verbose = false) {
82
185
  res.end("https only");
83
186
  return;
84
187
  }
85
- if (isBlockedHost(targetUrl.hostname)) {
86
- log(req.method ?? "?", targetUrl.hostname, req.url ?? "/", "BLOCKED", Date.now() - started, "host bloqué");
188
+ if (targetUrl.username || targetUrl.password) {
189
+ res.writeHead(400);
190
+ res.end("credentials not allowed");
191
+ return;
192
+ }
193
+ const targetHost = normalizeHostname(targetUrl.hostname);
194
+ const resolved = await resolvePublicAddresses(targetHost);
195
+ if (resolved.length === 0) {
196
+ log(req.method ?? "?", targetHost, req.url ?? "/", "BLOCKED", Date.now() - started, "host bloqué");
87
197
  res.writeHead(403);
88
198
  res.end("blocked host");
89
199
  return;
90
200
  }
201
+ const pinned = resolved[0];
91
202
  const upstream = new URL(req.url ?? "/", targetUrl);
92
203
  upstream.protocol = "https:";
93
204
  upstream.host = targetUrl.host;
94
205
  // Enforce host binding : un secret lié à un domaine ne peut partir que vers celui-ci
95
206
  for (const v of Object.values(req.headers)) {
96
- if (typeof v !== "string")
97
- continue;
98
- const bad = findHostViolation(v, targetUrl.hostname);
99
- if (bad) {
100
- log(req.method ?? "?", targetUrl.hostname, upstream.pathname, "BLOCKED", Date.now() - started, `${bad} non autorisé`);
207
+ const values = Array.isArray(v) ? v : typeof v === "string" ? [v] : [];
208
+ for (const item of values) {
209
+ const bad = findHostViolation(item, targetHost);
210
+ if (!bad)
211
+ continue;
212
+ log(req.method ?? "?", targetHost, upstream.pathname, "BLOCKED", Date.now() - started, bad);
101
213
  res.writeHead(403);
102
- res.end(`secret "${bad}" non autorisé pour ${targetUrl.hostname}`);
214
+ res.end(`secret bloque: ${bad}`);
103
215
  return;
104
216
  }
105
217
  }
@@ -109,34 +221,27 @@ function startProxy(secrets, hosts = {}, verbose = false) {
109
221
  const lk = k.toLowerCase();
110
222
  if (lk.startsWith("x-elding-"))
111
223
  continue;
112
- if (lk === "host" || lk === "connection" || lk === "content-length")
224
+ if (HOP_BY_HOP_HEADERS.has(lk))
113
225
  continue;
226
+ if (lk === "accept-encoding")
227
+ continue; // force identite : reponse en clair pour la redaction
114
228
  if (typeof v === "string") {
115
229
  rawHeaders[k] = v;
116
230
  headers[k] = substitute(v);
117
231
  }
118
- }
119
- const chunks = [];
120
- for await (const c of req)
121
- chunks.push(c);
122
- const hasBody = chunks.length > 0 && req.method !== "GET" && req.method !== "HEAD";
123
- const upstreamRes = await fetch(upstream.toString(), {
124
- method: req.method,
125
- headers,
126
- body: hasBody ? Buffer.concat(chunks) : undefined,
127
- });
128
- log(req.method ?? "?", targetUrl.hostname, upstream.pathname, upstreamRes.status, Date.now() - started, placeholdersIn(rawHeaders));
129
- res.writeHead(upstreamRes.status, Object.fromEntries(upstreamRes.headers));
130
- if (upstreamRes.body) {
131
- const reader = upstreamRes.body.getReader();
132
- for (;;) {
133
- const { done, value } = await reader.read();
134
- if (done)
135
- break;
136
- res.write(value);
232
+ else if (Array.isArray(v)) {
233
+ rawHeaders[k] = v;
234
+ headers[k] = v.map(substitute);
137
235
  }
138
236
  }
139
- res.end();
237
+ // Empeche l'upstream de compresser : sinon impossible de rediger les secrets reflechis.
238
+ if (secretValues.length > 0)
239
+ headers["accept-encoding"] = "identity";
240
+ // Streame le corps vers l'upstream sans le bufferiser en mémoire (évite un DoS sur gros upload)
241
+ const hasBody = req.method !== "GET" && req.method !== "HEAD";
242
+ const used = placeholdersIn(rawHeaders);
243
+ const status = await forwardHttps(req, res, upstream, headers, pinned, hasBody, secretValues);
244
+ log(req.method ?? "?", targetHost, upstream.pathname, status, Date.now() - started, used, used);
140
245
  }
141
246
  catch {
142
247
  if (!res.headersSent)
@@ -152,3 +257,112 @@ function startProxy(secrets, hosts = {}, verbose = false) {
152
257
  });
153
258
  });
154
259
  }
260
+ function normalizeAllowedHost(value) {
261
+ const raw = value.trim();
262
+ if (!raw || raw.length > MAX_HOST_LENGTH + 8)
263
+ return null;
264
+ try {
265
+ const url = new URL(raw.includes("://") ? raw : `https://${raw}`);
266
+ if (url.username || url.password)
267
+ return null;
268
+ return normalizeHostname(url.hostname);
269
+ }
270
+ catch {
271
+ return null;
272
+ }
273
+ }
274
+ function filterResponseHeaders(headers, secretValues, decodedBody) {
275
+ const out = {};
276
+ const redacting = secretValues.length > 0;
277
+ for (const [name, value] of Object.entries(headers)) {
278
+ const lower = name.toLowerCase();
279
+ if (HOP_BY_HOP_HEADERS.has(lower))
280
+ continue;
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;
294
+ }
295
+ return out;
296
+ }
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) {
321
+ return new Promise((resolve, reject) => {
322
+ const upstreamHostname = normalizeHostname(upstream.hostname);
323
+ const upstreamReq = https_1.default.request({
324
+ protocol: "https:",
325
+ hostname: upstreamHostname,
326
+ port: upstream.port ? Number(upstream.port) : 443,
327
+ path: `${upstream.pathname}${upstream.search}`,
328
+ method: req.method,
329
+ headers: { ...headers, host: upstream.host },
330
+ servername: net_1.default.isIP(upstreamHostname) ? undefined : upstreamHostname,
331
+ timeout: PROXY_TIMEOUT_MS,
332
+ // Node >=20 (autoSelectFamily) appelle lookup avec { all: true } et
333
+ // attend un tableau ; sinon la forme simple (address, family).
334
+ lookup: (_hostname, options, callback) => {
335
+ if (options && options.all)
336
+ callback(null, [{ address: pinned.address, family: pinned.family }]);
337
+ else
338
+ callback(null, pinned.address, pinned.family);
339
+ },
340
+ }, (upstreamRes) => {
341
+ const status = upstreamRes.statusCode ?? 502;
342
+ const body = responseBodyForRedaction(upstreamRes, secretValues.length > 0);
343
+ if (!body) {
344
+ upstreamRes.resume();
345
+ res.writeHead(502);
346
+ res.end("unsupported compressed response");
347
+ resolve(502);
348
+ return;
349
+ }
350
+ res.writeHead(status, filterResponseHeaders(upstreamRes.headers, secretValues, body.decoded));
351
+ const redactor = (0, redact_js_1.createRedactor)(secretValues);
352
+ (0, stream_1.pipeline)(body.stream, redactor, res, (err) => {
353
+ if (err)
354
+ reject(err);
355
+ else
356
+ resolve(status);
357
+ });
358
+ });
359
+ upstreamReq.on("timeout", () => upstreamReq.destroy(new Error("upstream timeout")));
360
+ upstreamReq.on("error", reject);
361
+ req.on("error", reject);
362
+ res.on("close", () => upstreamReq.destroy());
363
+ if (hasBody)
364
+ req.pipe(upstreamReq);
365
+ else
366
+ upstreamReq.end();
367
+ });
368
+ }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export declare function safeText(value: unknown, maxLength?: number): string;
2
+ export declare function safeError(value: unknown): string;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.safeText = safeText;
4
+ exports.safeError = safeError;
5
+ const ANSI_PATTERN = /[\u001b\u009b][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
6
+ const CONTROL_PATTERN = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
7
+ function safeText(value, maxLength = 500) {
8
+ return String(value ?? "")
9
+ .replace(ANSI_PATTERN, "")
10
+ .replace(CONTROL_PATTERN, "")
11
+ .slice(0, maxLength);
12
+ }
13
+ function safeError(value) {
14
+ return safeText(value instanceof Error ? value.message : value, 300) || "Erreur inconnue";
15
+ }
@@ -0,0 +1,2 @@
1
+ import { type ProjectConfig } from "./config.js";
2
+ export declare function ensureProjectTrusted(project: ProjectConfig): Promise<void>;
@@ -0,0 +1,33 @@
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.ensureProjectTrusted = ensureProjectTrusted;
7
+ const promises_1 = __importDefault(require("readline/promises"));
8
+ const process_1 = require("process");
9
+ const config_js_1 = require("./config.js");
10
+ const terminal_js_1 = require("./terminal.js");
11
+ async function ensureProjectTrusted(project) {
12
+ if ((0, config_js_1.isProjectTrusted)(project))
13
+ return;
14
+ const location = (0, config_js_1.projectTrustKey)();
15
+ const target = `${(0, terminal_js_1.safeText)(project.setName)} (${(0, terminal_js_1.safeText)(project.setId)})`;
16
+ const workspace = project.workspaceName ? ` / ${(0, terminal_js_1.safeText)(project.workspaceName)}` : "";
17
+ if (!process_1.stdin.isTTY || !process_1.stdout.isTTY) {
18
+ throw new Error(`Projet non approuve pour le set ${target}${workspace}. Lancez \`elding init\` ou \`elding use <set>\` interactivement.`);
19
+ }
20
+ const rl = promises_1.default.createInterface({ input: process_1.stdin, output: process_1.stdout });
21
+ try {
22
+ console.log(`Projet : ${(0, terminal_js_1.safeText)(location)}`);
23
+ console.log(`Set Elding : ${target}${workspace}`);
24
+ const answer = await rl.question("Approuver ce projet pour ce set ? [y/N] ");
25
+ if (!/^y(es)?$/i.test(answer.trim())) {
26
+ throw new Error("Projet non approuve.");
27
+ }
28
+ (0, config_js_1.trustProject)(project);
29
+ }
30
+ finally {
31
+ rl.close();
32
+ }
33
+ }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@elding/cli",
3
- "version": "0.3.0",
3
+ "version": "0.8.1",
4
4
  "description": "Elding CLI — zero .env, secrets from vault",
5
5
  "bin": {
6
- "elding": "./dist/index.js"
6
+ "elding": "dist/index.js"
7
7
  },
8
8
  "main": "./dist/index.js",
9
9
  "files": [
@@ -15,19 +15,20 @@
15
15
  "scripts": {
16
16
  "build": "tsc",
17
17
  "dev": "tsc --watch",
18
+ "test": "node --test test/*.test.mjs",
18
19
  "prepublishOnly": "npm run build"
19
20
  },
20
21
  "dependencies": {
21
- "commander": "^12.1.0",
22
- "open": "^10.1.0",
23
- "ora": "^8.1.1",
24
- "chalk": "^5.3.0",
25
- "inquirer": "^10.2.2"
22
+ "@napi-rs/keyring": "1.1.7",
23
+ "chalk": "5.6.2",
24
+ "commander": "12.1.0",
25
+ "inquirer": "14.0.2",
26
+ "open": "10.2.0",
27
+ "ora": "8.2.0"
26
28
  },
27
29
  "devDependencies": {
28
- "@types/node": "^22.0.0",
29
- "@types/inquirer": "^9.0.7",
30
- "typescript": "^5.5.0"
30
+ "@types/node": "22.19.21",
31
+ "typescript": "5.9.3"
31
32
  },
32
33
  "engines": {
33
34
  "node": ">=18"