Skip to content

v0.14.0

Choose a tag to compare

@mikecarroll mikecarroll released this 22 Sep 15:01
· 12 commits to main since this release
d2ffa3f

[0.14.0] - 2026-09-20

What's New

  • Nine new built-in providers — DeepSeek (api.deepseek.com), Mistral (api.mistral.ai), Perplexity (api.perplexity.ai), xAI (api.x.ai), Cohere (api.cohere.com/api.cohere.ai, v2 chat and v1/v2 embed only), TypeSafe Jev (api.typesafe.ai, the /v1/systemone endpoint only), Ollama (port 11434 or ollama.com, chat/generate/embed endpoints only), Amazon Bedrock (bedrock-runtime[-fips].<region>.amazonaws.com[.cn], /model/ and /openai/ paths only) and ElevenLabs (api.elevenlabs.io) are now monitored out of the box, in both Node.js and Edge runtimes. (#247, #248)
  • Azure inference coverage — Azure OpenAI dedicated hosts (openai.azure.com/.us/.cn), Azure AI Services and AI Foundry (cognitiveservices.azure.com/.us/.cn, services.ai.azure.com/.us, including Foundry project-scoped /api/projects/<project>/openai/... URLs), Azure AI Foundry serverless endpoints (inference.ai.azure.com, models.ai.azure.com) and Azure Machine Learning managed online endpoints (inference.ml.azure.com/.us) are now built-in providers. Previously the only Azure host recognized was models.inference.ai.azure.com, under GitHub Models. (#245, #246)
  • New optional pattern fields for custom patternsFile entries: requiresPathMatch, ports, and * in domains. requiresPathMatch: true makes a pattern's paths binding — the request path must start with one of them, ending on a path-segment boundary. ports lets a provider be identified by port on any host (used by Ollama's 11434), and * in a domain stands for exactly one DNS label (e.g. bedrock-runtime.*.amazonaws.com). All three are opt-in: every existing pattern, including custom ones, matches exactly as before. See docs/global-monitoring.md. (#248)
  • Note: ElevenLabs and Azure Machine Learning are matched host-wide (no path can distinguish inference traffic there), Ollama only covers its native /api/* endpoints on port 11434 or ollama.com, and the Azure OpenAI control-plane paths (/openai/files, /openai/batches, /openai/fine_tuning, /openai/models) are excluded by default so file uploads and models.list() aren't logged as inference. Request-option hostnames (https.request({ hostname: ... })) are now matched case-insensitively, same as URL-derived hostnames.

Upgrade Notes

  • More headers now show as [REDACTED] in logs. Header redaction is name-based: anything containing auth, key, token, secret or cookie (e.g. x-auth-token, www-authenticate) is redacted; rate-limit headers such as anthropic-ratelimit-tokens-remaining are exempt.
  • baseUrl with user:pass@ userinfo, a query string or a fragment now throws at construction (it could never work: the API path was swallowed into the query, or every call failed). Use the apiKey option for credentials.
  • Coolhand API read responses that aren't JSON objects (null, scalars) now throw, and createClientFile throws on metadata keys containing [, ] or control characters (they would open extra nesting levels in the multipart field name).
  • When auto-monitor has already patched http/https/fetch, new Coolhand() no longer patches again, so that instance's excludeApiPatterns/baseUrl/apiKey are not used for interception (it warns unless silent). Previously both layers wrapped every call and logged each request twice.
  • FormData, Blob and stream fetch() request bodies are logged as null (previously the useless string "[object FormData]"); typed-array bodies are now decoded as text instead of "123,34,...".
  • A custom patternsFile that native ESM builds silently ignored is now applied (Node 20.16+/22.3+; older Node in native ESM warns and keeps the built-in patterns, tracked in #253).
  • Every Coolhand API call now has a timeout (30s; 10 minutes for multipart uploads), so a hung endpoint can no longer keep pending log payloads alive forever.

Security

  • Ocp-Apim-Subscription-Key and subscription-key are now redacted from headers and query params. Azure AI Services calls authenticated with Azure API Management's subscription key had it forwarded to Coolhand in cleartext, while api-key/authorization on the same endpoint were already redacted. (#245)
  • New PatternMatchingService#sanitizeBody() redacts credentials from captured request and response bodies. Azure OpenAI's "On Your Data" feature embeds datastore credentials (an Azure AI Search admin key, a Cosmos DB or MongoDB connection string) under data_sources/dataSources; inside that subtree any key containing key, secret, password, token or connectionstring (case- and separator-insensitive) is redacted. Anywhere in a body it also redacts Anthropic mcp_servers[].authorization_token, OpenAI realtime client_secret (an ephemeral key returned in response bodies), and authorization/headers on OpenAI Responses MCP tool definitions. Bodies that aren't a single JSON document (NDJSON, a BOM-prefixed body, a top-level array, or one truncated at the 50 MB cap) are scrubbed by key name in linear time instead of being logged verbatim. Wired into both the http/https and fetch interception paths, in both the Coolhand class and the global monitor. (#245, #250)
  • xi-api-key (ElevenLabs) is now in DEFAULT_REDACTED_HEADERS, so a custom patternsFile entry for that host without a headers map can't leak it — the same defense-in-depth as x-goog-api-key and cf-aig-authorization before it.
  • Array-form request headers are now redacted. http.request/https.request accept options.headers as an array — flat (['Authorization', 'Bearer ...']) or as [name, value] pairs — but sanitizeHeaders ran Object.entries() over it, so the keys were array indexes and no credential header ever matched: a host app using that (valid) calling convention had its Authorization/x-api-key logged to Coolhand in cleartext.
  • The global monitor no longer logs the raw request body when sanitizeBody fails. sanitizeBody returns null when redaction itself throws (e.g. a request body nested deeply enough to overflow the stack), but both interception paths in global-monitor.ts then substituted the original body via ?? parsedRequestBody — logging exactly the credentials it could not strip.
  • sanitizeURL now strips user:pass@ userinfo and redacts more credential query params. A https://user:pass@host/... URL was logged with its credentials intact (reachable through the new port-based Ollama pattern), and the query-param list missed ocp-apim-subscription-key, api-key, x-api-key, client_secret, refresh_token, id_token and access-token.
  • A custom patternsFile with wrongly-typed paths or ports (or a non-string domains entry) now falls back to the built-in patterns, like any other malformed file. Previously the entry loaded, then threw inside the request matcher on every call, disabling matching for every pattern listed after it.
  • Further redaction gaps closed after a second review pass. Header names containing passw, credential, bearer, jwt or signature are now redacted; the query-param list adds api_token, auth_token, bearer_token, secret_key, private_key, access_key, auth, bearer and pwd; and inside data_sources, passwd, pwd and credential keys are redacted too.
  • sanitizeURL and sanitizeHeaders are now linear-time. A URL with ~100,000 repeated sensitive params took ~7.5s (URLSearchParams#set is quadratic on duplicates), and 40,000 repeated header names took ~10s — a synchronous stall of the host's event loop on the request hot path. Both now complete in milliseconds.
  • baseUrl validation errors no longer echo credentials. A scheme-less value (user:pass@host), leading whitespace, a password containing @, or a query string/fragment was previously quoted verbatim into the thrown error.
  • The Coolhand API's error response body is now truncated to 2,000 characters in the log line and thrown error, matching the existing cap on its read paths, so a misbehaving endpoint can't flood the host's logs.

Bug Fixes

  • The interceptor can no longer break or delay the host's own call. Capture failures are now swallowed instead of thrown into the host: req.end(callback) (a function as the first argument) no longer throws before the request is sent, fetch(url, null) no longer throws, and an error while capturing a fetch() response is no longer rethrown after a successful upstream call. The host-facing response tee now caps buffered bytes rather than lifetime bytes, so a prompt reader downloading a body larger than 50 MB is no longer cut off mid-body. (#250)
  • Aborted or destroyed http/https responses are now logged once and their dedup entry released, instead of being lost when end never fires. A request 'error' is re-thrown when Coolhand's is the only listener, matching Node's default instead of leaving the host's promise hanging.
  • Native ESM builds now honor patternsFile/COOLHAND_PATTERNS_FILE (see Upgrade Notes above).
  • Host matching: a trailing-dot FQDN (api.openai.com.) now matches, options.path is normalized like URL paths (//model/x, /a/../b) before requiresPathMatch is checked, and wildcard-only domains entries (*, **) are ignored instead of matching every host.
  • Request-body encoding was ignored when capturing req.write/req.end strings (e.g. hex), corrupting the logged body. (#227)

Maintenance

  • Domain matching now lives in one findDomainMatch helper shared by the async, sync and URL-based matchers (previously three duplicated loops), and a new test/provider-patterns.test.ts runs the same URL matrix through both the api-patterns.json patterns and the Edge-runtime fallback to catch drift between them.
  • The example scripts in examples/ now prove log delivery, not just interception. basic.js and feedback-example.js join openai-example.js/anthropic-example.js as live smoke tests against real provider/Coolhand API calls; anthropic-streaming.js gained an opt-in delivery-confirmation step. See examples/README.md.