@mcdmag/licensing-saas-sdk
v0.1.2
Published
SDK for integrating license validation, plugin encryption, and distribution with the licensing-saas platform
Downloads
393
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 (3 commands)
The fastest way to go from plugin code to a distributable installer:
# 1. Register as a tenant to get your API key (one-time)
curl -X POST https://licensing-saas.fly.dev/api/tenants \
-H "Content-Type: application/json" \
-d '{"name": "Your Company", "email": "[email protected]"}'
# → { "tenantId": "...", "apiKey": "sk-..." }
# 2. Initialize your plugin (one-time): generates a secret, registers it, saves config
npx licensing-saas-sdk init --name my-plugin --api-key sk-YOUR_API_KEY
# 3. Release (every time you ship): obfuscate + encrypt + upload + generate install.sh
npx licensing-saas-sdk release ./distThat's it. Step 1 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.
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:
curl -X POST https://licensing-saas.fly.dev/api/tenants \
-H "Content-Type: application/json" \
-d '{"name": "Your Company", "email": "[email protected]"}'
# → { "tenantId": "...", "apiKey": "..." }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) |
obfuscate
Obfuscate JavaScript files in a directory using control flow flattening and string encoding. Protects plugin source code from casual reverse-engineering.
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 | Deterministic seed for reproducible obfuscation (default: 0) |
| --exclude | No | File path patterns to skip (repeatable) |
What it does:
- Recursively finds all
.jsfiles in the directory - Applies javascript-obfuscator with:
- Control flow flattening (50% threshold)
- String array encoding (base64)
- Hexadecimal identifier names
- Node.js target
- Writes obfuscated files to
--outputor overwrites originals
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 | |--------|--------| | 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 | | Trust model | The SaaS provider stores encrypted secrets and can derive decryption keys. Plugin code is stored encrypted at rest. |
For full details, see docs/security-model.md.
