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

@seanstoves/ss-gitops-jsondb

v0.1.1

Published

GitHub App content-sync toolkit: mint an installation token, verify webhook signatures, pull/push a content repo over git, and compute which paths to evict. Native Node, no runtime deps.

Readme

ss-gitops-jsondb

Provenance. I wrote this code for my own site (seanstoves.com). Claude Code did the work of ripping it out of the app, decoupling it, and cleaning it up for distribution — I reviewed every line, but that's the honest origin story. Either way: read it before you trust it. You should do that with any dependency you didn't write — especially one that handles a write-capable token.

Your JSON files in git, used as a database — plus the GitHub-App loop that keeps them live. No Postgres, no Mongo, no daemon. Records are files, indexes are folders, history is git log, backup is a clone. A write is a commit; a push fires a webhook; the app pulls and evicts only the pages that changed.

Two halves, use either or both:

  1. A JSON store over git. readJson / listJson / writeJson / removeJson, where every read goes through hostile-input filesystem guards and every write is a real git commit. Git is the durable layer.
  2. The sync machinery. Mint a GitHub App installation token with native Web Crypto (no jsonwebtoken, no octokit), verify inbound webhooks in constant time, pull/push over git with the token never touching argv, and compute exactly which cache paths to evict.

Extracted from a production site (seanstoves.com) and genericized so you can run the same thing.

  • Zero runtime dependencies. Native Node 20+ only (crypto.subtle, child_process, fs). Nothing to audit for CVEs but Node itself.
  • Framework-agnostic core + thin, copy-and-edit adapters for Next.js and Express.
  • Auth is intentionally out of scope. This is the data + sync layer. Bring your own sessions/OAuth — a write-capable token minter has no business sharing a package with your auth.

MIT licensed. Use it, fork it, ship it.


Security scan

Last run 2026-06-14 against this repo's source. This one mints a write-capable token and writes files, so it got hammered harder than a markdown lib needs to. Re-run any row yourself.

| Area | What I tested | Result / posture | |---|---|---| | Supply chain | npm audit + retire.js 5.4.3; runtime-dependency count | 0 vulnerabilities, 0 runtime deps — no transitive tree to track | | Secrets | secretlint (recommend preset, validated against a planted token) + manual PEM / .env / credential sweep | clean — no keys, no .env, only empty placeholders | | Credential handling | token must never reach argv / /proc/<pid>/cmdline; webhook HMAC constant-time, raw-body, verify-before-parse; App key non-extractable | holds — token rides GIT_CONFIG only (confirmed at the OS boundary with a wrapped git), HMAC unbypassable, key non-extractable, cache key hardened injective | | Path / filesystem safety | traversal, absolute paths, the ..json dot-collapse; symlinked leaf / parent / nested dir on read, write, and delete — with real committed symlinks in a temp repo | containedsafePath + O_NOFOLLOW + realpath-ancestor checks keep every read/write/delete inside the root | | DoS / injection | eviction-regex ReDoS, SHA option-injection, argv-only spawn, output buffering | bounded — SHAs validated, no shell, git output capped; eviction input length-capped + the linear-pattern requirement documented on PatternRule | | Tests (npm test) | traversal & symlink-escape rejection, constant-time HMAC, write-is-a-commit, eviction ReDoS cap | 17 / 17 | | Adversarial review | 3 lenses — path-safety · credential-handling · DoS — run for real against a live temp repo | 2 medium + 3 low found → all fixed and regression-tested, 0 outstanding | | License | — | MIT, zero deps |

I'd rather show you the review found things and I fixed them than pretend it came back spotless. The two mediums were a mkdir that could build empty dirs outside the root through a pushed symlink (now realpath- checked before the write) and a ReDoS reachable only if you write a catastrophic eviction regex (now input-capped and documented). Both have a regression test. As always: the recommended install is Option B — vendor it and read it.


Install — pick your supply-chain tolerance

A. From npm — published with signed provenance, zero runtime deps:

npm install @seanstoves/ss-gitops-jsondb
# `next` is an optional peer dep, only for the Next adapter

Real talk: I'd skip this and vendor it (Option B). Yes, it's on npm for convenience — but even a zero-dependency package is still a supply-chain link you're choosing to trust: a future release, a hijacked account, a typosquatted name one fat-finger away. This thing mints a write-capable token and touches your filesystem — exactly the code you want to read in one sitting and own outright, not pull blind from a registry. Cloning the files in is the move I'd make.

B. Vendor it (zero dependencies, including this one). The whole library is native TypeScript — every import is from node:*. Copy src/ into your project and import locally; now you depend on no registry at all. This is the strongest version of "no supply chain," and honestly the one I'd pick — read the ~600 lines once, own them, no transitive deps, no Dependabot, no 3am CVE page.

# grab src/ straight from GitHub (or out of node_modules) into your tree
cp -r path/to/ss-gitops-jsondb/src src/lib/gitops

Either way you need Node 20+ (stable crypto.subtle + fetch).


The JSON store

Reads are traversal- and symlink-proof (the on-disk tree is push-writable, so it's a hostile boundary). A write does pull → write → commit → push — durability is git's job.

import { readJson, listJson, writeJson, removeJson } from '@seanstoves/ss-gitops-jsondb';

// READ — missing/out-of-root is null; malformed JSON throws (a corrupt record is a real problem)
const home = await readJson<{ name: string }>(CONTENT_DIR, 'home');          // home.json
const post = await readJson(CONTENT_DIR, 'blog/first');                      // blog/first.json

// LIST — slugs of the .json records in a folder, sorted
const slugs = await listJson(CONTENT_DIR, 'blog');                           // ['first', 'second']

// WRITE — this is a git commit. validate your shape FIRST; this guards paths + durability, not types.
await writeJson(git, token, 'blog/second', { title: 'Second' }, {
    message: 'content: add second post',
    author: { name: 'Editor', email: '[email protected]' },
});

// DELETE — also a commit
await removeJson(git, token, 'blog/first', { message: 'content: drop first' });

git is a GitConfig ({ contentDir, remoteUrl }) and token comes from installationToken (below). Single-writer by design — no locking; if two writers race, the second push is rejected and you retry, which is the correct boring behaviour for one admin editing content.


The sync loop

Setup: register the GitHub App

Accurate as of 2026-06-14. GitHub moves these screens around — if a label has changed, the concepts are what you want: an App ID, an installation ID, a Contents read/write permission, a Push webhook, and a private key.

You want a GitHub App (not an OAuth app, not a PAT) so the install is scoped to one repo and the token it mints is short-lived.

  1. Create the App. github.com → avatar → SettingsDeveloper settingsGitHub AppsNew GitHub App.
    • Name: anything unique, e.g. mysite-content-sync. Homepage URL: your site.
    • Webhook: Active ✔ · URL: https://yoursite.com/api/webhook/git-sync · Secret: openssl rand -hex 32, saved as GH_WEBHOOK_SECRET.
    • Repository permissions: Contents → Read and write, Metadata → Read-only.
    • Subscribe to events: Push ✔. Install on: Only on this account.
    • Create it, note the App IDGH_APP_ID.
  2. Private key. Same page → Generate a private key. A .pem downloads (the only copy — store it like a password). It's a PKCS#1 PEM; the toolkit bridges it to PKCS#8 for Web Crypto, so pass the text as-is. For env storage: base64 -w0 your-key.pem.
  3. Install on your content repo. Install AppOnly select repositories → your content repo. The install URL ends .../installations/<INSTALLATION_ID>GH_APP_INSTALLATION_ID.
  4. Clone the content repo on your server — that path is CONTENT_DIR.

Env (see .env.example):

GH_APP_ID=123456
GH_APP_INSTALLATION_ID=12345678
GH_APP_PRIVATE_KEY=<base64 of the .pem>      # decode before handing it to the lib
GH_WEBHOOK_SECRET=<the openssl rand value>
CONTENT_DIR=/srv/app/content
CONTENT_REMOTE=https://github.com/           # remote base the auth header is scoped to

The webhook route

handleWebhook runs the inbound loop framework-free — verify signature → confirm it's a push on your branch → mint token → pull → diff the SHAs → return the paths to evict.

Next.js — copy adapters/next to app/api/webhook/git-sync/route.ts and export POST:

import { revalidatePath } from 'next/cache';
import { handleWebhook, type EvictionConfig } from '@seanstoves/ss-gitops-jsondb';

const eviction: EvictionConfig = {
    patterns: [{ pattern: /^blog\/(.+)\.md$/, evict: (slug) => [`/blog/${slug}`], also: ['/blog', '/'] }],
    static: [{ file: 'home.json', evict: ['/'] }],
};

export async function POST(req: Request) {
    const raw = Buffer.from(await req.arrayBuffer()); // RAW bytes — required for the HMAC
    const result = await handleWebhook({
        rawBody: raw,
        signatureHeader: req.headers.get('x-hub-signature-256'),
        eventHeader: req.headers.get('x-github-event'),
        secret: process.env.GH_WEBHOOK_SECRET!,
        app: {
            appId: process.env.GH_APP_ID!,
            installationId: process.env.GH_APP_INSTALLATION_ID!,
            privateKeyPem: Buffer.from(process.env.GH_APP_PRIVATE_KEY!, 'base64').toString(),
        },
        git: { contentDir: process.env.CONTENT_DIR!, remoteUrl: process.env.CONTENT_REMOTE },
        eviction,
        fallbackPaths: ['/', '/blog'],
    });
    if (!result.ok) return new Response(result.reason, { status: result.status });
    for (const p of result.evictPaths) revalidatePath(p);
    return Response.json({ ok: true, files: result.changedFiles });
}

An Express example is in adapters/express — feed it the raw body (express.raw({ type: 'application/json' }), not express.json(), or the signature won't match). Point the App's webhook at the route, push, and the page updates.

Minting a token directly

writeJson, pull, and push all take a token; get one with:

import { installationToken } from '@seanstoves/ss-gitops-jsondb';

const token = await installationToken({
    appId: process.env.GH_APP_ID!,
    installationId: process.env.GH_APP_INSTALLATION_ID!,
    privateKeyPem: Buffer.from(process.env.GH_APP_PRIVATE_KEY!, 'base64').toString(),
    // apiBaseUrl: 'https://ghe.example.com/api/v3'  // GitHub Enterprise
});

It's hour-lived, cached in memory, refreshed 5 minutes early, and keyed per app+installation+host so a multi-app process never crosses tokens.


Why the security model matters

This code moves a write-capable GitHub token and reads a push-writable tree. The details are the point:

  • Token minted with crypto.subtle RS256. The private key is a non-extractable CryptoKey, never leaves the process. No third-party JWT lib.
  • The token rides into git via GIT_CONFIG, never argv. /proc/<pid>/cmdline is world-readable; an auth header there is a leak. It's injected through http.<base>.extraheader and scoped to the one remote.
  • Webhook signatures verified in constant time over the RAW body. crypto.subtle.verify, never a hand-rolled compare. Verify before you parse — reparsing the JSON changes the bytes and breaks the HMAC.
  • Changed files come from the before/after SHAs, not the payload. The payload's file list is attacker-influenced; diff the SHAs locally after pulling.
  • git is spawned argv-only, no shell. SHAs validated before a diff range; --no-renames -z makes a rename bust both old and new paths so nothing serves stale.
  • Filesystem access is O_NOFOLLOW + slug-whitelisted + asserted-inside-root, for both reads and writeJson. A symlink or ../ can't escape the content directory.
  • Eviction is data, not a side effect. The core returns paths; the adapter decides how to invalidate. Nothing in the core imports Next.

API

| Export | What | |---|---| | readJson / listJson / writeJson / removeJson | The git-backed JSON store. Write/remove are commits. | | installationToken(app) | Cached hour-lived App token (RS256 via Web Crypto). Multi-app / GHE safe. | | verifyGithubSig(secret, rawBody, header) | Constant-time HMAC check of X-Hub-Signature-256. | | handleWebhook(opts) | Inbound orchestrator → { changedFiles, evictPaths }. | | evictForFiles(files, config) | Pure file→paths eviction matrix. | | run / status / headSha / addAll / commit / pull / push / changedFiles | GitConfig-driven git ops. | | safePath / assertInsideRoot / openInRoot / readInRoot / readBytesInRoot / writeInRoot / SLUG_RE | The filesystem guards. |

Types: WriteOptions, AppConfig, GitConfig, GitResult, EvictionConfig, StaticRule, PatternRule, WebhookOptions, WebhookResult.


What this is not

  • Not a query engine. No SQL, no indexes beyond your folder layout, no joins. If you need those, you've outgrown flat files — use a database.
  • Not an auth system. Sessions, OAuth, RBAC: deliberately left out.
  • Not a deploy tool. It stores content and evicts cache; building and running the app is yours.

Contributing

Fork it, branch, open a PR. Keep it boring on purpose:

  • Zero runtime dependencies stays zero. This mints a write-capable token and touches the filesystem — a transitive dep is exactly the supply-chain risk it exists to avoid. A PR adding one to the prod tree won't land.
  • The tests are the contract. npm test (traversal & symlink-escape rejection, constant-time HMAC, write-is-a-commit, eviction cap) stays green. Touch the path guards, the token handling, or the HMAC and you add a test that proves the property still holds before you change the code.
  • The boundaries don't move. The token never reaches argv; every read/write/delete stays inside the content root; the webhook verifies before it parses. If a change touches any of those, say so loudly in the PR and bring the test.
  • Match the voice. Comment the hard stuff, skip the obvious, no marketing words.

Found a security issue? Don't open a public issue — use GitHub's private "Report a vulnerability" (the repo's Security tab), or email [email protected].

License

MIT © 2026 Sean Stoves