@elding/sdk 0.2.0 → 0.4.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/api.js CHANGED
@@ -2,25 +2,28 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.exchangeToken = exchangeToken;
4
4
  exports.fetchSecrets = fetchSecrets;
5
- const BASE_URL = process.env.ELDING_API_URL ?? "https://app.elding.io";
5
+ const apiUrl_js_1 = require("./apiUrl.js");
6
+ const BASE_URL = (0, apiUrl_js_1.resolveBaseUrl)();
7
+ const REQUEST_TIMEOUT_MS = 15_000;
8
+ const REFRESH_TOKEN = /^eld_rt_[a-f0-9]{64}$/i;
6
9
  async function exchangeToken(refreshToken) {
7
- const res = await fetch(`${BASE_URL}/api/cli/auth/token`, {
10
+ if (!REFRESH_TOKEN.test(refreshToken))
11
+ throw new Error("[elding] Token local invalide.");
12
+ const body = await requestJson("/api/cli/auth/token", {
8
13
  method: "POST",
9
14
  headers: { "Content-Type": "application/json" },
10
15
  body: JSON.stringify({ refreshToken }),
11
16
  });
12
- const body = (await res.json());
13
17
  if (!body.success || !body.accessToken)
14
- throw new Error(body.error ?? "Échec d'authentification Elding");
18
+ throw new Error(safeError(body.error) || "Échec d'authentification Elding");
15
19
  return body.accessToken;
16
20
  }
17
21
  async function fetchSecrets(accessToken, setId) {
18
- const res = await fetch(`${BASE_URL}/api/cli/secrets?setId=${encodeURIComponent(setId)}`, {
22
+ const body = await requestJson(`/api/cli/secrets?setId=${encodeURIComponent(setId)}`, {
19
23
  headers: { Authorization: `Bearer ${accessToken}` },
20
24
  });
21
- const body = (await res.json());
22
25
  if (!body.success || !body.secrets || typeof body.secrets !== "object")
23
- throw new Error(body.error ?? `Erreur ${res.status}`);
26
+ throw new Error(safeError(body.error) || "Réponse invalide");
24
27
  return sanitizeSecrets(body.secrets);
25
28
  }
26
29
  const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
@@ -30,9 +33,29 @@ function sanitizeSecrets(raw) {
30
33
  for (const [name, value] of Object.entries(raw)) {
31
34
  if (DANGEROUS_KEYS.has(name))
32
35
  continue;
36
+ if (name.length > 128)
37
+ continue;
33
38
  if (typeof value !== "string")
34
39
  continue;
35
40
  out[name] = value;
36
41
  }
37
42
  return out;
38
43
  }
44
+ async function requestJson(path, init) {
45
+ const res = await fetch(`${BASE_URL}${path}`, {
46
+ ...init,
47
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
48
+ });
49
+ const body = (await res.json().catch(() => null));
50
+ if (!res.ok)
51
+ throw new Error(safeError(body?.error) || `Erreur ${res.status}`);
52
+ if (!body || typeof body !== "object")
53
+ throw new Error("[elding] Réponse invalide.");
54
+ return body;
55
+ }
56
+ function safeError(value) {
57
+ return String(value ?? "")
58
+ .replace(/[\u001b\u009b][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g, "")
59
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "")
60
+ .slice(0, 300);
61
+ }
@@ -0,0 +1 @@
1
+ export declare function resolveBaseUrl(raw?: string | undefined): string;
package/dist/apiUrl.js ADDED
@@ -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] ELDING_API_URL invalide.");
32
+ }
33
+ if (url.username || url.password) {
34
+ throw new Error("[elding] ELDING_API_URL ne doit pas contenir d'identifiants.");
35
+ }
36
+ if (url.pathname !== "/" || url.search || url.hash) {
37
+ throw new Error("[elding] ELDING_API_URL doit etre une origine seule.");
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] ELDING_API_URL doit utiliser HTTPS, sauf pour localhost en developpement.");
44
+ }
package/dist/client.d.ts CHANGED
@@ -11,4 +11,5 @@ export declare class EldingClient {
11
11
  secret(name: string): string;
12
12
  secretOrUndefined(name: string): string | undefined;
13
13
  all(): Record<string, string>;
14
+ allWithEnvFallback(): Record<string, string>;
14
15
  }
package/dist/client.js CHANGED
@@ -22,7 +22,7 @@ class EldingClient {
22
22
  // 3. Fetch secrets
23
23
  const accessToken = await (0, api_js_1.exchangeToken)(refreshToken);
24
24
  const secrets = await (0, api_js_1.fetchSecrets)(accessToken, setId);
25
- return new EldingClient(secrets, options.envFallback ?? true);
25
+ return new EldingClient(secrets, options.envFallback ?? false);
26
26
  }
27
27
  secret(name) {
28
28
  const value = this.secrets[name];
@@ -44,11 +44,15 @@ class EldingClient {
44
44
  return undefined;
45
45
  }
46
46
  }
47
- // Returns all secrets as a plain object (e.g. to spread into a config)
47
+ // Returns only Elding vault secrets as a plain object.
48
48
  all() {
49
- if (this.envFallback)
50
- return { ...process.env, ...this.secrets };
51
49
  return { ...this.secrets };
52
50
  }
51
+ // Explicit escape hatch for legacy code that intentionally wants process.env fallback values.
52
+ allWithEnvFallback() {
53
+ if (!this.envFallback)
54
+ return this.all();
55
+ return { ...process.env, ...this.secrets };
56
+ }
53
57
  }
54
58
  exports.EldingClient = EldingClient;
package/dist/config.js CHANGED
@@ -8,10 +8,14 @@ exports.readProjectConfig = readProjectConfig;
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const os_1 = __importDefault(require("os"));
10
10
  const path_1 = __importDefault(require("path"));
11
+ const REFRESH_TOKEN = /^eld_rt_[a-f0-9]{64}$/i;
11
12
  function readGlobalConfig() {
12
13
  try {
13
14
  const file = path_1.default.join(os_1.default.homedir(), ".elding", "config.json");
14
- return JSON.parse(fs_1.default.readFileSync(file, "utf-8"));
15
+ const parsed = JSON.parse(fs_1.default.readFileSync(file, "utf-8"));
16
+ if (!parsed.refreshToken || !REFRESH_TOKEN.test(parsed.refreshToken))
17
+ return null;
18
+ return { refreshToken: parsed.refreshToken };
15
19
  }
16
20
  catch {
17
21
  return null;
@@ -19,9 +23,15 @@ function readGlobalConfig() {
19
23
  }
20
24
  function readProjectConfig() {
21
25
  try {
22
- return JSON.parse(fs_1.default.readFileSync(".elding.json", "utf-8"));
26
+ const parsed = JSON.parse(fs_1.default.readFileSync(".elding.json", "utf-8"));
27
+ if (!isNonEmptyString(parsed.setId) || !isNonEmptyString(parsed.setName))
28
+ return null;
29
+ return { setId: parsed.setId, setName: parsed.setName };
23
30
  }
24
31
  catch {
25
32
  return null;
26
33
  }
27
34
  }
35
+ function isNonEmptyString(value) {
36
+ return typeof value === "string" && value.trim().length > 0 && value.length <= 500;
37
+ }
@@ -0,0 +1,18 @@
1
+ import { type ClientOptions } from "./client.js";
2
+ export type ProviderConfig = {
3
+ apiKey: string;
4
+ baseURL?: string;
5
+ defaultHeaders?: Record<string, string>;
6
+ };
7
+ /**
8
+ * Config unifiée pour un provider : utilise le proxy si actif (clé jamais en mémoire),
9
+ * sinon récupère la vraie clé du vault (mode client). Le même code marche en dev
10
+ * (`elding proxy`) et en production (serverless), sans rien changer.
11
+ *
12
+ * @example
13
+ * import OpenAI from "openai";
14
+ * import { configure } from "@elding/sdk";
15
+ *
16
+ * const openai = new OpenAI(await configure("OPENAI_API_KEY", "https://api.openai.com"));
17
+ */
18
+ export declare function configure(secretName: string, target: string, options?: ClientOptions): Promise<ProviderConfig>;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.configure = configure;
4
+ const proxy_js_1 = require("./proxy.js");
5
+ const client_js_1 = require("./client.js");
6
+ const SECRET_NAME = /^[A-Z0-9_]+$/;
7
+ // Client mis en cache : on ne fetch les secrets qu'une seule fois par process.
8
+ let clientPromise = null;
9
+ function getClient(options) {
10
+ if (!clientPromise)
11
+ clientPromise = client_js_1.EldingClient.create(options);
12
+ return clientPromise;
13
+ }
14
+ /**
15
+ * Config unifiée pour un provider : utilise le proxy si actif (clé jamais en mémoire),
16
+ * sinon récupère la vraie clé du vault (mode client). Le même code marche en dev
17
+ * (`elding proxy`) et en production (serverless), sans rien changer.
18
+ *
19
+ * @example
20
+ * import OpenAI from "openai";
21
+ * import { configure } from "@elding/sdk";
22
+ *
23
+ * const openai = new OpenAI(await configure("OPENAI_API_KEY", "https://api.openai.com"));
24
+ */
25
+ async function configure(secretName, target, options = {}) {
26
+ if (!SECRET_NAME.test(secretName))
27
+ throw new Error("[elding] Nom de secret invalide (A-Z, 0-9, _).");
28
+ // Mode proxy (dev) : placeholder, la vraie clé n'entre jamais dans le process.
29
+ if ((0, proxy_js_1.isProxyActive)()) {
30
+ const { baseURL, headers } = (0, proxy_js_1.proxyConfig)(target);
31
+ return { apiKey: `{{${secretName}}}`, baseURL, defaultHeaders: headers };
32
+ }
33
+ // Mode client (prod / serverless) : la vraie clé est récupérée du vault.
34
+ const elding = await getClient(options);
35
+ return { apiKey: elding.secret(secretName) };
36
+ }
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import { EldingClient, type ClientOptions } from "./client.js";
2
2
  export { EldingClient };
3
3
  export type { ClientOptions };
4
4
  export { proxyConfig, isProxyActive, type ProxyConfig } from "./proxy.js";
5
+ export { configure, type ProviderConfig } from "./configure.js";
5
6
  /**
6
7
  * Crée un client Elding et charge tous les secrets du set configuré.
7
8
  *
package/dist/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isProxyActive = exports.proxyConfig = exports.EldingClient = void 0;
3
+ exports.configure = exports.isProxyActive = exports.proxyConfig = exports.EldingClient = void 0;
4
4
  exports.client = client;
5
5
  const client_js_1 = require("./client.js");
6
6
  Object.defineProperty(exports, "EldingClient", { enumerable: true, get: function () { return client_js_1.EldingClient; } });
7
7
  var proxy_js_1 = require("./proxy.js");
8
8
  Object.defineProperty(exports, "proxyConfig", { enumerable: true, get: function () { return proxy_js_1.proxyConfig; } });
9
9
  Object.defineProperty(exports, "isProxyActive", { enumerable: true, get: function () { return proxy_js_1.isProxyActive; } });
10
+ var configure_js_1 = require("./configure.js");
11
+ Object.defineProperty(exports, "configure", { enumerable: true, get: function () { return configure_js_1.configure; } });
10
12
  /**
11
13
  * Crée un client Elding et charge tous les secrets du set configuré.
12
14
  *
package/dist/proxy.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.proxyConfig = proxyConfig;
4
4
  exports.isProxyActive = isProxyActive;
5
+ const PROXY_TOKEN = /^[a-f0-9]{48}$/i;
5
6
  /**
6
7
  * Retourne la config à passer à un SDK provider pour router via le proxy Elding.
7
8
  * La vraie clé n'entre jamais dans votre process — utilisez le placeholder `{{NOM_SECRET}}`.
@@ -20,18 +21,47 @@ exports.isProxyActive = isProxyActive;
20
21
  * });
21
22
  */
22
23
  function proxyConfig(target) {
23
- const url = process.env.ELDING_PROXY_URL;
24
+ const url = validateProxyUrl(process.env.ELDING_PROXY_URL);
24
25
  const token = process.env.ELDING_PROXY_TOKEN;
25
- if (!url || !token)
26
+ if (!url || !token || !PROXY_TOKEN.test(token))
26
27
  throw new Error("[elding] Proxy non actif. Lancez via `elding proxy -- <cmd>`.");
28
+ const safeTarget = validateTarget(target);
27
29
  return {
28
30
  baseURL: url,
29
31
  headers: {
30
32
  "x-elding-token": token,
31
- "x-elding-target": target,
33
+ "x-elding-target": safeTarget,
32
34
  },
33
35
  };
34
36
  }
35
37
  function isProxyActive() {
36
- return !!process.env.ELDING_PROXY_URL && !!process.env.ELDING_PROXY_TOKEN;
38
+ try {
39
+ return !!validateProxyUrl(process.env.ELDING_PROXY_URL) && PROXY_TOKEN.test(process.env.ELDING_PROXY_TOKEN ?? "");
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ function validateProxyUrl(value) {
46
+ if (!value)
47
+ return null;
48
+ const url = new URL(value);
49
+ if (url.protocol !== "http:" ||
50
+ url.hostname !== "127.0.0.1" ||
51
+ url.username ||
52
+ url.password ||
53
+ url.pathname !== "/" ||
54
+ url.search ||
55
+ url.hash) {
56
+ throw new Error("[elding] ELDING_PROXY_URL invalide.");
57
+ }
58
+ return url.origin;
59
+ }
60
+ function validateTarget(value) {
61
+ const url = new URL(value);
62
+ if (url.protocol !== "https:" || url.username || url.password) {
63
+ throw new Error("[elding] La cible proxy doit etre une URL HTTPS sans identifiants.");
64
+ }
65
+ url.hash = "";
66
+ return url.origin;
37
67
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elding/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Elding SDK — accès aux secrets depuis le code, zéro .env",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -13,11 +13,12 @@
13
13
  "scripts": {
14
14
  "build": "tsc",
15
15
  "dev": "tsc --watch",
16
+ "test": "node --test test/*.test.mjs",
16
17
  "prepublishOnly": "npm run build"
17
18
  },
18
19
  "devDependencies": {
19
- "@types/node": "^22.0.0",
20
- "typescript": "^5.5.0"
20
+ "@types/node": "22.19.21",
21
+ "typescript": "5.9.3"
21
22
  },
22
23
  "engines": {
23
24
  "node": ">=18"