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

shadowtools

v4.1.0

Published

Manage Outline VPN (Shadowbox) servers and access keys from a local admin dashboard, the command line, or your own code: list, create, rename, delete, set data limits, report usage and show QR codes

Downloads

490

Readme

shadowtools

CI npm

Manage access keys on your Outline VPN (Shadowbox) servers, from a local admin dashboard, the terminal, or your own code. It lists, creates, renames and deletes keys, sets per-key data limits, reports how much data each key has used, and shows scannable QR codes so people can onboard with the Outline app instead of copy-pasting ss:// strings.

The dashboard manages the servers too: paste the access code from Outline Manager and it is saved for next time, so one panel covers every server you run rather than one shell per server.

It can also rewrite every access URL to use your own domain in place of the server's raw IP address — handy when you have pointed a domain at your Outline server, or when your provider's IP has been blocked and you have restored the server elsewhere.

This project supersedes outline-br, whose getKeys() module lives on here. See migrating from outline-br.

Prerequisites

  • Node.js 14 or newer (with npm)
  • One or more Outline servers set up via Outline Manager
  • Each server's access code — in Outline Manager open the server, go to Settings, and copy the line under Management API URL

Installation

git clone https://github.com/rahmanow/shadowtools.git
cd shadowtools
npm install

Configuration

Everything starts from your server's access code. In Outline Manager, open your server and go to Settings → Management API URL; it shows a line like:

{"apiUrl":"https://1.2.3.4:16942/AbCdEf123","certSha256":"E3823F9BB490D354...52F5A584"}

⚠️ That line grants full administrative control of your Outline server. Treat it like a password: don't commit it to version control or share it.

There are two ways to give it to shadowtools, and they work together.

Add it in the dashboard (easiest, and handles several servers)

node shadowtools.js ui

Open the printed URL, go to Servers → Add server, and paste the access code. It is saved to a config file readable only by you, so the CLI picks it up too and you do not have to paste it again. Repeat for each server you run, and switch between them from the header.

You can do the same from the terminal — the access code is read from stdin so it stays out of your shell history:

pbpaste | node shadowtools.js servers add Frankfurt

Or set environment variables (one server, nothing on disk)

| Setting | Environment variable | Description | | --- | --- | --- | | Management API URL | OUTLINE_API_URL | The apiUrl from the access code. | | Certificate fingerprint | OUTLINE_CERT_SHA256 | The certSha256 from the same line. Optional but recommended — see certificate pinning. | | Custom domain | OUTLINE_DOMAIN | A domain that points at your Outline server. Optional — leave empty to keep the raw IP in the access URLs. | | Config file location | SHADOWTOOLS_CONFIG | Override where saved servers are stored. Optional. | | Request timeout | OUTLINE_TIMEOUT_MS | How long to wait for a Management API response, in milliseconds. Optional, default 15000. |

A convenient way to keep these out of your shell history is a .env file (already git-ignored) that you source before running:

# .env
export OUTLINE_API_URL="https://1.2.3.4:16942/AbCdEf123"
export OUTLINE_CERT_SHA256="E3823F9BB490D354...52F5A584"
export OUTLINE_DOMAIN="vpn.example.com"
source .env

A server configured this way appears in the dashboard alongside your saved ones, marked environment. It is never written to the config file, and it cannot be edited or removed from the panel — change the variables instead. You can still make it the active server with one click.

Where saved servers live

$XDG_CONFIG_HOME/shadowtools/config.json, or ~/.config/shadowtools/config.json when that is unset. The file is written mode 0600 inside a 0700 directory, because it holds Management API URLs: anyone who can read it can administer every server listed in it. Point SHADOWTOOLS_CONFIG somewhere else if you prefer.

The editing constants at the top of shadowtools.js still work, if you would rather not use either mechanism.

Commands

node shadowtools.js <command> [options]

| Command | What it does | | --- | --- | | list | List every access key with its id, name, data limit and access URL | | add <name> | Create a new access key | | remove <key> | Delete an access key | | rename <key> <new name> | Rename an access key | | limit <key> <size\|none> | Set or clear a key's data limit | | limit server <size\|none> | Set or clear the server-wide default data limit | | usage | Show how much data each key has transferred | | qr <key> | Print a key's access URL as a scannable QR code | | servers | List the Outline servers this machine knows about | | servers add <name> | Save a server, reading its access code from stdin | | servers use <server> | Choose the server other commands act on | | servers remove <server> | Forget a saved server (the server itself is untouched) | | ui | Serve the admin dashboard until you stop it | | service install | Run the dashboard in the background, from login on | | service status | Whether it is running, and the URL to open | | service start / stop / restart | Control the background service | | service logs | Show what the background service has printed | | service url | Print the dashboard URL | | service uninstall | Stop it and remove the service definition |

<key> may be either a key id or a key name, and <server> either a server id or a server name. Running with no command at all is the same as list, so the original behaviour still works.

Key commands act on whichever server is active. Use servers use to change that, or --server to redirect a single command without changing it.

| Option | Applies to | Effect | | --- | --- | --- | | --qr | list, add | Also print a QR code for each key | | --json | list, usage, servers | Output JSON instead of a table | | --csv | list, usage, servers | Output CSV instead of a table | | --limit <size> | add | Give the new key a data limit straight away | | --server <s> | key commands | Act on this server instead of the active one | | --port <n> | ui, service install | Port for the dashboard (default 8787) | | --lines <n> | service logs | How many log lines to show (default 50) | | --follow | service logs | Keep printing new lines | | --rotate | service url | Mint a new token, invalidating existing links | | -h, --help | — | Show usage |

Sizes accept a unit suffix: 10GB, 500MB, 2TB, or a plain byte count.

Examples

List every key:

$ node shadowtools.js list
ID  NAME   LIMIT  ACCESS URL
--  -----  -----  -----------------------------------------------------
0   Alice  10 GB  ss://[email protected]:443/?outline=1
1   Bob    -      ss://[email protected]:444/?outline=1

Create a key with a 50 GB cap and show its QR code:

$ node shadowtools.js add Carol --limit 50GB --qr
Created key "Carol" (id 2)
Data limit: 50 GB
ss://[email protected]:502/?outline=1

Carol:
█▀▀▀▀▀█ ▀▄█▀▄ █▀▀▀▀▀█
█ ███ █ █▄▀ ▀ █ ███ █
...

See who is using how much:

$ node shadowtools.js usage
ID  NAME   USED    LIMIT  OF LIMIT
--  -----  ------  -----  --------
0   Alice  3.0 GB  10 GB  30%
1   Bob    512 MB  -      -

Total transferred: 3.5 GB

Cap a heavy user, then lift the cap later:

node shadowtools.js limit Alice 10GB
node shadowtools.js limit Alice none

Export usage for a spreadsheet:

node shadowtools.js usage --csv > usage.csv

Work across servers:

$ node shadowtools.js servers
   ID            NAME       HOST          SOURCE  CERT PINNED
-  ------------  ---------  ------------  ------  -----------
*  7009f5d5ff9d  Frankfurt  198.51.100.7  saved   yes
   d1cf000628fc  Singapore  203.0.113.4   saved   no

$ node shadowtools.js list --server Singapore
$ node shadowtools.js servers use Singapore

Admin dashboard

If you would rather click than type:

node shadowtools.js ui

It prints a URL to open:

Dashboard running. Open this URL:

  http://127.0.0.1:8787/?t=979ea2e12d7c5ba8e1d38631b2effd32583bca3b035775f3

It listens on localhost only, and the token in the URL authorises it.
Press Ctrl+C to stop.

You can run it before configuring anything: with no servers yet, it opens on the Servers section so you can paste your first access code. Use --port to move it off 8787. It follows your system light or dark theme.

Four sections, switched from the sidebar:

| Section | What it covers | | --- | --- | | Overview | Key count, total transferred, the default cap, and which keys have hit their limit | | Access keys | Every key with its usage, limit and access URL — add, rename, cap, delete, copy, or show a QR code | | Servers | Add, edit, test, switch between and remove servers; reveal a server's access code to copy elsewhere | | Settings | The active server's domain and default data cap, and where saved servers are stored |

The server picker in the header switches everything at once, with a dot showing whether that server is answering. When one is unreachable, Overview and Access keys say so and point you at Servers rather than showing a blank page — and Servers and Settings keep working, since that is where you go to fix it.

Running it in the background

ui serves the dashboard for as long as you keep the terminal open. To leave it running and bookmark it:

node shadowtools.js service install

That registers it with whatever service manager the platform already has — launchd on macOS, systemd's user instance on Linux — so it starts at login, comes back if it crashes, and answers on the same URL every time.

$ node shadowtools.js service status
running  pid 4821  port 8787
Dashboard: http://127.0.0.1:8787/?t=979ea2e12d7c5ba8e1d38631b2effd32583bca3b035775f3
Definition: ~/Library/LaunchAgents/com.rahmanow.shadowtools.plist
Log:        ~/Library/Logs/shadowtools/service.log

| Command | What it does | | --- | --- | | service install | Write the service definition and start it (--port to choose a port) | | service status | Whether it is running, its pid and port, and the URL to open | | service start / stop / restart | Control it | | service logs | What it has printed (--lines, --follow) | | service url | Print the dashboard URL (--rotate to invalidate existing links) | | service uninstall | Stop it and remove the definition; saved servers are untouched |

Nothing here runs as root and nothing is installed system-wide. These are per-user agents, which is the right privilege level for something holding one user's Outline credentials, and it means no step asks for your password. Windows has no equivalent here — run ui in a terminal instead.

Two consequences worth knowing:

  • The URL is now stable. The token lives in ~/.config/shadowtools/token (mode 0600) instead of being minted per run, so a bookmark keeps working across restarts. ui reads the same file, so both ways of starting the dashboard hand out the same URL. If a link ends up somewhere it should not, service url --rotate mints a new one and kills every old link.
  • The service cannot see your shell. A background agent does not inherit the environment you installed it from, and a service definition is an ordinary file that other tooling reads — so OUTLINE_API_URL and OUTLINE_CERT_SHA256 are deliberately not written into it. A server configured only through those variables will not appear in the background dashboard; save it with servers add first. install warns you when it sees them set.

Installing Shadowbox on a new server

Not yet — the Servers section marks where it lands. Today, provision a server with Outline Manager and paste its access code into the dashboard. The plan is to connect to a fresh host over SSH, run the Outline installer on it, and have the resulting access code register itself, with no round trip through Outline Manager.

How it is secured

A Management API URL is full administrative control of an Outline server, so the dashboard is deliberately narrow:

  • Credentials never leave the process. The browser talks only to this local server, which holds them and proxies each call. The server list the page receives carries redacted URLs — enough to tell two servers apart, not enough to use. The full access code is served by one endpoint that exists for that purpose, and only when you click the button that asks for it.
  • Loopback only. It binds 127.0.0.1, so nothing else on your network can reach it — not a shared-hosting concern, a deliberate limit.
  • Token-gated. A random token is minted at each start and carried in the printed URL. Every API call must present it in a header, so another page open in the same browser cannot drive it, and requiring a custom header means a cross-origin attempt hits a CORS preflight that is never answered.
  • Host-checked. Requests whose Host header is not the loopback address are refused, which is what stops DNS rebinding from turning an attacker's domain into a route to your machine.

When you run ui yourself the URL is printed to your terminal. Under the background service stdout is a log file, so the URL is deliberately not printed there — the log says only that the dashboard is listening, and service url is how you get the address. The log and its directory are created 0600/0700 rather than left at the service manager's world-readable default.

This is a single-user local tool: do not put it behind a reverse proxy or expose the port.

Using it from code

Everything the CLI does is available as a module. require the package and you get three helpers plus the underlying client:

const { listKeys, getUsage, getKeys, OutlineClient } = require('shadowtools');

const API = 'https://1.2.3.4:16942/AbCdEf123';
const options = { domain: 'vpn.example.com', certSha256: 'E3823F9B...52F5A584' };

// Structured key data
const keys = await listKeys(API, options);
// [{ id: '0', name: 'Alice', port: 443, dataLimitBytes: 10737418240,
//    accessUrl: 'ss://[email protected]:443/?outline=1' }, ...]

// Who is using how much
const usage = await getUsage(API, options);
// [{ id: '0', name: 'Alice', bytes: 3221225472, dataLimitBytes: 10737418240 }, ...]

options is optional throughout: domain swaps the host in each access URL, and certSha256 pins the server's certificate exactly as the CLI does.

For anything the helpers do not cover — creating, renaming and deleting keys, or setting data limits — use the client directly:

const client = new OutlineClient(API, 'E3823F9B...52F5A584');

const key = await client.createKey('Alice');
await client.setKeyDataLimit(key.id, 50 * 1024 ** 3);   // 50 GB
await client.renameKey(key.id, 'Alice B');
await client.clearKeyDataLimit(key.id);
await client.removeKey(key.id);

await client.setServerDataLimit(100 * 1024 ** 3);       // server-wide default
await client.clearServerDataLimit();

Failures caused by bad input or a bad response throw UserError, which is also exported, so you can tell them apart from bugs:

const { UserError } = require('shadowtools');

try {
    await listKeys(API, { certSha256: expected });
} catch (err) {
    if (err instanceof UserError) console.error(err.message);  // e.g. wrong certificate
    else throw err;
}

Migrating from outline-br

This project absorbed outline-br. Its getKeys(managementApiUrl, newDomain) is exported here with the same signature, the same Name -> ss://... output and the same printing behaviour, so existing code only needs its import changed:

// before
const keys = require('outline-br');
keys('https://1.2.3.4:16942/AbCdEf123', '87.65.43.21');

// after
const { getKeys } = require('shadowtools');
await getKeys('https://1.2.3.4:16942/AbCdEf123', '87.65.43.21');

Two differences, both additive: getKeys now also returns the lines it printed, and it accepts a third options argument for certSha256. Nothing that worked before behaves differently.

New code should prefer listKeys(), which returns structured data and leaves presentation to you, rather than printing to stdout.

Project layout

index.js           Programmatic API: listKeys, getUsage, getKeys
shadowtools.js     CLI entry point: argument parsing and commands
lib/outline.js     Outline Management API client
lib/config.js      Saved servers: access-code parsing and the on-disk store
lib/registry.js    The servers this install knows about, and their clients
lib/format.js      Byte formatting, size parsing, table/CSV output
lib/errors.js      UserError, for messages shown without a stack trace
lib/server.js      Dashboard: HTTP routes and their guards
lib/service.js     Running the dashboard as a launchd or systemd user service
lib/web.js         The dashboard page, inlined so it needs no assets
lib/qr.js          Vector QR codes for the dashboard
test/              Tests, run with the built-in Node test runner

Development

Run the test suite:

npm test

The tests cover the pure logic — size parsing and formatting, access-URL rewriting, table and CSV rendering, argument parsing and key lookup — along with the config store, the server registry, the dashboard's HTTP routes and its guards, driven over a real socket against a stand-in Outline server. They need Node 18 or newer for the built-in test runner, even though the tool itself runs on Node 14+.

Every push and pull request runs the suite on Node 18, 20 and 22 via GitHub Actions, along with a syntax check and an audit of production dependencies. See .github/workflows/ci.yml.

Releasing

Past releases are listed in CHANGELOG.md.

Releases publish from CI, so no npm token exists on anyone's machine or in repository secrets. Authentication uses npm trusted publishing: GitHub Actions proves its identity to the registry over OIDC, and npm attests the package's provenance automatically.

To ship a version:

npm version minor        # or patch / major - commits and tags
git push --follow-tags

Then publish a GitHub Release for that tag. release.yml installs, verifies the tag matches package.json, runs the tests through the prepublishOnly hook, and publishes. A tag that disagrees with the version fails before anything reaches the registry, which matters because a published npm version can never be replaced.

One-time setup

Trusted publishing has to be enabled on the registry side once, by a package maintainer:

  1. Open the package on npmjs.com, then Settings → Trusted Publisher.
  2. Choose GitHub Actions and enter the organization (rahmanow), the repository (shadowtools), and the workflow filename (release.yml).

Until that is configured, the workflow's publish step fails with an authentication error — which is the safe direction to fail in.

Certificate pinning

Outline servers use a self-signed TLS certificate for the Management API, so the usual chain validation cannot apply and the tool disables it (rejectUnauthorized: false). On its own that would leave the connection open to a man-in-the-middle: anything that can answer on the server's address would be trusted.

Setting OUTLINE_CERT_SHA256 closes that gap. The tool then checks the certificate the server presents against the fingerprint you configured, and aborts before sending anything if they differ:

$ OUTLINE_CERT_SHA256=aaaa...aaaa node shadowtools.js list
The server presented an unexpected TLS certificate, so the request was not sent.
  expected: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
  received: e3823f9bb490d35487ee013ec7d23a3662f76b95eb01a40f4851905152f5a584
Check OUTLINE_CERT_SHA256 against certSha256 in Outline Manager. If it has not
changed there, you may be talking to the wrong server.

The check runs at the end of the TLS handshake, before any request bytes leave your machine. That ordering matters: the path in the Management API URL is the admin credential, so it must never be sent to a server that hasn't been authenticated.

The fingerprint is accepted in either form — the bare hex Outline Manager reports as certSha256, or the colon-separated form openssl x509 -noout -fingerprint -sha256 prints — in any case.

Pinning is optional for backwards compatibility. Leave OUTLINE_CERT_SHA256 unset and the tool behaves as before, connecting without verifying which server answered. Set it whenever you can, especially on an untrusted network.

If you rebuild or migrate your server, its certificate changes; copy the new certSha256 from Outline Manager.

Why node:https rather than fetch()

The HTTP calls use the built-in node:https module because the global fetch() has no supported way to relax certificate checks or inspect the peer certificate for a single request — it ignores the agent option, and its dispatcher lives in undici, which would mean taking on a dependency to do what node:https already does.

Troubleshooting

  • No Outline server is configured yet — add one in the dashboard (node shadowtools.js ui), pipe an access code into servers add, or set OUTLINE_API_URL.
  • That server is already saved as ... — the same Management API URL is already in your config; edit that entry instead of adding a second one.
  • comes from OUTLINE_API_URL in your environment — that server is defined by environment variables, so the dashboard will not rewrite it. Change the variables, or add it as a saved server.
  • ... is not valid JSON, so it was left untouched — the config file was hand-edited into an invalid state. Fix or delete it; nothing is overwritten until it parses.
  • Could not reach the Outline server — check that the Management API URL is correct and that its port (usually 16942) is reachable from your machine.
  • did not respond within 15 seconds — the address accepted nothing and refused nothing, which usually means a wrong IP or a firewall dropping the port. Raise OUTLINE_TIMEOUT_MS if your link is genuinely that slow.
  • Cannot find module 'qrcode-terminal' — run npm install in the project directory first.
  • The server presented an unexpected TLS certificate — either OUTLINE_CERT_SHA256 is stale (recopy certSha256 from Outline Manager after rebuilding or migrating the server), or something other than your Outline server answered. See certificate pinning.
  • is not a SHA-256 certificate fingerprintOUTLINE_CERT_SHA256 must be 64 hex characters, with or without colons.
  • Port 8787 is already in use — often the background service is already serving it; run shadowtools service status for its URL. Otherwise pass --port 9000 (or any free port).
  • service status says running but nothing answers — the process started and then failed to bind, usually because something else holds the port. Check shadowtools service logs.
  • The background dashboard shows no servers, but the CLI does — the server is configured through OUTLINE_API_URL, which a background service cannot see. Save it with shadowtools servers add instead.
  • The dashboard says the token is invalid — it is regenerated on every start, so reopen the URL currently printed in your terminal.
  • Management API responded with 404 — your Outline server may be running an older release that lacks data-limit or metrics endpoints. Upgrade the server, or stick to list, add, remove and rename.
  • Keys print with the IP instead of your domain — make sure OUTLINE_DOMAIN (or the domain constant) is set and non-empty.

License

MIT, carried over from outline-br when the two projects merged.