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

@sealed-api-gateway/core

v1.0.2

Published

A same-origin BFF gateway for Next.js that seals every browser API call into one opaque POST. Zero dependencies, zero configuration.

Readme


What it does

Without it, your Network tab tells anyone who opens DevTools exactly how your API works:

GET   https://api.example.com/v1/orders?page=2&status=pending    200
POST  https://api.example.com/v1/orders                          201
      Authorization: Bearer eyJhbGciOi…

With it, those same calls look like this:

POST  /api/gw/_x    200
POST  /api/gw/_x    200
POST  /api/gw/_x    200

No hostname. No path. No query string. No real status code. No response body. No Cookie or Authorization header.

It covers every HTTP client at oncefetch, axios, SWR, React Query — because it wraps the two browser primitives they all end up calling. Nothing in your own code changes.

[!IMPORTANT] This is obfuscation, not secrecy. The browser has to decrypt in order to render, so anyone willing to read the running app gets the plaintext back, and your backend hostnames are still in the JavaScript bundle. It removes casual visibility; it does not make data secret. Your backend must still authorize every request.Threat model



At a glance

| | Supported | | --- | --- | | fetch | ✅ | | axios | ✅ | | React Query · SWR | ✅ | | Any client built on fetch or XMLHttpRequest | ✅ | | Next.js App Router | ✅ 13.4 – 16 | | Server Action transport (no route file) | ✅ default | | Route handler transport | ✅ fallback | | Automatic re-handshake and retry | ✅ | | Multi-instance / horizontal scaling | ✅ with SECURE_GATEWAY_KEY | | Zero runtime dependencies | ✅ | | Streaming responses | ❌ — why | | Pages Router | ❌ | | Edge runtime | ❌ — why |

Architecture

Before — every hop is visible in DevTools:

  Browser ──────────────────────────────▶ Backend
           GET /v1/orders?page=2
           Authorization: Bearer …

After — one opaque hop, and the second is server-to-server:

  Browser ──────────────▶ Your Next server ──────────────▶ Backend
           POST (sealed)                    GET /v1/orders?page=2
           no URL, no path,                 Authorization: Bearer …
           no status, no headers

Your backend now only ever sees traffic from your own server, so it can be firewalled to that single origin.

Contents


Install

npm i @sealed-api-gateway/core

Peers you almost certainly already have: next (≥ 13.4) and react (≥ 18.2). Nothing else — see zero dependencies.


Quick start

Two steps. No route file, no middleware change.

1. Mount the component

// app/layout.tsx
import { SecureApiGateway } from '@sealed-api-gateway/core';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <SecureApiGateway />
        {children}
      </body>
    </html>
  );
}

2. Point it at your backends

Any one of these — they are additive, so use whichever matches how your project already works:

# .env — if your backend URLs already live here, this is zero extra work
NEXT_PUBLIC_API_URL=https://api.example.com

# .env — an explicit list, if they do not
SEALED_GATEWAY_ORIGINS=https://api.example.com,https://auth.example.com
// sealed-gateway.config.json — if you hardcode URLs in code
{ "origins": ["https://api.example.com", "https://auth.example.com"] }

Restart, open DevTools, and your API calls are gone.

[!TIP] No axios instance to rewire, no endpoint list, no per-call opt-in. Requests to origins you have not declared — S3 presigned uploads, analytics, fonts — are left completely untouched.


Do I need a route file?

Usually not. The gateway reaches its server half through Server Actions, for which Next creates an endpoint automatically — nothing to add to your app/ directory. On the wire that is a POST to the current page URL, so there is not even a /api/gw/_x to notice.

You need one in exactly three cases:

| Situation | Why | | --- | --- | | You installed @sealed-api-gateway/console | It still reaches the gateway over HTTP for its handshake and unlock. | | Next 13.4 / 13.5 without experimental.serverActions | Server Actions are stable from Next 14; below that they need a config flag, and the route is the zero-config path. | | You prefer a real endpoint | Pass transport="route" and it is used unconditionally. |

If any apply, the CLI writes it for you:

npx sealed-gateway-init
// app/api/gw/[...path]/route.ts        ← generated for you
export const dynamic = 'force-dynamic';
export const runtime = 'nodejs';

export { GET, POST } from '@sealed-api-gateway/core/server';

Next derives routes from the filesystem and offers no API for a library to register one, so any route a package needs must physically exist in your project. The CLI writes it so you never have to.

dynamic and runtime must be literal. Next reads route-segment config by static analysis before any code runs, so a re-exported value is invisible to it. The route would be statically optimised, and every visitor served one cached handshake — containing the same ephemeral key. runtime = 'nodejs' is likewise required: the crypto uses Node APIs the Edge runtime does not provide.

npx sealed-gateway-init --dry-run           # print it, write nothing
npx sealed-gateway-init --route /api/proxy  # mount somewhere else
npx sealed-gateway-init --dir src/app       # if auto-detection misses

Choosing the transport

| transport | Behaviour | | --- | --- | | 'auto' (default) | Prefer Server Actions; fall back to the route if unavailable. Decided by trying, not by checking versions — experimental.serverActions is not observable from the client, and a wrong guess would silently disable the gateway. | | 'action' | Force Server Actions. Fails loudly if unusable. | | 'route' | Force the mounted route. |

<SecureApiGateway transport="route" />

Configuration

The gateway needs no configuration to work. Everything below is optional.

The one thing you must declare: your backends

Three sources, additive — use whichever suits your project:

| Source | Declare it like | Best for | | --- | --- | --- | | NEXT_PUBLIC_* variables | NEXT_PUBLIC_API_URL=https://api.example.com | Projects already keeping backend URLs in the environment — zero extra work | | SEALED_GATEWAY_ORIGINS | SEALED_GATEWAY_ORIGINS=https://a.example.com,https://b.example.com | URLs that live nowhere in the environment | | sealed-gateway.config.json | { "origins": ["https://api.example.com"] } | Projects that hardcode URLs in code and would rather declare them there |

JSON rather than JS for the config file, deliberately: the server reads it at runtime from inside a security boundary, and a format that had to be executed would mean running project code there. A malformed file throws naming the offending entry rather than being ignored.

[!WARNING] This list is the security boundary. The browser names its own target inside the sealed envelope, so without an allowlist the gateway would be an open relay into whatever your server can reach. Because the list comes from the environment — which a caller cannot influence — only your declared backends are reachable; anything else is rejected with 403.

Only http: and https: are honoured. file:, ftp:, data: and javascript: all parse as valid URLs and are refused.

Everything you can change

Every value below has a working default. Set none of them and the gateway behaves exactly as documented.

Environment variables — server-only

| Variable | Default | What it changes | | ----------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------- | | SECURE_GATEWAY_KEY | generated at boot | Pins the encryption keypair.Required for multi-instance deployswhy | | SECURE_GATEWAY_PASSWORD | unset | Switches on thedebug console. Unset ⇒ nothing is recorded anywhere. | | SECURE_GATEWAY_TIMEOUT_MS | 30000 | How long to wait for your backend. Range 1 000 – 300 000. | | SECURE_GATEWAY_MAX_BODY_MB | 25 | Largest sealed request accepted. Range 1 – 512. A memory guard, not a policy. | | SECURE_GATEWAY_RATE_LIMIT | 600 | Requests per minute per IP against the sealed endpoint. | | SECURE_GATEWAY_UNLOCK_TTL_HOURS | 8 | How long a console unlock lasts. Range 1 – 720. | | SECURE_GATEWAY_FORWARD_HEADERS | — | Extra request headers to forward, comma-separated. x-tenant-id,x-trace | | SECURE_GATEWAY_BLOCK_RESPONSE_HEADERS | — | Extra response headers to withhold, comma-separated. |

A malformed value throws at boot, naming the variable and the value, rather than silently falling back. SECURE_GATEWAY_TIMEOUT_MS=30s quietly becoming 30 seconds would be indistinguishable from it working.

Component props

| Prop | Type | Default | What it changes | | ------------ | ---------- | ------------- | --------------------------------------------------------------------------------- | | basePath | string | '/api/gw' | Where you mounted the route. Must match the folder holding[...path]/route.ts. |

<SecureApiGateway basePath="/api/proxy" />

What you deliberately cannot change

| Fixed behaviour | Why | | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | set-cookie is always stripped from responses | An upstream setting cookies onyour origin through the proxy is session fixation. | | content-length and content-encoding are always stripped | The body is re-serialised, so the original values would be lies the browser acts on. | | Forwarded headers are anallowlist, never a blocklist | A header the browser can set and the gateway blindly relays is a path past your app into your own backend. You can add to it; you cannot open it. | | The wire carries onlyGET and POST | Every other verb travelsinside the envelope. More verbs on the wire would be more for an observer to distinguish. |

Multi-instance deployments

Without SECURE_GATEWAY_KEY, each server process generates its own keypair. A browser that handshook with instance A gets a 409 from instance B, re-handshakes and retries — and across several instances a real share of requests exhaust their retries:

| Instances | Calls that fail | | --------- | --------------- | | 1 | 0 % | | 2 | ~13 % | | 3 | ~30 % | | 4 | ~42 % |

Single process: skip it. PM2 cluster, Kubernetes replicas, or serverless: set it.

npx sealed-gateway-key >> .env     # then restart

The generator strips any previous SECURE_GATEWAY_KEY line rather than accumulating them.

[!CAUTION] SECURE_GATEWAY_KEY is server-only. Never prefix it with NEXT_PUBLIC_ — that would inline the private key into the browser bundle and let anyone decrypt the traffic.


How it works

browser ──POST /api/gw/_x──▶ your Next server ──GET https://api.example.com/v1/orders?page=2──▶ backend
        ◀──200 (sealed)────                    ◀──200 { … }───────────────────────────────────

ECIES-style, and stateless on the server:

  1. The server holds a P-256 keypair — from SECURE_GATEWAY_KEY, or generated at boot.
  2. GET /api/gw/_pk returns its public key plus the origin allowlist.
  3. The browser generates an ephemeral P-256 pair and ECDH-derives a shared AES-GCM-256 key.
  4. Every request carries the browser's ephemeral public key, so the server re-derives the same key per request and needs no session store — it survives cold starts and horizontal scaling.
  5. Envelope on the wire: base64( ephemeralPublicKey ‖ iv ‖ ciphertext ).
  6. The response is sealed with the same key and a fresh IV. The wire status is always 200 — the real status is inside.

A stale key yields 409; the client re-handshakes and retries up to three times.


Threat model

What it defends

| Threat | How | | -------------------------------------- | ------------------------------------------------------------------------------------------------ | | Casual enumeration of your API surface | Paths, query strings and statuses never appear on the wire | | Token harvesting from the Network tab | The token travels inside the ciphertext, not in a header or cookie | | Direct traffic to your backend | It only ever receives requests from your Next server, so it can be firewalled to that one origin | | Scrapers built by reading DevTools | The observable surface is one opaque endpoint |

What it does not

| Not defended | Why | | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | | A determined operator of the browser | Decryption is client-side by necessity — hookfetch before the gateway does and the plaintext is yours | | Discovery of backend hostnames | They ship in the client bundle, because the browser must know what to route | | An unauthenticated or over-permissive backend | The gateway forwards requests; it does not authorize them | | That a request happened, its size, or its timing | Only the contents are hidden |


Compatibility

| | Supported | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | Next.js | 13.4 – 16, App Router | | React | 18.2 – 19 | | Node | ≥ 18.17 — the crypto usesglobalThis.crypto.subtle | | Runtime | nodejs. Edge is not supported. | | Context | HTTPS orlocalhost. In a non-secure context crypto.subtle is unavailable and the module declines to patch anything rather than half-working. |

On Next 13/14 with React 19, npm needs --legacy-peer-deps — those versions declare react: ^18.2.0. It builds and runs; the metadata simply predates React 19.

Pages Router is not supported. The route handler is App Router only.


Zero dependencies

"dependencies": {}, enforced by a test and by prepublishOnly. The crypto is globalThis.crypto.subtle directly — no node-forge, no jose, no polyfill. Your lockfile grows by one line.


Limitations

Design consequences, not defects:

  • Responses cannot stream. A body must be complete before it can be encrypted.
  • Upload and download progress events do not fire, and xhr.timeout is not honoured on sealed calls.
  • Request objects passed to fetch bypass sealing — their body is a stream that cannot be re-read. Pass a URL and init instead.
  • The rate limiter is per-process. Serverless instances do not share the counter; put a real limiter at the edge if you need a hard guarantee.

Troubleshooting

| Symptom | Cause | Fix | | ----------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------ | | Real URLs still visible in the Network tab | Not a secure context, so the module declined to patch | Serve over HTTPS orlocalhost | | Everything 404s | Route file missing or in the wrong folder | npx sealed-gateway-init | | 403 on a request that should work | Target origin is not in the allowlist | Add it as aNEXT_PUBLIC_*_URL and restart | | Intermittent failures under load | Multiple instances, each with its own keypair | SetSECURE_GATEWAY_KEY | | Afetch call is not sealed | ARequest object was passed | Pass a URL andinit instead | | Server throws at boot naming aSECURE_GATEWAY_* variable | A tunable has a malformed value | Fix it — the message names the variable and the value | | npm refuses to install on Next 13/14 + React 19 | Peer-dependency metadata | npm install --legacy-peer-deps |


The debug console

The gateway makes traffic unreadable in DevTools. @sealed-api-gateway/console is where you read it back — a Network-tab-style inspector with a folding JSON tree, binary preview, Copy-as-cURL and charts.

It needs no route file of its own; its unlock endpoint hangs off the route you already created.

<SecureApiGateway />
<GatewayConsole />
SECURE_GATEWAY_PASSWORD=pick-something-long

Then Ctrl/Cmd + Shift + D on any page.


API

// client
import { SecureApiGateway, DEFAULT_GATEWAY_ROOT, getGatewayRoot, setGatewayRoot } from '@sealed-api-gateway/core';

// server — route handlers, the allowlist, the crypto primitives, the tunables
import { GET, POST, deriveOrigins, seal, open, timeoutMs, rateLimit } from '@sealed-api-gateway/core/server';

// the plaintext log, shaped for useSyncExternalStore
import { subscribe, getEntries, getServerEntries } from '@sealed-api-gateway/core/recorder';


FAQ

Does this replace authentication? No. The gateway forwards requests; it does not authorize them. An endpoint that is unsafe when called directly is equally unsafe through the gateway.

Does it stop attackers? No. It removes casual visibility from the Network tab. Treat it as defence in depth, never as the defence.

Can backend URLs still be found? Yes — they are in the JavaScript bundle, because the browser has to know what to route. The gateway hides them from the network tab, not from the bundle.

Are requests actually encrypted? Yes. ECDH P-256 key agreement, AES-GCM-256 per request with a fresh IV.

Does it work with axios / fetch / React Query / SWR? All of them, unmodified. It wraps window.fetch and XMLHttpRequest, which every browser HTTP client ends up calling.

Does it affect SSR or Server Components? No. It patches browser globals only; server-side fetches are untouched.

Does it support streaming responses? No — see design decisions.

What happens if I set no password? The debug console does not exist, its endpoint 404s, and nothing is recorded anywhere. That is the default.

Can it run on Kubernetes / PM2 / serverless? Yes. Set SECURE_GATEWAY_KEY so every instance shares one keypair — without it each process generates its own and clients re-handshake on instance switches.

Does it work behind a reverse proxy or CDN? Yes. Forward x-forwarded-for if you want the per-IP rate limit to see real client addresses rather than your proxy.


Design decisions

The reasoning behind the choices most likely to raise an eyebrow.

Why only GET and POST on the wire? Every other verb travels inside the encrypted envelope and is reissued server-side. Fewer observable request shapes make traffic less descriptive, and more exported verbs would mean more surface for an observer to distinguish.

Why no streaming? A body must be complete before it can be sealed — AES-GCM authenticates the whole message. Streaming and authenticated encryption of the full payload are mutually exclusive here, and silently shipping unauthenticated chunks would be worse than not streaming.

Why AES-GCM? It is authenticated. A tampered ciphertext fails to decrypt rather than yielding plausible garbage, so a mangled body can never reach your application as data.

Why P-256 rather than X25519? crypto.subtle supports P-256 in every browser and Node version in range. X25519 is cleaner but is not universally available, and a polyfill would break the zero-dependency guarantee.

Why nodejs runtime, not Edge? The password check uses node:crypto for its timing-safe comparison, which Edge does not provide. A non-constant-time fallback would leak the password by timing.

Why zero dependencies? This is security-adjacent code. Every dependency is supply-chain risk and one more thing an auditor has to read. The crypto is globalThis.crypto.subtle directly.

Why is the allowlist server-side and non-negotiable? The browser names its own target inside the envelope. If the client could add to the allowlist, any caller could point the gateway at 169.254.169.254 and read your cloud credentials. Fail-closed is the only safe default.


Security principles

  • Zero dependencies — nothing to audit but this package.
  • Stateless — the shared key is re-derived per request; no session store, survives cold starts and horizontal scaling.
  • No cookies on sealed calls — credentials travel inside the ciphertext.
  • Per-request encryption — a fresh IV every time.
  • Explicit allowlist — declared server-side, never by the client.
  • Fail closed — an unconfigured deployment grants nothing; a non-secure context disables the module rather than half-working.
  • Honest scope — the README states what this does not protect, in the first screen.

Ecosystem

Works alongside, with no special configuration:

| | | | --- | --- | | Auth | NextAuth · Auth.js · Clerk · your own | | Data | React Query · SWR · axios · fetch | | Hosting | Vercel · PM2 · Docker · Kubernetes · any Node host |

Auth libraries are unaffected because the gateway operates below them: it patches the transport, not the caller.


Roadmap

| Status | | | --- | --- | | ✅ | Gateway with Server Action and route transports | | ✅ | Password-gated debug console | | ✅ | Three ways to declare origins | | ☐ | Console on Server Actions, so it needs no route either | | ☐ | Wildcard origins — https://*.example.com | | ☐ | Deployment guides and worked examples |

Changelog · Issues · Contributing

Licence

MIT © 2026 JAINEEL PATEL