@winnermarket/license
v1.0.0
Published
Ed25519-signed license files: issue, verify, and evaluate runtime status. Isomorphic verify (Node/browser/edge), Node-only issuer.
Maintainers
Readme
@winnermarket/license
Ed25519-signed license files for Techassistia products. Issue, verify, and evaluate runtime status from any JavaScript runtime (Node 18+, browsers, Edge, Cloudflare Workers, Deno, Bun).
Proprietary software. See LICENSE. Use requires an active commercial agreement with Techassistia.
Install
pnpm add @winnermarket/license
# or
npm i @winnermarket/licenseNode >= 18 required (uses WebCrypto's Ed25519 support).
Two entrypoints
| Entry | Runtime | Purpose |
|-------|---------|---------|
| @winnermarket/license | Isomorphic (Node/browser/edge) | Verify signatures, evaluate status |
| @winnermarket/license/issuer | Node only | Generate keypairs, sign new licenses |
The issuer subpath uses node:crypto directly and must never be bundled
in a client-side app. The main entry has zero Node-only dependencies.
Verify a license (client-side)
import { evaluateLicense } from "@winnermarket/license";
const license = JSON.parse(await fs.readFile("license.json", "utf8"));
const result = await evaluateLicense(license, {
publicKeyPem: process.env.LICENSE_PUBLIC_KEY!, // PEM SPKI
});
if (result.status === "blocked" || result.status === "invalid") {
throw new Error("Licence invalide — contactez l'administrateur");
}
if (result.status === "degraded") {
console.warn("Fonctionnalités désactivées :", result.disabledFeatures);
}Status lifecycle
| Status | Code | When |
|--------|------|------|
| missing | license.missing | No license provided |
| invalid | license.invalid_signature / license.invalid_format | Signature mismatch or unreadable |
| active | license.active / license.active.perpetual / license.active.expiring_soon | Valid |
| warning | license.warning.expired | Expired 0–7 days ago |
| degraded | license.degraded.expired | Expired 8–14 days — advanced features disabled |
| blocked | license.blocked.expired | Expired 15+ days — all features disabled |
The code field is a stable machine identifier suitable for i18n. The
message field is an English default for quick usage.
Customising advanced features
In degraded state, features listed in DEFAULT_ADVANCED_FEATURES are
disabled. Override with your own list:
import { evaluateLicense } from "@winnermarket/license";
await evaluateLicense(license, {
publicKeyPem,
advancedFeatures: ["ml", "api_keys"], // only these disable in degraded mode
});Loading from raw JSON
import { loadAndEvaluateLicense } from "@winnermarket/license";
const licenseJson = await fs.readFile("license.json", "utf8");
const result = await loadAndEvaluateLicense(licenseJson, { publicKeyPem });loadAndEvaluateLicense accepts null/undefined/empty → returns missing;
unparseable JSON → returns invalid with code license.invalid_format.
Issue a license (operator / server-side)
Only invoke this on a trusted server that holds the private key.
import { generateKeyPair, issueLicense } from "@winnermarket/license/issuer";
// 1. Generate a keypair once, store the private key securely (env, vault).
const { publicKey, privateKey } = generateKeyPair();
// 2. Ship the public key to client apps (env var, build-time constant, etc.)
// 3. Sign licenses as they are purchased:
const license = issueLicense({
tenantSlug: "acme-transport",
plan: "enterprise",
companyName: "Acme Transport Ltd.",
maxUsers: 250,
maxVehicles: 1000,
expiresAt: "2027-04-19",
privateKeyPem: privateKey,
});
// Send `JSON.stringify(license)` to the customer.Plan defaults
Each plan has sensible defaults for features and capacity limits. Any field can be overridden per-issue.
import { PLAN_DEFAULTS } from "@winnermarket/license";
console.log(PLAN_DEFAULTS.business);
// { features: ["fleet", "rental", "vtc", ...], maxUsers: 50, maxVehicles: 100 }Signature format
Licenses are signed with Ed25519 over JSON.stringify(payload). The signature
is stored base64-encoded.
{
"payload": { /* LicensePayload */ },
"signature": "base64-encoded Ed25519 signature",
"version": 1
}Because the payload is serialised by the consumer at verify time and the issuer
at sign time, the runtime JSON.stringify order must match. Since objects are
produced with a deterministic key order by issueLicense, this is safe — but
do not reorder or mutate the payload between issuing and shipping.
Security notes
- Protect the private key. It must never leave your operator server.
- Ship the public key with your application (env variable, build-time constant, config file). It does not need to be secret.
- Rotate keys by generating a new pair, re-issuing active licenses with the new key, and distributing the new public key in the next application release.
- Runtime clock is assumed trusted. If you need tamper-proof expiry (against clients with rolled-back clocks), pair this with an occasional online check.
© Techassistia. All rights reserved.
