@zarex/client 0.1.0 → 0.1.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.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @zarex/client
2
+
3
+ Client HTTP bas niveau du SDK Zarex, avec disjoncteur (circuit breaker) intégré pour échouer vite plutôt que de faire attendre vos visiteurs quand Zarex est lent ou injoignable.
4
+
5
+ > **Vous n'avez normalement pas besoin d'installer ce paquet directement.** Il est utilisé par [`@zarex/core`](https://www.npmjs.com/package/@zarex/core) et, par transitivité, par tous les adaptateurs de framework (`@zarex/express`, `@zarex/fastify`, `@zarex/next`, `@zarex/sveltekit`, `@zarex/bun`). Installez-le directement seulement si vous construisez votre propre intégration, en dehors de `@zarex/core`.
6
+
7
+ ## Qu'est-ce que Zarex ?
8
+
9
+ Zarex est une plateforme de sécurité applicative : un service qui protège une application web contre les bots, les abus de trafic, les injections et les fuites de données sensibles.
10
+
11
+ ## Utilisation
12
+
13
+ ```ts
14
+ import { createClient, parseDecision } from "@zarex/client";
15
+
16
+ const client = createClient({ key: process.env.ZAREX_KEY! });
17
+ const response = await client.decide(request);
18
+ const decision = parseDecision(response);
19
+ ```
20
+
21
+ ### `ClientOptions`
22
+
23
+ | Option | Type | Description |
24
+ |---|---|---|
25
+ | `key` | `string` | Clé API du projet. |
26
+ | `timeoutMs` | `number` | Délai maximal avant d'abandonner l'appel (défaut 500 ms). |
27
+ | `breaker` | `BreakerOptions \| false` | Configuration du disjoncteur, ou `false` pour le désactiver. |
28
+ | `baseUrl` | `string` | Inutile en usage normal. À ne définir que pour les tests ou un environnement local. |
29
+
30
+ ### Le disjoncteur (`CircuitBreaker`)
31
+
32
+ Après un nombre configurable d'échecs consécutifs, le disjoncteur s'ouvre : les appels suivants échouent immédiatement (sans round-trip réseau) pendant une période de pause, puis une requête de sonde est tentée pour voir si le service est de nouveau disponible. Ce comportement évite qu'un ralentissement ne se répercute sur chaque requête de votre application par un délai d'attente répété.
33
+
34
+ ```ts
35
+ import { CircuitBreaker } from "@zarex/client";
36
+
37
+ const breaker = new CircuitBreaker({ failureThreshold: 5, cooldownMs: 10_000 });
38
+ ```
39
+
40
+ ### Erreurs
41
+
42
+ | Classe | Quand |
43
+ |---|---|
44
+ | `ZarexConfigError` | Configuration invalide (clé manquante, option hors bornes…) — levée à la construction, jamais pendant le traitement d'une requête. |
45
+ | `ZarexClientError` | Erreur réseau ou réponse inattendue lors d'un appel — voir `error.code` (`ClientErrorCode`). |
46
+
47
+ Dans tous les adaptateurs de haut niveau, ces erreurs sont systématiquement interceptées : elles ne remontent jamais jusqu'à votre application (fail-open — si Zarex est injoignable, lent ou répond de façon inattendue, la requête du visiteur passe toujours).
48
+
49
+ ## Licence
50
+
51
+ MIT
package/dist/index.cjs CHANGED
@@ -111,6 +111,7 @@ var ZarexConfigError = class extends Error {
111
111
  var SDK_VERSION = "0.1.0";
112
112
  var MAX_RESPONSE_BYTES = 256 * 1024;
113
113
  var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
114
+ var DEFAULT_BASE_URL = "https://engine.zarex.dev";
114
115
  function readEnv(name) {
115
116
  try {
116
117
  return typeof process !== "undefined" ? process.env?.[name] : void 0;
@@ -127,10 +128,7 @@ function validateKey(key) {
127
128
  return key;
128
129
  }
129
130
  function resolveBaseUrl(baseUrl, allowInsecureHttp = false) {
130
- const raw = baseUrl ?? readEnv("ZAREX_URL");
131
- if (!raw) {
132
- throw new ZarexConfigError("Zarex: `baseUrl` is required (or set the ZAREX_URL environment variable).");
133
- }
131
+ const raw = baseUrl ?? readEnv("ZAREX_URL") ?? DEFAULT_BASE_URL;
134
132
  let u;
135
133
  try {
136
134
  u = new URL(raw);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/breaker.ts","../src/errors.ts","../src/client.ts"],"sourcesContent":["export { createClient, resolveBaseUrl, validateKey, parseDecision, SDK_VERSION } from \"./client.js\";\r\nexport type { ClientOptions, ZarexClient } from \"./client.js\";\r\nexport { CircuitBreaker } from \"./breaker.js\";\r\nexport type { BreakerOptions } from \"./breaker.js\";\r\nexport { ZarexClientError, ZarexConfigError } from \"./errors.js\";\r\nexport type { ClientErrorCode } from \"./errors.js\";\r\nexport type * from \"./types.js\";","/**\r\n * Disjoncteur côté SDK.\r\n *\r\n * Pourquoi : si le moteur est en panne ou très lent, CHAQUE requête du client\r\n * attendrait le délai maximal (ex. 500 ms) avant de laisser passer. Le site du\r\n * client serait ralenti par NOTRE panne. Après quelques échecs d'affilée, on\r\n * cesse d'appeler le moteur pendant un moment (« circuit ouvert ») : les\r\n * requêtes passent immédiatement. Puis UNE requête d'essai vérifie s'il est revenu.\r\n */\r\nexport interface BreakerOptions {\r\n /** Échecs consécutifs avant ouverture. Défaut 5. */\r\n failureThreshold?: number;\r\n /** Durée pendant laquelle on n'appelle plus le moteur. Défaut 10 000 ms. */\r\n cooldownMs?: number;\r\n /** Horloge (tests). */\r\n now?: () => number;\r\n}\r\n\r\ntype State = \"closed\" | \"open\" | \"half-open\";\r\n\r\nexport class CircuitBreaker {\r\n private state: State = \"closed\";\r\n private failures = 0;\r\n private openedAt = 0;\r\n private probing = false;\r\n private readonly threshold: number;\r\n private readonly cooldown: number;\r\n private readonly now: () => number;\r\n\r\n constructor(opts: BreakerOptions = {}) {\r\n this.threshold = opts.failureThreshold ?? 5;\r\n this.cooldown = opts.cooldownMs ?? 10_000;\r\n this.now = opts.now ?? Date.now;\r\n }\r\n\r\n /** true = l'appel est autorisé. Quand le circuit est semi-ouvert, un seul appel à la fois passe. */\r\n allow(): boolean {\r\n if (this.state === \"closed\") return true;\r\n if (this.state === \"open\") {\r\n if (this.now() - this.openedAt < this.cooldown) return false;\r\n this.state = \"half-open\";\r\n this.probing = false;\r\n }\r\n // half-open : un seul essai en vol\r\n if (this.probing) return false;\r\n this.probing = true;\r\n return true;\r\n }\r\n\r\n success(): void {\r\n this.failures = 0;\r\n this.probing = false;\r\n this.state = \"closed\";\r\n }\r\n\r\n /** À appeler pour une panne d'infrastructure (délai, réseau, 5xx), pas pour une erreur de configuration. */\r\n failure(): void {\r\n this.probing = false;\r\n if (this.state === \"half-open\") {\r\n this.trip();\r\n return;\r\n }\r\n this.failures++;\r\n if (this.failures >= this.threshold) this.trip();\r\n }\r\n \r\n /** Un appel autorisé qui se termine sans verdict sur la santé du moteur (ex. 401) : libère l'essai. */\r\n neutral(): void {\r\n this.probing = false;\r\n if (this.state === \"half-open\") this.state = \"closed\";\r\n }\r\n \r\n private trip(): void {\r\n this.state = \"open\";\r\n this.openedAt = this.now();\r\n }\r\n \r\n get isOpen(): boolean {\r\n return this.state !== \"closed\";\r\n }\r\n}","/**\r\n * Codes d'erreur du client. Le SDK ne lève JAMAIS ces erreurs jusqu'à\r\n * l'application du client : `protect()` les attrape et laisse passer la requête\r\n * (fail-open). Elles servent aux journaux et à `decision.error`.\r\n */\r\nexport type ClientErrorCode =\r\n | \"timeout\" // le moteur n'a pas répondu à temps\r\n | \"network\" // connexion impossible / coupée\r\n | \"circuit_open\" // trop d'échecs récents : appel évité pour protéger l'application\r\n | \"unauthorized\" // 401 : clé API absente, invalide ou révoquée\r\n | \"forbidden\" // 403\r\n | \"quota_exceeded\" // 429 : quota mensuel du plan atteint\r\n | \"invalid_request\" // 400/413/415 : bug du SDK ou règle invalide\r\n | \"server_error\" // 5xx\r\n | \"bad_response\"; // réponse 200 mais illisible / inattendue\r\n \r\nexport class ZarexClientError extends Error {\r\n readonly code: ClientErrorCode;\r\n readonly status?: number;\r\n /** Code d'erreur renvoyé par le moteur (ex. « too_large »), si présent. */\r\n readonly engineCode?: string;\r\n \r\n constructor(code: ClientErrorCode, message: string, opts: { status?: number; engineCode?: string } = {}) {\r\n super(message);\r\n this.name = \"ZarexClientError\";\r\n this.code = code;\r\n if (opts.status !== undefined) this.status = opts.status;\r\n if (opts.engineCode !== undefined) this.engineCode = opts.engineCode;\r\n }\r\n}\r\n \r\n/** Erreur de configuration : levée immédiatement à la création, jamais pendant une requête. */\r\nexport class ZarexConfigError extends Error {\r\n constructor(message: string) {\r\n super(message);\r\n this.name = \"ZarexConfigError\";\r\n }\r\n}","import { CircuitBreaker, type BreakerOptions } from \"./breaker.js\";\r\nimport { ZarexClientError, ZarexConfigError } from \"./errors.js\";\r\nimport type { DecideRequest, DecideResponse, Verdict } from \"./types.js\";\r\n\r\nexport interface ClientOptions {\r\n /** Clé API du projet (zarex_live_… / zarex_test_…). Jamais journalisée. */\r\n key: string;\r\n /** URL du moteur, ex. https://engine.exemple.com — obligatoire (ou variable ZAREX_URL). */\r\n baseUrl?: string;\r\n /** Délai maximal d'un appel, en ms. Défaut 500. */\r\n timeoutMs?: number;\r\n /** Autoriser http:// vers un hôte distant (déconseillé : la clé circulerait en clair). */\r\n allowInsecureHttp?: boolean;\r\n /** Disjoncteur. `false` pour le désactiver. */\r\n breaker?: BreakerOptions | false;\r\n /** Implémentation de fetch (tests, runtimes particuliers). */\r\n fetch?: typeof fetch;\r\n /** Version annoncée au moteur. */\r\n sdkVersion?: string;\r\n}\r\n\r\nexport interface ZarexClient {\r\n decide(request: DecideRequest): Promise<DecideResponse>;\r\n}\r\n\r\nexport const SDK_VERSION = \"0.1.0\";\r\nconst MAX_RESPONSE_BYTES = 256 * 1024;\r\nconst LOCAL_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"::1\"]);\r\n\r\nfunction readEnv(name: string): string | undefined {\r\n try {\r\n return typeof process !== \"undefined\" ? process.env?.[name] : undefined;\r\n } catch {\r\n return undefined;\r\n }\r\n}\r\n\r\n/** Valide la clé sans jamais la recopier dans un message d'erreur. */\r\nexport function validateKey(key: unknown): string {\r\n if (typeof key !== \"string\" || key.length < 16 || key.length > 256 || !/^[A-Za-z0-9_-]+$/.test(key)) {\r\n throw new ZarexConfigError(\r\n \"Zarex: `key` is missing or malformed (expected the project API key, e.g. from process.env.ZAREX_KEY).\"\r\n );\r\n }\r\n return key;\r\n}\r\n\r\nexport function resolveBaseUrl(baseUrl: string | undefined, allowInsecureHttp = false): string {\r\n const raw = baseUrl ?? readEnv(\"ZAREX_URL\");\r\n if (!raw) {\r\n throw new ZarexConfigError(\"Zarex: `baseUrl` is required (or set the ZAREX_URL environment variable).\");\r\n }\r\n let u: URL;\r\n try {\r\n u = new URL(raw);\r\n } catch {\r\n throw new ZarexConfigError(\"Zarex: `baseUrl` is not a valid URL.\");\r\n }\r\n if (u.protocol !== \"https:\" && u.protocol !== \"http:\") {\r\n throw new ZarexConfigError(\"Zarex: `baseUrl` must use https:// (or http:// for localhost).\");\r\n }\r\n if (u.username || u.password) {\r\n throw new ZarexConfigError(\"Zarex: `baseUrl` must not contain credentials.\");\r\n }\r\n if (u.protocol === \"http:\" && !LOCAL_HOSTS.has(u.hostname) && !allowInsecureHttp) {\r\n throw new ZarexConfigError(\r\n \"Zarex: refusing to send the API key over plain http:// to a remote host. Use https:// (or set allowInsecureHttp: true at your own risk).\"\r\n );\r\n }\r\n u.search = \"\";\r\n u.hash = \"\";\r\n return u.toString().replace(/\\/+$/, \"\");\r\n}\r\n\r\nexport function createClient(opts: ClientOptions): ZarexClient {\r\n const key = validateKey(opts.key);\r\n const base = resolveBaseUrl(opts.baseUrl, opts.allowInsecureHttp);\r\n const timeoutMs = opts.timeoutMs ?? 500;\r\n if (!Number.isFinite(timeoutMs) || timeoutMs < 10 || timeoutMs > 30_000) {\r\n throw new ZarexConfigError(\"Zarex: `timeoutMs` must be between 10 and 30000.\");\r\n }\r\n const doFetch = opts.fetch ?? (typeof fetch === \"function\" ? fetch : undefined);\r\n if (!doFetch) {\r\n throw new ZarexConfigError(\"Zarex: no global fetch available; pass `fetch` in the options (Node >= 18 has one).\");\r\n }\r\n const breaker = opts.breaker === false ? null : new CircuitBreaker(opts.breaker ?? {});\r\n const url = `${base}/v1/decide`;\r\n const version = opts.sdkVersion ?? SDK_VERSION;\r\n\r\n async function decide(request: DecideRequest): Promise<DecideResponse> {\r\n if (breaker && !breaker.allow()) {\r\n throw new ZarexClientError(\"circuit_open\", \"Zarex engine temporarily skipped after repeated failures\");\r\n }\r\n\r\n const ctrl = new AbortController();\r\n const timer = setTimeout(() => ctrl.abort(), timeoutMs);\r\n let outcome: \"ok\" | \"infra\" | \"neutral\" = \"neutral\";\r\n try {\r\n let res: Response;\r\n try {\r\n res = await doFetch!(url, {\r\n method: \"POST\",\r\n headers: {\r\n \"content-type\": \"application/json\",\r\n accept: \"application/json\",\r\n authorization: `Bearer ${key}`,\r\n \"user-agent\": `zarex-js/${version}`,\r\n },\r\n body: JSON.stringify(request),\r\n signal: ctrl.signal,\r\n // Ne JAMAIS suivre une redirection : elle renverrait la clé API ailleurs.\r\n // \"manual\" plutôt que \"error\" : Cloudflare Workers ne connaît pas \"error\". La réponse 3xx est refusée plus bas.\r\n redirect: \"manual\",\r\n });\r\n } catch (err) {\r\n outcome = \"infra\";\r\n if (ctrl.signal.aborted) throw new ZarexClientError(\"timeout\", `Zarex engine did not answer within ${timeoutMs} ms`);\r\n // Le message natif peut contenir l'URL : on n'en garde que la nature.\r\n throw new ZarexClientError(\"network\", `Zarex engine unreachable (${errKind(err)})`);\r\n }\r\n\r\n if (res.type === \"opaqueredirect\" || (res.status >= 300 && res.status < 400)) {\r\n outcome = \"infra\";\r\n void res.body?.cancel().catch(() => undefined);\r\n throw new ZarexClientError(\"network\", \"Zarex engine unreachable (unexpected redirect)\");\r\n }\r\n\r\n const text = await readCapped(res, MAX_RESPONSE_BYTES, ctrl.signal).catch(() => {\r\n outcome = \"infra\";\r\n if (ctrl.signal.aborted) throw new ZarexClientError(\"timeout\", `Zarex engine did not answer within ${timeoutMs} ms`);\r\n throw new ZarexClientError(\"bad_response\", \"Zarex engine response could not be read\");\r\n });\r\n\r\n if (res.status !== 200) {\r\n const engineCode = extractEngineCode(text);\r\n const code = statusToCode(res.status);\r\n outcome = code === \"server_error\" ? \"infra\" : \"neutral\";\r\n throw new ZarexClientError(code, `Zarex engine answered HTTP ${res.status}${engineCode ? ` (${engineCode})` : \"\"}`, {\r\n status: res.status,\r\n ...(engineCode ? { engineCode } : {}),\r\n });\r\n }\r\n\r\n // Un 200 avec un corps JSON illisible/inattendu est la MÊME défaillance (« bad_response ») que celle\r\n // de readCapped juste au-dessus, et doit compter de la même façon pour le disjoncteur (« infra ») —\r\n // sans ce catch, `outcome` restait à \"neutral\" (sa valeur par défaut, jamais réécrite si parseDecision\r\n // lève), donc un moteur qui renvoie du JSON corrompu en 200 n'ouvrait jamais le disjoncteur (trouvé\r\n // lors de l'audit du 04/11/2026, voir AUDIT_SECURITE_COMPLET.md).\r\n let parsed: DecideResponse;\r\n try {\r\n parsed = parseDecision(text);\r\n } catch (err) {\r\n outcome = \"infra\";\r\n throw err;\r\n }\r\n outcome = \"ok\";\r\n return parsed;\r\n } finally {\r\n clearTimeout(timer);\r\n if (breaker) {\r\n if (outcome === \"ok\") breaker.success();\r\n else if (outcome === \"infra\") breaker.failure();\r\n else breaker.neutral();\r\n }\r\n }\r\n }\r\n\r\n return { decide };\r\n}\r\n\r\nfunction errKind(err: unknown): string {\r\n const msg = err instanceof Error ? `${err.name} ${err.message} ${(err as { cause?: { code?: string } }).cause?.code ?? \"\"}` : \"\";\r\n if (/redirect/i.test(msg)) return \"unexpected redirect\";\r\n if (/ECONNREFUSED|refused/i.test(msg)) return \"connection refused\";\r\n if (/ENOTFOUND|EAI_AGAIN|getaddrinfo/i.test(msg)) return \"DNS lookup failed\";\r\n if (/certificate|TLS|SSL|CERT/i.test(msg)) return \"TLS error\";\r\n return \"network error\";\r\n}\r\n\r\nfunction statusToCode(status: number): ZarexClientError[\"code\"] {\r\n if (status === 401) return \"unauthorized\";\r\n if (status === 403) return \"forbidden\";\r\n if (status === 429) return \"quota_exceeded\";\r\n if (status >= 500) return \"server_error\";\r\n return \"invalid_request\";\r\n}\r\n\r\nfunction extractEngineCode(text: string): string | undefined {\r\n try {\r\n const code = (JSON.parse(text) as { error?: { code?: unknown } })?.error?.code;\r\n return typeof code === \"string\" && /^[a-z_]{1,40}$/.test(code) ? code : undefined;\r\n } catch {\r\n return undefined;\r\n }\r\n}\r\n\r\n/** Lit au plus `max` octets : une réponse démesurée ne doit pas saturer la mémoire du client. */\r\nasync function readCapped(res: Response, max: number, signal: AbortSignal): Promise<string> {\r\n const declared = Number(res.headers.get(\"content-length\"));\r\n if (Number.isFinite(declared) && declared > max) throw new Error(\"too large\");\r\n if (!res.body) return await res.text();\r\n const reader = res.body.getReader();\r\n const chunks: Uint8Array[] = [];\r\n let total = 0;\r\n for (;;) {\r\n if (signal.aborted) {\r\n await reader.cancel().catch(() => undefined);\r\n throw new Error(\"aborted\");\r\n }\r\n const { done, value } = await reader.read();\r\n if (done) break;\r\n total += value.byteLength;\r\n if (total > max) {\r\n await reader.cancel().catch(() => undefined);\r\n throw new Error(\"too large\");\r\n }\r\n chunks.push(value);\r\n }\r\n const all = new Uint8Array(total);\r\n let off = 0;\r\n for (const c of chunks) {\r\n all.set(c, off);\r\n off += c.byteLength;\r\n }\r\n return new TextDecoder().decode(all);\r\n}\r\n\r\nconst VERDICTS: ReadonlySet<string> = new Set<Verdict>([\"ALLOW\", \"DENY\", \"ERROR\"]);\r\n\r\n/** Validation stricte de la réponse : ce qu'on ne comprend pas n'est jamais interprété comme un verdict. */\r\nexport function parseDecision(text: string): DecideResponse {\r\n let v: unknown;\r\n try {\r\n v = JSON.parse(text);\r\n } catch {\r\n throw new ZarexClientError(\"bad_response\", \"Zarex engine returned invalid JSON\");\r\n }\r\n const o = v as Record<string, unknown> | null;\r\n if (!o || typeof o !== \"object\" || typeof o.verdict !== \"string\" || !VERDICTS.has(o.verdict) || !Array.isArray(o.results)) {\r\n throw new ZarexClientError(\"bad_response\", \"Zarex engine returned an unexpected response\");\r\n }\r\n const results = [];\r\n for (const r of o.results as unknown[]) {\r\n const x = r as Record<string, unknown> | null;\r\n if (!x || typeof x !== \"object\" || typeof x.rule_type !== \"string\" || typeof x.verdict !== \"string\" || !VERDICTS.has(x.verdict)) {\r\n throw new ZarexClientError(\"bad_response\", \"Zarex engine returned an unexpected rule result\");\r\n }\r\n results.push({\r\n rule_type: x.rule_type,\r\n verdict: x.verdict as Verdict,\r\n is_dry_run: x.is_dry_run === true,\r\n ...(x.reason && typeof x.reason === \"object\" ? { reason: x.reason as Record<string, unknown> } : {}),\r\n });\r\n }\r\n const warnings = Array.isArray(o.warnings)\r\n ? (o.warnings as unknown[]).filter((w): w is string => typeof w === \"string\" && /^[a-z_]{1,40}$/.test(w)).slice(0, 10)\r\n : undefined;\r\n const rawIp = o.ip as Record<string, unknown> | null | undefined;\r\n const ipInfo =\r\n rawIp && typeof rawIp === \"object\" && typeof rawIp.country === \"string\" && /^[A-Z]{2}$/.test(rawIp.country) && typeof rawIp.asn === \"string\" && /^AS\\d{1,10}$/.test(rawIp.asn)\r\n ? { country: rawIp.country, asn: rawIp.asn }\r\n : undefined;\r\n return {\r\n decision_id: typeof o.decision_id === \"string\" ? o.decision_id.slice(0, 64) : \"\",\r\n verdict: o.verdict as Verdict,\r\n results,\r\n ...(typeof o.ttl_ms === \"number\" ? { ttl_ms: o.ttl_ms } : {}),\r\n ...(warnings && warnings.length ? { warnings } : {}),\r\n ...(ipInfo ? { ip: ipInfo } : {}),\r\n ...(typeof o.ruleset_version === \"string\" && /^[A-Za-z0-9._+-]{1,64}$/.test(o.ruleset_version) ? { ruleset_version: o.ruleset_version } : {}),\r\n };\r\n}"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/breaker.ts","../src/errors.ts","../src/client.ts"],"sourcesContent":["export { createClient, resolveBaseUrl, validateKey, parseDecision, SDK_VERSION } from \"./client.js\";\r\nexport type { ClientOptions, ZarexClient } from \"./client.js\";\r\nexport { CircuitBreaker } from \"./breaker.js\";\r\nexport type { BreakerOptions } from \"./breaker.js\";\r\nexport { ZarexClientError, ZarexConfigError } from \"./errors.js\";\r\nexport type { ClientErrorCode } from \"./errors.js\";\r\nexport type * from \"./types.js\";","/**\r\n * Disjoncteur côté SDK.\r\n *\r\n * Pourquoi : si le moteur est en panne ou très lent, CHAQUE requête du client\r\n * attendrait le délai maximal (ex. 500 ms) avant de laisser passer. Le site du\r\n * client serait ralenti par NOTRE panne. Après quelques échecs d'affilée, on\r\n * cesse d'appeler le moteur pendant un moment (« circuit ouvert ») : les\r\n * requêtes passent immédiatement. Puis UNE requête d'essai vérifie s'il est revenu.\r\n */\r\nexport interface BreakerOptions {\r\n /** Échecs consécutifs avant ouverture. Défaut 5. */\r\n failureThreshold?: number;\r\n /** Durée pendant laquelle on n'appelle plus le moteur. Défaut 10 000 ms. */\r\n cooldownMs?: number;\r\n /** Horloge (tests). */\r\n now?: () => number;\r\n}\r\n\r\ntype State = \"closed\" | \"open\" | \"half-open\";\r\n\r\nexport class CircuitBreaker {\r\n private state: State = \"closed\";\r\n private failures = 0;\r\n private openedAt = 0;\r\n private probing = false;\r\n private readonly threshold: number;\r\n private readonly cooldown: number;\r\n private readonly now: () => number;\r\n\r\n constructor(opts: BreakerOptions = {}) {\r\n this.threshold = opts.failureThreshold ?? 5;\r\n this.cooldown = opts.cooldownMs ?? 10_000;\r\n this.now = opts.now ?? Date.now;\r\n }\r\n\r\n /** true = l'appel est autorisé. Quand le circuit est semi-ouvert, un seul appel à la fois passe. */\r\n allow(): boolean {\r\n if (this.state === \"closed\") return true;\r\n if (this.state === \"open\") {\r\n if (this.now() - this.openedAt < this.cooldown) return false;\r\n this.state = \"half-open\";\r\n this.probing = false;\r\n }\r\n // half-open : un seul essai en vol\r\n if (this.probing) return false;\r\n this.probing = true;\r\n return true;\r\n }\r\n\r\n success(): void {\r\n this.failures = 0;\r\n this.probing = false;\r\n this.state = \"closed\";\r\n }\r\n\r\n /** À appeler pour une panne d'infrastructure (délai, réseau, 5xx), pas pour une erreur de configuration. */\r\n failure(): void {\r\n this.probing = false;\r\n if (this.state === \"half-open\") {\r\n this.trip();\r\n return;\r\n }\r\n this.failures++;\r\n if (this.failures >= this.threshold) this.trip();\r\n }\r\n \r\n /** Un appel autorisé qui se termine sans verdict sur la santé du moteur (ex. 401) : libère l'essai. */\r\n neutral(): void {\r\n this.probing = false;\r\n if (this.state === \"half-open\") this.state = \"closed\";\r\n }\r\n \r\n private trip(): void {\r\n this.state = \"open\";\r\n this.openedAt = this.now();\r\n }\r\n \r\n get isOpen(): boolean {\r\n return this.state !== \"closed\";\r\n }\r\n}","/**\r\n * Codes d'erreur du client. Le SDK ne lève JAMAIS ces erreurs jusqu'à\r\n * l'application du client : `protect()` les attrape et laisse passer la requête\r\n * (fail-open). Elles servent aux journaux et à `decision.error`.\r\n */\r\nexport type ClientErrorCode =\r\n | \"timeout\" // le moteur n'a pas répondu à temps\r\n | \"network\" // connexion impossible / coupée\r\n | \"circuit_open\" // trop d'échecs récents : appel évité pour protéger l'application\r\n | \"unauthorized\" // 401 : clé API absente, invalide ou révoquée\r\n | \"forbidden\" // 403\r\n | \"quota_exceeded\" // 429 : quota mensuel du plan atteint\r\n | \"invalid_request\" // 400/413/415 : bug du SDK ou règle invalide\r\n | \"server_error\" // 5xx\r\n | \"bad_response\"; // réponse 200 mais illisible / inattendue\r\n \r\nexport class ZarexClientError extends Error {\r\n readonly code: ClientErrorCode;\r\n readonly status?: number;\r\n /** Code d'erreur renvoyé par le moteur (ex. « too_large »), si présent. */\r\n readonly engineCode?: string;\r\n \r\n constructor(code: ClientErrorCode, message: string, opts: { status?: number; engineCode?: string } = {}) {\r\n super(message);\r\n this.name = \"ZarexClientError\";\r\n this.code = code;\r\n if (opts.status !== undefined) this.status = opts.status;\r\n if (opts.engineCode !== undefined) this.engineCode = opts.engineCode;\r\n }\r\n}\r\n \r\n/** Erreur de configuration : levée immédiatement à la création, jamais pendant une requête. */\r\nexport class ZarexConfigError extends Error {\r\n constructor(message: string) {\r\n super(message);\r\n this.name = \"ZarexConfigError\";\r\n }\r\n}","import { CircuitBreaker, type BreakerOptions } from \"./breaker.js\";\r\nimport { ZarexClientError, ZarexConfigError } from \"./errors.js\";\r\nimport type { DecideRequest, DecideResponse, Verdict } from \"./types.js\";\r\n\r\nexport interface ClientOptions {\r\n /** Clé API du projet (zarex_live_… / zarex_test_…). Jamais journalisée. */\r\n key: string;\r\n /** Adresse du service Zarex. Inutile en usage normal (défaut : le service de production) — à ne définir que pour les tests ou un environnement local. Variable ZAREX_URL acceptée aussi. */\r\n baseUrl?: string;\r\n /** Délai maximal d'un appel, en ms. Défaut 500. */\r\n timeoutMs?: number;\r\n /** Autoriser http:// vers un hôte distant (déconseillé : la clé circulerait en clair). */\r\n allowInsecureHttp?: boolean;\r\n /** Disjoncteur. `false` pour le désactiver. */\r\n breaker?: BreakerOptions | false;\r\n /** Implémentation de fetch (tests, runtimes particuliers). */\r\n fetch?: typeof fetch;\r\n /** Version annoncée au moteur. */\r\n sdkVersion?: string;\r\n}\r\n\r\nexport interface ZarexClient {\r\n decide(request: DecideRequest): Promise<DecideResponse>;\r\n}\r\n\r\nexport const SDK_VERSION = \"0.1.0\";\r\nconst MAX_RESPONSE_BYTES = 256 * 1024;\r\nconst LOCAL_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"::1\"]);\r\n/**\r\n * Adresse du service Zarex en production — un seul service partagé par tous\r\n * les clients, différenciés par leur clé API (comme Stripe/Twilio : jamais\r\n * besoin de configurer où se trouve l'API). `baseUrl`/`ZAREX_URL` restent\r\n * disponibles pour la remplacer (tests, environnement local), mais plus\r\n * personne n'a besoin de le faire pour un usage normal.\r\n */\r\nexport const DEFAULT_BASE_URL = \"https://engine.zarex.dev\";\r\n\r\nfunction readEnv(name: string): string | undefined {\r\n try {\r\n return typeof process !== \"undefined\" ? process.env?.[name] : undefined;\r\n } catch {\r\n return undefined;\r\n }\r\n}\r\n\r\n/** Valide la clé sans jamais la recopier dans un message d'erreur. */\r\nexport function validateKey(key: unknown): string {\r\n if (typeof key !== \"string\" || key.length < 16 || key.length > 256 || !/^[A-Za-z0-9_-]+$/.test(key)) {\r\n throw new ZarexConfigError(\r\n \"Zarex: `key` is missing or malformed (expected the project API key, e.g. from process.env.ZAREX_KEY).\"\r\n );\r\n }\r\n return key;\r\n}\r\n\r\nexport function resolveBaseUrl(baseUrl: string | undefined, allowInsecureHttp = false): string {\r\n const raw = baseUrl ?? readEnv(\"ZAREX_URL\") ?? DEFAULT_BASE_URL;\r\n let u: URL;\r\n try {\r\n u = new URL(raw);\r\n } catch {\r\n throw new ZarexConfigError(\"Zarex: `baseUrl` is not a valid URL.\");\r\n }\r\n if (u.protocol !== \"https:\" && u.protocol !== \"http:\") {\r\n throw new ZarexConfigError(\"Zarex: `baseUrl` must use https:// (or http:// for localhost).\");\r\n }\r\n if (u.username || u.password) {\r\n throw new ZarexConfigError(\"Zarex: `baseUrl` must not contain credentials.\");\r\n }\r\n if (u.protocol === \"http:\" && !LOCAL_HOSTS.has(u.hostname) && !allowInsecureHttp) {\r\n throw new ZarexConfigError(\r\n \"Zarex: refusing to send the API key over plain http:// to a remote host. Use https:// (or set allowInsecureHttp: true at your own risk).\"\r\n );\r\n }\r\n u.search = \"\";\r\n u.hash = \"\";\r\n return u.toString().replace(/\\/+$/, \"\");\r\n}\r\n\r\nexport function createClient(opts: ClientOptions): ZarexClient {\r\n const key = validateKey(opts.key);\r\n const base = resolveBaseUrl(opts.baseUrl, opts.allowInsecureHttp);\r\n const timeoutMs = opts.timeoutMs ?? 500;\r\n if (!Number.isFinite(timeoutMs) || timeoutMs < 10 || timeoutMs > 30_000) {\r\n throw new ZarexConfigError(\"Zarex: `timeoutMs` must be between 10 and 30000.\");\r\n }\r\n const doFetch = opts.fetch ?? (typeof fetch === \"function\" ? fetch : undefined);\r\n if (!doFetch) {\r\n throw new ZarexConfigError(\"Zarex: no global fetch available; pass `fetch` in the options (Node >= 18 has one).\");\r\n }\r\n const breaker = opts.breaker === false ? null : new CircuitBreaker(opts.breaker ?? {});\r\n const url = `${base}/v1/decide`;\r\n const version = opts.sdkVersion ?? SDK_VERSION;\r\n\r\n async function decide(request: DecideRequest): Promise<DecideResponse> {\r\n if (breaker && !breaker.allow()) {\r\n throw new ZarexClientError(\"circuit_open\", \"Zarex engine temporarily skipped after repeated failures\");\r\n }\r\n\r\n const ctrl = new AbortController();\r\n const timer = setTimeout(() => ctrl.abort(), timeoutMs);\r\n let outcome: \"ok\" | \"infra\" | \"neutral\" = \"neutral\";\r\n try {\r\n let res: Response;\r\n try {\r\n res = await doFetch!(url, {\r\n method: \"POST\",\r\n headers: {\r\n \"content-type\": \"application/json\",\r\n accept: \"application/json\",\r\n authorization: `Bearer ${key}`,\r\n \"user-agent\": `zarex-js/${version}`,\r\n },\r\n body: JSON.stringify(request),\r\n signal: ctrl.signal,\r\n // Ne JAMAIS suivre une redirection : elle renverrait la clé API ailleurs.\r\n // \"manual\" plutôt que \"error\" : Cloudflare Workers ne connaît pas \"error\". La réponse 3xx est refusée plus bas.\r\n redirect: \"manual\",\r\n });\r\n } catch (err) {\r\n outcome = \"infra\";\r\n if (ctrl.signal.aborted) throw new ZarexClientError(\"timeout\", `Zarex engine did not answer within ${timeoutMs} ms`);\r\n // Le message natif peut contenir l'URL : on n'en garde que la nature.\r\n throw new ZarexClientError(\"network\", `Zarex engine unreachable (${errKind(err)})`);\r\n }\r\n\r\n if (res.type === \"opaqueredirect\" || (res.status >= 300 && res.status < 400)) {\r\n outcome = \"infra\";\r\n void res.body?.cancel().catch(() => undefined);\r\n throw new ZarexClientError(\"network\", \"Zarex engine unreachable (unexpected redirect)\");\r\n }\r\n\r\n const text = await readCapped(res, MAX_RESPONSE_BYTES, ctrl.signal).catch(() => {\r\n outcome = \"infra\";\r\n if (ctrl.signal.aborted) throw new ZarexClientError(\"timeout\", `Zarex engine did not answer within ${timeoutMs} ms`);\r\n throw new ZarexClientError(\"bad_response\", \"Zarex engine response could not be read\");\r\n });\r\n\r\n if (res.status !== 200) {\r\n const engineCode = extractEngineCode(text);\r\n const code = statusToCode(res.status);\r\n outcome = code === \"server_error\" ? \"infra\" : \"neutral\";\r\n throw new ZarexClientError(code, `Zarex engine answered HTTP ${res.status}${engineCode ? ` (${engineCode})` : \"\"}`, {\r\n status: res.status,\r\n ...(engineCode ? { engineCode } : {}),\r\n });\r\n }\r\n\r\n // Un 200 avec un corps JSON illisible/inattendu est la MÊME défaillance (« bad_response ») que celle\r\n // de readCapped juste au-dessus, et doit compter de la même façon pour le disjoncteur (« infra ») —\r\n // sans ce catch, `outcome` restait à \"neutral\" (sa valeur par défaut, jamais réécrite si parseDecision\r\n // lève), donc un moteur qui renvoie du JSON corrompu en 200 n'ouvrait jamais le disjoncteur (trouvé\r\n // lors de l'audit du 04/11/2026, voir AUDIT_SECURITE_COMPLET.md).\r\n let parsed: DecideResponse;\r\n try {\r\n parsed = parseDecision(text);\r\n } catch (err) {\r\n outcome = \"infra\";\r\n throw err;\r\n }\r\n outcome = \"ok\";\r\n return parsed;\r\n } finally {\r\n clearTimeout(timer);\r\n if (breaker) {\r\n if (outcome === \"ok\") breaker.success();\r\n else if (outcome === \"infra\") breaker.failure();\r\n else breaker.neutral();\r\n }\r\n }\r\n }\r\n\r\n return { decide };\r\n}\r\n\r\nfunction errKind(err: unknown): string {\r\n const msg = err instanceof Error ? `${err.name} ${err.message} ${(err as { cause?: { code?: string } }).cause?.code ?? \"\"}` : \"\";\r\n if (/redirect/i.test(msg)) return \"unexpected redirect\";\r\n if (/ECONNREFUSED|refused/i.test(msg)) return \"connection refused\";\r\n if (/ENOTFOUND|EAI_AGAIN|getaddrinfo/i.test(msg)) return \"DNS lookup failed\";\r\n if (/certificate|TLS|SSL|CERT/i.test(msg)) return \"TLS error\";\r\n return \"network error\";\r\n}\r\n\r\nfunction statusToCode(status: number): ZarexClientError[\"code\"] {\r\n if (status === 401) return \"unauthorized\";\r\n if (status === 403) return \"forbidden\";\r\n if (status === 429) return \"quota_exceeded\";\r\n if (status >= 500) return \"server_error\";\r\n return \"invalid_request\";\r\n}\r\n\r\nfunction extractEngineCode(text: string): string | undefined {\r\n try {\r\n const code = (JSON.parse(text) as { error?: { code?: unknown } })?.error?.code;\r\n return typeof code === \"string\" && /^[a-z_]{1,40}$/.test(code) ? code : undefined;\r\n } catch {\r\n return undefined;\r\n }\r\n}\r\n\r\n/** Lit au plus `max` octets : une réponse démesurée ne doit pas saturer la mémoire du client. */\r\nasync function readCapped(res: Response, max: number, signal: AbortSignal): Promise<string> {\r\n const declared = Number(res.headers.get(\"content-length\"));\r\n if (Number.isFinite(declared) && declared > max) throw new Error(\"too large\");\r\n if (!res.body) return await res.text();\r\n const reader = res.body.getReader();\r\n const chunks: Uint8Array[] = [];\r\n let total = 0;\r\n for (;;) {\r\n if (signal.aborted) {\r\n await reader.cancel().catch(() => undefined);\r\n throw new Error(\"aborted\");\r\n }\r\n const { done, value } = await reader.read();\r\n if (done) break;\r\n total += value.byteLength;\r\n if (total > max) {\r\n await reader.cancel().catch(() => undefined);\r\n throw new Error(\"too large\");\r\n }\r\n chunks.push(value);\r\n }\r\n const all = new Uint8Array(total);\r\n let off = 0;\r\n for (const c of chunks) {\r\n all.set(c, off);\r\n off += c.byteLength;\r\n }\r\n return new TextDecoder().decode(all);\r\n}\r\n\r\nconst VERDICTS: ReadonlySet<string> = new Set<Verdict>([\"ALLOW\", \"DENY\", \"ERROR\"]);\r\n\r\n/** Validation stricte de la réponse : ce qu'on ne comprend pas n'est jamais interprété comme un verdict. */\r\nexport function parseDecision(text: string): DecideResponse {\r\n let v: unknown;\r\n try {\r\n v = JSON.parse(text);\r\n } catch {\r\n throw new ZarexClientError(\"bad_response\", \"Zarex engine returned invalid JSON\");\r\n }\r\n const o = v as Record<string, unknown> | null;\r\n if (!o || typeof o !== \"object\" || typeof o.verdict !== \"string\" || !VERDICTS.has(o.verdict) || !Array.isArray(o.results)) {\r\n throw new ZarexClientError(\"bad_response\", \"Zarex engine returned an unexpected response\");\r\n }\r\n const results = [];\r\n for (const r of o.results as unknown[]) {\r\n const x = r as Record<string, unknown> | null;\r\n if (!x || typeof x !== \"object\" || typeof x.rule_type !== \"string\" || typeof x.verdict !== \"string\" || !VERDICTS.has(x.verdict)) {\r\n throw new ZarexClientError(\"bad_response\", \"Zarex engine returned an unexpected rule result\");\r\n }\r\n results.push({\r\n rule_type: x.rule_type,\r\n verdict: x.verdict as Verdict,\r\n is_dry_run: x.is_dry_run === true,\r\n ...(x.reason && typeof x.reason === \"object\" ? { reason: x.reason as Record<string, unknown> } : {}),\r\n });\r\n }\r\n const warnings = Array.isArray(o.warnings)\r\n ? (o.warnings as unknown[]).filter((w): w is string => typeof w === \"string\" && /^[a-z_]{1,40}$/.test(w)).slice(0, 10)\r\n : undefined;\r\n const rawIp = o.ip as Record<string, unknown> | null | undefined;\r\n const ipInfo =\r\n rawIp && typeof rawIp === \"object\" && typeof rawIp.country === \"string\" && /^[A-Z]{2}$/.test(rawIp.country) && typeof rawIp.asn === \"string\" && /^AS\\d{1,10}$/.test(rawIp.asn)\r\n ? { country: rawIp.country, asn: rawIp.asn }\r\n : undefined;\r\n return {\r\n decision_id: typeof o.decision_id === \"string\" ? o.decision_id.slice(0, 64) : \"\",\r\n verdict: o.verdict as Verdict,\r\n results,\r\n ...(typeof o.ttl_ms === \"number\" ? { ttl_ms: o.ttl_ms } : {}),\r\n ...(warnings && warnings.length ? { warnings } : {}),\r\n ...(ipInfo ? { ip: ipInfo } : {}),\r\n ...(typeof o.ruleset_version === \"string\" && /^[A-Za-z0-9._+-]{1,64}$/.test(o.ruleset_version) ? { ruleset_version: o.ruleset_version } : {}),\r\n };\r\n}"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -182,7 +182,7 @@ interface DecideResponse {
182
182
  interface ClientOptions {
183
183
  /** Clé API du projet (zarex_live_… / zarex_test_…). Jamais journalisée. */
184
184
  key: string;
185
- /** URL du moteur, ex. https://engine.exemple.com — obligatoire (ou variable ZAREX_URL). */
185
+ /** Adresse du service Zarex. Inutile en usage normal (défaut : le service de production) — à ne définir que pour les tests ou un environnement local. Variable ZAREX_URL acceptée aussi. */
186
186
  baseUrl?: string;
187
187
  /** Délai maximal d'un appel, en ms. Défaut 500. */
188
188
  timeoutMs?: number;
package/dist/index.d.ts CHANGED
@@ -182,7 +182,7 @@ interface DecideResponse {
182
182
  interface ClientOptions {
183
183
  /** Clé API du projet (zarex_live_… / zarex_test_…). Jamais journalisée. */
184
184
  key: string;
185
- /** URL du moteur, ex. https://engine.exemple.com — obligatoire (ou variable ZAREX_URL). */
185
+ /** Adresse du service Zarex. Inutile en usage normal (défaut : le service de production) — à ne définir que pour les tests ou un environnement local. Variable ZAREX_URL acceptée aussi. */
186
186
  baseUrl?: string;
187
187
  /** Délai maximal d'un appel, en ms. Défaut 500. */
188
188
  timeoutMs?: number;
package/dist/index.js CHANGED
@@ -78,6 +78,7 @@ var ZarexConfigError = class extends Error {
78
78
  var SDK_VERSION = "0.1.0";
79
79
  var MAX_RESPONSE_BYTES = 256 * 1024;
80
80
  var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
81
+ var DEFAULT_BASE_URL = "https://engine.zarex.dev";
81
82
  function readEnv(name) {
82
83
  try {
83
84
  return typeof process !== "undefined" ? process.env?.[name] : void 0;
@@ -94,10 +95,7 @@ function validateKey(key) {
94
95
  return key;
95
96
  }
96
97
  function resolveBaseUrl(baseUrl, allowInsecureHttp = false) {
97
- const raw = baseUrl ?? readEnv("ZAREX_URL");
98
- if (!raw) {
99
- throw new ZarexConfigError("Zarex: `baseUrl` is required (or set the ZAREX_URL environment variable).");
100
- }
98
+ const raw = baseUrl ?? readEnv("ZAREX_URL") ?? DEFAULT_BASE_URL;
101
99
  let u;
102
100
  try {
103
101
  u = new URL(raw);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/breaker.ts","../src/errors.ts","../src/client.ts"],"sourcesContent":["/**\r\n * Disjoncteur côté SDK.\r\n *\r\n * Pourquoi : si le moteur est en panne ou très lent, CHAQUE requête du client\r\n * attendrait le délai maximal (ex. 500 ms) avant de laisser passer. Le site du\r\n * client serait ralenti par NOTRE panne. Après quelques échecs d'affilée, on\r\n * cesse d'appeler le moteur pendant un moment (« circuit ouvert ») : les\r\n * requêtes passent immédiatement. Puis UNE requête d'essai vérifie s'il est revenu.\r\n */\r\nexport interface BreakerOptions {\r\n /** Échecs consécutifs avant ouverture. Défaut 5. */\r\n failureThreshold?: number;\r\n /** Durée pendant laquelle on n'appelle plus le moteur. Défaut 10 000 ms. */\r\n cooldownMs?: number;\r\n /** Horloge (tests). */\r\n now?: () => number;\r\n}\r\n\r\ntype State = \"closed\" | \"open\" | \"half-open\";\r\n\r\nexport class CircuitBreaker {\r\n private state: State = \"closed\";\r\n private failures = 0;\r\n private openedAt = 0;\r\n private probing = false;\r\n private readonly threshold: number;\r\n private readonly cooldown: number;\r\n private readonly now: () => number;\r\n\r\n constructor(opts: BreakerOptions = {}) {\r\n this.threshold = opts.failureThreshold ?? 5;\r\n this.cooldown = opts.cooldownMs ?? 10_000;\r\n this.now = opts.now ?? Date.now;\r\n }\r\n\r\n /** true = l'appel est autorisé. Quand le circuit est semi-ouvert, un seul appel à la fois passe. */\r\n allow(): boolean {\r\n if (this.state === \"closed\") return true;\r\n if (this.state === \"open\") {\r\n if (this.now() - this.openedAt < this.cooldown) return false;\r\n this.state = \"half-open\";\r\n this.probing = false;\r\n }\r\n // half-open : un seul essai en vol\r\n if (this.probing) return false;\r\n this.probing = true;\r\n return true;\r\n }\r\n\r\n success(): void {\r\n this.failures = 0;\r\n this.probing = false;\r\n this.state = \"closed\";\r\n }\r\n\r\n /** À appeler pour une panne d'infrastructure (délai, réseau, 5xx), pas pour une erreur de configuration. */\r\n failure(): void {\r\n this.probing = false;\r\n if (this.state === \"half-open\") {\r\n this.trip();\r\n return;\r\n }\r\n this.failures++;\r\n if (this.failures >= this.threshold) this.trip();\r\n }\r\n \r\n /** Un appel autorisé qui se termine sans verdict sur la santé du moteur (ex. 401) : libère l'essai. */\r\n neutral(): void {\r\n this.probing = false;\r\n if (this.state === \"half-open\") this.state = \"closed\";\r\n }\r\n \r\n private trip(): void {\r\n this.state = \"open\";\r\n this.openedAt = this.now();\r\n }\r\n \r\n get isOpen(): boolean {\r\n return this.state !== \"closed\";\r\n }\r\n}","/**\r\n * Codes d'erreur du client. Le SDK ne lève JAMAIS ces erreurs jusqu'à\r\n * l'application du client : `protect()` les attrape et laisse passer la requête\r\n * (fail-open). Elles servent aux journaux et à `decision.error`.\r\n */\r\nexport type ClientErrorCode =\r\n | \"timeout\" // le moteur n'a pas répondu à temps\r\n | \"network\" // connexion impossible / coupée\r\n | \"circuit_open\" // trop d'échecs récents : appel évité pour protéger l'application\r\n | \"unauthorized\" // 401 : clé API absente, invalide ou révoquée\r\n | \"forbidden\" // 403\r\n | \"quota_exceeded\" // 429 : quota mensuel du plan atteint\r\n | \"invalid_request\" // 400/413/415 : bug du SDK ou règle invalide\r\n | \"server_error\" // 5xx\r\n | \"bad_response\"; // réponse 200 mais illisible / inattendue\r\n \r\nexport class ZarexClientError extends Error {\r\n readonly code: ClientErrorCode;\r\n readonly status?: number;\r\n /** Code d'erreur renvoyé par le moteur (ex. « too_large »), si présent. */\r\n readonly engineCode?: string;\r\n \r\n constructor(code: ClientErrorCode, message: string, opts: { status?: number; engineCode?: string } = {}) {\r\n super(message);\r\n this.name = \"ZarexClientError\";\r\n this.code = code;\r\n if (opts.status !== undefined) this.status = opts.status;\r\n if (opts.engineCode !== undefined) this.engineCode = opts.engineCode;\r\n }\r\n}\r\n \r\n/** Erreur de configuration : levée immédiatement à la création, jamais pendant une requête. */\r\nexport class ZarexConfigError extends Error {\r\n constructor(message: string) {\r\n super(message);\r\n this.name = \"ZarexConfigError\";\r\n }\r\n}","import { CircuitBreaker, type BreakerOptions } from \"./breaker.js\";\r\nimport { ZarexClientError, ZarexConfigError } from \"./errors.js\";\r\nimport type { DecideRequest, DecideResponse, Verdict } from \"./types.js\";\r\n\r\nexport interface ClientOptions {\r\n /** Clé API du projet (zarex_live_… / zarex_test_…). Jamais journalisée. */\r\n key: string;\r\n /** URL du moteur, ex. https://engine.exemple.com — obligatoire (ou variable ZAREX_URL). */\r\n baseUrl?: string;\r\n /** Délai maximal d'un appel, en ms. Défaut 500. */\r\n timeoutMs?: number;\r\n /** Autoriser http:// vers un hôte distant (déconseillé : la clé circulerait en clair). */\r\n allowInsecureHttp?: boolean;\r\n /** Disjoncteur. `false` pour le désactiver. */\r\n breaker?: BreakerOptions | false;\r\n /** Implémentation de fetch (tests, runtimes particuliers). */\r\n fetch?: typeof fetch;\r\n /** Version annoncée au moteur. */\r\n sdkVersion?: string;\r\n}\r\n\r\nexport interface ZarexClient {\r\n decide(request: DecideRequest): Promise<DecideResponse>;\r\n}\r\n\r\nexport const SDK_VERSION = \"0.1.0\";\r\nconst MAX_RESPONSE_BYTES = 256 * 1024;\r\nconst LOCAL_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"::1\"]);\r\n\r\nfunction readEnv(name: string): string | undefined {\r\n try {\r\n return typeof process !== \"undefined\" ? process.env?.[name] : undefined;\r\n } catch {\r\n return undefined;\r\n }\r\n}\r\n\r\n/** Valide la clé sans jamais la recopier dans un message d'erreur. */\r\nexport function validateKey(key: unknown): string {\r\n if (typeof key !== \"string\" || key.length < 16 || key.length > 256 || !/^[A-Za-z0-9_-]+$/.test(key)) {\r\n throw new ZarexConfigError(\r\n \"Zarex: `key` is missing or malformed (expected the project API key, e.g. from process.env.ZAREX_KEY).\"\r\n );\r\n }\r\n return key;\r\n}\r\n\r\nexport function resolveBaseUrl(baseUrl: string | undefined, allowInsecureHttp = false): string {\r\n const raw = baseUrl ?? readEnv(\"ZAREX_URL\");\r\n if (!raw) {\r\n throw new ZarexConfigError(\"Zarex: `baseUrl` is required (or set the ZAREX_URL environment variable).\");\r\n }\r\n let u: URL;\r\n try {\r\n u = new URL(raw);\r\n } catch {\r\n throw new ZarexConfigError(\"Zarex: `baseUrl` is not a valid URL.\");\r\n }\r\n if (u.protocol !== \"https:\" && u.protocol !== \"http:\") {\r\n throw new ZarexConfigError(\"Zarex: `baseUrl` must use https:// (or http:// for localhost).\");\r\n }\r\n if (u.username || u.password) {\r\n throw new ZarexConfigError(\"Zarex: `baseUrl` must not contain credentials.\");\r\n }\r\n if (u.protocol === \"http:\" && !LOCAL_HOSTS.has(u.hostname) && !allowInsecureHttp) {\r\n throw new ZarexConfigError(\r\n \"Zarex: refusing to send the API key over plain http:// to a remote host. Use https:// (or set allowInsecureHttp: true at your own risk).\"\r\n );\r\n }\r\n u.search = \"\";\r\n u.hash = \"\";\r\n return u.toString().replace(/\\/+$/, \"\");\r\n}\r\n\r\nexport function createClient(opts: ClientOptions): ZarexClient {\r\n const key = validateKey(opts.key);\r\n const base = resolveBaseUrl(opts.baseUrl, opts.allowInsecureHttp);\r\n const timeoutMs = opts.timeoutMs ?? 500;\r\n if (!Number.isFinite(timeoutMs) || timeoutMs < 10 || timeoutMs > 30_000) {\r\n throw new ZarexConfigError(\"Zarex: `timeoutMs` must be between 10 and 30000.\");\r\n }\r\n const doFetch = opts.fetch ?? (typeof fetch === \"function\" ? fetch : undefined);\r\n if (!doFetch) {\r\n throw new ZarexConfigError(\"Zarex: no global fetch available; pass `fetch` in the options (Node >= 18 has one).\");\r\n }\r\n const breaker = opts.breaker === false ? null : new CircuitBreaker(opts.breaker ?? {});\r\n const url = `${base}/v1/decide`;\r\n const version = opts.sdkVersion ?? SDK_VERSION;\r\n\r\n async function decide(request: DecideRequest): Promise<DecideResponse> {\r\n if (breaker && !breaker.allow()) {\r\n throw new ZarexClientError(\"circuit_open\", \"Zarex engine temporarily skipped after repeated failures\");\r\n }\r\n\r\n const ctrl = new AbortController();\r\n const timer = setTimeout(() => ctrl.abort(), timeoutMs);\r\n let outcome: \"ok\" | \"infra\" | \"neutral\" = \"neutral\";\r\n try {\r\n let res: Response;\r\n try {\r\n res = await doFetch!(url, {\r\n method: \"POST\",\r\n headers: {\r\n \"content-type\": \"application/json\",\r\n accept: \"application/json\",\r\n authorization: `Bearer ${key}`,\r\n \"user-agent\": `zarex-js/${version}`,\r\n },\r\n body: JSON.stringify(request),\r\n signal: ctrl.signal,\r\n // Ne JAMAIS suivre une redirection : elle renverrait la clé API ailleurs.\r\n // \"manual\" plutôt que \"error\" : Cloudflare Workers ne connaît pas \"error\". La réponse 3xx est refusée plus bas.\r\n redirect: \"manual\",\r\n });\r\n } catch (err) {\r\n outcome = \"infra\";\r\n if (ctrl.signal.aborted) throw new ZarexClientError(\"timeout\", `Zarex engine did not answer within ${timeoutMs} ms`);\r\n // Le message natif peut contenir l'URL : on n'en garde que la nature.\r\n throw new ZarexClientError(\"network\", `Zarex engine unreachable (${errKind(err)})`);\r\n }\r\n\r\n if (res.type === \"opaqueredirect\" || (res.status >= 300 && res.status < 400)) {\r\n outcome = \"infra\";\r\n void res.body?.cancel().catch(() => undefined);\r\n throw new ZarexClientError(\"network\", \"Zarex engine unreachable (unexpected redirect)\");\r\n }\r\n\r\n const text = await readCapped(res, MAX_RESPONSE_BYTES, ctrl.signal).catch(() => {\r\n outcome = \"infra\";\r\n if (ctrl.signal.aborted) throw new ZarexClientError(\"timeout\", `Zarex engine did not answer within ${timeoutMs} ms`);\r\n throw new ZarexClientError(\"bad_response\", \"Zarex engine response could not be read\");\r\n });\r\n\r\n if (res.status !== 200) {\r\n const engineCode = extractEngineCode(text);\r\n const code = statusToCode(res.status);\r\n outcome = code === \"server_error\" ? \"infra\" : \"neutral\";\r\n throw new ZarexClientError(code, `Zarex engine answered HTTP ${res.status}${engineCode ? ` (${engineCode})` : \"\"}`, {\r\n status: res.status,\r\n ...(engineCode ? { engineCode } : {}),\r\n });\r\n }\r\n\r\n // Un 200 avec un corps JSON illisible/inattendu est la MÊME défaillance (« bad_response ») que celle\r\n // de readCapped juste au-dessus, et doit compter de la même façon pour le disjoncteur (« infra ») —\r\n // sans ce catch, `outcome` restait à \"neutral\" (sa valeur par défaut, jamais réécrite si parseDecision\r\n // lève), donc un moteur qui renvoie du JSON corrompu en 200 n'ouvrait jamais le disjoncteur (trouvé\r\n // lors de l'audit du 04/11/2026, voir AUDIT_SECURITE_COMPLET.md).\r\n let parsed: DecideResponse;\r\n try {\r\n parsed = parseDecision(text);\r\n } catch (err) {\r\n outcome = \"infra\";\r\n throw err;\r\n }\r\n outcome = \"ok\";\r\n return parsed;\r\n } finally {\r\n clearTimeout(timer);\r\n if (breaker) {\r\n if (outcome === \"ok\") breaker.success();\r\n else if (outcome === \"infra\") breaker.failure();\r\n else breaker.neutral();\r\n }\r\n }\r\n }\r\n\r\n return { decide };\r\n}\r\n\r\nfunction errKind(err: unknown): string {\r\n const msg = err instanceof Error ? `${err.name} ${err.message} ${(err as { cause?: { code?: string } }).cause?.code ?? \"\"}` : \"\";\r\n if (/redirect/i.test(msg)) return \"unexpected redirect\";\r\n if (/ECONNREFUSED|refused/i.test(msg)) return \"connection refused\";\r\n if (/ENOTFOUND|EAI_AGAIN|getaddrinfo/i.test(msg)) return \"DNS lookup failed\";\r\n if (/certificate|TLS|SSL|CERT/i.test(msg)) return \"TLS error\";\r\n return \"network error\";\r\n}\r\n\r\nfunction statusToCode(status: number): ZarexClientError[\"code\"] {\r\n if (status === 401) return \"unauthorized\";\r\n if (status === 403) return \"forbidden\";\r\n if (status === 429) return \"quota_exceeded\";\r\n if (status >= 500) return \"server_error\";\r\n return \"invalid_request\";\r\n}\r\n\r\nfunction extractEngineCode(text: string): string | undefined {\r\n try {\r\n const code = (JSON.parse(text) as { error?: { code?: unknown } })?.error?.code;\r\n return typeof code === \"string\" && /^[a-z_]{1,40}$/.test(code) ? code : undefined;\r\n } catch {\r\n return undefined;\r\n }\r\n}\r\n\r\n/** Lit au plus `max` octets : une réponse démesurée ne doit pas saturer la mémoire du client. */\r\nasync function readCapped(res: Response, max: number, signal: AbortSignal): Promise<string> {\r\n const declared = Number(res.headers.get(\"content-length\"));\r\n if (Number.isFinite(declared) && declared > max) throw new Error(\"too large\");\r\n if (!res.body) return await res.text();\r\n const reader = res.body.getReader();\r\n const chunks: Uint8Array[] = [];\r\n let total = 0;\r\n for (;;) {\r\n if (signal.aborted) {\r\n await reader.cancel().catch(() => undefined);\r\n throw new Error(\"aborted\");\r\n }\r\n const { done, value } = await reader.read();\r\n if (done) break;\r\n total += value.byteLength;\r\n if (total > max) {\r\n await reader.cancel().catch(() => undefined);\r\n throw new Error(\"too large\");\r\n }\r\n chunks.push(value);\r\n }\r\n const all = new Uint8Array(total);\r\n let off = 0;\r\n for (const c of chunks) {\r\n all.set(c, off);\r\n off += c.byteLength;\r\n }\r\n return new TextDecoder().decode(all);\r\n}\r\n\r\nconst VERDICTS: ReadonlySet<string> = new Set<Verdict>([\"ALLOW\", \"DENY\", \"ERROR\"]);\r\n\r\n/** Validation stricte de la réponse : ce qu'on ne comprend pas n'est jamais interprété comme un verdict. */\r\nexport function parseDecision(text: string): DecideResponse {\r\n let v: unknown;\r\n try {\r\n v = JSON.parse(text);\r\n } catch {\r\n throw new ZarexClientError(\"bad_response\", \"Zarex engine returned invalid JSON\");\r\n }\r\n const o = v as Record<string, unknown> | null;\r\n if (!o || typeof o !== \"object\" || typeof o.verdict !== \"string\" || !VERDICTS.has(o.verdict) || !Array.isArray(o.results)) {\r\n throw new ZarexClientError(\"bad_response\", \"Zarex engine returned an unexpected response\");\r\n }\r\n const results = [];\r\n for (const r of o.results as unknown[]) {\r\n const x = r as Record<string, unknown> | null;\r\n if (!x || typeof x !== \"object\" || typeof x.rule_type !== \"string\" || typeof x.verdict !== \"string\" || !VERDICTS.has(x.verdict)) {\r\n throw new ZarexClientError(\"bad_response\", \"Zarex engine returned an unexpected rule result\");\r\n }\r\n results.push({\r\n rule_type: x.rule_type,\r\n verdict: x.verdict as Verdict,\r\n is_dry_run: x.is_dry_run === true,\r\n ...(x.reason && typeof x.reason === \"object\" ? { reason: x.reason as Record<string, unknown> } : {}),\r\n });\r\n }\r\n const warnings = Array.isArray(o.warnings)\r\n ? (o.warnings as unknown[]).filter((w): w is string => typeof w === \"string\" && /^[a-z_]{1,40}$/.test(w)).slice(0, 10)\r\n : undefined;\r\n const rawIp = o.ip as Record<string, unknown> | null | undefined;\r\n const ipInfo =\r\n rawIp && typeof rawIp === \"object\" && typeof rawIp.country === \"string\" && /^[A-Z]{2}$/.test(rawIp.country) && typeof rawIp.asn === \"string\" && /^AS\\d{1,10}$/.test(rawIp.asn)\r\n ? { country: rawIp.country, asn: rawIp.asn }\r\n : undefined;\r\n return {\r\n decision_id: typeof o.decision_id === \"string\" ? o.decision_id.slice(0, 64) : \"\",\r\n verdict: o.verdict as Verdict,\r\n results,\r\n ...(typeof o.ttl_ms === \"number\" ? { ttl_ms: o.ttl_ms } : {}),\r\n ...(warnings && warnings.length ? { warnings } : {}),\r\n ...(ipInfo ? { ip: ipInfo } : {}),\r\n ...(typeof o.ruleset_version === \"string\" && /^[A-Za-z0-9._+-]{1,64}$/.test(o.ruleset_version) ? { ruleset_version: o.ruleset_version } : {}),\r\n };\r\n}"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/breaker.ts","../src/errors.ts","../src/client.ts"],"sourcesContent":["/**\r\n * Disjoncteur côté SDK.\r\n *\r\n * Pourquoi : si le moteur est en panne ou très lent, CHAQUE requête du client\r\n * attendrait le délai maximal (ex. 500 ms) avant de laisser passer. Le site du\r\n * client serait ralenti par NOTRE panne. Après quelques échecs d'affilée, on\r\n * cesse d'appeler le moteur pendant un moment (« circuit ouvert ») : les\r\n * requêtes passent immédiatement. Puis UNE requête d'essai vérifie s'il est revenu.\r\n */\r\nexport interface BreakerOptions {\r\n /** Échecs consécutifs avant ouverture. Défaut 5. */\r\n failureThreshold?: number;\r\n /** Durée pendant laquelle on n'appelle plus le moteur. Défaut 10 000 ms. */\r\n cooldownMs?: number;\r\n /** Horloge (tests). */\r\n now?: () => number;\r\n}\r\n\r\ntype State = \"closed\" | \"open\" | \"half-open\";\r\n\r\nexport class CircuitBreaker {\r\n private state: State = \"closed\";\r\n private failures = 0;\r\n private openedAt = 0;\r\n private probing = false;\r\n private readonly threshold: number;\r\n private readonly cooldown: number;\r\n private readonly now: () => number;\r\n\r\n constructor(opts: BreakerOptions = {}) {\r\n this.threshold = opts.failureThreshold ?? 5;\r\n this.cooldown = opts.cooldownMs ?? 10_000;\r\n this.now = opts.now ?? Date.now;\r\n }\r\n\r\n /** true = l'appel est autorisé. Quand le circuit est semi-ouvert, un seul appel à la fois passe. */\r\n allow(): boolean {\r\n if (this.state === \"closed\") return true;\r\n if (this.state === \"open\") {\r\n if (this.now() - this.openedAt < this.cooldown) return false;\r\n this.state = \"half-open\";\r\n this.probing = false;\r\n }\r\n // half-open : un seul essai en vol\r\n if (this.probing) return false;\r\n this.probing = true;\r\n return true;\r\n }\r\n\r\n success(): void {\r\n this.failures = 0;\r\n this.probing = false;\r\n this.state = \"closed\";\r\n }\r\n\r\n /** À appeler pour une panne d'infrastructure (délai, réseau, 5xx), pas pour une erreur de configuration. */\r\n failure(): void {\r\n this.probing = false;\r\n if (this.state === \"half-open\") {\r\n this.trip();\r\n return;\r\n }\r\n this.failures++;\r\n if (this.failures >= this.threshold) this.trip();\r\n }\r\n \r\n /** Un appel autorisé qui se termine sans verdict sur la santé du moteur (ex. 401) : libère l'essai. */\r\n neutral(): void {\r\n this.probing = false;\r\n if (this.state === \"half-open\") this.state = \"closed\";\r\n }\r\n \r\n private trip(): void {\r\n this.state = \"open\";\r\n this.openedAt = this.now();\r\n }\r\n \r\n get isOpen(): boolean {\r\n return this.state !== \"closed\";\r\n }\r\n}","/**\r\n * Codes d'erreur du client. Le SDK ne lève JAMAIS ces erreurs jusqu'à\r\n * l'application du client : `protect()` les attrape et laisse passer la requête\r\n * (fail-open). Elles servent aux journaux et à `decision.error`.\r\n */\r\nexport type ClientErrorCode =\r\n | \"timeout\" // le moteur n'a pas répondu à temps\r\n | \"network\" // connexion impossible / coupée\r\n | \"circuit_open\" // trop d'échecs récents : appel évité pour protéger l'application\r\n | \"unauthorized\" // 401 : clé API absente, invalide ou révoquée\r\n | \"forbidden\" // 403\r\n | \"quota_exceeded\" // 429 : quota mensuel du plan atteint\r\n | \"invalid_request\" // 400/413/415 : bug du SDK ou règle invalide\r\n | \"server_error\" // 5xx\r\n | \"bad_response\"; // réponse 200 mais illisible / inattendue\r\n \r\nexport class ZarexClientError extends Error {\r\n readonly code: ClientErrorCode;\r\n readonly status?: number;\r\n /** Code d'erreur renvoyé par le moteur (ex. « too_large »), si présent. */\r\n readonly engineCode?: string;\r\n \r\n constructor(code: ClientErrorCode, message: string, opts: { status?: number; engineCode?: string } = {}) {\r\n super(message);\r\n this.name = \"ZarexClientError\";\r\n this.code = code;\r\n if (opts.status !== undefined) this.status = opts.status;\r\n if (opts.engineCode !== undefined) this.engineCode = opts.engineCode;\r\n }\r\n}\r\n \r\n/** Erreur de configuration : levée immédiatement à la création, jamais pendant une requête. */\r\nexport class ZarexConfigError extends Error {\r\n constructor(message: string) {\r\n super(message);\r\n this.name = \"ZarexConfigError\";\r\n }\r\n}","import { CircuitBreaker, type BreakerOptions } from \"./breaker.js\";\r\nimport { ZarexClientError, ZarexConfigError } from \"./errors.js\";\r\nimport type { DecideRequest, DecideResponse, Verdict } from \"./types.js\";\r\n\r\nexport interface ClientOptions {\r\n /** Clé API du projet (zarex_live_… / zarex_test_…). Jamais journalisée. */\r\n key: string;\r\n /** Adresse du service Zarex. Inutile en usage normal (défaut : le service de production) — à ne définir que pour les tests ou un environnement local. Variable ZAREX_URL acceptée aussi. */\r\n baseUrl?: string;\r\n /** Délai maximal d'un appel, en ms. Défaut 500. */\r\n timeoutMs?: number;\r\n /** Autoriser http:// vers un hôte distant (déconseillé : la clé circulerait en clair). */\r\n allowInsecureHttp?: boolean;\r\n /** Disjoncteur. `false` pour le désactiver. */\r\n breaker?: BreakerOptions | false;\r\n /** Implémentation de fetch (tests, runtimes particuliers). */\r\n fetch?: typeof fetch;\r\n /** Version annoncée au moteur. */\r\n sdkVersion?: string;\r\n}\r\n\r\nexport interface ZarexClient {\r\n decide(request: DecideRequest): Promise<DecideResponse>;\r\n}\r\n\r\nexport const SDK_VERSION = \"0.1.0\";\r\nconst MAX_RESPONSE_BYTES = 256 * 1024;\r\nconst LOCAL_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"::1\"]);\r\n/**\r\n * Adresse du service Zarex en production — un seul service partagé par tous\r\n * les clients, différenciés par leur clé API (comme Stripe/Twilio : jamais\r\n * besoin de configurer où se trouve l'API). `baseUrl`/`ZAREX_URL` restent\r\n * disponibles pour la remplacer (tests, environnement local), mais plus\r\n * personne n'a besoin de le faire pour un usage normal.\r\n */\r\nexport const DEFAULT_BASE_URL = \"https://engine.zarex.dev\";\r\n\r\nfunction readEnv(name: string): string | undefined {\r\n try {\r\n return typeof process !== \"undefined\" ? process.env?.[name] : undefined;\r\n } catch {\r\n return undefined;\r\n }\r\n}\r\n\r\n/** Valide la clé sans jamais la recopier dans un message d'erreur. */\r\nexport function validateKey(key: unknown): string {\r\n if (typeof key !== \"string\" || key.length < 16 || key.length > 256 || !/^[A-Za-z0-9_-]+$/.test(key)) {\r\n throw new ZarexConfigError(\r\n \"Zarex: `key` is missing or malformed (expected the project API key, e.g. from process.env.ZAREX_KEY).\"\r\n );\r\n }\r\n return key;\r\n}\r\n\r\nexport function resolveBaseUrl(baseUrl: string | undefined, allowInsecureHttp = false): string {\r\n const raw = baseUrl ?? readEnv(\"ZAREX_URL\") ?? DEFAULT_BASE_URL;\r\n let u: URL;\r\n try {\r\n u = new URL(raw);\r\n } catch {\r\n throw new ZarexConfigError(\"Zarex: `baseUrl` is not a valid URL.\");\r\n }\r\n if (u.protocol !== \"https:\" && u.protocol !== \"http:\") {\r\n throw new ZarexConfigError(\"Zarex: `baseUrl` must use https:// (or http:// for localhost).\");\r\n }\r\n if (u.username || u.password) {\r\n throw new ZarexConfigError(\"Zarex: `baseUrl` must not contain credentials.\");\r\n }\r\n if (u.protocol === \"http:\" && !LOCAL_HOSTS.has(u.hostname) && !allowInsecureHttp) {\r\n throw new ZarexConfigError(\r\n \"Zarex: refusing to send the API key over plain http:// to a remote host. Use https:// (or set allowInsecureHttp: true at your own risk).\"\r\n );\r\n }\r\n u.search = \"\";\r\n u.hash = \"\";\r\n return u.toString().replace(/\\/+$/, \"\");\r\n}\r\n\r\nexport function createClient(opts: ClientOptions): ZarexClient {\r\n const key = validateKey(opts.key);\r\n const base = resolveBaseUrl(opts.baseUrl, opts.allowInsecureHttp);\r\n const timeoutMs = opts.timeoutMs ?? 500;\r\n if (!Number.isFinite(timeoutMs) || timeoutMs < 10 || timeoutMs > 30_000) {\r\n throw new ZarexConfigError(\"Zarex: `timeoutMs` must be between 10 and 30000.\");\r\n }\r\n const doFetch = opts.fetch ?? (typeof fetch === \"function\" ? fetch : undefined);\r\n if (!doFetch) {\r\n throw new ZarexConfigError(\"Zarex: no global fetch available; pass `fetch` in the options (Node >= 18 has one).\");\r\n }\r\n const breaker = opts.breaker === false ? null : new CircuitBreaker(opts.breaker ?? {});\r\n const url = `${base}/v1/decide`;\r\n const version = opts.sdkVersion ?? SDK_VERSION;\r\n\r\n async function decide(request: DecideRequest): Promise<DecideResponse> {\r\n if (breaker && !breaker.allow()) {\r\n throw new ZarexClientError(\"circuit_open\", \"Zarex engine temporarily skipped after repeated failures\");\r\n }\r\n\r\n const ctrl = new AbortController();\r\n const timer = setTimeout(() => ctrl.abort(), timeoutMs);\r\n let outcome: \"ok\" | \"infra\" | \"neutral\" = \"neutral\";\r\n try {\r\n let res: Response;\r\n try {\r\n res = await doFetch!(url, {\r\n method: \"POST\",\r\n headers: {\r\n \"content-type\": \"application/json\",\r\n accept: \"application/json\",\r\n authorization: `Bearer ${key}`,\r\n \"user-agent\": `zarex-js/${version}`,\r\n },\r\n body: JSON.stringify(request),\r\n signal: ctrl.signal,\r\n // Ne JAMAIS suivre une redirection : elle renverrait la clé API ailleurs.\r\n // \"manual\" plutôt que \"error\" : Cloudflare Workers ne connaît pas \"error\". La réponse 3xx est refusée plus bas.\r\n redirect: \"manual\",\r\n });\r\n } catch (err) {\r\n outcome = \"infra\";\r\n if (ctrl.signal.aborted) throw new ZarexClientError(\"timeout\", `Zarex engine did not answer within ${timeoutMs} ms`);\r\n // Le message natif peut contenir l'URL : on n'en garde que la nature.\r\n throw new ZarexClientError(\"network\", `Zarex engine unreachable (${errKind(err)})`);\r\n }\r\n\r\n if (res.type === \"opaqueredirect\" || (res.status >= 300 && res.status < 400)) {\r\n outcome = \"infra\";\r\n void res.body?.cancel().catch(() => undefined);\r\n throw new ZarexClientError(\"network\", \"Zarex engine unreachable (unexpected redirect)\");\r\n }\r\n\r\n const text = await readCapped(res, MAX_RESPONSE_BYTES, ctrl.signal).catch(() => {\r\n outcome = \"infra\";\r\n if (ctrl.signal.aborted) throw new ZarexClientError(\"timeout\", `Zarex engine did not answer within ${timeoutMs} ms`);\r\n throw new ZarexClientError(\"bad_response\", \"Zarex engine response could not be read\");\r\n });\r\n\r\n if (res.status !== 200) {\r\n const engineCode = extractEngineCode(text);\r\n const code = statusToCode(res.status);\r\n outcome = code === \"server_error\" ? \"infra\" : \"neutral\";\r\n throw new ZarexClientError(code, `Zarex engine answered HTTP ${res.status}${engineCode ? ` (${engineCode})` : \"\"}`, {\r\n status: res.status,\r\n ...(engineCode ? { engineCode } : {}),\r\n });\r\n }\r\n\r\n // Un 200 avec un corps JSON illisible/inattendu est la MÊME défaillance (« bad_response ») que celle\r\n // de readCapped juste au-dessus, et doit compter de la même façon pour le disjoncteur (« infra ») —\r\n // sans ce catch, `outcome` restait à \"neutral\" (sa valeur par défaut, jamais réécrite si parseDecision\r\n // lève), donc un moteur qui renvoie du JSON corrompu en 200 n'ouvrait jamais le disjoncteur (trouvé\r\n // lors de l'audit du 04/11/2026, voir AUDIT_SECURITE_COMPLET.md).\r\n let parsed: DecideResponse;\r\n try {\r\n parsed = parseDecision(text);\r\n } catch (err) {\r\n outcome = \"infra\";\r\n throw err;\r\n }\r\n outcome = \"ok\";\r\n return parsed;\r\n } finally {\r\n clearTimeout(timer);\r\n if (breaker) {\r\n if (outcome === \"ok\") breaker.success();\r\n else if (outcome === \"infra\") breaker.failure();\r\n else breaker.neutral();\r\n }\r\n }\r\n }\r\n\r\n return { decide };\r\n}\r\n\r\nfunction errKind(err: unknown): string {\r\n const msg = err instanceof Error ? `${err.name} ${err.message} ${(err as { cause?: { code?: string } }).cause?.code ?? \"\"}` : \"\";\r\n if (/redirect/i.test(msg)) return \"unexpected redirect\";\r\n if (/ECONNREFUSED|refused/i.test(msg)) return \"connection refused\";\r\n if (/ENOTFOUND|EAI_AGAIN|getaddrinfo/i.test(msg)) return \"DNS lookup failed\";\r\n if (/certificate|TLS|SSL|CERT/i.test(msg)) return \"TLS error\";\r\n return \"network error\";\r\n}\r\n\r\nfunction statusToCode(status: number): ZarexClientError[\"code\"] {\r\n if (status === 401) return \"unauthorized\";\r\n if (status === 403) return \"forbidden\";\r\n if (status === 429) return \"quota_exceeded\";\r\n if (status >= 500) return \"server_error\";\r\n return \"invalid_request\";\r\n}\r\n\r\nfunction extractEngineCode(text: string): string | undefined {\r\n try {\r\n const code = (JSON.parse(text) as { error?: { code?: unknown } })?.error?.code;\r\n return typeof code === \"string\" && /^[a-z_]{1,40}$/.test(code) ? code : undefined;\r\n } catch {\r\n return undefined;\r\n }\r\n}\r\n\r\n/** Lit au plus `max` octets : une réponse démesurée ne doit pas saturer la mémoire du client. */\r\nasync function readCapped(res: Response, max: number, signal: AbortSignal): Promise<string> {\r\n const declared = Number(res.headers.get(\"content-length\"));\r\n if (Number.isFinite(declared) && declared > max) throw new Error(\"too large\");\r\n if (!res.body) return await res.text();\r\n const reader = res.body.getReader();\r\n const chunks: Uint8Array[] = [];\r\n let total = 0;\r\n for (;;) {\r\n if (signal.aborted) {\r\n await reader.cancel().catch(() => undefined);\r\n throw new Error(\"aborted\");\r\n }\r\n const { done, value } = await reader.read();\r\n if (done) break;\r\n total += value.byteLength;\r\n if (total > max) {\r\n await reader.cancel().catch(() => undefined);\r\n throw new Error(\"too large\");\r\n }\r\n chunks.push(value);\r\n }\r\n const all = new Uint8Array(total);\r\n let off = 0;\r\n for (const c of chunks) {\r\n all.set(c, off);\r\n off += c.byteLength;\r\n }\r\n return new TextDecoder().decode(all);\r\n}\r\n\r\nconst VERDICTS: ReadonlySet<string> = new Set<Verdict>([\"ALLOW\", \"DENY\", \"ERROR\"]);\r\n\r\n/** Validation stricte de la réponse : ce qu'on ne comprend pas n'est jamais interprété comme un verdict. */\r\nexport function parseDecision(text: string): DecideResponse {\r\n let v: unknown;\r\n try {\r\n v = JSON.parse(text);\r\n } catch {\r\n throw new ZarexClientError(\"bad_response\", \"Zarex engine returned invalid JSON\");\r\n }\r\n const o = v as Record<string, unknown> | null;\r\n if (!o || typeof o !== \"object\" || typeof o.verdict !== \"string\" || !VERDICTS.has(o.verdict) || !Array.isArray(o.results)) {\r\n throw new ZarexClientError(\"bad_response\", \"Zarex engine returned an unexpected response\");\r\n }\r\n const results = [];\r\n for (const r of o.results as unknown[]) {\r\n const x = r as Record<string, unknown> | null;\r\n if (!x || typeof x !== \"object\" || typeof x.rule_type !== \"string\" || typeof x.verdict !== \"string\" || !VERDICTS.has(x.verdict)) {\r\n throw new ZarexClientError(\"bad_response\", \"Zarex engine returned an unexpected rule result\");\r\n }\r\n results.push({\r\n rule_type: x.rule_type,\r\n verdict: x.verdict as Verdict,\r\n is_dry_run: x.is_dry_run === true,\r\n ...(x.reason && typeof x.reason === \"object\" ? { reason: x.reason as Record<string, unknown> } : {}),\r\n });\r\n }\r\n const warnings = Array.isArray(o.warnings)\r\n ? (o.warnings as unknown[]).filter((w): w is string => typeof w === \"string\" && /^[a-z_]{1,40}$/.test(w)).slice(0, 10)\r\n : undefined;\r\n const rawIp = o.ip as Record<string, unknown> | null | undefined;\r\n const ipInfo =\r\n rawIp && typeof rawIp === \"object\" && typeof rawIp.country === \"string\" && /^[A-Z]{2}$/.test(rawIp.country) && typeof rawIp.asn === \"string\" && /^AS\\d{1,10}$/.test(rawIp.asn)\r\n ? { country: rawIp.country, asn: rawIp.asn }\r\n : undefined;\r\n return {\r\n decision_id: typeof o.decision_id === \"string\" ? o.decision_id.slice(0, 64) : \"\",\r\n verdict: o.verdict as Verdict,\r\n results,\r\n ...(typeof o.ttl_ms === \"number\" ? { ttl_ms: o.ttl_ms } : {}),\r\n ...(warnings && warnings.length ? { warnings } : {}),\r\n ...(ipInfo ? { ip: ipInfo } : {}),\r\n ...(typeof o.ruleset_version === \"string\" && /^[A-Za-z0-9._+-]{1,64}$/.test(o.ruleset_version) ? { ruleset_version: o.ruleset_version } : {}),\r\n };\r\n}"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zarex/client",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Zarex — client HTTP du Decision Engine (fetch, délai, disjoncteur, fail-open)",
5
5
  "license": "MIT",
6
6
  "publishConfig": {