Skip to main content

Safety Primitives Reference

Canonical reference for the cross-cutting safety helpers introduced in the PR #1019 review fix-up. Use this when adding new providers / modalities, or when touching any code that:

  • downloads URLs returned by external APIs
  • logs HTTP responses or arbitrary records
  • wraps provider streaming with OTel spans
  • discriminates a NeuroLink SDK instance from an opaque unknown
  • routes between image-gen and text-gen paths

All helpers live in src/lib/utils/ or src/lib/telemetry/ and are re-exported from the appropriate barrel.


1. SSRF-hardened binary download — safeDownload

Use for: every fetch() of a URL that came from somewhere other than a hardcoded literal — caller arguments, third-party API responses, redirects.

import { safeDownload } from "../utils/safeFetch.js";
import { MAX_VIDEO_BYTES } from "../utils/sizeGuard.js";

const buffer = await safeDownload(videoUrl, {
maxBytes: MAX_VIDEO_BYTES,
label: "MyProvider video",
timeoutMs: 60_000, // optional, default 60_000
signal: callerAbortSignal, // optional
});

Guarantees (these were enforced by test/continuous-test-suite-ssrf.ts, now removed — the guarantees still hold in the implementation, but nothing tests them):

  • Resolves and validates the hostname against blocked CIDRs: RFC 1918, loopback, link-local, CGNAT, IPv6 loopback / link-local / ULA, IPv4-mapped IPv6 (both dotted-decimal and hex forms), Alibaba metadata 100.100.100.200, cloud metadata 169.254.169.254, and encoded IPv4 forms (octal 0177.0.0.1, decimal-int 2130706433).
  • Pins the resolved IP onto the actual TCP connection via an undici Agent so DNS rebinding (resolver returns public IP for the guard, private IP for the real request) can't bypass.
  • Uses redirect: "manual" — a 3xx → private-IP redirect would otherwise sneak past the guard.
  • Caps total bytes via readBoundedBuffer from sizeGuard.ts.
  • Re-throws on DNS lookup failure (the previous assertSafeUrl silently allowed; both forms still rejected here).

Lower-level alternatives (when you must fetch() yourself and only want validation):

import { assertSafeUrl, validateAndResolveUrl } from "../utils/ssrfGuard.js";

// Just validate (throws on bad URL).
await assertSafeUrl(url);

// Validate + return the resolved IP so you can pin yourself.
const { url, ip, family } = await validateAndResolveUrl(url);

ESLint enforcement: none yet — prefer safeDownload over hand-rolled fetch + assertSafeUrl + readBoundedBuffer chains.

Tests: none — the 40-case ssrf suite covering H01 + H06 was removed with the unit suites. Review-enforced.


2. Log redaction — sanitizeForLog, sanitizeRecord, sanitizeHeaders

Use for: any HTTP response body, request payload, or arbitrary record that goes through logger.{info,warn,error,debug}(...).

import {
sanitizeForLog,
sanitizeRecord,
sanitizeHeaders,
} from "../utils/logSanitize.js";

// Free-form text (HTTP body, error message)
logger.warn("upstream error", { body: sanitizeForLog(raw, 500) });

// Structured payload (recursive — handles cycles via [Circular])
logger.debug("request context", sanitizeRecord(context));

// HTTP headers (redacts Authorization / Cookie / X-API-Key wholesale)
logger.debug("request headers", { headers: sanitizeHeaders(req.headers) });

CoverageSECRET_PATTERN in src/lib/utils/logSanitize.ts:

TypeTokens covered
Auth schemesBearer X, Token X (Replicate), Basic X (D-ID) — all with required whitespace
Bare token prefixessk-…, pk-…, r8_…, gsk_…, xai-…, tgp_…, fw_…, pplx-…, pa-…, jina_…, fish-…
Generic kvapi_key=…, access_token: …, secret_key=…, refresh_token=… (URLs and JSON)
Object keysapiKey, api_key, accessToken, refresh_token, secret, password, authorization, oauth, credentials
Header namesauthorization, cookie, set-cookie, x-api-key, api-key, apikey, x-auth-token, x-csrf-token

ESLint enforcement: neurolink/no-inline-secret-regex blocks any inline regex used in .replace() that matches a known token-marker substring outside logSanitize.ts. Bypass via // eslint-disable-next-line with justification only when the redaction is unrelated (e.g. CLI flag form --token foo).

Tests: none — the 41-case log-sanitize suite covering H03 + H04 was removed with the unit suites. Review-enforced.


3. OTel stream spans — withClientStreamSpan / withStreamSpan

Use for: any provider method that returns a producer (StreamResult, AsyncIterable-returning function). NOT for one-shot operations — those still use withClientSpan / withSpan.

import {
withClientStreamSpan,
tracers,
ATTR,
} from "../telemetry/index.js";

protected async executeStream(options): Promise<StreamResult> {
return withClientStreamSpan(
{
name: "neurolink.provider.stream",
tracer: tracers.provider,
attributes: {
[ATTR.GEN_AI_SYSTEM]: "<provider>",
[ATTR.GEN_AI_MODEL]: this.modelName,
[ATTR.GEN_AI_OPERATION]: "stream",
[ATTR.NL_STREAM_MODE]: true,
},
},
async () => this.executeStreamInner(options),
(r) => r.stream, // selector: extract iterable
(r, wrapped) => ({ ...r, stream: wrapped }), // setter: attach wrapped iterable
);
}

Why not withClientSpan for streams: the one-shot variant ends the span as soon as the callback's promise resolves. For a stream that means the span captures only the setup phase — the actual chunks, token usage, and finish reason all happen later (during iteration), after the span has already ended. The result: gen_ai.usage.* and gen_ai.response.finish_reason are missing from the span, duration is meaningless (tens of ms instead of seconds), and child spans outlive the parent in the trace tree.

withClientStreamSpan wraps the returned iterable so the span stays open until the consumer reaches end-of-stream / errors / aborts.

Lifecycle guarantees (these were enforced by test/continuous-test-suite-stream-span.ts, now removed — still true of the implementation, but no longer tested):

  • Span unfinished after the wrapper returns.
  • Span ends OK when the consumer reaches the end of the iterable.
  • Span ends ERROR + recordException when the consumer throws (and recordException is called BEFORE span.end() so the event isn't silently dropped).
  • Span ends ERROR immediately if the fn callback itself rejects.
  • Attributes set in options are preserved through wrapping.

Migrated providers (15 streaming spans across 13 files): baseProvider.stream (top-level), cohere, groq, xai, togetherAi, fireworks, perplexity, deepseek, llamaCpp, lmStudio, nvidiaNim, cloudflare, ollama (×2 — with/without tools), googleAiStudio, googleVertex.

Tests: none — the 105-case stream-span suite was removed with the unit suites. Its pattern sweep for legacy withClientSpan( on streaming paths is no longer run; check by hand.


Use for: the sdk?: unknown parameter on provider constructors that the factory passes in.

import { isNeuroLink } from "../neurolink.js";

constructor(
modelName?: string,
sdk?: unknown,
_region?: string,
credentials?: NeurolinkCredentials["<provider>"],
) {
const validatedNeurolink = isNeuroLink(sdk) ? sdk : undefined;
super(modelName, "<provider>" as AIProviderName, validatedNeurolink);
// ...
}

Why not duck-typing: the previous pattern was sdk && typeof sdk === "object" && "getInMemoryServers" in sdk — if NeuroLink ever renames that method, the SDK reference is silently dropped (no compile error, no runtime warning) and downstream tool / MCP / event-emitter resolution all break with a confusing "no tools" symptom.

isNeuroLink uses a Symbol.for("@juspay/neurolink/sdk-brand") that survives minification and isn't tied to method names.

Migrated providers (18): cohere, fireworks, groq, ideogram, jina, llamaCpp, lmStudio, mistral, nvidiaNim, perplexity, togetherAi, replicate, stability, recraft, xai, voyage, cloudflare, deepseek.

Tests: none. The pattern sweep in test/continuous-test-suite-stream-span.ts used to assert each provider uses isNeuroLink( and has no leftover "getInMemoryServers" in sdk reference; that suite was removed with the unit suites, so a provider added with duck-typing will no longer be caught.


5. Image-gen routing — isImageGenerationModel

Use for: detecting whether a model name should dispatch to executeImageGeneration() instead of the chat path.

import { isImageGenerationModel } from "../core/constants.js";

if (isImageGenerationModel(this.modelName) && !requestsNonImageOutput) {
return this.executeImageGeneration(options);
}

Boundary-aware match: the model name must equal a known image-model entry OR contain it as a prefix bordered by -, _, :, /, ., or end-of-string. Prevents accidental matches like a fine-tune named gpt-image-1-finetune-2025-q1 triggering image-gen routing for what's actually a chat model.

Source list: IMAGE_GENERATION_MODELS in src/lib/core/constants.ts.


6. Provider error convention — typed errors only

Use for: every formatProviderError implementation in any chat / image / embedding provider.

import {
AuthenticationError,
InvalidModelError,
NetworkError,
ProviderError,
RateLimitError,
} from "../types/index.js";

protected formatProviderError(error: unknown): Error {
if (error instanceof TimeoutError) {
return new NetworkError(`Request timed out: ${error.message}`, "<name>");
}
if (message.includes("401")) return new AuthenticationError(...);
if (message.includes("429")) return new RateLimitError(...);
if (message.includes("404")) return new InvalidModelError(...);
return new ProviderError(`<Name> error: ${message}`, "<name>");
}

Why typed: baseProvider.handleProviderError classifies errors via instanceof against the typed hierarchy and sets error.type on the OTel span ("auth_failure", "rate_limit", "network", "invalid_model", "timeout", or "provider_error"). Plain new Error() always falls through to the default tag, erasing fidelity from observability dashboards and breaking alerts that filter by error.type.

ESLint enforcement: neurolink/provider-typed-errors blocks return new Error(...) from any formatProviderError method body inside src/lib/providers/*.ts.


7. Shared logging fetch — createLoggingFetch

Use for: the fetch option on createOpenAI({...}) / similar SDK client constructors when you want non-2xx upstream responses logged with sanitized output.

import { createLoggingFetch } from "../utils/loggingFetch.js";

const cohere = createOpenAI({
apiKey: this.apiKey,
baseURL: this.baseURL,
fetch: createLoggingFetch("cohere"), // ← single shared impl
});

Body opt-in: response bodies are NOT logged by default. Set NEUROLINK_DEBUG_HTTP=1 to enable body logging — bodies are run through sanitizeForLog to redact tokens.

Previously duplicated in: cohere, xai, groq, togetherAi, fireworks, perplexity, cloudflare, llamaCpp, lmStudio, nvidiaNim, deepseek (11 near-identical copies with subtle differences). Now centralised.


8. Test scripts

⚠️ These three suites no longer exist. ssrf, log-sanitize and stream-span each imported the primitive out of src/lib/ and asserted on it directly, so they were removed when the suites became end-to-end only (CLAUDE.md rule 15).

Removed suiteCovered
test/continuous-test-suite-ssrf.tsH01 + H06 bypass categories, handler-coverage audit
test/continuous-test-suite-log-sanitize.tsH03 + H04 token formats, record/header sanitization, H04 regression grep
test/continuous-test-suite-stream-span.tsH07 span lifetime + error path + recordException ordering, M08 typed-error sweep, M09 brand check sweep

Nothing has replaced them. The primitives themselves are unchanged and the eslint rules that force callers through them still apply, but the bypass categories above are now caught only by review. Treat the checklist below as the live control.


9. Universal safety checklist (paste into PR description)

When adding any new provider / modality / handler, tick:

  • All caller-influenced URL downloads go through safeDownload (or predictionLifecycle.downloadPredictionOutput for Replicate-based handlers)
  • All HTTP response bodies sanitized via sanitizeForLog / sanitizeRecord / sanitizeHeaders (NO inline regex)
  • Streaming spans wrapped in withClientStreamSpan (NOT withClientSpan)
  • Provider SDK reference validated via isNeuroLink(sdk) (NOT duck-typing)
  • formatProviderError returns typed errors (AuthenticationError / RateLimitError / InvalidModelError / NetworkError / ProviderError / NeuroLinkError) — never plain Error
  • Reviewed by hand against §8 — the ssrf / log-sanitize / stream-span suites that used to gate this were removed with the unit suites

If a custom redaction or fetch pattern is genuinely required, add an eslint-disable-next-line with a one-line justification rather than silently bypassing the centralized helper.