# Skill: Licensing System (Microservice)

## Identity
- **Skill ID**: `licensing-system`
- **Domain**: Commercial Plugin Licensing, Software Updates
- **Technologies**: Node.js, Prisma, Crypto, Semver
- **Source Agent**: `backend-app-expert.md`

## When to Load This Skill

Load this skill when working on:
- License key generation and validation
- Plugin activation/deactivation endpoints
- Serving plugin updates (`.zip` downloads)
- Version checking APIs
- License restriction enforcement (domain limits)

**File patterns:**
- `api/src/services/license*.ts`
- `api/src/routes/licenses.ts`
- `api/src/routes/updates.ts`

## Core Patterns

### 1. Database Schema (Prisma)

```prisma
model License {
  id            String   @id @default(uuid())
  key           String   @unique // Hashed or Encrypted
  key_prefix    String   // First 4 chars for display
  user_id       String
  user          User     @relation(fields: [user_id], references: [id])
  plan_tier     String   // 'starter', 'pro', 'enterprise'
  status        String   // 'active', 'expired', 'revoked'
  domain_limit  Int      // 1, 5, or Unlimited
  expires_at    DateTime
  created_at    DateTime @default(now())
  
  activations   LicenseActivation[]
}

model LicenseActivation {
  id          String   @id @default(uuid())
  license_id  String
  license     License  @relation(fields: [license_id], references: [id])
  domain      String
  wp_version  String?
  plugin_version String?
  ip_address  String?
  activated_at DateTime @default(now())
  last_check_at DateTime @updatedAt

  @@unique([license_id, domain])
}

model PluginRelease {
  id          String   @id @default(uuid())
  version     String   @unique // '1.0.0'
  file_path   String   // S3 path
  changelog   String   @db.Text
  released_at DateTime @default(now())
  is_stable   Boolean  @default(true)
  min_wp_version String
  min_php_version String
}
```

### 2. License Verification Logic

```typescript
// services/license.ts
import crypto from 'crypto';

export class LicenseService {
  
  // Activate a license for a domain
  async activate(key: string, domain: string): Promise<ActivationResult> {
    const license = await prisma.license.findFirst({
      where: { key: this.hashKey(key) }
    });

    if (!license) throw new Error('Invalid license key');
    if (license.status !== 'active') throw new Error('License is not active');
    if (license.expires_at < new Date()) throw new Error('License expired');

    // Check domain limit
    const activationCount = await prisma.licenseActivation.count({
      where: { license_id: license.id }
    });

    // Check if already activated for this domain
    const existing = await prisma.licenseActivation.findUnique({
      where: { license_id_domain: { license_id: license.id, domain } }
    });

    if (!existing && activationCount >= license.domain_limit) {
      throw new Error('Domain limit reached');
    }

    // Register activation
    if (!existing) {
      await prisma.licenseActivation.create({
        data: {
          license_id: license.id,
          domain,
          activated_at: new Date()
        }
      });
    }

    return { success: true, expires_at: license.expires_at, plan: license.plan_tier };
  }

  // Hash key for storage (never store plain text)
  private hashKey(key: string): string {
    return crypto.createHash('sha256').update(key).digest('hex');
  }
}
```

### 3. Update Check Endpoint

```typescript
// routes/updates.ts
router.get('/updates/check', async (req, res) => {
  const { license_key, domain, current_version } = req.query;
  
  // Validate license
  const isValid = await licenseService.validate(license_key, domain);
  if (!isValid) return res.status(403).json({ error: 'Unauthorized' });

  // Find latest version
  const latest = await prisma.pluginRelease.findFirst({
    where: { is_stable: true },
    orderBy: { released_at: 'desc' }
  });

  if (semver.gt(latest.version, current_version)) {
    return res.json({
      new_version: latest.version,
      package_url: `/v1/updates/download?v=${latest.version}&key=${license_key}`,
      changelog: latest.changelog
    });
  }

  res.json({ new_version: current_version }); // No update
});
```

## Anti-Patterns (Forbidden)
| Mistake | Fix |
|---------|-----|
| Storing license keys in plain text | Hash them (SHA-256) |
| Using auto-increment IDs for licenses | Use UUIDs or formatting string (MPZ-XXXX) |
| Allowing unlimited checks without validation | Validate license status on every update check |
| Serving .zip files directly | Use signed URLs (S3/CloudFront) with expiry |

## Integration with Other Skills
- **payment-integration**: Create license upon successful subscription.
- **authentication-security**: Protect admin routes for license management.

## Validation Checklist
- [ ] License keys are hashed
- [ ] Domain limits are strictly enforced
- [ ] Expired licenses cannot activate new domains
- [ ] Update endpoint validates license before returning URL
