Files
MokoGitea-Fork/wiki/license-management.md
Jonathan Miller 44107d6485
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Branch Policy Check / Verify merge target (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
PR RC Release / Build RC Release (pull_request) Has been cancelled
Branch Cleanup / Delete merged branch (pull_request) Has been cancelled
Universal: Build & Release / Promote to RC (pull_request) Has been cancelled
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
docs: update CHANGELOG and wiki for v1.26.1-moko.06.02.00 final
Changelog: comprehensive entry covering all features, security
fixes, platform feeds, UI changes, and settings restructure.

Wiki: all 7 platform feeds now listed as Production. Revision 1.5
added covering sub-orgs, visibility modes, settings pages, and
security hardening.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 20:31:06 -05:00

17 KiB

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.

Feed Visibility

Controls what unauthenticated users see when accessing update feed URLs:

Mode Update Feed Release Page Download Files
Public Full feed with download URLs Public Public
No-download Version info only (URLs stripped) Public (notes visible) "Sign in to download"
Hidden Empty feed Redirects to login Login required

The no-download mode is ideal for commercial extensions: customers see that updates exist (motivating purchase/renewal) but cannot download without signing in. Release notes and changelogs remain publicly readable.

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.

Package Archiving

Packages can be archived instead of permanently deleted. This is the recommended workflow for end-of-life packages.

How archiving works:

  • Archived packages are hidden from the main list
  • Existing keys from archived packages continue to work (validation, heartbeats, update feeds)
  • No new keys can be generated from an archived package
  • Archived packages appear in a collapsible "Archived Packages" section at the bottom
  • Admins can restore (unarchive) a package at any time
  • Only site admins and org owners can permanently delete a package

Permanent deletion removes the package record entirely. Any keys that reference it will become orphaned and fail validation. Use with caution.

Step-by-Step: Setting Up Licensing for a Joomla Extension

1. Enable Licensing

Organization level (recommended — applies to all repos):

  1. Go to your org → SettingsLicensing & Update Streams
  2. Check Enable licensing for this organization
  3. Check Require license key for all update feeds
  4. Click Save

Repository level (per-repo override):

  1. Go to your repo → Settings → scroll to Licensing & Updates
  2. Check Enable licensing for this repository
  3. Select the Update Feed Format (Joomla, Dolibarr, or Both)
  4. Check Require license key for update feeds
  5. Click Save Changes

2. Configure Extension Metadata

In Organization Settings → Licensing & Update Streams, under Extension Metadata:

  1. Set the Platform (Joomla, Dolibarr, WordPress, etc.)
  2. Set the Extension Name (e.g., pkg_mokowaas) — this becomes the <element> in the XML feed
  3. Set the Display Name (e.g., "Package - MokoWaaS") — shown in Joomla update manager
  4. Set the Extension Type (component, module, plugin, package, template, library)
  5. Set the Target Version regex (e.g., (5|6)\..* for Joomla 5 and 6)
  6. Set the PHP Minimum if applicable (e.g., 8.1)
  7. Set Maintainer and Maintainer URL

3. Create a License Package

  1. Navigate to the Licenses tab (repo or org level)
  2. Click New Package
  3. Fill in:
    • Name: e.g., "Pro Annual"
    • Duration: e.g., 365 days (0 = lifetime)
    • Max Sites: e.g., 3 (0 = unlimited)
    • Channels: check which update streams this package grants access to
  4. Click Create License Package

4. Generate a License Key

  1. On the Licenses page, find your package row
  2. Click the + button to generate a key
  3. The full key (e.g., MOKO-A1B2-C3D4-E5F6-G7H8) is shown in a success banner
  4. Copy the key and send it to your customer

5. Set Up the Client (Joomla Example)

In the customer's Joomla admin:

  1. Go to System → Update Sites
  2. Add or edit the update site URL: https://git.mokoconsulting.tech/{org}/{repo}/updates.xml
  3. If prompted for a Download Key, enter the license key
  4. Joomla will automatically append &dlid=MOKO-XXXX-... to download requests

6. Monitor Usage

  • The Last Seen column shows when each key last checked for updates
  • Click the edit (pencil) button to view/modify key details
  • Domain restrictions are auto-populated on first use (lock-on-first-use)

Step-by-Step: Payment Integration (Stripe/PayPal Webhook)

1. Create a Webhook Endpoint

Configure your payment provider to POST to:

POST https://git.mokoconsulting.tech/api/v1/repos/{org}/{repo}/license-keys/purchase

With headers:

Authorization: token YOUR_API_TOKEN
Content-Type: application/json

2. Webhook Payload

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

The payment_ref provides idempotency — duplicate webhooks return the existing key instead of creating duplicates.

3. Response

On first creation:

{
  "id": 42,
  "raw_key": "MOKO-A1B2-C3D4-E5F6-G7H8",
  "package_id": 1,
  "licensee_name": "John Doe",
  ...
}

Send the raw_key to the customer via email.

Step-by-Step: Validating a Key (Client-Side)

Your extension can validate its own key against the server:

POST https://git.mokoconsulting.tech/api/v1/repos/{org}/{repo}/license-keys/validate
Content-Type: application/json

{"key": "MOKO-A1B2-C3D4-E5F6-G7H8", "domain": "example.com"}

Response:

{
  "valid": true,
  "package_name": "Pro Annual",
  "channels": "[\"stable\",\"release-candidate\"]",
  "expires_at": "2027-06-01T00:00:00Z",
  "sites_used": 1,
  "max_sites": 3
}

This endpoint is public — no authentication required. Use it in your extension's license check flow.

Supported Platforms

The update feed system currently supports:

Platform Feed URL Format Status
Joomla /{repo}/updates.xml XML with <downloadkey> Production
Dolibarr /{repo}/updates/dolibarr.json JSON Production
WordPress /{repo}/updates/wordpress.json PUC-compatible JSON Production
Composer /{repo}/updates/packages.json packages.json Production
PrestaShop /{repo}/updates/prestashop.xml Module update XML Production
Drupal /{repo}/updates/drupal.xml Update status XML Production
WHMCS /{repo}/updates/whmcs.json Module update JSON Production
Changelog /{repo}/changelog.xml Joomla changelog XML Production

All platforms share the same licensing backend — the same keys, packages, and validation work across all feed formats.


Repo: MokoGitea

Revision Date Author Description
1.0 2026-05-30 Jonathan Miller (@jmiller) Initial version
1.1 2026-05-31 Jonathan Miller (@jmiller) Add feature toggle, unified update system, tag enforcement, heartbeat tracking
1.2 2026-05-31 Jonathan Miller (@jmiller) Add permissions (TypeLicenses unit), renewal, auto-domain, custom keys, UI/UX cleanup
1.3 2026-06-01 Jonathan Miller (@jmiller) Add package archiving, expanded delete permissions, migration v340, API renew, step-by-step guides
1.4 2026-06-02 Jonathan Miller (@jmiller) WordPress feed, feed visibility modes, download gating, RepoScope enforcement, API package CRUD, settings API, combolist channel picker, double confirmation modals, extension metadata in repo settings, domain lock timer, Joomla-standard tags, SHA256 in XML, changelog XML, no-download release page mode
1.5 2026-06-02 Jonathan Miller (@jmiller) All 7 platform feeds (Composer, PrestaShop, Drupal, WHMCS), enterprise sub-org hierarchy, three-level repo visibility (Public/Private/Hidden), styled 403 page with login form, separate licensing/advanced settings pages, icons on all navbars, manual stream mapping, configurable key prefix, feed always public, xorm column name fixes, security hardening