@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.
- package/dist/commands/init.js +16 -17
- package/dist/commands/login.js +13 -2
- package/dist/commands/proxy.js +5 -1
- package/dist/commands/run.js +8 -1
- package/dist/index.js +6 -2
- package/dist/lib/api.d.ts +1 -0
- package/dist/lib/apiUrl.js +2 -2
- package/dist/lib/proxyServer.js +78 -11
- package/dist/lib/redact.d.ts +3 -0
- package/dist/lib/redact.js +58 -0
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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: "
|
|
53
|
+
type: "select",
|
|
57
54
|
name: "setId",
|
|
58
55
|
message: "Quel set utiliser pour ce projet ?",
|
|
59
|
-
choices: orgSets.map((s) =>
|
|
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
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -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
|
|
46
|
-
|
|
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();
|
package/dist/commands/proxy.js
CHANGED
|
@@ -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
|
|
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
|
},
|
package/dist/commands/run.js
CHANGED
|
@@ -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 = {
|
|
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.
|
|
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", "
|
|
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
package/dist/lib/apiUrl.js
CHANGED
|
@@ -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://
|
|
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://
|
|
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;
|
package/dist/lib/proxyServer.js
CHANGED
|
@@ -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
|
-
|
|
278
|
+
const lower = name.toLowerCase();
|
|
279
|
+
if (HOP_BY_HOP_HEADERS.has(lower))
|
|
268
280
|
continue;
|
|
269
|
-
|
|
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
|
|
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
|
-
|
|
286
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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,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
|
+
}
|