Clone
2
License Management
Jonathan Miller edited this page 2026-05-31 10:34:58 -05:00

License Management System

MokoGitea includes a built-in commercial license management system for organizations that sell extensions (Joomla, Dolibarr, etc.). It handles license key generation, validation, update feed gating, domain enforcement, and payment integration.

Overview

The license system operates at two levels:

  • Organization-level: Manages packages and keys shared across all repos in an org
  • Repository-level: Same features scoped to a single repo, plus update feed URLs

Core Concepts

License Packages

A package defines a subscription tier (e.g., "Pro Annual", "Basic Monthly"). Each package specifies:

Field Description
Name Human-readable name (e.g., "Pro Annual")
Duration Key validity in days. 0 = lifetime
Max Sites Maximum unique domains. 0 = unlimited
Channels Which update streams this package grants access to
Active Whether the package is enabled

License Keys

Keys are generated from packages. Format: MOKO-XXXX-XXXX-XXXX-XXXX.

Important: Keys are hashed (SHA-256) before storage. The full key is only shown once at creation time. Store it securely.

Each key can have:

  • Licensee name/email for customer tracking
  • Domain restriction (comma-separated allowed domains)
  • Max sites override (0 = use package default)
  • Custom expiry date

Master Package

Every org/repo automatically gets a "Master (Internal)" package with an internal key. This provides unlimited access to all channels and cannot be edited or deleted. Master keys can only be revoked.

Web UI

Repository Licenses Page

/{owner}/{repo}/licenses

  • View all packages and issued keys
  • Create new packages with channel selection
  • Generate keys from packages (auto-generated or custom key for admins)
  • Renew expired keys (sync icon, extends by package duration)
  • Edit packages and keys (pencil icon)
  • Revoke keys (X icon, with confirmation modal)
  • Delete packages (trash icon, site admins only, with confirmation modal)
  • Copy update feed URLs (clipboard button with tooltip feedback)
  • Copy newly created keys (clipboard button in success message)

Organization Licenses Page

/{org}/-/licenses

Same features as repo level, scoped to the organization. Packages and keys created here are shared across all repos in the org.

API Endpoints

All endpoints are under /api/v1/repos/{owner}/{repo}/.

Authenticated (requires token + repo admin)

Method Path Description
GET /license-packages List all packages
POST /license-packages Create a package
GET /license-keys List all keys
POST /license-keys Create a key (returns full key)
PATCH /license-keys/{id} Edit a key
DELETE /license-keys/{id} Delete a key
GET /license-keys/{id}/usage Get usage logs (last 100)
POST /license-keys/purchase Create key from payment webhook

Public (no auth required)

Method Path Description
POST /license-keys/validate Validate a key + domain

Purchase Webhook

POST /api/v1/repos/{owner}/{repo}/license-keys/purchase

Designed for payment system integration (Stripe, PayPal, etc.):

{
  "package_id": 1,
  "licensee_name": "John Doe",
  "licensee_email": "john@example.com",
  "domain": "example.com",
  "payment_ref": "stripe_pi_xxx"
}

The payment_ref field provides idempotency -- if a key already exists with that reference, the existing key is returned instead of creating a duplicate.

Response includes the full raw_key (only on first creation).

Validation Endpoint

POST /api/v1/repos/{owner}/{repo}/license-keys/validate

For client-side license checks:

{
  "key": "MOKO-XXXX-XXXX-XXXX-XXXX",
  "domain": "example.com"
}

Response:

{
  "valid": true,
  "package_name": "Pro Annual",
  "channels": "[\"stable\",\"release-candidate\"]",
  "expires_at": "2027-05-30T00:00:00Z",
  "sites_used": 2,
  "max_sites": 5
}

Update Feed Integration

Joomla (updates.xml)

GET /{owner}/{repo}/updates.xml

When RequireKey is enabled for a repo, the XML feed includes a <downloadkey> element:

<update>
  <name>MokoOnyx</name>
  <version>3.10.9</version>
  <downloads>
    <downloadurl type="full" format="zip">https://git.mokoconsulting.tech/.../mokoonyx.zip</downloadurl>
  </downloads>
  <downloadkey prefix="&amp;dlid=" suffix=""/>
</update>

This tells Joomla to show a Download Key field in System > Update Sites. The key is sent as &dlid=MOKO-XXXX-... on download requests.

The server accepts keys via three query parameters: key, download_key, or dlid.

Dolibarr (dolibarr.json)

GET /{owner}/{repo}/updates/dolibarr.json

Uses the same license key validation as the Joomla XML feed. All platforms share the same licensing system. Invalid or missing keys receive an empty response.

Domain Enforcement

When a key has a DomainRestriction set (comma-separated domains):

  1. The domain from the request is checked against the allowed list
  2. Comparison is case-insensitive
  3. Master/internal keys bypass domain checks

Auto-Association (Lock-on-First-Use)

When a key has no DomainRestriction set and a domain is provided during validation:

  1. The domain is automatically appended to the key's DomainRestriction
  2. On subsequent heartbeats, additional domains can be added (up to MaxSites)
  3. Once MaxSites is reached, no new domains are accepted
  4. This provides zero-config domain locking — the first site to use a key "claims" it

Site Limit Enforcement

When MaxSites is set (on key or package):

  1. The system counts unique domains from usage records
  2. If the requesting domain is already known, it's allowed through
  3. If the domain is new and the limit is reached, the request is rejected
  4. Error: site limit reached (N/N)
  5. Auto-association also respects this limit — domains won't be added past the cap

Channel System

Channels control which update streams a license key grants access to. Available channels come from the org's Update Stream configuration:

Default Joomla streams: stable, release-candidate, beta, alpha, development

Channels are selected via checkboxes when creating/editing a package. They're stored as a JSON array (e.g., ["stable","release-candidate"]).

A package with no channels selected grants access to all channels.

Enabling the Licensing System

Licensing is off by default and must be enabled explicitly.

Organization Level

Go to Organization Settings > Licenses & Update Streams and check:

  • Enable licensing system -- shows the Licenses tab for all org members
  • Require license key for update feeds -- gates update XML/JSON feeds behind key validation

Repository Level

Go to Repository Settings > Advanced and check:

  • Enable licensing system -- shows the Licenses tab for this repo
  • Require license key for update feed access -- gates this repo's update feeds

Either org-level or repo-level enabling is sufficient to show the Licenses tab.

Key Heartbeat

Every time a license key is successfully validated (via update feed or API), the LastHeartbeatUnix timestamp is updated. This is shown as "Last Seen" in the keys table and included in API responses as last_heartbeat.

Use this to identify inactive keys or monitor customer usage patterns.

Release Tag Enforcement

When licensing is enabled, release tags with prerelease suffixes (e.g., -rc, -beta, -alpha, -dev) must match a configured update stream suffix. This ensures the update feed generator can correctly categorize releases.

  • Tags without prerelease suffixes (e.g., v1.0.0) are always allowed (stable stream)
  • Tags with suffixes must match a stream (e.g., v1.0.0-rc1 matches the release-candidate stream with suffix -rc)
  • When licensing is disabled, any tag format is accepted

Permissions

Licenses use Gitea's unit permission system (TypeLicenses). Teams can be granted Read, Write, or Admin access.

Action Required Permission
View licenses page Unit Read (or org member)
Create/edit packages Unit Write
Generate/revoke/renew keys Unit Write
Edit keys Unit Write
Set custom key value Site admin or org owner
Delete packages Site admin only
Edit/delete master package Blocked (not allowed)
Edit master keys Blocked (only revoke allowed)

Note: Admin-level teams implicitly have admin access to all units (including Licenses) even without explicit TeamUnit records. This means existing admin teams automatically gain license access.

Key Renewal

The Renew action extends a key's expiration by the package's DurationDays:

  • If the key is still valid, renewal extends from the current expiry date
  • If the key has already expired, renewal extends from now
  • Renewal also re-activates revoked keys
  • For lifetime packages (DurationDays=0), renewal extends by 365 days

Custom Keys

Site admins and org owners can set a custom key value instead of auto-generating. Use cases:

  • Migrating from an existing licensing system
  • Keys that match external records or payment systems
  • Deterministic keys for testing

The custom key is hashed identically to auto-generated keys — no security difference.


Repo: MokoGitea

Revision Date Author Description
1.0 2026-05-30 Claude (Opus 4.6) Initial version
1.1 2026-05-31 Claude (Opus 4.6) Add feature toggle, unified update system, tag enforcement, heartbeat tracking
1.2 2026-05-31 Claude (Opus 4.6) Add permissions (TypeLicenses unit), renewal, auto-domain, custom keys, UI/UX cleanup

Joomla Standard Compliance

MokoGitea's update feed follows the Joomla update server specification. Key alignment with the Akeeba Backup Pro reference implementation:

Download Key (<dlid>)

Extensions declare <dlid prefix="dlid=" suffix=""/> in their package manifest (pkg_*.xml). This tells Joomla to show a "Download Key" field in System > Update Sites. The key is appended as ?dlid=MOKO-XXXX-... to download requests.

MokoGitea serves the <downloadkey> element in updates.xml when RequireKey is enabled:

<downloadkey prefix="dlid=" suffix=""/>

The server accepts keys via three query parameters: key, download_key, or dlid.

XML Fields

Field Description Example
<name> Extension display name Package - MokoWaaS
<element> Joomla element identifier pkg_mokowaas
<type> Extension type package, component, plugin
<version> Semantic version 02.29.04
<downloads> Download URL(s) with type/format .zip attachment URL
<tags> Release channel stable, release-candidate
<targetplatform> Compatible Joomla versions name="joomla" version="(5|6)\..*"
<sha256> Checksum for integrity SHA-256 of zip
<sha512> Checksum (optional) SHA-512 of zip
<php_minimum> Minimum PHP version 7.4
<infourl> Release info page Link to release on Gitea
<changelogurl> Changelog file CHANGELOG.md raw URL
<downloadkey> Download key config prefix="dlid=" suffix=""

Platform Enforcement

Feed endpoints are gated by the repo's update platform setting:

Platform Setting /updates.xml /updates/dolibarr.json
joomla Served 404
dolibarr 404 Served
both Served Served
(empty/default) Served 404

Planned Improvements

  • Feed visibility vs download gating (#346) — Option to show the feed without a key but gate downloads (Akeeba-style)
  • Release asset download gating (#347) — Block direct .zip download without valid key
  • Repo settings tab for manifest metadata (#315) — Configure element, type, target platform per repo