@elding/cli 0.2.0 → 0.8.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.
Files changed (41) hide show
  1. package/dist/commands/doctor.d.ts +1 -0
  2. package/dist/commands/doctor.js +58 -0
  3. package/dist/commands/init.js +29 -5
  4. package/dist/commands/keys.d.ts +1 -0
  5. package/dist/commands/keys.js +31 -0
  6. package/dist/commands/login.js +40 -25
  7. package/dist/commands/open.d.ts +1 -0
  8. package/dist/commands/open.js +13 -0
  9. package/dist/commands/proxy.d.ts +6 -1
  10. package/dist/commands/proxy.js +23 -7
  11. package/dist/commands/run.d.ts +4 -1
  12. package/dist/commands/run.js +17 -6
  13. package/dist/commands/sets.d.ts +1 -0
  14. package/dist/commands/sets.js +26 -0
  15. package/dist/commands/status.js +3 -2
  16. package/dist/commands/use.d.ts +1 -0
  17. package/dist/commands/use.js +37 -0
  18. package/dist/commands/whoami.d.ts +1 -0
  19. package/dist/commands/whoami.js +12 -0
  20. package/dist/index.js +75 -9
  21. package/dist/lib/api.d.ts +11 -1
  22. package/dist/lib/api.js +65 -20
  23. package/dist/lib/apiUrl.d.ts +1 -0
  24. package/dist/lib/apiUrl.js +44 -0
  25. package/dist/lib/config.d.ts +11 -0
  26. package/dist/lib/config.js +100 -7
  27. package/dist/lib/env.d.ts +5 -0
  28. package/dist/lib/env.js +49 -0
  29. package/dist/lib/keychain.d.ts +3 -0
  30. package/dist/lib/keychain.js +39 -0
  31. package/dist/lib/logBatcher.d.ts +5 -0
  32. package/dist/lib/logBatcher.js +42 -0
  33. package/dist/lib/proxyServer.d.ts +10 -1
  34. package/dist/lib/proxyServer.js +219 -48
  35. package/dist/lib/session.d.ts +1 -0
  36. package/dist/lib/session.js +17 -0
  37. package/dist/lib/terminal.d.ts +2 -0
  38. package/dist/lib/terminal.js +15 -0
  39. package/dist/lib/trust.d.ts +2 -0
  40. package/dist/lib/trust.js +33 -0
  41. package/package.json +11 -10
package/dist/lib/api.js CHANGED
@@ -1,58 +1,88 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BASE_URL = void 0;
4
+ exports.listKeys = listKeys;
3
5
  exports.exchangeToken = exchangeToken;
6
+ exports.exchangeLoginCode = exchangeLoginCode;
7
+ exports.reportProxyLogs = reportProxyLogs;
4
8
  exports.revokeToken = revokeToken;
5
9
  exports.getMe = getMe;
6
10
  exports.listSets = listSets;
7
11
  exports.fetchSecrets = fetchSecrets;
8
- const BASE_URL = process.env.ELDING_API_URL ?? "https://app.elding.io";
12
+ const apiUrl_js_1 = require("./apiUrl.js");
13
+ const terminal_js_1 = require("./terminal.js");
14
+ exports.BASE_URL = (0, apiUrl_js_1.resolveBaseUrl)();
15
+ const REQUEST_TIMEOUT_MS = 15_000;
16
+ const LOG_TIMEOUT_MS = 5_000;
17
+ const REFRESH_TOKEN = /^eld_rt_[a-f0-9]{64}$/i;
18
+ const LOGIN_CODE = /^[A-Za-z0-9._~-]{16,512}$/;
19
+ async function listKeys(accessToken, setId) {
20
+ const body = await requestJson(`/api/cli/keys?setId=${encodeURIComponent(setId)}`, {
21
+ headers: { Authorization: `Bearer ${accessToken}` },
22
+ });
23
+ return Array.isArray(body.keys) ? body.keys : [];
24
+ }
9
25
  async function exchangeToken(refreshToken) {
10
- const res = await fetch(`${BASE_URL}/api/cli/auth/token`, {
26
+ if (!REFRESH_TOKEN.test(refreshToken))
27
+ throw new Error("Token local invalide. Relancez `elding login`.");
28
+ const body = await requestJson("/api/cli/auth/token", {
11
29
  method: "POST",
12
30
  headers: { "Content-Type": "application/json" },
13
31
  body: JSON.stringify({ refreshToken }),
14
32
  });
15
- const body = (await res.json());
16
33
  if (!body.success || !body.accessToken)
17
- throw new Error(body.error ?? "Impossible d'obtenir un access token");
34
+ throw new Error((0, terminal_js_1.safeText)(body.error, 200) || "Impossible d'obtenir un access token");
18
35
  return body.accessToken;
19
36
  }
37
+ async function exchangeLoginCode(code, state, callbackUrl) {
38
+ if (!LOGIN_CODE.test(code))
39
+ throw new Error("Code d'autorisation invalide.");
40
+ const body = await requestJson("/api/cli/auth/exchange", {
41
+ method: "POST",
42
+ headers: { "Content-Type": "application/json" },
43
+ body: JSON.stringify({ code, state, callbackUrl }),
44
+ });
45
+ if (!body.success || !body.refreshToken || !REFRESH_TOKEN.test(body.refreshToken)) {
46
+ throw new Error((0, terminal_js_1.safeText)(body.error, 200) || "Impossible de finaliser l'authentification.");
47
+ }
48
+ return body.refreshToken;
49
+ }
50
+ async function reportProxyLogs(accessToken, setId, setName, entries) {
51
+ await fetch(`${exports.BASE_URL}/api/cli/proxy-logs`, {
52
+ method: "POST",
53
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}` },
54
+ body: JSON.stringify({ setId, setName, entries }),
55
+ signal: AbortSignal.timeout(LOG_TIMEOUT_MS),
56
+ }).catch(() => { });
57
+ }
20
58
  async function revokeToken(refreshToken) {
21
- await fetch(`${BASE_URL}/api/cli/auth/logout`, {
59
+ if (!REFRESH_TOKEN.test(refreshToken))
60
+ return;
61
+ await fetch(`${exports.BASE_URL}/api/cli/auth/logout`, {
22
62
  method: "POST",
23
63
  headers: { "Content-Type": "application/json" },
24
64
  body: JSON.stringify({ refreshToken }),
65
+ signal: AbortSignal.timeout(LOG_TIMEOUT_MS),
25
66
  }).catch(() => { });
26
67
  }
27
68
  async function getMe(accessToken) {
28
- const res = await fetch(`${BASE_URL}/api/cli/me`, {
69
+ const body = await requestJson("/api/cli/me", {
29
70
  headers: { Authorization: `Bearer ${accessToken}` },
30
71
  });
31
- if (!res.ok)
32
- throw new Error("Impossible de récupérer l'utilisateur");
33
- const body = (await res.json());
34
72
  if (!body.success || !body.user)
35
73
  throw new Error("Réponse invalide");
36
74
  return body.user;
37
75
  }
38
76
  async function listSets(accessToken) {
39
- const res = await fetch(`${BASE_URL}/api/cli/sets`, {
77
+ const body = await requestJson("/api/cli/sets", {
40
78
  headers: { Authorization: `Bearer ${accessToken}` },
41
79
  });
42
- if (!res.ok)
43
- throw new Error("Impossible de récupérer les sets");
44
- const body = (await res.json());
45
80
  return Array.isArray(body.sets) ? body.sets : [];
46
81
  }
47
- async function fetchSecrets(accessToken, setId) {
48
- const res = await fetch(`${BASE_URL}/api/cli/secrets?setId=${encodeURIComponent(setId)}`, {
82
+ async function fetchSecrets(accessToken, setId, mode = "proxy") {
83
+ const body = await requestJson(`/api/cli/secrets?setId=${encodeURIComponent(setId)}&mode=${mode}`, {
49
84
  headers: { Authorization: `Bearer ${accessToken}` },
50
85
  });
51
- if (!res.ok) {
52
- const body = (await res.json().catch(() => ({})));
53
- throw new Error(body.error ?? `Erreur ${res.status}`);
54
- }
55
- const body = (await res.json());
56
86
  if (!body.success || !body.secrets || typeof body.secrets !== "object")
57
87
  throw new Error("Réponse invalide");
58
88
  return {
@@ -67,9 +97,24 @@ function sanitizeSecrets(raw) {
67
97
  for (const [name, value] of Object.entries(raw)) {
68
98
  if (DANGEROUS_KEYS.has(name))
69
99
  continue;
100
+ if (name.length > 128)
101
+ continue;
70
102
  if (typeof value !== "string")
71
103
  continue;
72
104
  out[name] = value;
73
105
  }
74
106
  return out;
75
107
  }
108
+ async function requestJson(path, init) {
109
+ const res = await fetch(`${exports.BASE_URL}${path}`, {
110
+ ...init,
111
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
112
+ });
113
+ const body = (await res.json().catch(() => null));
114
+ if (!res.ok) {
115
+ throw new Error((0, terminal_js_1.safeText)(body?.error, 200) || `Erreur ${res.status}`);
116
+ }
117
+ if (!body || typeof body !== "object")
118
+ throw new Error("Réponse invalide");
119
+ return body;
120
+ }
@@ -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>): 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>;