npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cogs/fetch-client

v0.4.0

Published

xior-based HTTP transport for Kubb v5 generated API clients — the single seam where base URL, auth, headers, timeouts and error shape live

Readme

@cogs/fetch-client

The HTTP transport seam that Kubb-generated API clients compile against. One place to configure base URL, auth, per-request headers, timeouts, credentials, and error shape — swap or instrument the transport without regenerating a single client.

Built on xior (the single runtime dependency), which runs on native fetch. Node 20+ / modern browsers.

Why a seam

Generated clients should never import fetch or a specific HTTP library directly. If they do, every regen re-bakes transport policy into hundreds of files, and changing auth means changing generated code.

With Kubb v5, pluginFetch bundles its own runtime into src/.kubb/client.ts. @cogs/kubb-transforms appends a seam block to it, run via output.postGenerate, which points the runtime's transport at this package's ./kubb-transport subpath:

import { kubbTransport, rejectDotSegment, normalizeKubbError } from '@cogs/fetch-client/kubb-transport'

kubbTransport sends through the configureClient() instance, so base URL, getToken, getHeaders and the timeout apply to every generated call. rejectDotSegment refuses a ./.. path param and normalizeKubbError keeps error messages free of request paths. Failures from generated code are Kubb ResponseErrors (.status, .data, and .error mirrored from .data).

The Kubb v4 client contract (Client, RequestConfig, ResponseErrorConfig, the ./client default export, FetchClient#request and its interceptors) is gone; Kubb v5 output does not import any of it.

Configure once at bootstrap

import { configureClient } from '@cogs/fetch-client'

configureClient({
  baseUrl: process.env.NEXT_PUBLIC_API_URL!,
  getToken: () => supabase.auth.getSession().then((s) => s.data.session?.access_token),
  getHeaders: () => ({ 'X-Selected-Org-Id': selectedOrgId() }),
})

Every generated call now flows through it. Call configureClient again to rebuild after a token refresh or tenant switch.

Hand-written callers (SSE, scripts) can use an isolated instance instead of the module singleton; the Kubb v5 kubbTransport always sends through the configureClient() singleton:

import { createFetchClient } from '@cogs/fetch-client'
const api = createFetchClient({ baseUrl })
const res = await api.stream('/events')

Server mode (MCP servers, workers)

A process that serves many users must not hold one ambient token. Server mode reads the bearer, extra headers and base URL from an AsyncLocalStorage store, so interleaved requests never see each other's identity:

import { configureClient } from '@cogs/fetch-client'
import { requestAuthContext } from '@cogs/fetch-client/request-context'

configureClient({ mode: 'server', baseUrl: 'https://api.example.com', context: requestAuthContext })

// in the request dispatcher
requestAuthContext.run(
  { token, headers: { 'X-Selected-Org-Id': orgId }, baseUrl },
  () => handler(args),
)
  • getToken / getHeaders are rejected by type and at runtime.
  • A request made outside a store throws before any network call; so does an empty token.
  • The store owns Authorization and every header it sets; per-call variants in any casing are dropped.
  • Any origin other than the store's (or configured) base is refused, so the bearer cannot leak. This covers stream() too.
  • A rejected request's XiorError.config.headers still holds the bearer: never serialize those errors into tool results or logs.
  • request-context is a separate Node-only subpath, so browser bundles never load node:async_hooks.

configureClient({ owner }) takes a symbol; isConfiguredBy(owner) / isConfigured() let a provider check that the live instance is still its own after another provider (for example an unauthenticated instance on public pages) took over.

Error contract

Generated calls go through kubbTransport and reject with Kubb's ResponseError on a non-2xx; network and timeout errors propagate unchanged as raw xior errors.

  • Kubb ResponseError (non-2xx): err.status, err.data (parsed body), and err.error (mirrored from data); the message is Request failed with status code N
  • Network/timeout: a raw xior error. isXiorError(err) (re-exported) narrows it, err.response is undefined, and isAbortError(err) detects timeout/abort

@cogs/react-query's createQueryClient reads err.status ?? err.response?.status to detect a 401 and trigger logout.

Body handling

  • xior serialises plain objects to JSON and sets the content type; FormData and friends are passed through so the runtime sets the boundary
  • 204/205 resolve with data: undefined
  • Query params are serialised OpenAPI-style: null/undefined dropped, arrays repeat the key, Date → ISO, nested objects → JSON

Streaming (SSE)

xior always resolves a parsed body, so a caller that needs the live Response (to read response.body) uses streamRequest(path, init) (or instance.stream(path, init)). It reuses the configured base URL, getToken and getHeaders, and sends Accept: text/event-stream. The timeout bounds only the wait for response headers, never the body, so a long-lived stream is not cut off.

Optional: multi-service config registry + URL routing trie

Everything above assumes one API. If a process talks to several generated (Kubb) APIs — each with its own base URL, auth, or headers — this package also ships an optional, additive registry + routing layer. Single-API consumers can ignore this section entirely and keep using configureClient.

The pieces:

  • Config registry (setConfig/getConfig/setConfigs/updateConfig/addConfig/getAllConfigs) — a generic Record<string, Partial<ClientConfig>> keyed by whatever name you choose (a "confkey"). Ships empty; you decide the keys.
  • Operations registry (addOperation/addOperations/getOperation/getAllOperations/updateOperation/deleteOperation) — a typed map of { path, method, confkey, operationId? }, one entry per generated endpoint, pointing at which confkey it belongs to. addOperation/addOperations take a ConflictPolicy ('throw' | 'ignore' | 'overwrite', default 'throw') for what happens when a key is registered twice with a different definition.
  • OperationTriePath — a URL + HTTP-method routing trie. initializeTrie(operations) builds a shared trie from an operations snapshot; findMatchingOperation(url, method) resolves the operation key for an outgoing request.
import {
  setConfig,
  addOperations,
  getAllOperations,
  initializeTrie,
  findMatchingOperation,
  getOperation,
  getConfig,
} from '@cogs/fetch-client'

setConfig('envmgr', { baseUrl: 'https://envmgr.internal' })
setConfig('yellowpages', { baseUrl: 'https://yellowpages.internal' })

addOperations({
  listEnvironments: { path: '/api/environments', method: 'get', confkey: 'envmgr' },
  getService: { path: '/api/services/:id', method: 'get', confkey: 'yellowpages' },
})

initializeTrie(getAllOperations())

const opKey = findMatchingOperation('/api/services/42', 'GET') // 'getService'
const confkey = opKey ? getOperation(opKey)?.confkey : undefined // 'yellowpages'
const config = confkey ? getConfig(confkey) : undefined // { baseUrl: 'https://yellowpages.internal' }

Pair config with createFetchClient (or a small wrapper of your own) to build/select the right FetchClient instance per service. This layer is pure bookkeeping — it does not itself perform requests.