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

@yashau/prick

v2026.1031.1

Published

Portable Runtime Injection of Cloudflare (stored) Keys — command-line client for a self-hosted secrets manager on Cloudflare Workers and D1.

Readme

Portable Runtime Injection of Cloudflare (stored) Keys

A self-hosted secrets manager that runs entirely on your own Cloudflare account. One Worker, one D1 database, and nothing else to operate.

CI CodeQL License: MIT crates.io npm Docs

Install · Examples · Access control · Getting started · Quickstart · CLI reference · Docs


prk login https://secrets.example.com
prk secrets set DATABASE_URL --project api --env production   # prompts, masked
prk run --project api --env production -- ./deploy.sh         # injected, never written to disk

The name is the job description: keys live in your Cloudflare account, and prk injects them into a process at runtime — portably, and without ever touching disk.

🧩 What it is

| | | | :---------- | :--------------------------------------------------------------------------------------------------------------------------- | | prk | The command-line client: one static Rust binary, talking HTTP to your Worker. | | Web UI | A SvelteKit admin console served from the same Worker. | | Auth | Cloudflare Access — SSO for people, service tokens for CI. Identity comes from the signed JWT. | | Storage | D1. Values are AES-256-GCM, and each ciphertext is cryptographically bound to the row that holds it. | | MCP | An MCP server, so a coding assistant can manage secrets without reading them. |

⚙️ How it works

  prk ─────┐                    ┌───────────────────────────────┐
           ├──▶ Cloudflare ────▶│  Worker                       │
  browser ─┘      Access        │  · verifies the signed JWT    │──▶  D1
                (SSO / tokens)  │  · resolves grants in D1      │   (encrypted)
                                │  · encrypts, decrypts, audits │
                                └───────────────────────────────┘

Everything sits behind one Access-protected hostname. Access authenticates at the edge before the Worker runs; the Worker independently verifies the signed JWT to learn who is calling, then consults its own grant table to decide what they may do.

📦 Install

prk is one static binary. Every channel below serves the same prebuilt artefact from the release page, and every one of those is provenance-attested — gh attestation verify <archive> --repo yashau/prick checks it against this repository.

brew install yashau/prick/prk    # macOS, Linux
cargo binstall prk               # anywhere with a Rust toolchain
npm install -g @yashau/prick     # anywhere with Node
winget install yashau.prick      # Windows

Scoop takes the bucket first:

scoop bucket add prick https://github.com/yashau/scoop-bucket
scoop install prick/prk

cargo install prk builds from source instead, and the release page carries plain archives for eight platforms with a SHA256SUMS and an SPDX SBOM beside them.

If you already use mise, let it own the install:

mise use -g npm:@yashau/prick

npm's global prefix sits inside the active Node's install directory, so a plain npm install -g lands somewhere that is only on PATH while mise is activated — and moves the day mise bumps Node. mise's own npm backend puts a prk shim in the shims directory that is already on your PATH, and keeps it there across Node upgrades.

The npm route adds a Node process to every invocation, which prk doctor reports; the direct installs above avoid it. See Install the CLI for the whole picture, shell completions included.

💻 Examples

Deploying takes five commands — see Getting started — and everything below assumes you have run prk login.

[!TIP] Set the project and environment once and the flags disappear from every command for the rest of the day. A direnv file per repository does the same thing permanently.

export PRK_PROJECT=api PRK_ENV=production
prk run -- npm start

Secrets reach the child through its environment block and nowhere else: no temporary .env, no fifo, nothing written to disk. Everything after -- belongs to the child, so its own flags are unambiguous even when prk understands them too:

prk run -- npm test --json -q

prk run never invokes a shell — that is what keeps quoting predictable — so ask for one when you want one:

prk run -- sh -c 'migrate && npm start'

On Unix prk replaces itself with the child via execvp, so the exit code and signal disposition are the command's own and a script cannot tell it ran under prk run.

Check what went in, without seeing values:

prk run -vv -- node -e 'console.log("up")'
injecting 12 secrets into the child environment
variables: DATABASE_URL, REDIS_URL, STRIPE_SECRET_KEY, …
up

⚠️ The value is never an argument. An argument lands in your shell history and is visible in ps output to every user on the machine. The masked prompt and --stdin are the two ways in.

prk secrets set STRIPE_SECRET_KEY --description "Live mode, rotates quarterly"
Value for STRIPE_SECRET_KEY:
Added `STRIPE_SECRET_KEY` (rev 12).

Or pipe it, which is the same write without a terminal:

openssl rand -hex 32 | prk secrets set SESSION_SECRET --stdin

Descriptions are stored in plaintext beside the key name, so they are safe to read in a listing — and must never contain a value.

prk projects create "API service" --slug api
prk env create Production
prk env create Staging
Created project `API service` (api).
Created environment `Production` (production).
Created environment `Staging` (staging).

The slug is what every command, URL and --scope addresses, and it is permanent. The display name is free text and can be renamed.

🛑 upload replaces the environment by default — keys the file does not name are deleted. Look before you leap.

prk secrets upload .env --dry-run
3 added, 1 changed, 2 removed (dry run; nothing was written).

Once it says what you expected:

prk secrets upload .env --reason "initial import from the repo"
git rm --cached .env && rm .env

--reason is copied verbatim into the audit row. Use --merge to add keys without removing the ones the file leaves out. The whole file goes in one request, as one transaction with its audit row inside it — so a failed import leaves the environment exactly as it was.

export DATABASE_URL="$(prk secrets get DATABASE_URL)"

prk secrets get prints the bare value on stdout and nothing else, so it composes. Every reveal is audited, and the audit row records why: reveal, copy, export or run. That is what lets a reader tell "someone looked at it" from "someone took it".

prk secrets download --format env --output .env
docker run --env-file .env myimage
rm -f .env

--output creates the file 0600. Prefer prk run, which writes nothing — or forward named variables into the container instead, since a container does not inherit the docker client's environment:

prk run -- docker run -e DATABASE_URL -e STRIPE_SECRET_KEY myimage

Four export formats are available — env, shell, yaml and json — and every one of them quotes unconditionally rather than "when necessary". Conditional quoting means modelling the consumer's grammar exactly, and a single miss is either a command injection or a silently altered value.

Point the job at prick and let it 404 on the first run. That refusal is not a mistake — it is how a service token introduces itself:

prk access identities --denied
e367826f93b8d71185e03fe518aff3b4.access	service	1 attempt(s)

There is the subject, so nobody copies an opaque hex string between two consoles:

prk access grant e367826f93b8d71185e03fe518aff3b4.access --role reader --scope api:production
prk access rename e367826f93b8d71185e03fe518aff3b4.access "api deploy job"

Name it while you still know what it is. An access list of hex strings is how a stale token survives three audits: nobody could say what it was for, so nobody was willing to be the one who removed it.

Then, in the workflow:

env:
  PRK_API_URL: https://secrets.example.com
  PRK_PROJECT: api
  PRK_ENV: production
  PRK_ACCESS_CLIENT_ID: ${{ secrets.PRICK_CLIENT_ID }}
  PRK_ACCESS_CLIENT_SECRET: ${{ secrets.PRICK_CLIENT_SECRET }}
steps:
  - run: prk run --no-input -- ./deploy.sh

--no-input fails immediately instead of blocking on a prompt no CI job can answer.

prk secrets history DATABASE_URL
v4	set	[email protected]
v3	set	[email protected]
v2	delete	[email protected]	DELETED
v1	set	[email protected]

Tombstones are shown rather than skipped — "this key was deleted at version 2" answers half the questions this command gets asked.

prk secrets rollback DATABASE_URL --to 3 --reason "bad connection string in v4"
Restored `DATABASE_URL` from version 3 as version 5 (rev 46).

The old plaintext is re-encrypted as a new version bound to it, so an old ciphertext blob replayed into the current row still fails its tag check.

prk access explain [email protected]
[email protected]	user
groups	contractors, platform
billing:production	admin	via group `platform` on `billing:*`
     reader	a direct grant	on `billing:production`
  -> admin	group `platform`	on `billing:*`

-> marks the grant the server reported as decisive — the one to remove. Revoking Bob's direct reader grant would change nothing; taking him out of platform is the answer.

prk access disable [email protected]

One write, checked before grants are resolved, so it outranks every grant at every scope — direct, group-held and BOOTSTRAP_ADMINS alike. Nothing has to be enumerated, so nothing can be missed.

prk access enable puts it all back exactly as it was, because disabling never touched the grants. Run prk access explain first to see what re-enabling would restore.

--json is a contract rather than a formatting flag:

| Outcome | stdout | stderr | | ------- | ----------------- | ----------------------- | | Success | one JSON document | empty | | Failure | empty | one JSON error envelope |

Both halves are guaranteed, so a redirect that fails writes an empty file rather than a truncated one that still parses.

# Refuse to deploy from an environment holding a secret that will not decrypt.
unreadable=$(prk secrets list --json | jq '[.[] | select(.unreadable)] | length')
[ "$unreadable" -eq 0 ] || { echo "$unreadable secret(s) will not decrypt" >&2; exit 1; }
# Optimistic write: refuse if anyone else touched the environment in between.
rev=$(prk env list --json | jq -r '.[] | select(.slug == "production") | .rev')
prk secrets upload .env --expected-rev "$rev" --no-input
# Diff two environments by key. Names only — no values are involved.
diff <(prk secrets list --json -E staging    | jq -r '.[].key' | sort) \
     <(prk secrets list --json -E production | jq -r '.[].key' | sort)

Complete walkthroughs live in Examples; every command, flag and exit code is in the CLI reference.

🏛️ Design decisions worth knowing

🛡️ Access control

A grant is a role — what you may do — at a scope — where you may do it.

| Role | Read metadata | Read values | Write secrets | Manage grants | | :------- | :-----------: | :---------: | :-----------: | :-----------: | | reader | ✅ | ✅ | — | — | | writer | ✅ | ✅ | ✅ | — | | admin | ✅ | ✅ | ✅ | ✅ |

Totally ordered: reader < writer < admin.

| Scope | Written as | Covers | | :------------ | :---------------- | :--------------------------------- | | global | *:* (default) | Every project on the server | | project | acme:* | Every environment in acme | | environment | acme:production | One environment, and only that one |

Grants inherit downwards only: a project grant covers every environment in it, and an environment admin is not a project admin.

prk access grant [email protected] --role admin                             # everything
prk access grant [email protected]   --role writer --scope acme:*             # one project
prk access grant [email protected]    --role reader --scope acme:production    # one environment
prk access grant [email protected]  --role reader --scope acme:staging --expires-in 30

A group is a named set of identities, and holds grants of its own — so the same scopes are handed out to a roster rather than to a person. Membership on its own confers nothing.

[!NOTE] Your effective role at a scope is the maximum over your direct grants and the grants of every group you belong to: purely additive, with no deny rules, because a deny that silently overrides an explicit grant is the most confusing thing an authorization system can have.

It resolves in one query, so a 200-secret operation performs one authorization lookup rather than two hundred.

prk access explain — GET /identities/{id}/effective-permissions — answers "why does Bob have production?" by naming the grant or group that conferred it. There is a worked example above.

🚀 Getting started

Deploy it to your own account. This repository never deploys it for you.

git clone https://github.com/yashau/prick && cd prick
mise trust && mise run bootstrap
pnpm --filter @prick/app exec wrangler d1 create prick   # paste the database_id into wrangler.jsonc
# set "routes" and the ACCESS_* vars in packages/app/wrangler.jsonc — see the Quickstart
openssl rand -base64 32 | pnpm --filter @prick/app exec wrangler secret put MASTER_KEY
pnpm --filter @prick/app exec wrangler d1 migrations apply prick --remote
pnpm --filter @prick/app exec wrangler deploy

[!IMPORTANT] Put Cloudflare Access in front of the hostname before you put a secret in it. The Worker ships with workers_dev and preview_urls disabled and CI asserts both on every push, because an unprotected hostname serves the entire application without a JWT.

Then sign in. The first sign-in creates a profile — a local name for the server and its session — so later commands need no URL:

prk login https://secrets.example.com
Signing in to https://secrets.example.com as profile `default`
Signed in to https://secrets.example.com (profile default)

A second server is a second profile, not a replacement:

prk profile add client-b https://secrets.client-b.example
prk login --profile client-b
prk --profile client-b secrets list --project api --env production

With two or more profiles, a command that changes data must name its server; the default alone is refused. See Multiple servers.

Quickstart · Authentication · Access control · Threat model

🔑 MASTER_KEY is the whole ballgame

[!CAUTION] Lose it and every stored secret is unrecoverable. A D1 export without it is just ciphertext — which is a feature rather than a bug, and also the most common way people lose everything. Back up the key before you store the first secret.

Rotation is incremental, and both the settings screen and prk keyring status tell you when it is safe to drop the retired key — counted live, every time you ask.

prk keyring rekey --until-done

See Backup and recovery and Key rotation.

📊 Status

The CLI is published on crates.io, npm, Homebrew, Scoop and WinGet, and the Worker is yours to deploy. Releases are CalVer, cut from a git tag, and every archive carries a provenance attestation and an SBOM.

The test suite is real: six suites — Rust, Worker + UI, end-to-end, scripts, the GitHub Action and the MCP server — and CI runs every one of them on every push. prick-core additionally carries a machine-checked purity proof under miri, and cargo audit bin works on a downloaded binary because the dependency list is embedded in it. mise run test prints the current counts, which is the only place they stay right.

Migrations are additive only, the API is versioned at /api/v1, and a client and server on different versions is supported.

🛠️ Development

The only thing you install is mise. It pins everything else.

mise run dev      # Worker + UI
mise run test     # every suite
mise run ci       # a superset of CI — run before opening a PR

See CONTRIBUTING.md and AGENTS.md.

⚖️ License

MIT