@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.
- package/dist/commands/doctor.js +4 -3
- package/dist/commands/init.js +30 -7
- package/dist/commands/keys.js +4 -3
- package/dist/commands/login.js +51 -25
- package/dist/commands/proxy.d.ts +6 -1
- package/dist/commands/proxy.js +23 -7
- package/dist/commands/run.d.ts +4 -1
- package/dist/commands/run.js +16 -5
- package/dist/commands/sets.js +4 -2
- package/dist/commands/status.js +3 -2
- package/dist/commands/use.js +13 -5
- package/dist/commands/whoami.js +2 -1
- package/dist/index.js +14 -9
- package/dist/lib/api.d.ts +5 -0
- package/dist/lib/api.js +58 -22
- package/dist/lib/apiUrl.d.ts +1 -0
- package/dist/lib/apiUrl.js +44 -0
- package/dist/lib/config.d.ts +11 -0
- package/dist/lib/config.js +100 -7
- package/dist/lib/env.d.ts +5 -0
- package/dist/lib/env.js +49 -0
- package/dist/lib/keychain.d.ts +3 -0
- package/dist/lib/keychain.js +39 -0
- package/dist/lib/logBatcher.d.ts +5 -0
- package/dist/lib/logBatcher.js +42 -0
- package/dist/lib/proxyServer.d.ts +10 -1
- package/dist/lib/proxyServer.js +272 -58
- package/dist/lib/redact.d.ts +3 -0
- package/dist/lib/redact.js +58 -0
- package/dist/lib/terminal.d.ts +2 -0
- package/dist/lib/terminal.js +15 -0
- package/dist/lib/trust.d.ts +2 -0
- package/dist/lib/trust.js +33 -0
- package/package.json +11 -10
package/dist/lib/proxyServer.js
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
|
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
|
|
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"]
|
|
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 (
|
|
86
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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,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,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,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
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Elding CLI — zero .env, secrets from vault",
|
|
5
5
|
"bin": {
|
|
6
|
-
"elding": "
|
|
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
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
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": "
|
|
29
|
-
"
|
|
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"
|