@mcdmag/licensing-saas-sdk
v0.1.3
Published
SDK for integrating license validation, plugin encryption, and distribution with the licensing-saas platform
Maintainers
Readme
@mcdmag/licensing-saas-sdk
SDK for plugin authors to integrate license validation, encrypt plugin artifacts, and distribute plugins to end users via the licensing-saas platform.
What This SDK Does
This SDK provides two things:
- A library — validate license keys in your plugin at runtime (online, offline, or cached)
- A CLI — package, encrypt, obfuscate, upload, and generate installers for your plugin
Prerequisites
- Node.js >= 18
- npm (or any compatible package manager)
Install
npm install @mcdmag/licensing-saas-sdkQuick Start (4 commands)
The fastest way to go from plugin code to a distributable installer:
# 1. Create an account (one-time)
npx licensing-saas-sdk register --name "Your Company" --email [email protected] --password YOUR_PASSWORD
# → Account created! Tenant ID, API Key, Public Key displayed
# 2. Login (when session expires)
npx licensing-saas-sdk login --email [email protected] --password YOUR_PASSWORD
# 3. Initialize your plugin (one-time): generates a secret, registers it, saves config
npx licensing-saas-sdk init --name my-plugin --api-key lsk_YOUR_API_KEY
# 4. Release (every time you ship): obfuscate + encrypt + upload + generate install.sh
npx licensing-saas-sdk release ./distStep 1 creates your account and gives you an API key. The init command uses it to register your plugin and saves everything to .licensingrc.json. The release command reads that config and does the rest in one step.
Credentials are stored in ~/.licensing-saas/credentials.json (directory mode 0o700, file mode 0o600).
Important: Add
.licensingrc.jsonto your.gitignore— it contains your plugin secret and API key.
What happens under the hood
initgenerates a cryptographic plugin secret, registers the plugin with the platform, and saves all credentials to.licensingrc.jsonreleaseobfuscates.jsfiles, tars + encrypts with AES-256-GCM, uploads the artifact, and generatesinstall.sh
End users install with their license key
LICENSE_KEY=their-license-key bash install.shThe installer validates the license, downloads the encrypted artifact, decrypts it locally, and extracts the plugin to ~/.claude/plugins/<plugin-name>. If the plugin contains an mcp-server.js entry point, the installer also registers it as a Claude Code MCP server so it's available in Claude Code automatically.
How Licensing Works
sequenceDiagram
participant A as Plugin Author
participant S as SaaS Platform
participant U as End User
A->>A: 1. Build plugin
A->>A: 2. package (CLI)<br/>obfuscate + tar + encrypt
A->>S: 3. upload (CLI) → .enc blob
A-->>U: 4. Share install.sh
U->>S: 5. Run install.sh
S->>S: Validate license + derive key
S->>U: 6. .enc + decryption key
U->>U: 7. Decrypt + extract
U->>S: 8. Plugin validates license at runtimeHow Encryption Works
Plugin code is never stored in plaintext on the platform:
- Obfuscate — The CLI copies your plugin to a temp directory and obfuscates all
.jsfiles (control flow flattening, string encoding, identifier mangling). This is on by default; skip with--no-obfuscate - Package — The obfuscated copy is tarred and encrypted with AES-256-GCM
- Key derivation — The encryption key is derived from your
pluginSecretusing HKDF-SHA256 with a random salt per artifact - Upload — The encrypted
.encblob + encryption metadata (salt, IV, authTag) are uploaded to the platform - Install — When a licensed user runs
install.sh, the platform validates their license, derives the decryption key from the storedpluginSecret, and returns it alongside the encrypted download - Decrypt — The installer decrypts locally using AES-256-GCM and extracts the plugin (code is still obfuscated)
HKDF parameters:
- IKM:
pluginSecret(32-byte hex, generated once per plugin) - Salt: random 32 bytes (unique per artifact)
- Info:
"plugin-encryption"(fixed context string) - Output: 32-byte AES-256 key
How License Validation Works
License keys use the format <base64-payload>.<base64-signature>:
- Payload — JSON with:
licenseId,pluginId,tenantId,email,plan,expiresAt,features - Signature — Ed25519 signature over the payload bytes
- Validation is three-tier with automatic fallback:
| Tier | Method | When | |------|--------|------| | 1 | Online | POST to license server with key + machine fingerprint | | 2 | Offline | Ed25519 signature verification (no network needed) | | 3 | Cached | Read from local cache file (7-day TTL) |
Online is attempted first. If it fails (network down, server error), offline Ed25519 verification runs. If that fails too (e.g., expired key), the SDK checks for a cached validation from a previous successful online check.
Machine Fingerprinting
getMachineFingerprint() creates a SHA-256 hash from:
hostname | username | platform | arch | cpuModelThis is sent during online validation for seat enforcement. No PII is stored or transmitted — only the hash.
Step-by-Step Guide
If you prefer more control, or want to understand each step, here's the full walkthrough.
1. Register as a Tenant
Create a tenant account on the platform to get your API key:
# Using the CLI (recommended)
npx licensing-saas-sdk register --name "Your Company" --email [email protected] --password YOUR_PASSWORD
# → Account created! Tenant ID, API Key, Public Key displayed
# Or via the API directly
curl -X POST https://licensing-saas.fly.dev/api/auth/register \
-H "Content-Type: application/json" \
-d '{"name": "Your Company", "email": "[email protected]", "password": "YOUR_PASSWORD"}'
# → { "tenantId": "...", "apiKey": "lsk_...", "publicKeyBase64": "...", "jwt": "..." }2. Generate a Plugin Secret
The SDK exports a generatePluginSecret() helper that creates a cryptographically random 32-byte hex string:
import { generatePluginSecret } from "@mcdmag/licensing-saas-sdk"
const secret = generatePluginSecret()
console.log(secret)
// → e.g. "a3f1...c9d2" (64 hex characters)Save this secret securely — you'll use it for every package command. It cannot be recovered from the platform.
3. Create a Plugin
Register your plugin on the platform using the secret you generated:
curl -X POST https://licensing-saas.fly.dev/api/plugins \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "my-plugin", "pluginSecret": "YOUR_GENERATED_SECRET_HEX"}'
# → { "pluginId": "...", "publicKeyBase64": "..." }Save the returned pluginId and publicKeyBase64 — you'll need both.
4. Issue a License Key
License keys are created through the platform API (not this SDK). Create one for a user:
curl -X POST https://licensing-saas.fly.dev/api/licenses \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"pluginId": "YOUR_PLUGIN_ID",
"email": "[email protected]",
"plan": "pro",
"features": ["feature-a", "feature-b"]
}'
# → { "licenseKey": "eyJsa....<base64-payload>.<base64-signature>" }Distribute this key to your customer.
5. Add License Validation to Your Plugin
Use validateLicense in your plugin code to gate features behind a valid license:
import { validateLicense } from "@mcdmag/licensing-saas-sdk"
try {
const result = await validateLicense(licenseKey, {
publicKeyBase64: "YOUR_PUBLIC_KEY_BASE64",
apiUrl: "https://licensing-saas.fly.dev",
pluginId: "your-plugin-id",
})
if (result.valid) {
console.log(`Plan: ${result.payload.plan}`)
console.log(`Source: ${result.source}`) // 'online' | 'offline' | 'cached'
console.log(`Features: ${result.payload.features.join(", ")}`)
}
} catch (err) {
// All three validation tiers failed (online, offline, cached)
console.error("License validation failed:", err.message)
}6. Package & Upload
# Package: obfuscate + tar + encrypt (obfuscation is on by default)
npx licensing-saas-sdk package ./my-plugin --plugin-secret YOUR_SECRET_HEX
# → my-plugin.tar.gz.enc + my-plugin.tar.gz.enc.meta.json
# Upload to platform
npx licensing-saas-sdk upload ./my-plugin.tar.gz.enc \
--api-key YOUR_API_KEY \
--plugin-id YOUR_PLUGIN_ID7. Generate Installer for End Users
npx licensing-saas-sdk generate-installer \
--plugin-id YOUR_PLUGIN_ID \
--plugin-name my-plugin \
--public-key YOUR_PUBLIC_KEY_BASE64
# → install.shAPI Reference
validateLicense(key, options): Promise<LicenseResult>
Validate a license key using three-tier fallback: online → offline → cached.
const result = await validateLicense(key, {
publicKeyBase64: "...", // Required — Ed25519 public key (DER, base64)
apiUrl: "...", // License server URL for online validation
pluginId: "...", // Plugin ID (used for caching)
machineId: "...", // Override machine fingerprint
hostname: "...", // Override hostname
timeout: 5000, // Online request timeout in ms (default: 5000)
cacheDir: "...", // Cache directory (default: ~/.claude)
})Returns:
{
valid: boolean
payload: LicensePayload
source: "online" | "offline" | "cached"
}Throws if all three tiers fail, with a combined error message.
validateOffline(key, publicKeyBase64): LicenseResult
Verify a license key offline using Ed25519 signature verification. No network call.
Throws:
"Malformed license key"— no.separator"Invalid license signature"— Ed25519 verification failed"License payload is not valid JSON"— corrupted payload"License has expired"—expiresAtis in the past
getMachineFingerprint(): string
Returns a 64-character hex SHA-256 hash of hostname|username|platform|arch|cpuModel.
Types
interface LicensePayload {
licenseId: string
pluginId: string
tenantId: string
email: string
plan: string
purchasedAt?: string // ISO 8601
issuedAt?: string // ISO 8601
expiresAt: string | null // null = permanent license
features: string[]
}
interface LicenseResult {
valid: boolean
payload: LicensePayload
source: "online" | "offline" | "cached"
}
interface ValidateOptions {
publicKeyBase64: string
apiUrl?: string
pluginId?: string
machineId?: string
hostname?: string
timeout?: number
cacheDir?: string
}CLI Reference
init
One-time setup: generates a plugin secret, registers the plugin with the platform, and saves all credentials to .licensingrc.json.
licensing-saas-sdk init --name <plugin-name> --api-key <key> [--api-url <url>] [--output <path>]| Flag | Required | Description |
| ---- | -------- | ----------- |
| --name | Yes | Plugin name |
| --api-key | Yes | Tenant API key (from tenant registration) |
| --api-url | No | Platform URL (default: https://licensing-saas.fly.dev) |
| --output | No | Config file path (default: ./.licensingrc.json) |
release
Package, upload, and generate installer in one command. Reads credentials from .licensingrc.json (created by init).
licensing-saas-sdk release <directory> [--config <path>] [--no-obfuscate] [--skip-installer] [--installer-output <path>]| Flag | Required | Description |
| ---- | -------- | ----------- |
| --config | No | Config file path (default: ./.licensingrc.json) |
| --no-obfuscate | No | Skip obfuscation (not recommended) |
| --skip-installer | No | Skip generating install.sh |
| --installer-output | No | Installer output path (default: ./install.sh) |
| --obfuscate-seed | No | Deterministic seed for reproducible obfuscation |
| --obfuscate-exclude | No | File path patterns to skip (repeatable) |
package
Obfuscate, package, and encrypt a plugin directory into an .enc tarball. Obfuscation is on by default — source code is never readable inside the artifact.
licensing-saas-sdk package <directory> --plugin-secret <hex> [--output <path>] [--no-obfuscate] [--obfuscate-seed <n>] [--obfuscate-exclude <pattern>...]| Flag | Required | Description |
|------|----------|-------------|
| --plugin-secret | Yes | 32-byte hex string |
| --output | No | Output path for .enc file |
| --no-obfuscate | No | Skip obfuscation (not recommended) |
| --obfuscate-seed | No | Deterministic seed for reproducible obfuscation |
| --obfuscate-exclude | No | File path patterns to skip (repeatable) |
Output: <dir>.tar.gz.enc + <dir>.tar.gz.enc.meta.json (contains salt, sha256, iv, authTag)
upload
Upload an encrypted artifact to the platform.
licensing-saas-sdk upload <file> --api-key <key> --plugin-id <id> [--api-url <url>]| Flag | Required | Description |
|------|----------|-------------|
| --api-key | Yes | Tenant API key |
| --plugin-id | Yes | Target plugin ID |
| --api-url | No | Platform URL (default: https://licensing-saas.fly.dev) |
generate-installer
Generate a bash install.sh script for end users.
licensing-saas-sdk generate-installer \
--plugin-id <id> \
--plugin-name <name> \
[--api-url <url>] \
[--public-key <base64>] \
[--output <path>]| Flag | Required | Description |
|------|----------|-------------|
| --plugin-id | Yes | Plugin ID |
| --plugin-name | Yes | Human-readable plugin name |
| --api-url | No | Platform URL (default: https://licensing-saas.fly.dev) |
| --public-key | No | Ed25519 public key for offline validation |
| --output | No | Output path (default: ./install.sh) |
register
Create a new account on the licensing platform.
licensing-saas-sdk register --name <name> --email <email> --password <password> [--api-url <url>]| Flag | Required | Description |
|------|----------|-------------|
| --name | Yes | Company or developer name |
| --email | Yes | Account email |
| --password | Yes | Account password (min 8 chars). Also reads LICENSING_SAAS_PASSWORD env var |
| --api-url | No | Platform URL (default: https://licensing-saas.fly.dev) |
On success, displays the tenant ID, API key, and public key. Save your API key — it will not be shown again. Credentials are saved to ~/.licensing-saas/credentials.json.
login
Authenticate with the licensing platform and store credentials locally.
licensing-saas-sdk login --email <email> --password <password> [--api-url <url>]| Flag | Required | Description |
|------|----------|-------------|
| --email | Yes | Account email |
| --password | Yes | Account password. Also reads LICENSING_SAAS_PASSWORD env var |
| --api-url | No | Platform URL (default: https://licensing-saas.fly.dev) |
Saves JWT and tenant ID to ~/.licensing-saas/credentials.json (directory mode 0o700, file mode 0o600).
api-keys
Manage API keys. Requires login first.
licensing-saas-sdk api-keys <list|create|revoke> [options]Subcommands:
| Subcommand | Description |
|------------|-------------|
| list | List all API keys with ID, prefix, label, dates, and status |
| create [--label <label>] | Create a new API key (raw key shown only once) |
| revoke --id <id> | Revoke an API key by ID |
API keys use the lsk_ prefix format. The raw key is displayed only at creation time and cannot be retrieved later.
obfuscate
Apply deterministic source hardening to JavaScript files without introducing malware-like runtime behavior. The SDK's repository-owned transformer parses and reprints each file, removes comments and source-map directives, and rejects invalid JavaScript before writing output.
The transformer deliberately does not flatten control flow, encode strings,
inject dead code, add anti-debugging logic, call eval, or ship a runtime
decoder. This makes release artifacts auditable and reduces antivirus false
positives. It is a conservative source-hardening layer, not a substitute for
encryption or access control.
When to use this vs
package: Thepackagecommand already runs obfuscation by default before encrypting. Use the standaloneobfuscatecommand only when you want to obfuscate code without packaging it — for example, to inspect the obfuscated output, or to obfuscate before running integration tests against obfuscated code.
licensing-saas-sdk obfuscate <directory> [--output <path>] [--seed <n>] [--exclude <pattern>...]| Flag | Required | Description |
|------|----------|-------------|
| --output | No | Output directory (default: overwrites in-place via atomic swap) |
| --seed | No | Retained for compatibility; output is deterministic without randomness |
| --exclude | No | File path patterns to skip (repeatable) |
What it does:
- Recursively finds all
.jsfiles in the directory - Parses every selected file before changing any input
- Removes comments and source-map directives using the TypeScript parser/printer
- Preserves executable statements and string literals in plain, reviewable JavaScript
- Writes files atomically to
--outputor overwrites originals - Adds no runtime decoder, network behavior, dynamic evaluation, or injected code
Example — obfuscate without packaging (e.g., for inspection):
# Build your plugin
npm run build
# Obfuscate the dist/, excluding test files
licensing-saas-sdk obfuscate ./dist --exclude __tests__ --exclude .test.
# Inspect the output, then package + encrypt as usual
licensing-saas-sdk package ./dist --plugin-secret YOUR_SECRET_HEX --no-obfuscateNote the --no-obfuscate flag on package — since you already obfuscated manually, this avoids double-obfuscating.
Security Model
| Aspect | Detail |
|--------|--------|
| Authentication | Dual-auth: JWT (HS256, 24h expiry) for account management, API keys (lsk_ prefix) for programmatic access |
| Password hashing | bcrypt (cost factor 12) |
| Encryption | AES-256-GCM (authenticated encryption) |
| Key derivation | HKDF-SHA256 from pluginSecret + random salt |
| License signing | Ed25519 (small keys, fast verification, deterministic) |
| Machine binding | SHA-256 fingerprint (no PII transmitted) |
| Cache TTL | 7 days for offline fallback |
| Credential storage | ~/.licensing-saas/credentials.json with directory 0o700, file 0o600 |
| Trust model | The SaaS provider stores encrypted secrets and can derive decryption keys. Plugin code is stored encrypted at rest. |
| Database | Dual backend: SQLite (default, single-node) or PostgreSQL (multi-node). Controlled by DB_DRIVER env var. |
For full details, see docs/security-model.md.
