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

@crxpay/sdk

v0.1.2

Published

Chrome Extension Subscriptions, Paywalls & A/B Testing SDK — Stripe Connect, MV3-native, signed offline cache.

Readme

@crxpay/sdk

Chrome Extension Subscriptions, Paywalls & A/B Testing — drop-in SDK for Manifest V3.

@crxpay/sdk handles the parts of charging for a Chrome extension that shouldn't be left to a developer in a hurry: subscription state, entitlement checks, signed offline cache, paywall opening, license-key activation, and Stripe Checkout / Customer Portal integration. It is designed to be dropped into a Manifest V3 background service worker and forgotten.

If you have used ExtPay, the API is intentionally similar — but the SDK is MV3-native, the cache survives offline, and the subscription state is signed so it cannot be flipped to "active" from DevTools.


Install

npm install @crxpay/sdk
# or
pnpm add @crxpay/sdk

Peer dependencies: react >= 18 (only when using the /react entry — the core SDK has no React dependency).

Quick start (background service worker)

Where to get your publishable key: dashboard.crxpay.io → register your extension → API keys in the project sidebar. The key starts with crxpay_pub_. Safe to commit (it's the public key — like Stripe's pk_test_/pk_live_).

// background.ts
import { CrxPay } from '@crxpay/sdk/background';

const client = CrxPay.configure({
  apiKey: 'crxpay_pub_…', // copied from /p/<your-extension>/api-keys
  mode: 'test',           // 'test' for sandbox, 'live' for production. Default: 'live'.
});

// Required: forward popup / options / content-script messages to the SDK.
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg?.type?.startsWith('CRXPAY_')) {
    client.handleMessage(msg, sender).then(sendResponse);
    return true; // keep the message channel open for the async response
  }
});

// Fires when the user transitions from non-paid → active or trialing.
client.onPaid.addListener((subscription) => {
  // unlock premium features here
});

Reading subscription state from a popup or content script

// popup.ts
import { CrxPay } from '@crxpay/sdk/content';

const subscription = await CrxPay.getSubscription();

if (subscription.hasEntitlement('pro')) {
  showProUI();
} else {
  await CrxPay.openCheckout(); // opens hosted checkout in a new tab
}

Bundling for MV3 — read this before you ship

The SDK ships as ESM with xstate and zod declared as bare external imports (you bring your own copies, bundlers tree-shake the unused parts). This is normal for npm libraries — but Manifest V3 service workers cannot resolve bare module specifiers like import { x } from 'xstate'. They only accept relative paths (./foo.js) or chrome-extension:// URLs.

Symptom if you skip this: the service worker silently fails on first load. No console error, no SDK behavior, your popup looks dead. Ask us how we know.

Solution: bundle your background script with esbuild (or any bundler that inlines transitive deps). A minimal build.mjs:

import { build } from 'esbuild';
await build({
  entryPoints: ['background.js'],
  bundle: true,                   // inline xstate + zod + the SDK
  format: 'esm',
  platform: 'browser',
  target: ['chrome120'],
  outfile: 'background.bundle.js',
});

Then point your manifest at the bundled file:

{
  "background": { "service_worker": "background.bundle.js", "type": "module" }
}

If you'd rather see real working examples, check apps/extension-test/ and apps/extension-test-2/ in the repo — both ship a build-bg.mjs that demonstrates the pattern.

Verifying boot in dev

Drop this into your popup or an options page to confirm the SDK actually came up — not just imported:

const reply = await chrome.runtime.sendMessage({ type: 'CRXPAY_HEALTH' });
// reply.status === 'ok'        — booted cleanly
//          === 'booting'       — still starting (call again in a sec)
//          === 'degraded'      — booted but with errors (see reply.errors)

(In your background script, route CRXPAY_HEALTH to client.healthCheck() — see the message-router snippet below.)

manifest.json — what the SDK needs

{
  "manifest_version": 3,
  "background": { "service_worker": "background.js", "type": "module" },
  "permissions": ["storage", "alarms", "tabs"],
  "host_permissions": ["https://api.crxpay.io/*"]
}

That's it. No web_accessible_resources, no content_scripts unless you want SDK access from a content script (in which case import from @crxpay/sdk/content).

API surface

| Entry | What's exported | Use it from | |---|---|---| | @crxpay/sdk/background | CrxPay.configure({...}) returning a client with handleMessage, onPaid, etc. | Background service worker only | | @crxpay/sdk/content | CrxPay.getSubscription(), openCheckout(), etc. — every call is forwarded to the background via chrome.runtime.sendMessage | Popup, options page, content scripts | | @crxpay/sdk/react | <CrxPayProvider>, useSubscription(), useOpenCheckout() | Any React UI inside the extension | | @crxpay/sdk/server | License-key signature verifier (offline) | Your own backend, if you want to validate keys before hitting our API | | @crxpay/sdk | Re-exports types | Anywhere |

The full TypeScript surface ships with the package — every method has inline JSDoc.

Result envelopes — the SDK never throws

Every public method returns a Result<T>:

type Result<T> =
  | { ok: true;  data: T }
  | { ok: false; error: { code: string; message: string } };

This is the same pattern Stripe's newer SDKs use. It means a misconfigured SDK can never crash your background worker — the worst case is an { ok: false } you can branch on.


Security model

What we defend against

  • Tampered local cache — every cache entry is HMAC-signed with a key derived from the extension's chrome.runtime.id plus the publishable API key. A user opening DevTools and editing the cached subscription to flip status: "none""active" will see the cache discarded silently on the next read. Verified in src/__tests__/security-smoke.test.ts.
  • Cross-extension cache replay — copying a cache entry from extension A into extension B does not work; the HMAC key is install-scoped.
  • Sibling extension lifting license keys / customer JWTs — license keys and the customer JWT issued after magic-link verification are encrypted with AES-GCM (key derived from chrome.runtime.id via PBKDF2-100k). A different extension installed in the same browser profile cannot decrypt them.
  • Foreign-extension messagesclient.handleMessage rejects messages whose sender.id does not match chrome.runtime.id. Defense in depth if you ever wire externally_connectable.
  • Hostile public input — every value passed to a network call goes through a Zod pre-flight (identify, openCheckout, activateKey, …). Malformed input is rejected before it touches the API.

What we do NOT defend against

  • A motivated attacker with persistent local access can re-derive the AES-GCM key (PBKDF2 inputs are public). The encryption raises the bar against sibling extensions and DevTools snooping; it is not designed to defeat a forensic adversary.
  • A customer who captures the encrypted cache while subscribed and restores the snapshot after canceling will appear "active" until the cache TTL expires — 60 seconds for license-backed subscriptions, 4 hours otherwise. If you need hard-cancel-now semantics, force a client.syncSubscription() on every entitlement check.
  • The publishable API key is, by design, public. It can be read from any packaged extension. Authorization for sensitive operations happens server-side via the customer JWT and org-binding on the extension row.

Reporting vulnerabilities

Please email [email protected]. Do not open public issues for security reports.


Development

pnpm install
pnpm test       # 135 tests, including security smoke tests
pnpm build      # outputs to dist/ — no .map files, no source leak

License

MIT — see LICENSE in this package.