JSPM

@arraypress/license-keys

1.0.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 6
  • Score
    100M100P100Q41428F
  • License MIT

Generate and validate human-readable license keys (XXXX-XXXX-... format). Configurable alphabet, group count, and separator. Pure functions, zero dependencies.

Package Exports

  • @arraypress/license-keys

Readme

@arraypress/license-keys

Generate and validate human-readable license keys (XXXX-XXXX-... format). Configurable alphabet, group count, and separator. Pure functions, zero dependencies.

npm install @arraypress/license-keys

Quick start

import {
  generateLicenseKey,
  isValidLicenseKeyFormat,
  isLicenseUsable,
} from '@arraypress/license-keys';

// Default: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX (32 hex chars)
const key = generateLicenseKey();
// → '7F3A-9C12-AB54-D9E1-2F08-7BC3-44E5-1D2A'

// Validate the shape on input
isValidLicenseKeyFormat(key);     // true
isValidLicenseKeyFormat('hello'); // false

// Validate a stored license row
isLicenseUsable({ status: 'active', expires_at: '2026-12-31T23:59:59Z' });
// → { valid: true }
isLicenseUsable({ status: 'revoked', expires_at: null });
// → { valid: false, reason: 'License has been revoked' }

Why

Every licensed-software product reinvents the same key-generation and validation primitives. This package is just those primitives — pure functions, no DB or storage assumptions, fully tested.

What it covers:

  • Generation — random key in your chosen alphabet/shape
  • Parsing — normalise customer-typed input (trim, uppercase, strip whitespace)
  • Reformatting — re-apply canonical group/separator format to legacy keys
  • Format validation — reject malformed input before hitting the DB
  • Status + expiry checks — pure date comparison, no DB

What it deliberately doesn't cover:

  • Storage / DB writes — you control the schema
  • Atomic activation race-checks — those need your DB's locking, not a library
  • Customer / order linkage — that's app-specific

API

generateLicenseKey(options?)

generateLicenseKey()
// → '7F3A-9C12-...-1D2A' (8 groups of 4 hex chars)

generateLicenseKey({ groups: 4, groupSize: 5, alphabet: 'unambiguous' })
// → 'A3K9P-Q2NTV-...-MR8XF' (4 groups of 5 unambiguous chars)

generateLicenseKey({ separator: '_' })
// → '7F3A_9C12_...'

Options:

Option Default Description
groups 8 Number of groups (1-32)
groupSize 4 Characters per group (1-16)
separator '-' String between groups
alphabet 'hex' 'hex' / 'alphanumeric' / 'unambiguous'

Alphabets

  • hex (default) — 0-9A-F. Familiar GUID-shaped keys.
  • alphanumeric0-9A-Z. Higher entropy per char, no exclusions.
  • unambiguous — Crockford-flavoured base32. Excludes I, L, O, U (the visually confusable glyphs). Best for keys customers type into a form.

parseLicenseKey(input)

Normalise for storage / comparison: trim, uppercase, strip whitespace. Keeps the separator.

parseLicenseKey('  7f3a-9c12-...  ');
// → '7F3A-9C12-...'

stripFormatting(input, separator?)

Strip the separator and whitespace, leaving just alphabet chars. Useful for comparing against legacy unformatted storage.

stripFormatting('7F3A-9C12-...');
// → '7F3A9C12...'

formatLicenseKey(input, options?)

Re-apply the canonical group/separator format to an unformatted key.

formatLicenseKey('7F3A9C12...', { groupSize: 4 });
// → '7F3A-9C12-...'

isValidLicenseKeyFormat(key, options?)

Verify a key matches the configured shape. Pass alphabet-matching options if you've moved off the default hex shape.

isValidLicenseKeyFormat('7F3A-9C12-...');                       // true
isValidLicenseKeyFormat('A3K9P-Q2NTV-...', { groupSize: 5 });   // true
isValidLicenseKeyFormat('not-a-key');                            // false

isLicenseExpired(expiresAt, now?)

Has a license expired? Accepts ISO 8601 strings or Date objects. Naive timestamps without Z are treated as UTC (matches SQLite / D1 default storage). null / undefined / '' are treated as "no expiry" — always returns false.

isLicenseExpired(null);                          // false (no expiry)
isLicenseExpired('2020-01-01T00:00:00Z');        // true
isLicenseExpired('2030-01-01T00:00:00Z');        // false

isLicenseUsable(license, now?)

The composite check. Returns { valid, reason? } so the rejection message can be surfaced to the user verbatim.

isLicenseUsable(null);
// → { valid: false, reason: 'License not found' }

isLicenseUsable({ status: 'revoked', expires_at: null });
// → { valid: false, reason: 'License has been revoked' }

isLicenseUsable({ status: 'active', expires_at: '2030-01-01T00:00:00Z' });
// → { valid: true }

License

MIT