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

@opengovsg/validators

v1.4.1

Published

A set of [zod](https://zod.dev/)-based validators providing sensible defaults to prevent common security vulnerabilities: path traversal, open redirects, XSS, and SSRF. Each validator is its own subpath export, so you only pull in what you use.

Readme

@opengovsg/validators

A set of zod-based validators providing sensible defaults to prevent common security vulnerabilities: path traversal, open redirects, XSS, and SSRF. Each validator is its own subpath export, so you only pull in what you use.

Install

pnpm add @opengovsg/validators zod

zod (^3.25.0 || ^4.0.0) is a peer dependency - you supply the version your app already uses.

Path validation

import { createPathSchema } from '@opengovsg/validators/path'

const pathSchema = createPathSchema({ basePath: '/app/content' })

const contentSubmissionSchema = z.object({
  fullPermalink: pathSchema,
  title: z.string(),
})

fullPermalink, when resolved relative to the working directory of the Node process, must lie within /app/content.

Email validation

import { createEmailSchema } from '@opengovsg/validators/email'

const emailSchema = createEmailSchema({ domains: [{ domain: 'gov.sg', includeSubdomains: true }] })

const formSchema = z.object({
  name: z.string(),
  email: emailSchema,
})

email must be a valid email address whose domain is gov.sg or a subdomain of it.

URL validation

UrlValidator allowlists known-safe hosts, for validating redirect targets and other URLs your own app navigates to:

import { UrlValidator } from '@opengovsg/validators/url'

const validator = new UrlValidator({
  whitelist: {
    protocols: ['http', 'https', 'mailto'],
    hosts: ['open.gov.sg'],
  },
})

validator.parse(userSuppliedRedirectUrl)

RelUrlValidator is a convenience subclass for the common case of validating a relative post-login redirect against the current origin:

import { RelUrlValidator } from '@opengovsg/validators/url'

const validator = new RelUrlValidator(window.location.origin)
window.location.pathname = validator.parsePathname(redirectUrl, '/home') // falls back to /home if invalid

createUrlSchema returns a plain zod schema for composing into a larger schema instead of using the class API.

Webhook URL validation

WebhookUrlValidator is the inverse of UrlValidator: instead of allowlisting known-safe hosts for redirects within your own app, it blocklists private/internal network targets for arbitrary, user-supplied URLs that your server will make outbound requests to - the shape of the problem when a user registers a webhook destination. There are two places to use it: when the URL is first saved, and every time you actually send to it.

webhookUrlSchema is plain sync validation and safe to use on the client (e.g. immediate form feedback) as well as the server. WebhookUrlValidator lives under the separate /server/webhook-url subpath because it resolves DNS with node:dns/promises - server-only code that a browser or edge bundle can't include. Only import WebhookUrlValidator from server-side code.

1. Validating the URL when it's saved as config

Use webhookUrlSchema wherever a webhook URL is taken as input, so a request to save an obviously unsafe URL is rejected before it ever reaches storage:

import { webhookUrlSchema } from '@opengovsg/validators/webhook-url'

const saveWebhookConfigSchema = z.object({
  url: webhookUrlSchema,
  events: z.array(z.string()),
})

export const saveWebhookConfig = async (input: unknown) => {
  const { url, events } = saveWebhookConfigSchema.parse(input) // throws ZodError if url is an obvious blocked target
  await db.webhookConfig.upsert({ url: url.href, events })
}

Built on zod's z.httpUrl(), this only accepts http/https URLs with a real domain name as the host - no literal IP address survives this check, public or private, since z.httpUrl()'s hostname format already excludes every form of IP literal (v4, v6, and obfuscated encodings like decimal/octal/hex or IPv4-mapped IPv6). localhost and its *.localhost subdomains (RFC 6761) are rejected explicitly, since those are still valid-looking domains that the format check alone wouldn't catch.

It cannot catch a hostname that merely resolves to a private address (e.g. metadata.google.internal, which is a syntactically valid domain but resolves to the 169.254.169.254 cloud metadata address) - that's what step 2 is for, which is why saving alone is not the full protection.

Not using zod for this input, but still on the server? new WebhookUrlValidator().validate(rawUrl) does the same check directly, returning a URL or throwing WebhookUrlValidationError.

2. Sending a webhook

Use WebhookUrlValidator.fetch to actually deliver, every time an event fires - not just once at registration. It re-runs the sync checks, resolves the hostname, validates every resolved IP (catching DNS rebinding: a hostname that resolved to a safe address when saved but an internal one now, or that has a mix of safe and unsafe A/AAAA records), and only then makes the request, rejecting redirects instead of following them:

import { WebhookUrlValidator } from '@opengovsg/validators/server/webhook-url'

const webhookValidator = new WebhookUrlValidator()

export const sendWebhook = async (config: { url: string; events: string[] }, payload: unknown) => {
  try {
    const response = await webhookValidator.fetch(config.url, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify(payload),
    })
    if (!response.ok) {
      // the destination is reachable but rejected the request - your usual retry/backoff logic applies
    }
  } catch (error) {
    if (error instanceof Error && error.name === 'WebhookUrlValidationError') {
      // the stored URL is no longer safe to deliver to (e.g. DNS now resolves to a private address),
      // or the destination responded with a redirect - either way, don't retry; flag or disable the
      // webhook config, or ask the user for the final destination URL instead of one that redirects
    }
    throw error
  }
}

There's no separate "validate at save time" step to remember here: fetch always validates first, so step 1 exists purely for fast feedback on obviously bad input - fetch is what actually enforces the protection, on every delivery. Redirects aren't followed at all - fetch rejects them outright with a WebhookUrlValidationError telling you to provide the final destination URL, rather than a webhook config silently pointing somewhere it was never validated to reach.

Catch WebhookUrlValidationError by checking error.name === 'WebhookUrlValidationError' rather than instanceof - the class is exported as a type only (matching UrlValidationError above), so instanceof doesn't work when importing from the public entry point.

Using a different HTTP client

Prefer WebhookUrlValidator.fetch by default - it's the only path that can't be used without also getting the redirect protection. If you need a different client (retries, streaming, an existing wrapper with its own interceptors), use validateAsync to get the same validation, then make the request yourself:

import ky from 'ky'

const validatedUrl = await webhookValidator.validateAsync(config.url)
const response = await ky.post(validatedUrl, { json: payload, redirect: 'error' })

You're now responsible for making sure your client actually rejects redirects rather than silently following them - clients differ here. ky is fetch-based and forwards redirect: 'error' straight through, same as the example above. axios, by contrast, follows redirects by default and needs explicit configuration (e.g. maxRedirects: 0) to stop - check your client's behavior rather than assuming it matches fetch's.

API reference

The full generated API reference is published to the starter-kitty docsite.