npm package: @mcdmag/licensing-saas-sdk
Description: SDK for integrating license validation, plugin encryption, and distribution with the licensing-saas platform
Latest version: 0.1.2

# @mcdmag/licensing-saas-sdk

[![npm version](https://img.shields.io/npm/v/@mcdmag/licensing-saas-sdk)](https://www.npmjs.com/package/@mcdmag/licensing-saas-sdk)
[![license](https://img.shields.io/npm/l/@mcdmag/licensing-saas-sdk)](./LICENSE)

SDK for plugin authors to integrate license validation, encrypt plugin artifacts, and distribute plugins to end users via the [licensing-saas](https://licensing-saas.fly.dev) platform.

## What This SDK Does

This SDK provides two things:

1. **A library** — validate license keys in your plugin at runtime (online, offline, or cached)
2. **A CLI** — package, encrypt, obfuscate, upload, and generate installers for your plugin

## Prerequisites

- **Node.js >= 18**
- npm (or any compatible package manager)

## Install

```bash
npm install @mcdmag/licensing-saas-sdk
```

## Quick Start (3 commands)

The fastest way to go from plugin code to a distributable installer:

```bash
# 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": "you@example.com"}'
# → { "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 ./dist
```

That'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.json` to your `.gitignore` — it contains your plugin secret and API key.

### What happens under the hood

1. **`init`** generates a cryptographic plugin secret, registers the plugin with the platform, and saves all credentials to `.licensingrc.json`
2. **`release`** obfuscates `.js` files, tars + encrypts with AES-256-GCM, uploads the artifact, and generates `install.sh`

### End users install with their license key

```bash
LICENSE_KEY=their-license-key bash install.sh
```

The 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](https://docs.anthropic.com/en/docs/claude-code) so it's available in Claude Code automatically.

---

### How Licensing Works

```mermaid
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 runtime
```

### How Encryption Works

Plugin code is **never stored in plaintext** on the platform:

1. **Obfuscate** — The CLI copies your plugin to a temp directory and obfuscates all `.js` files (control flow flattening, string encoding, identifier mangling). This is on by default; skip with `--no-obfuscate`
2. **Package** — The obfuscated copy is tarred and encrypted with **AES-256-GCM**
3. **Key derivation** — The encryption key is derived from your `pluginSecret` using **HKDF-SHA256** with a random salt per artifact
4. **Upload** — The encrypted `.enc` blob + encryption metadata (salt, IV, authTag) are uploaded to the platform
5. **Install** — When a licensed user runs `install.sh`, the platform validates their license, derives the decryption key from the stored `pluginSecret`, and returns it alongside the encrypted download
6. **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 | cpuModel
```
This 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:

```bash
curl -X POST https://licensing-saas.fly.dev/api/tenants \
  -H "Content-Type: application/json" \
  -d '{"name": "Your Company", "email": "you@example.com"}'
# → { "tenantId": "...", "apiKey": "..." }
```

### 2. Generate a Plugin Secret

The SDK exports a `generatePluginSecret()` helper that creates a cryptographically random 32-byte hex string:

```typescript
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:

```bash
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:

```bash
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": "customer@example.com",
    "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:

```typescript
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

```bash
# 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_ID
```

### 7. Generate Installer for End Users

```bash
npx licensing-saas-sdk generate-installer \
  --plugin-id YOUR_PLUGIN_ID \
  --plugin-name my-plugin \
  --public-key YOUR_PUBLIC_KEY_BASE64
# → install.sh
```

## API Reference

### `validateLicense(key, options): Promise<LicenseResult>`

Validate a license key using three-tier fallback: online → offline → cached.

```typescript
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:**
```typescript
{
  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"` — `expiresAt` is in the past

### `getMachineFingerprint(): string`

Returns a 64-character hex SHA-256 hash of `hostname|username|platform|arch|cpuModel`.

### Types

```typescript
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`.

```bash
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`).

```bash
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.

```bash
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.

```bash
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.

```bash
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`:** The `package` command already runs obfuscation by default before encrypting. Use the standalone `obfuscate` command 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.

```bash
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 `.js` files in the directory
- Applies [javascript-obfuscator](https://github.com/nicepkg/javascript-obfuscator) with:
  - Control flow flattening (50% threshold)
  - String array encoding (base64)
  - Hexadecimal identifier names
  - Node.js target
- Writes obfuscated files to `--output` or overwrites originals

**Example — obfuscate without packaging (e.g., for inspection):**

```bash
# 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-obfuscate
```

Note 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](docs/security-model.md).

## License

[MIT](./LICENSE)