@elding/cli 0.1.0 → 0.2.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.
@@ -0,0 +1 @@
1
+ export declare function logout(): Promise<void>;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.logout = logout;
4
+ const config_js_1 = require("../lib/config.js");
5
+ const api_js_1 = require("../lib/api.js");
6
+ async function logout() {
7
+ const { default: chalk } = await import("chalk");
8
+ const { default: ora } = await import("ora");
9
+ const config = (0, config_js_1.readConfig)();
10
+ if (!config) {
11
+ console.log(chalk.dim("Déjà déconnecté."));
12
+ return;
13
+ }
14
+ const spinner = ora("Déconnexion...").start();
15
+ // Révoque côté serveur (best-effort) puis efface le token local
16
+ await (0, api_js_1.revokeToken)(config.refreshToken);
17
+ (0, config_js_1.clearConfig)();
18
+ spinner.succeed(chalk.green("Déconnecté. Token révoqué et supprimé localement."));
19
+ }
@@ -0,0 +1 @@
1
+ export declare function proxy(cmd: string, args: string[]): Promise<void>;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.proxy = proxy;
4
+ const child_process_1 = require("child_process");
5
+ const config_js_1 = require("../lib/config.js");
6
+ const api_js_1 = require("../lib/api.js");
7
+ const proxyServer_js_1 = require("../lib/proxyServer.js");
8
+ async function proxy(cmd, args) {
9
+ const { default: chalk } = await import("chalk");
10
+ const { default: ora } = await import("ora");
11
+ const config = (0, config_js_1.readConfig)();
12
+ if (!config) {
13
+ console.error(chalk.red("Non connecté. Lancez `elding login` d'abord."));
14
+ process.exit(1);
15
+ }
16
+ const project = (0, config_js_1.readProject)();
17
+ if (!project) {
18
+ console.error(chalk.red("Projet non initialisé. Lancez `elding init` d'abord."));
19
+ process.exit(1);
20
+ }
21
+ const spinner = ora("Démarrage du proxy...").start();
22
+ let secrets;
23
+ let hosts;
24
+ try {
25
+ const accessToken = await (0, api_js_1.exchangeToken)(config.refreshToken);
26
+ ({ secrets, hosts } = await (0, api_js_1.fetchSecrets)(accessToken, project.setId));
27
+ }
28
+ catch (err) {
29
+ spinner.fail(chalk.red(err instanceof Error ? err.message : "Erreur inconnue"));
30
+ process.exit(1);
31
+ }
32
+ const server = await (0, proxyServer_js_1.startProxy)(secrets, hosts);
33
+ spinner.succeed(chalk.green(`Proxy actif sur ${server.url} — ${Object.keys(secrets).length} secret(s)`));
34
+ console.log(chalk.dim("Les clés restent dans le proxy, jamais dans la mémoire de l'app."));
35
+ const child = (0, child_process_1.spawn)(cmd, args, {
36
+ env: {
37
+ ...process.env,
38
+ ELDING_PROXY_URL: server.url,
39
+ ELDING_PROXY_TOKEN: server.token,
40
+ },
41
+ stdio: "inherit",
42
+ shell: true,
43
+ });
44
+ const shutdown = () => server.close();
45
+ process.on("SIGINT", shutdown);
46
+ process.on("SIGTERM", shutdown);
47
+ child.on("exit", (code) => {
48
+ server.close();
49
+ process.exit(code ?? 0);
50
+ });
51
+ }
@@ -21,7 +21,7 @@ async function run(cmd, args) {
21
21
  let secrets;
22
22
  try {
23
23
  const accessToken = await (0, api_js_1.exchangeToken)(config.refreshToken);
24
- secrets = await (0, api_js_1.fetchSecrets)(accessToken, project.setId);
24
+ ({ secrets } = await (0, api_js_1.fetchSecrets)(accessToken, project.setId));
25
25
  spinner.succeed(chalk.green(`${Object.keys(secrets).length} secret(s) chargé(s).`));
26
26
  }
27
27
  catch (err) {
@@ -0,0 +1 @@
1
+ export declare function status(): Promise<void>;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.status = status;
4
+ const config_js_1 = require("../lib/config.js");
5
+ const api_js_1 = require("../lib/api.js");
6
+ async function status() {
7
+ const { default: chalk } = await import("chalk");
8
+ const config = (0, config_js_1.readConfig)();
9
+ if (!config) {
10
+ console.log(chalk.yellow("Non connecté.") + chalk.dim(" Lancez `elding login`."));
11
+ return;
12
+ }
13
+ // Vérifie la validité du token + récupère l'utilisateur
14
+ let user = null;
15
+ try {
16
+ const accessToken = await (0, api_js_1.exchangeToken)(config.refreshToken);
17
+ user = await (0, api_js_1.getMe)(accessToken);
18
+ }
19
+ catch {
20
+ console.log(chalk.red("Token expiré ou révoqué.") + chalk.dim(" Relancez `elding login`."));
21
+ return;
22
+ }
23
+ const project = (0, config_js_1.readProject)();
24
+ const proxyActive = !!process.env.ELDING_PROXY_URL;
25
+ console.log(chalk.green("● Connecté"));
26
+ console.log(` ${chalk.dim("Utilisateur")} ${user.email}${user.name ? chalk.dim(` (${user.name})`) : ""}`);
27
+ console.log(` ${chalk.dim("Set actif")} ${project ? `${project.setName} ${chalk.dim(`(${project.setId})`)}` : chalk.yellow("aucun — `elding init`")}`);
28
+ console.log(` ${chalk.dim("Proxy")} ${proxyActive ? chalk.green("actif") : chalk.dim("inactif")}`);
29
+ }
package/dist/index.js CHANGED
@@ -5,6 +5,9 @@ const commander_1 = require("commander");
5
5
  const login_js_1 = require("./commands/login.js");
6
6
  const init_js_1 = require("./commands/init.js");
7
7
  const run_js_1 = require("./commands/run.js");
8
+ const proxy_js_1 = require("./commands/proxy.js");
9
+ const logout_js_1 = require("./commands/logout.js");
10
+ const status_js_1 = require("./commands/status.js");
8
11
  const program = new commander_1.Command();
9
12
  program
10
13
  .name("elding")
@@ -39,4 +42,33 @@ program
39
42
  process.exit(1);
40
43
  });
41
44
  });
45
+ program
46
+ .command("proxy <cmd>")
47
+ .description("Lancer une commande derrière un proxy local qui injecte les clés — jamais en mémoire de l'app")
48
+ .allowUnknownOption()
49
+ .action(async (cmd, options, command) => {
50
+ const args = command.args.slice(1);
51
+ await (0, proxy_js_1.proxy)(cmd, args).catch((err) => {
52
+ console.error(err.message);
53
+ process.exit(1);
54
+ });
55
+ });
56
+ program
57
+ .command("logout")
58
+ .description("Révoquer le token et le supprimer localement")
59
+ .action(async () => {
60
+ await (0, logout_js_1.logout)().catch((err) => {
61
+ console.error(err.message);
62
+ process.exit(1);
63
+ });
64
+ });
65
+ program
66
+ .command("status")
67
+ .description("Afficher l'état de connexion, le set actif et le proxy")
68
+ .action(async () => {
69
+ await (0, status_js_1.status)().catch((err) => {
70
+ console.error(err.message);
71
+ process.exit(1);
72
+ });
73
+ });
42
74
  program.parse();
package/dist/lib/api.d.ts CHANGED
@@ -3,5 +3,14 @@ export type ApiKeySetItem = {
3
3
  name: string;
4
4
  };
5
5
  export declare function exchangeToken(refreshToken: string): Promise<string>;
6
+ export declare function revokeToken(refreshToken: string): Promise<void>;
7
+ export declare function getMe(accessToken: string): Promise<{
8
+ email: string;
9
+ name: string | null;
10
+ }>;
6
11
  export declare function listSets(accessToken: string): Promise<ApiKeySetItem[]>;
7
- export declare function fetchSecrets(accessToken: string, setId: string): Promise<Record<string, string>>;
12
+ export type SecretsResult = {
13
+ secrets: Record<string, string>;
14
+ hosts: Record<string, string>;
15
+ };
16
+ export declare function fetchSecrets(accessToken: string, setId: string): Promise<SecretsResult>;
package/dist/lib/api.js CHANGED
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.exchangeToken = exchangeToken;
4
+ exports.revokeToken = revokeToken;
5
+ exports.getMe = getMe;
4
6
  exports.listSets = listSets;
5
7
  exports.fetchSecrets = fetchSecrets;
6
8
  const BASE_URL = process.env.ELDING_API_URL ?? "https://app.elding.io";
@@ -15,6 +17,24 @@ async function exchangeToken(refreshToken) {
15
17
  throw new Error(body.error ?? "Impossible d'obtenir un access token");
16
18
  return body.accessToken;
17
19
  }
20
+ async function revokeToken(refreshToken) {
21
+ await fetch(`${BASE_URL}/api/cli/auth/logout`, {
22
+ method: "POST",
23
+ headers: { "Content-Type": "application/json" },
24
+ body: JSON.stringify({ refreshToken }),
25
+ }).catch(() => { });
26
+ }
27
+ async function getMe(accessToken) {
28
+ const res = await fetch(`${BASE_URL}/api/cli/me`, {
29
+ headers: { Authorization: `Bearer ${accessToken}` },
30
+ });
31
+ if (!res.ok)
32
+ throw new Error("Impossible de récupérer l'utilisateur");
33
+ const body = (await res.json());
34
+ if (!body.success || !body.user)
35
+ throw new Error("Réponse invalide");
36
+ return body.user;
37
+ }
18
38
  async function listSets(accessToken) {
19
39
  const res = await fetch(`${BASE_URL}/api/cli/sets`, {
20
40
  headers: { Authorization: `Bearer ${accessToken}` },
@@ -35,7 +55,10 @@ async function fetchSecrets(accessToken, setId) {
35
55
  const body = (await res.json());
36
56
  if (!body.success || !body.secrets || typeof body.secrets !== "object")
37
57
  throw new Error("Réponse invalide");
38
- return sanitizeSecrets(body.secrets);
58
+ return {
59
+ secrets: sanitizeSecrets(body.secrets),
60
+ hosts: body.hosts && typeof body.hosts === "object" ? sanitizeSecrets(body.hosts) : {},
61
+ };
39
62
  }
40
63
  const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
41
64
  // Garde seulement les paires clé/valeur string sûres — évite injection d'objets ou pollution de prototype dans process.env
@@ -0,0 +1,6 @@
1
+ export type ProxyServer = {
2
+ url: string;
3
+ token: string;
4
+ close: () => void;
5
+ };
6
+ export declare function startProxy(secrets: Record<string, string>, hosts?: Record<string, string>): Promise<ProxyServer>;
@@ -0,0 +1,133 @@
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.startProxy = startProxy;
7
+ const http_1 = __importDefault(require("http"));
8
+ const crypto_1 = require("crypto");
9
+ 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))
26
+ return true;
27
+ return false;
28
+ }
29
+ // hosts: map nom_secret -> domaine autorisé. Si défini, le secret ne peut être envoyé qu'à ce host.
30
+ function startProxy(secrets, hosts = {}) {
31
+ const token = (0, crypto_1.randomBytes)(24).toString("hex");
32
+ // Vérifie que chaque placeholder utilisé est autorisé pour ce host. Retourne le nom fautif, ou null.
33
+ const findHostViolation = (val, targetHost) => {
34
+ for (const m of val.matchAll(PLACEHOLDER)) {
35
+ const name = m[1];
36
+ const allowed = hosts[name];
37
+ if (allowed && allowed !== targetHost)
38
+ return name;
39
+ }
40
+ return null;
41
+ };
42
+ const substitute = (val) => val.replace(PLACEHOLDER, (_, name) => secrets[name] ?? "");
43
+ const server = http_1.default.createServer(async (req, res) => {
44
+ try {
45
+ if (req.headers["x-elding-token"] !== token) {
46
+ res.writeHead(401);
47
+ res.end("unauthorized");
48
+ return;
49
+ }
50
+ const target = req.headers["x-elding-target"];
51
+ if (typeof target !== "string") {
52
+ res.writeHead(400);
53
+ res.end("missing x-elding-target");
54
+ return;
55
+ }
56
+ let targetUrl;
57
+ try {
58
+ targetUrl = new URL(target);
59
+ }
60
+ catch {
61
+ res.writeHead(400);
62
+ res.end("bad target");
63
+ return;
64
+ }
65
+ if (targetUrl.protocol !== "https:") {
66
+ res.writeHead(400);
67
+ res.end("https only");
68
+ return;
69
+ }
70
+ if (isBlockedHost(targetUrl.hostname)) {
71
+ res.writeHead(403);
72
+ res.end("blocked host");
73
+ return;
74
+ }
75
+ const upstream = new URL(req.url ?? "/", targetUrl);
76
+ upstream.protocol = "https:";
77
+ upstream.host = targetUrl.host;
78
+ // Enforce host binding : un secret lié à un domaine ne peut partir que vers celui-ci
79
+ for (const v of Object.values(req.headers)) {
80
+ if (typeof v !== "string")
81
+ continue;
82
+ const bad = findHostViolation(v, targetUrl.hostname);
83
+ if (bad) {
84
+ res.writeHead(403);
85
+ res.end(`secret "${bad}" non autorisé pour ${targetUrl.hostname}`);
86
+ return;
87
+ }
88
+ }
89
+ const headers = {};
90
+ for (const [k, v] of Object.entries(req.headers)) {
91
+ const lk = k.toLowerCase();
92
+ if (lk.startsWith("x-elding-"))
93
+ continue;
94
+ if (lk === "host" || lk === "connection" || lk === "content-length")
95
+ continue;
96
+ if (typeof v === "string")
97
+ headers[k] = substitute(v);
98
+ }
99
+ const chunks = [];
100
+ for await (const c of req)
101
+ chunks.push(c);
102
+ const hasBody = chunks.length > 0 && req.method !== "GET" && req.method !== "HEAD";
103
+ const upstreamRes = await fetch(upstream.toString(), {
104
+ method: req.method,
105
+ headers,
106
+ body: hasBody ? Buffer.concat(chunks) : undefined,
107
+ });
108
+ res.writeHead(upstreamRes.status, Object.fromEntries(upstreamRes.headers));
109
+ if (upstreamRes.body) {
110
+ const reader = upstreamRes.body.getReader();
111
+ for (;;) {
112
+ const { done, value } = await reader.read();
113
+ if (done)
114
+ break;
115
+ res.write(value);
116
+ }
117
+ }
118
+ res.end();
119
+ }
120
+ catch {
121
+ if (!res.headersSent)
122
+ res.writeHead(502);
123
+ res.end("proxy error");
124
+ }
125
+ });
126
+ return new Promise((resolve) => {
127
+ server.listen(0, "127.0.0.1", () => {
128
+ const addr = server.address();
129
+ const port = typeof addr === "object" && addr ? addr.port : 0;
130
+ resolve({ url: `http://127.0.0.1:${port}`, token, close: () => server.close() });
131
+ });
132
+ });
133
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elding/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Elding CLI — zero .env, secrets from vault",
5
5
  "bin": {
6
6
  "elding": "./dist/index.js"