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

urlguard

v1.0.0

Published

A tiny TypeScript URL builder that keeps path params, query params, and policy-checked URLs separate.

Downloads

49

Readme

A tiny TypeScript URL builder for keeping path params, query params, and policy-checked URLs separate.

urlguard helps build URLs without string concatenation. This prevents common path traversal vulnerabilities: user-input can't escape the URL part they were meant for, and URLs that come from user input can be checked against an allowlist before you redirect or fetch.

Install

pnpm add urlguard

Usage

import { safeExternalUrl, safeRedirectUrl, url } from "urlguard";

const apiUrl = url("https://api.example.com/v1")
  .path("/users/:userId/repos", {
    userId: 1337,
  })
  .query({
    q: "hello&admin=true",
    page: 1,
    includeArchived: false,
  })
  .toString();

// "https://api.example.com/v1/users/1337/repos?q=hello%26admin%3Dtrue&page=1&includeArchived=false"

Relative URLs work too:

url().path("/users/:id", { id: "alice" }).query({ tab: "settings" }).toString();
// "/users/alice?tab=settings"

Why not just URL encode it?

encodeURIComponent encodes characters, but it doesn't validate anything:

const href = `/files/${encodeURIComponent(userInput)}`;
// userInput = ".." -> "/files/.." -> "/"
// userInput = "" -> "/files/"

And encoding only helps if the value is decoded exactly once. Many stacks decode more than that, a reverse proxy might normalize %2e%2e or %252e%252e back into ...

Policy-checked URLs

Use safeRedirectUrl for user-controlled redirect targets and safeExternalUrl for user-controlled absolute URLs.

const next = new URL(request.url).searchParams.get("next") ?? "/";
const redirectUrl = safeRedirectUrl(next, {
  baseUrl: "https://app.example.com",
});
// next = "/dashboard?tab=billing"   -> "https://app.example.com/dashboard?tab=billing"
// next = "https://evil.example.com" -> throws UrlGuardError

const docsUrl = safeExternalUrl(userProvidedUrl, {
  allowedHosts: ["docs.example.com"],
});
// userProvidedUrl = "https://docs.example.com/start"          -> "https://docs.example.com/start"
// userProvidedUrl = "https://evil.example.com/start"          -> throws UrlGuardError
// userProvidedUrl = "https://[email protected]" -> throws UrlGuardError

By default, external URLs allow HTTPS only and reject embedded credentials. Redirect URLs resolve relative inputs against baseUrl and are restricted to that origin unless allowedOrigins or allowedHosts is set; a host allowlist replaces the base-origin default, so include the base hostname there when relative redirects should stay allowed.

API

Builder

function url(base?: string | URL, options?: UrlBuilderOptions): UrlBuilder;

class UrlBuilder {
  // Append a path template with whole-segment :params.
  path(template: string, params?: PathParams): UrlBuilder;
  // Append one dynamic path segment.
  segment(value: string | number | boolean): UrlBuilder;
  // Append query params. Arrays repeat the key, null and undefined are skipped.
  query(params: QueryParams): UrlBuilder;
  // Return the final branded URL string.
  toString(): SafeUrlString;
  // Return a URL object. Requires an absolute base.
  toURL(): URL;
}

Every method returns a new builder, so chains can be forked and reused. Path params are inferred from the template, so missing or extra params are compile errors:

url().path("/users/:id", { id: "alice" }); // ok
url().path("/users/:id", {});              // type error: missing "id"

Standalone path helpers

function path(template: string, params?: PathParams): SafePathString;
function encodePathParam(value: string | number | boolean): SafePathString;

Policy checks

function safeExternalUrl(input: string | URL, policy?: ExternalUrlPolicy): SafeExternalUrlString;
function safeRedirectUrl(input: string | URL, policy: RedirectUrlPolicy): SafeRedirectUrlString;

interface ExternalUrlPolicy {
  allowedProtocols?: readonly string[];          // default: ["https"]
  allowedOrigins?: readonly string[];            // exact scheme + host + port matches
  allowedHosts?: readonly string[];              // hostnames without ports
  allowedPorts?: readonly (number | string)[];   // effective ports, defaults like 443 count
  allowCredentials?: boolean;                    // default: false
}

interface RedirectUrlPolicy extends ExternalUrlPolicy {
  baseUrl: string | URL;                         // trusted base for relative redirects
  allowRelative?: boolean;                       // default: true
  // allowedProtocols default: ["http", "https"]
  // allowedOrigins default: the baseUrl origin, unless allowedHosts is set
}

Errors

Everything above throws UrlGuardError on bad input. Each function has a try* twin (tryUrl, tryPath, tryEncodePathParam, trySafeExternalUrl, trySafeRedirectUrl) that returns a result instead:

class UrlGuardError extends TypeError {
  code:
    | "BASE_URL_INVALID"
    | "EXTERNAL_URL_REJECTED"
    | "PATH_SEGMENT_REJECTED"
    | "PATH_TEMPLATE_INVALID"
    | "QUERY_PARAM_INVALID"
    | "REDIRECT_URL_REJECTED";
}

const result = trySafeRedirectUrl(next, { baseUrl: "https://app.example.com" });
if (result.kind === "Ok") {
  result.value; // SafeRedirectUrlString
} else {
  result.error; // UrlGuardError
}

License

MIT