@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.
@@ -0,0 +1 @@
1
+ export declare function resolveBaseUrl(raw?: string | undefined): string;
@@ -0,0 +1,44 @@
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.resolveBaseUrl = resolveBaseUrl;
7
+ const net_1 = __importDefault(require("net"));
8
+ const DEFAULT_BASE_URL = "https://app.elding.io";
9
+ function stripBrackets(hostname) {
10
+ return hostname.startsWith("[") && hostname.endsWith("]")
11
+ ? hostname.slice(1, -1)
12
+ : hostname;
13
+ }
14
+ function isLoopbackHost(hostname) {
15
+ const host = stripBrackets(hostname.toLowerCase().replace(/\.$/, ""));
16
+ if (host === "localhost")
17
+ return true;
18
+ if (net_1.default.isIPv4(host))
19
+ return host.startsWith("127.");
20
+ if (net_1.default.isIPv6(host))
21
+ return host === "::1" || host === "0:0:0:0:0:0:0:1";
22
+ return false;
23
+ }
24
+ function resolveBaseUrl(raw = process.env.ELDING_API_URL) {
25
+ const input = raw?.trim() || DEFAULT_BASE_URL;
26
+ let url;
27
+ try {
28
+ url = new URL(input);
29
+ }
30
+ catch {
31
+ throw new Error("ELDING_API_URL invalide.");
32
+ }
33
+ if (url.username || url.password) {
34
+ throw new Error("ELDING_API_URL ne doit pas contenir d'identifiants.");
35
+ }
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.");
38
+ }
39
+ if (url.protocol === "https:")
40
+ return url.origin;
41
+ if (url.protocol === "http:" && isLoopbackHost(url.hostname))
42
+ return url.origin;
43
+ throw new Error("ELDING_API_URL doit utiliser HTTPS, sauf pour localhost en developpement.");
44
+ }
@@ -1,6 +1,12 @@
1
1
  export type EldingConfig = {
2
2
  refreshToken: string;
3
3
  workspaceId?: string;
4
+ trustedProjects?: Record<string, TrustedProject>;
5
+ };
6
+ export type TrustedProject = {
7
+ setId: string;
8
+ setName?: string;
9
+ trustedAt: string;
4
10
  };
5
11
  export declare function readConfig(): EldingConfig | null;
6
12
  export declare function writeConfig(config: EldingConfig): void;
@@ -8,6 +14,11 @@ export declare function clearConfig(): void;
8
14
  export type ProjectConfig = {
9
15
  setId: string;
10
16
  setName: string;
17
+ workspaceId?: string;
18
+ workspaceName?: string;
11
19
  };
12
20
  export declare function readProject(): ProjectConfig | null;
13
21
  export declare function writeProject(cfg: ProjectConfig): void;
22
+ export declare function projectTrustKey(cwd?: string): string;
23
+ export declare function isProjectTrusted(project: ProjectConfig, cwd?: string): boolean;
24
+ export declare function trustProject(project: ProjectConfig, cwd?: string): void;
@@ -8,26 +8,79 @@ exports.writeConfig = writeConfig;
8
8
  exports.clearConfig = clearConfig;
9
9
  exports.readProject = readProject;
10
10
  exports.writeProject = writeProject;
11
+ exports.projectTrustKey = projectTrustKey;
12
+ exports.isProjectTrusted = isProjectTrusted;
13
+ exports.trustProject = trustProject;
11
14
  const fs_1 = __importDefault(require("fs"));
12
15
  const os_1 = __importDefault(require("os"));
13
16
  const path_1 = __importDefault(require("path"));
17
+ const keychain_js_1 = require("./keychain.js");
14
18
  const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), ".elding");
15
19
  const CONFIG_FILE = path_1.default.join(CONFIG_DIR, "config.json");
16
- function readConfig() {
20
+ const REFRESH_TOKEN = /^eld_rt_[a-f0-9]{64}$/i;
21
+ function readMeta() {
17
22
  try {
18
- const raw = fs_1.default.readFileSync(CONFIG_FILE, "utf-8");
19
- return JSON.parse(raw);
23
+ const parsed = JSON.parse(fs_1.default.readFileSync(CONFIG_FILE, "utf-8"));
24
+ return {
25
+ refreshToken: typeof parsed.refreshToken === "string" && REFRESH_TOKEN.test(parsed.refreshToken)
26
+ ? parsed.refreshToken
27
+ : undefined,
28
+ workspaceId: typeof parsed.workspaceId === "string" ? parsed.workspaceId : undefined,
29
+ trustedProjects: parsed.trustedProjects && typeof parsed.trustedProjects === "object"
30
+ ? parsed.trustedProjects
31
+ : undefined,
32
+ };
20
33
  }
21
34
  catch {
22
35
  return null;
23
36
  }
24
37
  }
38
+ function readConfig() {
39
+ const meta = readMeta();
40
+ const kcToken = (0, keychain_js_1.keychainGetToken)();
41
+ const token = kcToken && REFRESH_TOKEN.test(kcToken) ? kcToken : meta?.refreshToken ?? null;
42
+ if (!token)
43
+ return null;
44
+ // Migration : un token herite du fichier remonte dans le trousseau si possible.
45
+ if (!kcToken && meta?.refreshToken && (0, keychain_js_1.keychainSetToken)(meta.refreshToken)) {
46
+ writeConfig({ refreshToken: token, workspaceId: meta.workspaceId, trustedProjects: meta.trustedProjects });
47
+ }
48
+ return { refreshToken: token, workspaceId: meta?.workspaceId, trustedProjects: meta?.trustedProjects };
49
+ }
25
50
  function writeConfig(config) {
26
- fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true });
51
+ ensureConfigDir();
52
+ const inKeychain = (0, keychain_js_1.keychainSetToken)(config.refreshToken);
53
+ const meta = {
54
+ workspaceId: config.workspaceId,
55
+ trustedProjects: config.trustedProjects,
56
+ refreshToken: inKeychain ? undefined : config.refreshToken,
57
+ };
58
+ atomicWriteJson(CONFIG_FILE, meta, 0o600);
59
+ fs_1.default.chmodSync(CONFIG_FILE, 0o600);
60
+ }
61
+ function ensureConfigDir() {
62
+ try {
63
+ const stat = fs_1.default.lstatSync(CONFIG_DIR);
64
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
65
+ throw new Error(`${CONFIG_DIR} doit etre un dossier non symbolique.`);
66
+ }
67
+ }
68
+ catch (err) {
69
+ if (err.code !== "ENOENT")
70
+ throw err;
71
+ fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
72
+ }
27
73
  fs_1.default.chmodSync(CONFIG_DIR, 0o700);
28
- fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
74
+ }
75
+ function atomicWriteJson(file, value, mode) {
76
+ const dir = path_1.default.dirname(file);
77
+ const tmp = path_1.default.join(dir, `.${path_1.default.basename(file)}.${process.pid}.${Date.now()}.tmp`);
78
+ fs_1.default.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode });
79
+ fs_1.default.chmodSync(tmp, mode);
80
+ fs_1.default.renameSync(tmp, file);
29
81
  }
30
82
  function clearConfig() {
83
+ (0, keychain_js_1.keychainDeleteToken)();
31
84
  try {
32
85
  fs_1.default.unlinkSync(CONFIG_FILE);
33
86
  }
@@ -37,12 +90,52 @@ const PROJECT_FILE = ".elding.json";
37
90
  function readProject() {
38
91
  try {
39
92
  const raw = fs_1.default.readFileSync(PROJECT_FILE, "utf-8");
40
- return JSON.parse(raw);
93
+ const parsed = JSON.parse(raw);
94
+ if (!isNonEmptyString(parsed.setId) || !isNonEmptyString(parsed.setName))
95
+ return null;
96
+ return {
97
+ setId: parsed.setId,
98
+ setName: parsed.setName,
99
+ workspaceId: isNonEmptyString(parsed.workspaceId) ? parsed.workspaceId : undefined,
100
+ workspaceName: isNonEmptyString(parsed.workspaceName) ? parsed.workspaceName : undefined,
101
+ };
41
102
  }
42
103
  catch {
43
104
  return null;
44
105
  }
45
106
  }
46
107
  function writeProject(cfg) {
47
- fs_1.default.writeFileSync(PROJECT_FILE, JSON.stringify(cfg, null, 2));
108
+ atomicWriteJson(PROJECT_FILE, cfg, 0o644);
109
+ }
110
+ function projectTrustKey(cwd = process.cwd()) {
111
+ try {
112
+ return fs_1.default.realpathSync(cwd);
113
+ }
114
+ catch {
115
+ return path_1.default.resolve(cwd);
116
+ }
117
+ }
118
+ function isProjectTrusted(project, cwd = process.cwd()) {
119
+ const config = readConfig();
120
+ const trusted = config?.trustedProjects?.[projectTrustKey(cwd)];
121
+ return trusted?.setId === project.setId;
122
+ }
123
+ function trustProject(project, cwd = process.cwd()) {
124
+ const config = readConfig();
125
+ if (!config)
126
+ throw new Error("Non connecté. Lancez `elding login` d'abord.");
127
+ writeConfig({
128
+ ...config,
129
+ trustedProjects: {
130
+ ...(config.trustedProjects ?? {}),
131
+ [projectTrustKey(cwd)]: {
132
+ setId: project.setId,
133
+ setName: project.setName,
134
+ trustedAt: new Date().toISOString(),
135
+ },
136
+ },
137
+ });
138
+ }
139
+ function isNonEmptyString(value) {
140
+ return typeof value === "string" && value.trim().length > 0 && value.length <= 500;
48
141
  }
@@ -0,0 +1,5 @@
1
+ export type SafeEnvResult = {
2
+ env: Record<string, string>;
3
+ rejected: string[];
4
+ };
5
+ export declare function filterSecretsForEnv(secrets: Record<string, string>): SafeEnvResult;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.filterSecretsForEnv = filterSecretsForEnv;
4
+ const ENV_NAME = /^[A-Z_][A-Z0-9_]*$/;
5
+ const DANGEROUS_ENV_NAMES = new Set([
6
+ "BASH_ENV",
7
+ "CDPATH",
8
+ "ENV",
9
+ "GIT_CONFIG",
10
+ "GIT_CONFIG_GLOBAL",
11
+ "GIT_CONFIG_NOSYSTEM",
12
+ "GIT_CONFIG_SYSTEM",
13
+ "HOME",
14
+ "IFS",
15
+ "LD_AUDIT",
16
+ "LD_LIBRARY_PATH",
17
+ "LD_PRELOAD",
18
+ "NODE_OPTIONS",
19
+ "NODE_PATH",
20
+ "NPM_CONFIG_USERCONFIG",
21
+ "PATH",
22
+ "PERL5LIB",
23
+ "PYTHONHOME",
24
+ "PYTHONPATH",
25
+ "RUBYOPT",
26
+ "SHELL",
27
+ "ZDOTDIR",
28
+ ]);
29
+ const DANGEROUS_ENV_PREFIXES = [
30
+ "DYLD_",
31
+ "LD_",
32
+ "npm_config_",
33
+ "NPM_CONFIG_",
34
+ ];
35
+ function filterSecretsForEnv(secrets) {
36
+ const env = Object.create(null);
37
+ const rejected = [];
38
+ for (const [name, value] of Object.entries(secrets)) {
39
+ const dangerous = !ENV_NAME.test(name) ||
40
+ DANGEROUS_ENV_NAMES.has(name) ||
41
+ DANGEROUS_ENV_PREFIXES.some((prefix) => name.startsWith(prefix));
42
+ if (dangerous) {
43
+ rejected.push(name);
44
+ continue;
45
+ }
46
+ env[name] = value;
47
+ }
48
+ return { env, rejected };
49
+ }
@@ -0,0 +1,3 @@
1
+ export declare function keychainGetToken(): string | null;
2
+ export declare function keychainSetToken(token: string): boolean;
3
+ export declare function keychainDeleteToken(): void;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.keychainGetToken = keychainGetToken;
4
+ exports.keychainSetToken = keychainSetToken;
5
+ exports.keychainDeleteToken = keychainDeleteToken;
6
+ const keyring_1 = require("@napi-rs/keyring");
7
+ // Le refresh token vit dans le trousseau de l'OS (Keychain macOS, Credential
8
+ // Manager Windows, Secret Service Linux) plutot qu'en clair sur disque : illisible
9
+ // par un simple `cat`, donc invisible pour un agent IA ou une dependance verolee.
10
+ const SERVICE = "elding";
11
+ const ACCOUNT = "refresh-token";
12
+ function entry() {
13
+ return new keyring_1.Entry(SERVICE, ACCOUNT);
14
+ }
15
+ function keychainGetToken() {
16
+ try {
17
+ return entry().getPassword();
18
+ }
19
+ catch {
20
+ return null; // absent ou trousseau indisponible
21
+ }
22
+ }
23
+ function keychainSetToken(token) {
24
+ try {
25
+ entry().setPassword(token);
26
+ return true;
27
+ }
28
+ catch {
29
+ return false; // trousseau indisponible -> l'appelant retombe sur le fichier
30
+ }
31
+ }
32
+ function keychainDeleteToken() {
33
+ try {
34
+ entry().deleteCredential();
35
+ }
36
+ catch {
37
+ /* deja absent ou indisponible */
38
+ }
39
+ }
@@ -0,0 +1,5 @@
1
+ import type { ProxyLogEntry } from "./proxyServer.js";
2
+ export declare function createLogBatcher(refreshToken: string, setId: string, setName: string): {
3
+ add(e: ProxyLogEntry): void;
4
+ stop(): Promise<void>;
5
+ };
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createLogBatcher = createLogBatcher;
4
+ const api_js_1 = require("./api.js");
5
+ // Bufferise les logs du proxy et les envoie au cloud par lots (best-effort, non bloquant).
6
+ function createLogBatcher(refreshToken, setId, setName) {
7
+ let buffer = [];
8
+ let cachedToken = "";
9
+ let tokenExp = 0;
10
+ async function getToken() {
11
+ if (cachedToken && Date.now() < tokenExp)
12
+ return cachedToken;
13
+ cachedToken = await (0, api_js_1.exchangeToken)(refreshToken);
14
+ tokenExp = Date.now() + 14 * 60 * 1000; // l'access token vit 15min
15
+ return cachedToken;
16
+ }
17
+ async function flush() {
18
+ if (buffer.length === 0)
19
+ return;
20
+ const batch = buffer;
21
+ buffer = [];
22
+ try {
23
+ const token = await getToken();
24
+ await (0, api_js_1.reportProxyLogs)(token, setId, setName, batch);
25
+ }
26
+ catch {
27
+ /* best-effort — on ne réinjecte pas, pas de boucle d'erreur */
28
+ }
29
+ }
30
+ const timer = setInterval(() => void flush(), 10_000);
31
+ return {
32
+ add(e) {
33
+ buffer.push(e);
34
+ if (buffer.length >= 25)
35
+ void flush();
36
+ },
37
+ async stop() {
38
+ clearInterval(timer);
39
+ await flush();
40
+ },
41
+ };
42
+ }
@@ -3,4 +3,13 @@ export type ProxyServer = {
3
3
  token: string;
4
4
  close: () => void;
5
5
  };
6
- export declare function startProxy(secrets: Record<string, string>, hosts?: Record<string, string>, verbose?: boolean): Promise<ProxyServer>;
6
+ export type ProxyLogEntry = {
7
+ method: string;
8
+ host: string;
9
+ path: string;
10
+ status: number;
11
+ latencyMs: number;
12
+ blocked: boolean;
13
+ secretNames: string;
14
+ };
15
+ export declare function startProxy(secrets: Record<string, string>, hosts?: Record<string, string>, verbose?: boolean, onLog?: (e: ProxyLogEntry) => void): Promise<ProxyServer>;