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

chrome-review

v0.1.0

Published

Pre-submission auditor for Chrome Web Store (Manifest V3) extensions. Static analysis: permission-to-usage diff, remote-hosted-code detection, host-permission overbreadth, source-map-aware bundled-code honesty. No extension code is executed. No telemetry.

Readme

chrome-review — CLI

Static analyzer CLI for unpacked Manifest V3 Chrome extensions. No extension code is executed. The only outbound HTTP request the CLI makes is a codeload tarball fetch against github.com when the input is a GitHub URL (Pass 3 / W1-P3 / Decision D29). No telemetry, no phone-home, no OAuth flow.

Invocations

# Local unpacked extension directory
npx chrome-review ./path/to/extension

# Public GitHub repo, default branch (tries main, then master)
npx chrome-review https://github.com/<owner>/<repo>

# Specific branch, tag, or commit SHA
npx chrome-review https://github.com/<owner>/<repo>/tree/<ref>

# Specific ref + subpath as the extension root
npx chrome-review https://github.com/<owner>/<repo>/tree/<ref>/<subpath>

npx chrome-review@<version> resolves to the published npm package (see docs/release.md for the release runbook). In a cloned workspace, npx chrome-review <input> resolves to the local workspace-installed bin and also works after npm install && npm run build.

<path> must be a directory containing a manifest.json.

Private-beta tarball install

The private beta is distributed through local npm pack tarballs, not the npm registry. Install all four tarballs together because the CLI depends on the internal parser/rules/types packages by package name:

npm run build
PACK_DIR=$(mktemp -d)
npm pack --pack-destination "$PACK_DIR" \
  --workspace @chrome-review/types \
  --workspace @chrome-review/parser \
  --workspace @chrome-review/rules \
  --workspace chrome-review

SMOKE_DIR=$(mktemp -d)
cd "$SMOKE_DIR"
npm init -y
npm install \
  "$PACK_DIR"/chrome-review-types-0.1.0.tgz \
  "$PACK_DIR"/chrome-review-parser-0.1.0.tgz \
  "$PACK_DIR"/chrome-review-rules-0.1.0.tgz \
  "$PACK_DIR"/chrome-review-0.1.0.tgz
npx chrome-review /absolute/path/to/unpacked-extension
npx chrome-review --json /absolute/path/to/unpacked-extension

This is a private-beta packaging path only. It is not an npm publish flow and does not promise a stable 1.0 JSON contract.

Environment

| Variable | Purpose | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | GITHUB_TOKEN | If set, sent as Authorization: Bearer <token> on the codeload tarball fetch. Required for private repos and lifts the unauthenticated rate limit. Never logged. (D30.) |

No config file. No other environment variables. The CLI never reads ~/.chrome-review/* or similar.

GitHub URL ingestion lifecycle

When the input is a GitHub URL, the CLI:

  1. Parses the URL into {owner, repo, ref?, subpath?}.
  2. Fetches https://codeload.github.com/<owner>/<repo>/tar.gz/<ref> via Node's native fetch. If no ref was given, tries main then master.
  3. Extracts the tarball into a fresh subdirectory of os.tmpdir() named chrome-review-<random>.
  4. Analyzes the extracted repo (or the subpath subtree) exactly as if it were a local directory.
  5. Cleans up the temp directory — on success, on every typed error path, and on the four process-level exit modes (exit, SIGINT, SIGTERM, uncaughtException). The cleanup LOGIC for each mode is unit-tested (via test-only exports that run the handler body without actually signaling the test process). Handler idempotency — calling ensureProcessCleanupHandlers multiple times never adds duplicate listeners — is also pinned by a unit test. Actual OS signal delivery is not exercised in automated tests because real SIGINT would kill the test runner; manual smoke via SIGINT'ing a running CLI is documented in docs/release.md.

Known limitations (GitHub URL ingestion)

The fetch path in packages/cli/src/github-url.ts is deliberately minimal in v0.1.0. Three hardening items are deferred to a later pass and listed in ROADMAP.md's next-pass candidates:

  • No request timeout. A hung codeload.github.com response will block the CLI indefinitely. Abort with Ctrl-C; the temp directory is removed by the SIGINT cleanup handler.
  • No size limit. The full repository tarball is always downloaded — the /tree/<ref>/<subpath> URL form narrows what the analyzer walks after extraction, but codeload still ships the whole repo. Very large repositories will consume bandwidth and disk accordingly; clone locally and run against a path for tighter control.
  • No automatic retry. Transient network failures (fetch-thrown errors, 5xx responses) exit immediately with a Retry, or analyze locally after cloning suggestion in the error message.

Options

| Option | Description | | -------------- | ------------------------------------------------- | | --json | Emit the full AnalyzerReport as JSON on stdout. | | -h, --help | Print usage and exit 0. |

Default output is a human-readable, multi-section report keyed off the same underlying AnalyzerReport structure that --json emits verbatim.

Exit codes

| Code | Meaning | | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 0 | Analyzer ran cleanly. May still include LOW / MEDIUM findings — they are informational. | | 1 | Analyzer ran AND produced ≥1 HIGH-confidence finding (undeclared-but-used permission, OR remote-hosted-code). Treat this as a high-risk static pre-submission signal, not a guaranteed Chrome Web Store outcome. | | 1 | Analyzer threw an uncaught exception (manifest unreadable, walker crash, etc). The exception message is printed to stderr. | | 2 | CLI usage / input error: unknown flag, wrong arg count, path doesn't exist or isn't a directory, OR a recognized GitHub URL ingestion failure (see below). |

Code 1 covers two runtime cases (HIGH finding vs analysis exception). Disambiguate via stderr: an exception writes Analysis failed: ...; a HIGH finding writes only to stdout.

GitHub URL error cases (all exit 2)

Every failure is surfaced as a specific actionable message, not a generic "not found" / "error". The message classes, each pinned by a unit test in tests/unit/github-url.test.mjs:

| GitHubUrlError.kind | When | Message shape | | --------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | invalid_url | URL didn't match the three accepted shapes. | Invalid GitHub URL: <url> followed by the three accepted shapes. | | not_found | 404 on every candidate ref (including the main/master fallback). | Repository `<owner>/<repo>` not found at ref `<ref>`. Check the URL. If this is a private repo, set GITHUB_TOKEN. | | auth_failed | 401 from codeload, or 403 without rate-limit headers. | GITHUB_TOKEN rejected. Verify the token is valid and has \repo` scope for private repos.(401) or a repo-may-be-private variant. | |rate_limited | 403 withx-ratelimit-remaining: 0or anx-ratelimit-reset. | GitHub rate limit exceeded. Resets at . Set GITHUB_TOKEN to lift the limit, or wait. | |network |fetchthrew (ECONNRESET, timeout, DNS) or non-4xx status. |Network error fetching tarball: . Retry, or analyze locally after cloning. | |extract | Tarball extracted but the bytes were not a valid gzipped tar. |Failed to extract tarball: . The repo may have an unusual structure. | |subpath_missing | Subpath didn't exist inside the extracted tarball. | ``Subpathnot found in/@`. Available top-level entries: .`` |

CI scripts that need to distinguish a HIGH-finding exit 1 from an analysis-exception exit 1 should inspect --json: a HIGH finding produces a valid report; an exception produces no JSON at all.

--json output shape

--json prints a single JSON object conforming to schema/report.schema.json (JSON Schema draft-07).

The schema is 0.x — UNSTABLE. Field additions, renames, and removals can occur in any 0.x release. The schema will be frozen at 1.0; until then, treat the JSON as best-effort machine-readable output rather than a stable contract.

Schema versioning

Starting with 0.3.0 (Pass 2 reopen 2 / W5-R2 / Decision D21), the schema $id encodes the current version and every emitted report carries a top-level schemaVersion field so downstream tools can key behavior on it. Current version is 0.3.1 (Pass 3 / W3-P3 added side_panel_html to EntryPoint.kind). REPORT_SCHEMA_VERSION is exported from @chrome-review/types; the CLI fills the field when it builds the report. A unit test (tests/unit/json-schema.test.mjs) asserts the schema's const, the schema $id, and the runtime export stay in lock-step.

Pass 3 schema changes bump the version. The $id moves, the schemaVersion.const in the schema moves, and REPORT_SCHEMA_VERSION moves — all in the same commit.

Changelog

  • 0.3.1 — current (Pass 3 / W3-P3 / Decision D34). Additive-only widening of EntryPoint.kind enum to include side_panel_html, and the corresponding ManifestAnalysis.sidePanelHtml?: DeclaredString field surfacing the manifest's side_panel.default_path. Reports from extensions without a side panel are byte-for-byte compatible with 0.3.0; consumers that enumerate EntryPoint.kind values should add side_panel_html to their switch (or treat unknown values permissively).

  • 0.3.0 (Pass 2 reopen 2 / reopen 3). First versioned release; cumulative state of the Pass 2 reopen + reopen-2 + reopen-3 era. Adds:

    • schemaVersion top-level field, required, const: "0.3.0".
    • Pass 2 reopen 3 / W2-R3 — ApiUsage.escapeForm?: string (enum of return_escape / top_level_let / top_level_var / function_scope / block_scope / rebinding / object_property_store / assignment_binding). Additive optional field naming the specific escape form behind an escape_scope usage. Absent on non-escape usages. Added under the 0.x UNSTABLE disclaimer without a version bump; downstream consumers that ignore unknown optional fields are unaffected.
    • Pass 2 reopen 2 / W2-R2 — ExecuteScriptInjection.contributedHostPattern?: string recording a URL pattern threaded from a statically analyzable surrounding chrome.tabs.query({url: LITERAL}) callback.
    • Pass 2 reopen W5 — ExecuteScriptInjection rename: locsite; target is now a discriminated union ({resolved: true, tabId?, frameIds?, documentIds?, allFrames?} or {resolved: false, reason: string}); origin: 'tabs_legacy''tabs-legacy'.
    • Pass 2 reopen W2 — new UnresolvedReference.reason values: dynamic_network_url_non_literal, bare_module_specifier_not_resolved.
    • Pass 2 reopen W1 — PartiallyAnalyzedFile.resolvedSources?: string[].
  • 0.2.x — pre-versioning era (Pass 2 initial close-out, reconstructed from git history). The $id in that era was report-0.x.json and reports had no schemaVersion field. Consumers that saw reports without schemaVersion are reading pre-0.3.0 output. The cumulative schema shape at end of 0.2 corresponds to Pass 2 §A1–§A7 landings.

  • 0.1.x — initial Pass 1 (reconstructed from git history). First AnalyzerReport shape; permission findings, remote-code findings, host-scope findings, caveats. No executeScript / networkUrlLiteral / partially-analyzed extensions yet.

Recent breaking changes

  • Pass 2 reopen 2 / W5-R2 (Decision D21) — schema versioning. See "Schema versioning" above. The $id moved from report-0.x.json to report-0.3.0.json; schemaVersion: "0.3.0" is now a required field on every emitted report.
  • Pass 2 reopen 2 / W2-R2 (Decision D20) — ExecuteScriptInjection.contributedHostPattern. Optional string carrying a URL pattern (match pattern or literal URL prefix) when the parser detected that the injection's target.tabId was threaded from a surrounding chrome.tabs.query({url: LITERAL}) callback. Host-scope folds this host into observed scope; consumers can surface it directly.
  • Pass 2 reopen W5 (Decision D16) — ExecuteScriptInjection shape. Three field renames in one go:
    • locsite (same SourceLocation shape).
    • top-level targetReason removed; folded into target as a discriminated union: { resolved: true, tabId?, frameIds?, documentIds?, allFrames? } when statically resolvable, { resolved: false, reason: string } when not. The set of reason values is unchanged.
    • origin: 'tabs_legacy''tabs-legacy' (kebab-case).
  • Pass 2 reopen W2 — new UnresolvedReference.reason values.
    • dynamic_network_url_non_literal — emitted for fetch / XHR / WebSocket / EventSource / sendBeacon / importScripts calls whose URL argument is fully dynamic or a template literal with no extractable host.
    • bare_module_specifier_not_resolved — emitted for CJS require('pkg') and ESM import x from 'pkg' (D15: the analyzer narrows the contract to local-looking specifiers; bare imports are recorded but not resolved).
  • Pass 2 reopen W1 — PartiallyAnalyzedFile gained resolvedSources. Optional string array, populated for source_map_resolved entries with the on-disk paths of original sources the map declares.

The schema is validated against every corpus entry's --json output as part of the test suite (tests/unit/json-schema.test.mjs). A schema break will fail CI.

The top-level shape (corresponds to AnalyzerReport in @chrome-review/types):

{
  "schemaVersion": "0.3.1",
  "parser": {
    "extensionRoot": "/abs/path/to/extension",
    "manifest": {
      /* ManifestAnalysis */
    },
    "entryPoints": [
      /* EntryPoint[] */
    ],
    "apiUsages": [
      /* ApiUsage[] */
    ],
    "unresolved": [
      /* UnresolvedReference[] */
    ],
    "scannedFiles": ["bg.js", "popup.js"],
    "unparseableFiles": ["dist/bundle.js"],
    "htmlFiles": ["popup.html"],
    "dnrRuleSets": [
      /* DnrRuleSetSummary[] */
    ],
    "notes": [
      /* AnalysisNote[] */
    ],
  },
  "rules": {
    "permissionFindings": [
      /* PermissionFinding[] */
    ],
    "remoteCodeFindings": [
      /* RemoteCodeFinding[] */
    ],
    "hostScopeFindings": [
      /* HostScopeFinding[] */
    ],
    "caveats": [
      /* AnalysisNote[] */
    ],
  },
}

Each finding object carries confidence: "HIGH" | "MEDIUM" | "LOW". The exit-code policy keys off confidence === "HIGH" for undeclared_but_used permission findings and any remoteCodeFindings entry.

Network behavior

The CLI makes exactly one kind of outbound HTTP request: a tarball fetch to codeload.github.com when a GitHub URL is provided as the positional argument. Local path inputs trigger no network activity.

What the CLI does NOT do

  • Execute extension code.
  • Phone home / emit telemetry.
  • Guarantee Chrome Web Store approval or guarantee rejection prevention.
  • Suppress findings — there is no .chrome-review.json ignore mechanism in v0.1.0. (Listed under ROADMAP.md next-pass candidates.)