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

request-signals

v0.11.0

Published

Unified client signal layer — parse structured data from HTTP requests: client IP, user agent browser and OS, bot detection, language and locale, cookies, etc.

Readme

request-signals

Parse HTTP request headers into typed, structured client signals.

request-signals is built around a chainable plugin API. You compose the signals you want, parse a portable request-like input, and get a typed result that grows as plugins are added.

The library is runtime-agnostic:

  • it does not require Node.js request objects
  • it does not require framework request objects
  • it works well with Headers, URL, and plain strings

When To Use It

Use request-signals when you want to:

  • normalize request headers into predictable structured data
  • extract client-facing signals such as user agent, language, media preferences, client hints, origin, referer, and client IP candidates
  • compose a typed request parsing pipeline with reusable plugins

When Not To Use It

Do not use request-signals as:

  • a trust engine
  • an allowlist or origin-verification layer
  • a CSRF detector
  • a proxy-trust resolver
  • a security policy framework

It parses and normalizes signals. It does not decide which headers are trustworthy in your deployment.

Install

npm install request-signals
# or
npx nypm add request-signals

High-Level Usage

The plugin API is the primary way to use this library.

import {
  createRequestSignals,
  acceptLanguagePlugin,
  clientIpPlugin,
  cookiePlugin,
  secFetchPlugin,
  userAgentPlugin,
} from 'request-signals'

const signals = createRequestSignals()
  .use(userAgentPlugin())
  .use(cookiePlugin())
  .use(acceptLanguagePlugin())
  .use(clientIpPlugin())
  .use(secFetchPlugin())

const result = await signals.parse({
  headers: new Headers({
    'user-agent':
      'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
    cookie: 'session=abc; theme=dark',
    'accept-language': 'es-ES,es;q=0.9,en;q=0.8',
    'x-forwarded-for': '203.0.113.43, 198.51.100.17',
    'sec-fetch-site': 'same-origin',
    'sec-fetch-mode': 'navigate',
    'sec-fetch-dest': 'document',
    'sec-fetch-user': '?1',
  }),
})

console.log(result.userAgent.browser.name)
console.log(result.cookies.values.session)
console.log(result.acceptLanguage.preferred)
console.log(result.clientIp.ip)
console.log(result.secFetch.site)

You can also use parseHeaders() when all you have is a header source:

import { createRequestSignals, acceptLanguagePlugin } from 'request-signals'

const signals = createRequestSignals().use(acceptLanguagePlugin())

const result = await signals.parseHeaders({
  'accept-language': 'fr-CA,fr;q=0.9,en;q=0.7',
})

console.log(result.acceptLanguage.languages)

Input Shape

The builder accepts a portable request-like input:

type HeaderSource =
  | Headers
  | Record<string, string | string[] | undefined>
  | Iterable<[string, string]>

type RequestLikeInput = {
  headers: HeaderSource
  ip?: string
}

Authoring A Custom Plugin

The built-in plugins are regular plugins. You can extend the pipeline with your own signal objects.

import { createRequestSignals } from 'request-signals'
import type { RequestSignalsPlugin } from 'request-signals/types'

type RequestIdSignal = {
  requestId: {
    raw: string | null
    present: boolean
  }
}

const requestIdPlugin = (): RequestSignalsPlugin<Record<string, never>, RequestIdSignal> => ({
  name: 'request-id',
  parse: ({ request }) => {
    let raw = request.headers.get('x-request-id')

    return {
      requestId: {
        raw,
        present: raw !== null,
      },
    }
  },
})

const signals = createRequestSignals().use(requestIdPlugin())

const result = await signals.parseHeaders({
  'x-request-id': 'req_123',
})

console.log(result.requestId.present)

Plugin Usage

Each plugin adds one namespaced signal object to the result.

userAgentPlugin()

Parses the user-agent header into a stable structured shape.

The result always includes an isBot field.

  • by default, userAgentPlugin() enables bot detection
  • if you pass userAgentPlugin({ detectBots: false }), the field is still present but remains false
import { createRequestSignals, userAgentPlugin } from 'request-signals'

const signals = createRequestSignals().use(userAgentPlugin())

const result = await signals.parseHeaders({
  'user-agent':
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
})

console.log(result.userAgent)
// {
//   raw,
//   browser: { name, version },
//   os: { name, version, versionName },
//   platform: { type, vendor, model },
//   engine: { name, version },
//   cpu: { architecture },
//   isBot
// }

Disable bot detection if you only want UA parsing:

import { createRequestSignals, userAgentPlugin } from 'request-signals'

const signals = createRequestSignals().use(
  userAgentPlugin({
    detectBots: false,
  }),
)

acceptLanguagePlugin()

Parses accept-language into ordered normalized language preferences.

import { createRequestSignals, acceptLanguagePlugin } from 'request-signals'

const signals = createRequestSignals().use(acceptLanguagePlugin())

const result = await signals.parseHeaders({
  'accept-language': 'es-ES,es;q=0.9,en;q=0.8',
})

console.log(result.acceptLanguage)
// {
//   raw: 'es-ES,es;q=0.9,en;q=0.8',
//   languages: [
//     { tag: 'es-es', quality: 1 },
//     { tag: 'es', quality: 0.9 },
//     { tag: 'en', quality: 0.8 }
//   ],
//   preferred: ['es-es', 'es', 'en']
// }

acceptEncodingPlugin()

Parses accept-encoding into ordered encoding preferences and wildcard presence.

import { createRequestSignals, acceptEncodingPlugin } from 'request-signals'

const signals = createRequestSignals().use(acceptEncodingPlugin())

const result = await signals.parseHeaders({
  'accept-encoding': 'gzip, br;q=0.8, *;q=0.1',
})

console.log(result.acceptEncoding)
// {
//   raw,
//   encodings: [
//     { encoding: 'gzip', quality: 1 },
//     { encoding: 'br', quality: 0.8 },
//     { encoding: '*', quality: 0.1 }
//   ],
//   preferred: ['gzip', 'br', '*'],
//   wildcard: true
// }

acceptPlugin()

Parses accept into ordered media ranges.

import { createRequestSignals, acceptPlugin } from 'request-signals'

const signals = createRequestSignals().use(acceptPlugin())

const result = await signals.parseHeaders({
  accept: 'text/html;level=1, application/json;q=0.8, */*;q=0.1',
})

console.log(result.accept.mediaRanges)
// [
//   {
//     value: 'text/html',
//     type: 'text',
//     subtype: 'html',
//     quality: 1,
//     parameters: { level: '1' }
//   },
//   ...
// ]

contentTypePlugin()

Parses content-type into type, subtype, mime type, and parameters.

import { createRequestSignals, contentTypePlugin } from 'request-signals'

const signals = createRequestSignals().use(contentTypePlugin())

const result = await signals.parseHeaders({
  'content-type': 'application/json; charset=utf-8',
})

console.log(result.contentType)
// {
//   raw,
//   mimeType: 'application/json',
//   type: 'application',
//   subtype: 'json',
//   parameters: { charset: 'utf-8' }
// }

cookiePlugin()

Parses the request cookie header into a simple key-value object.

  • duplicate cookies use last write wins
  • the parser/plugin supports a generic record type when you want typed cookie keys
import { createRequestSignals, cookiePlugin } from 'request-signals'

const signals =
  createRequestSignals().use(
    cookiePlugin<{
      session?: string
      theme?: string
    }>(),
  )

const result = await signals.parseHeaders({
  cookie: 'session=abc; theme=dark; session=updated',
})

console.log(result.cookies)
// {
//   raw: 'session=abc; theme=dark; session=updated',
//   values: {
//     session: 'updated',
//     theme: 'dark'
//   }
// }

clientIpPlugin()

Parses common proxy-related IP headers and RFC 7239 Forwarded into a deterministic client IP signal.

This parser is intentionally conservative and policy-free. It parses header values only; it does not decide which proxies are trusted.

import { createRequestSignals, clientIpPlugin } from 'request-signals'

const signals = createRequestSignals().use(clientIpPlugin())

const result = await signals.parseHeaders({
  'x-forwarded-for': '203.0.113.43, 198.51.100.17',
})

console.log(result.clientIp)
// {
//   ip: '203.0.113.43',
//   source: 'X-Forwarded-For',
//   raw: '203.0.113.43, 198.51.100.17'
// }

You can also customize the header priority:

import { createRequestSignals, clientIpPlugin } from 'request-signals'

const signals = createRequestSignals().use(
  clientIpPlugin({
    headerCandidates: ['X-Real-IP', 'X-Forwarded-For'],
  }),
)

originPlugin()

Parses the origin header into a minimal structured result.

This plugin is parsing-only. It does not implement allowlists, same-site decisions, or any other policy layer.

import { createRequestSignals, originPlugin } from 'request-signals'

const signals = createRequestSignals().use(originPlugin())

const result = await signals.parseHeaders({
  origin: 'https://app.example.com',
})

console.log(result.origin)
// {
//   raw: 'https://app.example.com',
//   url: URL('https://app.example.com/'),
//   valid: true
// }

The special Origin: null case is represented as:

const result = await signals.parseHeaders({
  origin: 'null',
})

console.log(result.origin)
// {
//   raw: 'null',
//   url: null,
//   valid: true
// }

refererPlugin()

Parses the referer header into a raw value plus parsed URL.

import { createRequestSignals, refererPlugin } from 'request-signals'

const signals = createRequestSignals().use(refererPlugin())

const result = await signals.parseHeaders({
  referer: 'https://example.com/docs?page=1',
})

console.log(result.referer)
// {
//   raw: 'https://example.com/docs?page=1',
//   url: URL('https://example.com/docs?page=1'),
//   valid: true
// }

secFetchPlugin()

Parses Fetch Metadata request headers:

  • sec-fetch-site
  • sec-fetch-mode
  • sec-fetch-dest
  • sec-fetch-user

These headers are useful when present, but they are not guaranteed across all browsers, clients, or request contexts.

import { createRequestSignals, secFetchPlugin } from 'request-signals'

const signals = createRequestSignals().use(secFetchPlugin())

const result = await signals.parseHeaders({
  'sec-fetch-site': 'same-origin',
  'sec-fetch-mode': 'navigate',
  'sec-fetch-dest': 'document',
  'sec-fetch-user': '?1',
})

console.log(result.secFetch)
// {
//   site: 'same-origin',
//   mode: 'navigate',
//   dest: 'document',
//   user: true
// }

secChUaPlugin()

Parses modern User-Agent Client Hints headers:

  • sec-ch-ua
  • sec-ch-ua-platform
  • sec-ch-ua-mobile

These headers are optional in practice. Presence varies by browser, browser version, request context, and client hint policy.

import { createRequestSignals, secChUaPlugin } from 'request-signals'

const signals = createRequestSignals().use(secChUaPlugin())

const result = await signals.parseHeaders({
  'sec-ch-ua': '"Chromium";v="122", "Google Chrome";v="122"',
  'sec-ch-ua-platform': '"Windows"',
  'sec-ch-ua-mobile': '?0',
})

console.log(result.secChUa)
// {
//   raw: {
//     brands: '"Chromium";v="122", "Google Chrome";v="122"',
//     platform: '"Windows"',
//     mobile: '?0'
//   },
//   brands: [
//     { brand: 'Chromium', version: '122' },
//     { brand: 'Google Chrome', version: '122' }
//   ],
//   platform: 'Windows',
//   mobile: false
// }

Set-Cookie Serializer

Set-Cookie is a response header, so it is not part of the request plugin pipeline. The library still exposes a serializer so you can generate response cookies without adding another dependency.

import { serializeSetCookie } from 'request-signals'

const responseHeaders = [
  ['set-cookie', serializeSetCookie({ name: 'a', value: '1' })],
  ['set-cookie', serializeSetCookie({ name: 'b', value: '2' })],
]

It supports:

  • expires
  • maxAge
  • domain
  • path
  • secure
  • httpOnly
  • sameSite
  • partitioned

Example:

import { serializeSetCookie } from 'request-signals'

let setCookie = serializeSetCookie({
  name: 'session',
  value: 'abc123',
  maxAge: 3600,
  path: '/',
  httpOnly: true,
  secure: true,
  sameSite: 'Lax',
})

console.log(setCookie)
// session=abc123; Max-Age=3600; Path=/; HttpOnly; SameSite=Lax; Secure

Direct Parser Imports

The plugin API should be your first choice because it gives you:

  • one consistent request parsing pipeline
  • type accumulation as plugins are chained
  • one unified result object

If you only need a single parser, you can import parsers directly:

import {
  parseAcceptLanguage,
  parseCookie,
  parseClientIp,
  parseOrigin,
  parseReferer,
  parseSecChUa,
  parseSecFetch,
  parseUserAgent,
  serializeSetCookie,
} from 'request-signals'

const acceptLanguage = parseAcceptLanguage('en-US,en;q=0.8')
const cookies = parseCookie('session=abc; theme=dark')
const clientIp = parseClientIp(new Headers({ 'x-forwarded-for': '203.0.113.43' }))
const origin = parseOrigin('https://example.com')
const referer = parseReferer('https://example.com/docs')
const secChUa = parseSecChUa(
  new Headers({
    'sec-ch-ua': '"Chromium";v="122"',
    'sec-ch-ua-platform': '"macOS"',
    'sec-ch-ua-mobile': '?0',
  }),
)
const secFetch = parseSecFetch(
  new Headers({
    'sec-fetch-site': 'same-origin',
    'sec-fetch-mode': 'navigate',
  }),
)
const userAgent = parseUserAgent('Mozilla/5.0 ...')
const setCookie = serializeSetCookie({ name: 'session', value: 'abc' })

Available direct parsers:

  • parseAcceptLanguage()
  • parsePreferredLanguage()
  • parsePreferredLang()
  • parseAcceptEncoding()
  • parseAccept()
  • parseContentType()
  • parseCookie()
  • parseClientIp()
  • parseOrigin()
  • parseReferer()
  • parseSecFetch()
  • parseSecChUa()
  • parseUserAgent()
  • serializeSetCookie()

Public Utilities

The final bundle also exposes a small utility surface:

  • isBot(userAgent: string)
    • checks a raw user-agent string with the package bot detector
  • toHeaders(headerSource)
    • normalizes a supported header input into a Headers instance

Notes

  • Parsers aim to be safe and non-throwing for malformed input.
  • Structured outputs are intentionally small and predictable.
  • The library parses signals; it does not make trust or security policy decisions for you.

License

MIT