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

findxpand

v0.1.1

Published

Server-side SEO remediation at your origin. One deploy; fixes arrive as a pushed manifest.

Readme

findxpand

Server-side SEO remediation at your origin, for Express, Connect, Fastify's http layer, Nest, or a bare http.createServer. One deploy; approved fixes arrive afterwards as a pushed manifest, with no further release.

Start here

npx findxpand@latest init
pnpm dlx findxpand@latest init
yarn dlx findxpand@latest init
bunx --bun findxpand@latest init

init reads your project rather than asking you about it: the lockfile says which package manager, package.json says which framework and runtime, and the answer is the exact install for this repository — including the cases where the answer is "not this route, use that one instead".

The whole install

NODE_OPTIONS="--require findxpand/auto" node server.js

Nothing in your application changes. FINDXPAND_TOKEN in the same environment is the only other thing needed, and findxpand init mints one for you.

Under Bun, --require does not exist and --preload is the equivalent:

bun --preload findxpand/auto server.js

Unverified. Bun was not installed on the machine this package shipped from, so that line is reasoned from Bun's node:http compatibility and has not been run by us. --preload also applies to one process, and a child bun does not inherit it — if your start script shells out to another bun, the flag belongs on that one, or preload = ["findxpand/auto"] goes in bunfig.toml.

Why not app.use()

Because there is no position in an Express stack that also uses compression where mounting by hand does the right thing. Measured 31 Aug 2026:

| mount order | result | |---|---| | findxpand then compression | skip-encoded — the page is served unmodified, /status reports healthy, the fix silently never lands | | compression then findxpand | 2069 bytes of plain HTML on the wire under content-encoding: gzip — undecodable |

Express applies each middleware's res.write patch in mount order, so whichever one is mounted first ends up innermost and sees what the others produced. There is no third position, so the step with no right answer is gone rather than reworded.

/auto sidesteps it: it hooks Server.prototype.emit('request'), so it runs outside the application entirely, and it owns the encoding negotiation — a page the manifest names is fetched from your app as identity and compressed here. A path the manifest does not name is not touched at all, so your own compression still runs on the rest of the site exactly as before.

findxpand() is still exported for embedding, and is what /auto calls.

That is the only deploy. Approved fixes arrive afterwards as a manifest pushed to /__findxpand/manifest, and no further release is needed.

Pick a cache path that survives a restart. /var/tmp is fine on a VM and wrong on a container or a serverless platform, where it is discarded and every restart drops every rule until the next push. Use a mounted volume, and if the platform has no durable disk at all, say so — the manifest-in-the-repository mode exists for exactly that and does not need one.

What it reaches, and what it does not

The hook patches emit('request') on http.Server and https.Server, so it reaches every framework that dispatches through one — Express, Connect, Koa, Fastify, Nest, and a bare http.createServer, however the listener was attached.

| Not reached | Why | Use instead | |---|---|---| | http2.createSecureServer | HTTP/2 sessions do not emit request on an http.Server | the sidecar | | Deno | no node:http server in the path | the sidecar | | Bun.serve | Bun's own server; it never constructs a node:http.Server, so there is no emit to patch | the sidecar | | Express/Koa/Fastify/Nest on Bun | reached — Bun's node:http layer does emit request | bun --preload above (unverified) | | Next.js, Nuxt | the App Router has no middleware chain to mount into and middleware.ts runs on the Edge runtime, never seeing the rendered body; Nitro builds to a serverless handler rather than a server you can wrap | the sidecar, when self-hosted | | Vercel, Netlify, GitHub Pages, S3, Akamai NetStorage | the host serves the response, so there is no process to install into and nothing to put a container in front of | the Cloudflare edge route, or a pull request |

findxpand init refuses each of those by name, with the reason and with the route that does work. A refusal with no alternative is not a refusal worth printing.

findxpand doctor

The failure this package can have is invisible by construction: attached in a process the site's traffic does not reach, or holding a manifest whose keys match no path, it serves every page untouched and every other signal looks healthy.

npx findxpand doctor                       # http://127.0.0.1:3000
npx findxpand doctor --port 8080
npx findxpand doctor --url https://staging.example

It resolves the package in your project (not the copy running the command), finds the token in --token, FINDXPAND_TOKEN or .env, reads /__findxpand/status, and then does the thing a checklist cannot: it fetches one path the manifest names and looks for the x-findxpand-origin-mw header on the answer. That header is the only first-hand evidence that your site's traffic passes through the middleware at all.

| exit | meaning | |---|---| | 0 | confirmed working — a rewrite was counted, or we saw the marker ourselves | | 1 | confirmed broken — degraded, a page without the marker, a 401, or traffic arriving that matches no rule | | 2 | could not examine — nothing answered, or it answers and has never been in the request path |

2 is never reported as health. A middleware that answers /status and has never handled a request is not a healthy one; it is one nothing has looked at, and that distinction is the whole reason this command exists.

findxpand status prints the same /status document as JSON, for a monitor.

Not a Node app? Run it as a sidecar

The same code, with a reverse proxy where your application would be. Nothing installs into your stack and it does not matter what your stack is — PHP, Rails, Java, .NET, Go, or static files behind nginx.

docker run -d --restart unless-stopped -p 8080:8080 \
  -e FINDXPAND_TOKEN=... \
  -e FINDXPAND_ORIGIN=http://your-app:3000 \
  -e FINDXPAND_CACHE_FILE=/var/lib/findxpand/manifest.json \
  -v findxpand:/var/lib/findxpand \
  ghcr.io/nextoria-information-technology-llc/findxpand-proxy:0.1.1

Then point whatever routes traffic to your app at the sidecar instead. That is the only infrastructure change.

Keep both halves of the cache — the -e and the -v. Without the volume the manifest lives inside the container, and the first restart serves your old titles back with nothing anywhere reporting a problem.

| Variable | Meaning | |---|---| | FINDXPAND_TOKEN | Required. The shared secret, as above. | | FINDXPAND_ORIGIN | Required. Where your application listens. | | FINDXPAND_PORT | What the sidecar listens on. Default 8080. | | FINDXPAND_CACHE_FILE | Manifest cache. Put it on a mounted volume. | | FINDXPAND_MANIFEST_FILE | A manifest committed to your repository, for the no-inbound-endpoint mode. | | FINDXPAND_ENABLED | false leaves it running and stops it touching responses. | | FINDXPAND_COMPRESS | Gzip on the way out. Default on. | | FINDXPAND_OVERRIDE_HOST | Send the origin its own hostname instead of the visitor's. Default off, and leave it off unless virtual-host routing needs it — the origin builds canonical URLs from Host. | | FINDXPAND_TIMEOUT_MS | How long to wait for the origin. Default 30000. | | FINDXPAND_REQUIRE_SIGNATURE | Verify the signature on every push. Default on. |

The in-process package reads FINDXPAND_TOKEN, FINDXPAND_CACHE_FILE, FINDXPAND_MANIFEST_FILE, FINDXPAND_MAX_BYTES, FINDXPAND_ENABLED and FINDXPAND_REQUIRE_SIGNATURE from the same environment.

GET /__findxpand/health answers 200 with no token, for a container platform's liveness probe. /status stays behind the token because it describes your SEO configuration.

It asks your origin for uncompressed HTML (Accept-Encoding: identity) and compresses on the way out itself. That is not optional: a rewrite cannot happen on bytes that arrived gzipped.

Every visitor gets identical bytes. There is no user-agent branch anywhere in it. Serving crawlers something different is cloaking, and the whole point of a server-side rewrite is that it needs no such trick.

Options

For findxpand(options) when you are embedding it rather than using /auto.

| Option | Meaning | |---|---| | token | Shared secret. Required; the admin endpoints 401 without it. | | cacheFile | Where a pushed manifest is persisted. Without it, every restart drops every rule until the next push. | | manifestFile | A manifest committed to your repository, for the no-inbound-endpoint mode. Read at boot; a push overrides it. | | enabled | false leaves the middleware mounted and stops it touching responses. | | maxBytes | Largest HTML body to buffer. Default 2 MB; larger responses stream through untouched. | | requireSignature | Verify the signature on every push. Default true. See below. |

What it does to a response

Only when the path has a rule, the status is 200, the content type is HTML, and nothing has already compressed it. Everything else is passed through and marked:

| x-findxpand-origin-mw | Meaning | |---|---| | 1 | rewritten | | pass | a rule existed but changed nothing | | error | the rewrite raised; your original bytes were served unchanged | | skip-encoded | it arrived already compressed, so it was passed through untouched. With /auto this cannot happen — the hook asks your app for identity and re-encodes afterwards. Mounted by hand it means something compressed the response before the rewrite could see it, and there is no mount position that avoids that | | skip-large | over maxBytes, served whole rather than truncated | | redirect | served from the manifest's redirect table | | status | a 404/410/451 rule answered before your app was reached |

On any exception, the original response is served. transform runs inside a try/catch and the buffered bytes are sent untouched if it throws — that is the reason for buffering at all. A bug of ours cannot take down a page.

A path with no rule never reaches any of this: next() is called before a single response method is patched, so your JSON APIs, file downloads and webhooks are never buffered.

setHeader and writeHead are both read

It does not matter which your application uses. Node does not surface headers passed to writeHead(status, headers) through getHeader(), so until 31 Aug 2026 an app that set its content type that way had every page passed through untouched — and /__findxpand/status reported degraded: false with considered: 0, because nothing had been examined and nothing counted that. Both forms now reach the same place. Object, flat array and list-of-pairs are all accepted, as Node accepts them.

Endpoints

Both require Authorization: Bearer <token>, are no-store, and carry x-robots-tag: noindex.

  • GET /__findxpand/status — version, rule count, age, health, and the manifest
  • POST /__findxpand/manifest — replace the rules; echoes the stored version

It tells you when it is doing nothing

/__findxpand/status reports counters, applied_age, degraded and degraded_reason as well as the rule count. This is the failure worth monitoring: attached where no rule matches, the middleware serves every page untouched and every other signal looks healthy.

{
  "degraded": true,
  "degraded_reason": "12 response(s) arrived already compressed and could not be rewritten - the middleware is mounted after compression, and needs to be mounted before it",
  "counters": { "considered": 12, "applied": 0, "skip_encoded": 12, "error": 0 }
}

Alert on degraded. applied_age is seconds since a rewrite last landed, or null if one never has.

That sentence is generated by the engine and is shared word for word by all four implementations, so it still says "mount it before compression" — the advice from before /auto existed. Read it as: something compressed these responses before we saw them, and the answer is to attach with /auto, which asks your app for identity and does the compressing itself. Rewording it here alone would mean two health verdicts that disagree.

One counter is reported but is deliberately not part of the degraded verdict. unmatched counts requests that reached the middleware and matched no rule — requests rather than responses, so images and stylesheets are in it too. It exists because considered: 0 on its own is unreadable: a manifest whose keys match nothing looks exactly like a middleware nobody has sent a request to yet, and that ambiguity is how a key-format disagreement between the sender and this package stayed invisible for a fortnight. unmatched climbing while considered stays at zero, on an install with rules loaded, means the keys are not matching your paths. It is left out of degraded until every implementation's health verdict reads it, rather than added to one of them.

findxpand doctor is the outside-in version of that reading, and the one that can also tell you our own request never reached the middleware at all.

Pushes are signed, not merely authenticated

Every manifest push carries x-findxpand-signature and x-findxpand-timestamp: HMAC-SHA256 over timestamp.body, keyed by your token. Unsigned, altered or stale pushes are refused with 401.

The bearer token proves who sent a push and says nothing about what was in it. Anything holding it — a logging proxy, a mirrored request, an old CI secret — could otherwise replay a captured manifest indefinitely, or swap the manifest inside one. The timestamp is inside the digest, so a captured push expires; the window is 300 seconds, which means the clock on that host has to be roughly right.

There is one supported reason to turn this off, and it is an intermediary that rewrites or re-encodes request bodies, since that invalidates any signature over them.

Why it never calls us

It cannot. There is no outbound request anywhere in this package, and no runtime dependencies at all. If Findxpand is unavailable, your site serves the last manifest it received, indefinitely.