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

@flarecart/licensing

v1.0.0

Published

Licence-key seats for software sellers: validate a key (and one installation of it) with a reason a client can show, claim a seat race-free so parallel activations can't overrun the limit, and release it by an instance id the server issued. In-memory and

Readme

@flarecart/licensing

Licence-key seats for software sellers: validate a key (and one installation of it) with a reason a client can show, claim a seat race-free so parallel activations can't overrun the limit, and release it by an instance id the server issued. Storage behind one interface, with an in-memory store and a Cloudflare D1 store included.

Why

A licence server looks like three endpoints and a counter. The holes are all in the details:

  • A key for somebody else's product. A key is a string anybody can buy, from any seller, in test mode, for nothing. Unless the check names the product, a key bought for one thing unlocks another.
  • A client-chosen instance id. Clients name themselves by site URL or machine name, which are guessable, and a guessable id lets anybody release somebody else's seat.
  • A race on the last seat. Count, then insert, as two statements: ten parallel activations of a one-seat key each count zero and each take one. On D1 there's no transaction to put them in.
  • Checking only the key. A key that's fine but whose seat was released on another machine is exactly what a client checking only the key never sees.
  • A bool instead of a reason. "Invalid" makes the seller's support team find out what happened; "expired" lets their software say "renew here".

Features

  • ✅ validate(): the key, optionally one installation, optionally the product; precedence not found → revoked → expired → installation.
  • 💺 claim(): race-free without a transaction (one conditional statement); instance ids issued here (128 random bits); a fingerprint only as an idempotency hint, so a reinstall gets its seat back.
  • 🔓 release(): by the issued instance id only, scoped to the key; the seat is kept, inactive, so a key's history stays visible.
  • 🗣️ Verdicts with words: stable wire values, a sentence for each refusal, and changed so check-ins on startup aren't announced.
  • 🗄️ Bring your own storage, or use the in-memory store or the Cloudflare D1 store (schema included, your own columns welcome).
  • 🔑 Readable keys: 150 bits of Crockford base32, read back however they were typed.

Install

npm install @flarecart/licensing

Node 22+, Cloudflare Workers, Deno and Bun: anything with Web Crypto. No dependencies.

Usage

Create the tables in a migration, with your own columns after them:

import { d1Schema } from '@flarecart/licensing';

console.log(d1Schema());
// then, in the same migration:
//   ALTER TABLE licenses ADD COLUMN order_id TEXT;
//   ALTER TABLE licenses ADD COLUMN subscription_id TEXT;

Issue a licence when an order is paid:

import { d1Store } from '@flarecart/licensing';

const store = d1Store(env.DB, { ip: (address) => hashIp(address) });

const license = await store.issue({
  product: 'prod_ABC',                 // what it unlocks
  seats: 3,                            // installations at once
  expiresAt: null,                     // or a subscription's period end (unix ms)
  extra: { order_id: 'ord_9f2c…' },    // your columns
});
license.key;                           // '7KQ2M-9XWPA-H3D8F-TV0RC-Z4N6B-E1JYS': show it to the buyer

Answer the seller's software:

import { createSeats } from '@flarecart/licensing';

const seats = createSeats({ store, requireProduct: true });

const taken = await seats.claim(key, { name: 'Studio Mac', fingerprint, ip, product: 'prod_ABC' });
taken.ok;                              // true
taken.reason;                          // 'activated', 'already_active', 'limit_reached'…
taken.seat.instance;                   // the client keeps this
taken.changed;                         // announce only when true
taken.message;                         // 'This licence key has reached its activation limit…' when refused

const check = await seats.validate(key, { instance, product: 'prod_ABC' });
check.valid;                           // false
check.verdict;                         // 'instance_deactivated'

await seats.release(key, instance);    // the seat as it was, or null

Run them:

await store.update(license.id, { revoked: true });             // refunded
await store.update(license.id, { expiresAt: periodEnd });      // a subscription renewed
await store.releaseAll(license.id);                            // "reset activations"
await store.seats(license.id);                                 // every installation, newest first

API

createSeats({ store, now?, requireProduct? })

Returns { validate, claim, release }.

| | | |---|---| | validate(key, { instance?, product? }) | A Check: { valid, verdict, message, license, inUse, seat }. Changes nothing. | | claim(key, { name?, fingerprint?, ip?, product? }) | An Activation: { ok, reason, claim, verdict, message, changed, inUse, seats, seat }. | | release(key, instance) | The Seat as it was, or null when nothing was released. |

Keys are passed through normaliseKey() first. requireProduct turns a check without a product into product_required: on for a public API, off for your own screens.

Verdicts and claims

| Verdict | | |---|---| | ok | | | not_found | No such key, or a key for another product. | | revoked | | | expired | Past expiresAt (it still works at its last millisecond). | | instance_not_found | That installation isn't on this licence. | | instance_deactivated | It was, and was released. | | limit_reached | No seat free. | | product_required | requireProduct is on and none was sent. | | unavailable | For an app to send when licensing is switched off. |

Claims: activated, reactivated (a released seat with the same fingerprint, taken again), already_active (nothing changed). verdictMessage(verdict) and claimChanged(claim) are exported for apps that build their own answers.

Stores

memoryStore({ ip?, now? }) and d1Store(db, { ip?, now?, licensesTable?, seatsTable? }) both implement:

  • SeatStore, what the rules need: license(key), seatsInUse(license), seat(license, instance), seatForFingerprint(license, fingerprint), addSeat(license, newSeat), reactivate(license, seat, name), release(license, instance).
  • Running licences: issue(input), update(id, { revoked?, expiresAt?, seats? }), seats(licenseId), releaseAll(licenseId).

ip is what to keep of a client's address (a keyed hash, say); by default nothing is kept. d1Schema(options?) returns the CREATE TABLE SQL.

Writing your own store: addSeat and reactivate must take a seat only while fewer than license.seats are active, in one atomic step, and addSeat must refuse a second seat for a fingerprint the licence already has. On SQL that's one conditional statement:

INSERT OR IGNORE INTO license_seats (license_id, instance, …)
SELECT ?, ?, …
WHERE (SELECT count(*) FROM license_seats WHERE license_id = ? AND active = 1) < ?

with a unique index on (license_id, fingerprint), and changes saying whether it took. Seats must be scoped to their licence, or one key could release another's.

Keys

| | | |---|---| | newLicenseKey() | 30 Crockford base32 characters (150 random bits) in six groups: 7KQ2M-9XWPA-H3D8F-TV0RC-Z4N6B-E1JYS. | | normaliseKey(input) | Case, spaces, dashes and O / I / L forgiven; a key from another system comes back trimmed and upper-cased. | | newInstanceId() | 128 random bits, hex. | | newLicenseId() | lic_ and 64 random bits, hex. | | KEY_ALPHABET | 0123456789ABCDEFGHJKMNPQRSTVWXYZ |

Notes and limits

  • Keys are stored as issued, not hashed. A buyer needs to see their key again (on an account page, in a "resend" email), and a hash can't be shown. A key only ever unlocks seats of one licence, which a seller can revoke; keep the table as private as your orders.
  • A key for another product reads as not_found, not as something of its own: asking with your product reveals nothing about keys that aren't yours.
  • Fingerprints are hints, never authority. They're guessable, so they only give a reinstall its seat back. Releasing takes the issued instance id.
  • Expiry is the app's to resolve. A licence that follows a subscription keeps its period end in expiresAt; move it with update() when the subscription renews or ends.
  • Your columns: add them to the licences table in your migration, pass them to issue() as extra, and read them back from license.record. Names are checked as plain identifiers before they reach SQL.
  • History is kept. A released seat stays, inactive: "this key has moved between forty machines" is what separates somebody with several laptops from a key posted on a forum.

The JavaScript twin of SugarCommerce's PHP licensing. The difference is the store: the PHP one runs the claim in a transaction, which D1 can't hold open for a Worker, so this one takes a seat in a single conditional statement.

Testing

npm test

67 tests: the PHP twin's suite against both stores (product scoping, every verdict and their precedence, expiry to the millisecond, an installation released elsewhere, another key's instance, issued instance ids, the last seat, fingerprints as idempotency, bad keys claiming nothing, release by instance only and only once, stable wire values), the races (ten activations of a one-seat key at once, one client activating twice at once, a reactivation and a new activation sharing the last seat), running licences, keys, and the D1 store's schema, table names and your own columns, on real SQLite.

License

MIT