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

@mutmutco/installer-gate

v1.2.0

Published

Installer gate server library — GitHub device flow, allowlists, and signed release manifests. Mounted by a product's own server.

Readme

@mutmutco/installer-gate

A small, framework-agnostic node:http server library a product mounts to run its installer gate: it drives the GitHub device flow (or verifies a Google bearer), enforces an allowlist, and serves an Ed25519-signed release manifest plus the release files it describes.

  • Node >= 22, ESM, zero runtime dependencies (node: builtins only).
  • The library never hardcodes a credential: the GitHub client id/secret and every product-specific piece (manifest, signer, file reader, bearer verifier) are injected by the adopter.
  • The published package ships a built ESM bundle (dist/index.js) plus declarations (dist/*.d.ts); npm run build produces both — esbuild bundles the entry and tsc -p tsconfig.build.json emits types only. The adopter imports @mutmutco/installer-gate directly; no bundler step is required.

Mounting

import http from 'node:http';
import { sign as cryptoSign } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { createGateHandler } from '@mutmutco/installer-gate';

const gate = createGateHandler({
  kind: 'github',
  githubClientId: process.env.GITHUB_CLIENT_ID!,
  githubClientSecret: process.env.GITHUB_CLIENT_SECRET!,
  allowlist: { source: 'roster', getLogins: async () => rosterLogins() },
  release: {
    manifest: async () => ({
      version: currentVersion(),
      files: releaseFiles(), // [{ path, sha256, size }]
    }),
    sign: (canonicalBytes) => cryptoSign(null, canonicalBytes, privateKey), // Ed25519, detached
    readFile: async (path) => readFile(join(releaseRoot, path)),
  },
});

http.createServer((req, res) => {
  if (!gate(req, res)) myOwnRouter(req, res); // false = not a /gate or /release path
}).listen(8787);

createGateHandler(config) returns (req, res) => boolean. It owns /gate/* and /release/*; for those it handles the request and returns true. For every other path it returns false and touches nothing, so the adopter's router continues.

Config reference

| Field | Type | Notes | | --- | --- | --- | | kind | 'github' \| 'google' | Selects the identity lane. | | githubClientId | string | github kind. The gate rejects a device-flow client_id that does not match it. | | githubClientSecret | string | github kind. Passed in by the adopter — never committed here. | | githubWebBase | string? | github kind. GitHub web origin for the device flow — POST /login/device/code and POST /login/oauth/access_token. Default https://github.com; override for GHES or tests. Independent of githubApiBase. | | githubApiBase | string? | github kind. GitHub REST origin for identity lookups (GET /user). Default https://api.github.com; override for GHES or tests. Independent of githubWebBase. | | verifyBearer | (token) => Promise<{sub}\|null> | google kind. The product's own OAuth server. | | allowlist | { source: 'roster' \| 'logins', getLogins?: () => Promise<string[]> } | Allowed identities. | | release | { manifest, sign, readFile, currentVersion?, launcher? } | How release artifacts are described, signed and read. | | tokenTtlSeconds | number? | Default 3600; values above 3600 are clamped to 3600. | | revoke | string[] \| (() => Promise<string[]>)? | Explicit revocations, re-read every call. | | tokenSecret | string? | HMAC key for the gate's own tokens. github derives one from the client secret; google mints no tokens. | | now | () => number? | Injectable clock (epoch ms) for tests. |

release.manifest() returns { version, files: [{ path, sha256, size }] }. The gate stamps created (ISO 8601) and signs; release.readFile(path) throws when a file is absent.

release.launcher() (optional, #7070) returns the same { version, files } shape for the launcher binaries: version is the launcher version and each path is a platform key (win-x64, darwin-arm64, …). It is served signed like the manifest at the public GET /release/launcher, so keep it cheap — never hash binaries per request. Absent or throwing → 404 { error: "not_found" }.

GitHub hosts (#6783)

GitHub serves the device-flow endpoints and the REST API from different hosts, so the gate has one base per host. githubWebBase (default https://github.com) carries /login/device/code and /login/oauth/access_token; githubApiBase (default https://api.github.com) carries /user. Both go through one normalizer (http(s) scheme check + trailing-slash strip), and an override of either never moves the other. Calling /user on github.com answers 406, which the handler turns into 502 upstream_error; that is exactly the sign-in failure #6783 fixed. Consumer action: if you previously set githubApiBase to https://github.com (its old meaning), drop it or move that value to githubWebBase — as of 0.1.1 githubApiBase means the REST host.

Wire contract v1

| Method | Path | Body / headers | Response | | --- | --- | --- | --- | | POST | /gate/device/code | { client_id } | 200 { device_code, user_code, verification_uri, verification_uri_complete?, expires_in, interval } | | POST | /gate/device/token | { client_id, device_code } | 200 { access_token, refresh_token, expires_in } or 400 { error } | | POST | /gate/refresh | { refresh_token } | 200 { access_token, expires_in } or 403 { error } | | GET | /release/launcher | none (public) | 200 { version, created, files, signature } — the signed launcher record, or 404 { error } | | GET | /release/manifest | Authorization: Bearer <access_token> | 200 { version, created, files, signature } | | GET | /release/<path> | Authorization: Bearer <access_token> | raw bytes, or 404 { error } |

Device-flow 400 errors are exactly authorization_pending, slow_down, expired_token, denied (GitHub's access_denied maps to denied; any other GitHub error is terminal → denied).

Refusals: 401 { error: "unauthorized" } for a missing/invalid/expired bearer; 403 { error: "forbidden" } for an allowlist miss or a revoked identity.

Token format

The gate mints its own opaque bearer credentials after the identity is proven:

<base64url(JSON payload)> . <base64url(HMAC-SHA256(payloadB64, tokenSecret))>
payload = { sub, exp, jti, typ }

sub is the identity (GitHub login or Google sub), exp is epoch seconds, jti a random id, and typ is 'access' or 'refresh'. HMAC-SHA256 is the chosen scheme: the token only ever travels back to the gate that minted it, so a symmetric MAC is sufficient and needs no keypair. Tokens are opaque to the client; mintToken/verifyToken are exported for adopters that need to inspect one. expires_in is always <= 3600.

Allowlists

  • 'logins' — an explicit list served by getLogins().
  • 'roster' — the same interface; the adopter wires a roster adapter later. The gate treats both sources identically.
  • Matching is case-insensitive and trims whitespace (GitHub logins are case-insensitive).
  • Fail closed: a missing getLogins, an empty list, or a throw allows nobody.
  • The allowlist is checked at token mint and on every gated read, and revoke (a list or a function) is re-read on every call — so an allowlist edit or a revocation refuses the very next refresh and release read. Refresh mints a new access token only if the identity still passes.

Release signing

GET /release/manifest returns a base64 detached Ed25519 signature over exactly the UTF-8 bytes of the canonical JSON:

{"created":<created>,"files":[{"path":<path>,"sha256":<sha256>,"size":<size>}],"version":<version>}

Keys sorted lexicographically, arrays in given order, no whitespace. The launcher side verifies with the exported helpers:

import { verifySignedManifest, verifyReleaseFile } from '@mutmutco/installer-gate';

verifySignedManifest(manifest, publicKey);          // signature over the canonical bytes
verifyReleaseFile(manifest.files[i], fileBytes);    // size + SHA-256 of a downloaded file

buildCanonicalManifest(manifest) returns the exact bytes if you need them; canonicalJson(value) is the underlying serializer. GET /release/<path> refuses any path that escapes the release root (.., empty/. segments, backslashes, absolute paths) with 404 { error: "not_found" }.

The google kind

kind: 'google' does not implement OAuth. verifyBearer(token) is the product's own OAuth server; the library verifies the returned sub against the allowlist on every gated read. There is no device flow and no gate-issued refresh token for this kind, so /gate/device/* returns 404 { error: "not_found" }.

Contract-compliance notes (decisions taken)

  • Allowlist miss at token mint is 403 { error: "forbidden" }, matching the contract's global refusal rule; the device-token 400 set is reserved for GitHub's poll errors.
  • created is an ISO 8601 UTC string (new Date(now).toISOString()), so it is JSON-quoted in the canonical form.
  • Token secret is an added config field. github derives it from githubClientSecret when absent; google (which mints nothing) generates an ephemeral one.
  • /release/<path> traversal is refused rather than delegated to the adopter's readFile.
  • No secrets are committed; the client secret and signer key are injected at mount time.