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

@iso4/fetch

v0.0.1

Published

Hardened FetchHandler for the @iso4/sandbox runtime: DNS pinning, host allowlist, private-IP blocking, no auto-redirect

Readme

@iso4/fetch

Secure HTTP plumbing for host-authored APIs exposed to @iso4/sandbox agents.

The pattern

The sandbox bridge only carries plain data — the agent can never receive a real Response object with .json() or .text(). Exposing a raw fetch global is therefore the wrong shape.

The right pattern: author a typed host module with a domain-specific API and wire it to real HTTP internally. The agent learns the API from TypeScript types you provide and never touches raw fetch.

// agent code — clean, typed by the host
import { getUsers, queueDeletion } from 'host:api'

const users = await getUsers() // res.body is a ready-to-use array
const ack = await queueDeletion(42) // no real DELETE was sent
// host code
import { createSafeFetch } from '@iso4/fetch'

const fetch = createSafeFetch({
  rules: {
    host: 'api.example.com',
    // all routes on this origin get auth injected automatically
    middleware: async (ctx, next) => {
      ctx.req.header('authorization', `Bearer ${await vault.get('token')}`)
      return next()
    },
    routes: [
      {
        path: '/v1/users/**',
        methods: 'GET',
        // real HTTP + unwrap the response for the agent
        middleware: async (_ctx, next) => {
          const res = await next()
          return await res.json() // agent gets res.body as a parsed object
        },
      },
      {
        path: '/v1/items/:id',
        methods: 'DELETE',
        // no HTTP — synthesise the response entirely
        middleware: async (ctx, _next) => ({
          status: 202,
          headers: { 'content-type': 'application/json' },
          body: { queued: true, id: ctx.req.params['id'] },
        }),
      },
    ],
  },
})

Install

npm i @iso4/sandbox @iso4/fetch

Rules

Every request must match a declared rule before any middleware runs or any network call is made. Unmatched requests are denied immediately.

const fetch = createSafeFetch({
  rules: [
    {
      host: 'api.github.com', // exact hostname or '*.example.com' (single level)
      httpsOnly: true, // default — set false to allow HTTP
      // port: 443                 // default; list explicit ports to allow others
      routes: [
        { path: '/repos/**', methods: ['GET', 'POST'] },
        { path: '/repos/:owner/:repo/issues/:id', methods: 'GET' },
      ],
    },
    {
      host: 'raw.githubusercontent.com',
      routes: [{ path: '/**', methods: 'GET' }],
    },
  ],
})

Path patterns use rou3 / URLPattern syntax:

| Pattern | Matches | | ----------------------- | -------------------------------------------------------------- | | /users/:id | /users/42 — named param, available as ctx.req.params.id | | /api/** | /api, /api/users, /api/users/123 — zero-or-more segments | | /files/:ext(png\|jpg) | /files/png — constrained param |

An empty routes: [] denies all paths on that origin.

Middleware

Middleware is the single extension point. The (ctx, next) pattern covers every use case: request mutation, response inspection, logging, and synthetic overrides.

Three levels, running in order — global wraps origin wraps route:

createSafeFetch({
  middleware: someGlobalMiddleware, // global — every request
  rules: {
    host: 'api.example.com',
    middleware: someOriginMiddleware, // origin — every request to this host
    routes: [{
      path: '/v1/**',
      middleware: someRouteMiddleware, // route — this path only
    }],
  },
})

Request mutation (auth, URL rewrite, headers)

async function someRequestMiddleware(ctx, next) {
  ctx.req.header('authorization', `Bearer ${token}`) // injected, agent never sees it
  ctx.req.setUrl(ctx.req.url.replace('/v1/', '/v2/'))
  return next()
}

ctx.req fields: url (via setUrl()), method, headers (mutable object or header(name, val)), body (via setBody()), params, hop, raw (original unmodified bridge request).

Response inspection / logging

async function someResponseMiddleware(ctx, next) {
  const t = Date.now()
  const res = await next()
  console.log(`${ctx.req.method} ${ctx.req.url} → ${res.status} (${Date.now() - t}ms)`)
  return res
}

Response rewrite (replaces transform)

async function someResponseRewriteMiddleware(_ctx, next) {
  const res = await next()
  const data = JSON.parse(new TextDecoder().decode(res.body as Uint8Array))
  return { ...res, body: data } // agent gets res.body as a parsed object
}

Synthetic response — no HTTP (replaces handle)

Skip next() entirely. No network call is made.

async function someSyntheticResponseMiddleware(ctx, _next) {
  return {
    status: 202,
    headers: { 'content-type': 'application/json' },
    body: { queued: true, id: ctx.req.params['id'] },
  }
}

Policy callback

For dynamic allow/deny logic that can't be expressed as static rules — per-tenant checks, rate limiting, etc. Falls back to this when no origin rule matches, or is the sole mechanism when rules is omitted.

createSafeFetch({
  policy: async ({ host, method, resolvedIp }) => {
    if (resolvedIp && isInternal(resolvedIp))
return false
    const tenant = await db.findByHost(host)
    return tenant?.allowedMethods.includes(method) ?? false
  },
})

policy runs only when no rules origin matched. If an origin matches but no route does the request is denied without consulting policy.

Security defaults

| Threat | Mitigation | | ---------------------- | ---------------------------------------------------------------------------- | | SSRF / private IP | DNS pre-resolved before every request; loopback, RFC1918, link-local blocked | | DNS rebinding | undici DNS interceptor pins the connection to the resolved IP | | Redirect bypass | No auto-follow by default; allow/deny re-checked on each hop | | Response amplification | Body streamed with maxBodyBytes cap | | Host auth leakage | Isolated undici Agent — no shared pool, cookies, or auth with the host app | | Path traversal | Paths decoded and ./..-normalised before route matching | | Double-encoded paths | Detected and rejected at parse time |

License

MIT