Search by

finext / license-client

KetanGupta0

Client SDK for self-hosted Finext products to activate, verify, and heartbeat against the Finext License Server.

Package info

github.com/KetanGupta0/license-client

pkg:composer/finext/license-client

Statistics

Installs: 8

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.1 2026-07-20 14:23 UTC

This package is auto-updated.

Last update: 2026-09-20 14:52:31 UTC


README

Client SDK for self-hosted Finext products to activate, verify, and heartbeat against the Finext License Server. Ships as a Composer package consumed by each product (artiqpay, dating-app, ecommerce, finext-website, finsolvitonline, fixpay, ms2pay, proxypayonline, etc.) — not part of the license-server repo itself, since it has an independent release lifecycle and must never leak server-side secrets or paths into redistributed product code.

Installation

composer require finext/license-client

During local development (before this package is published), require it via a Composer path repository in the host app's composer.json:

{
    "repositories": [
        { "type": "path", "options": { "symlink": true }, "url": "../license-client" }
    ],
    "require": {
        "finext/license-client": "@dev"
    }
}

Publish the config file:

php artisan vendor:publish --tag=license-client-config

Configuration

All values are set via .env; see config/license-client.php for the full list. The essentials:

LICENSE_SERVER_URL=https://license.finextsolution.com

# Issued once per product from the license-server admin panel
# (Product → API Keys). Shipped inside this product's source.
LICENSE_PRODUCT_ID=5
LICENSE_BOOTSTRAP_KEY_ID=pak_xxxxxxxxxxxxxxxxxxxx
LICENSE_BOOTSTRAP_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Entered by the customer at install time
LICENSE_KEY=XXXXX-XXXXX-XXXXX-XXXXX

# Pinned Ed25519 public key so a compromised/MITM'd public-key endpoint
# can't silently swap in a different signing key. Get the current value
# from GET /api/v1/license/public-key on the license server.
LICENSE_PINNED_KEY_ID=v1
LICENSE_PINNED_PUBLIC_KEY=base64-encoded-public-key

LICENSE_DEFAULT_GRACE_PERIOD_DAYS=3
LICENSE_HTTP_TIMEOUT_SECONDS=10

Local state (installation ID, per-activation secret, encrypted verification cache, cached public key) is written under storage/app/license/ with 0600 permissions and should stay out of version control.

Security model

Two different guarantees protect two different directions of traffic:

  • Server → client (responses): every response is signed with Ed25519 and verified against the pinned/cached public key before any of its contents are trusted. A response that fails verification is treated exactly like a network failure — never surfaced as a real answer. This is a genuine cryptographic authenticity guarantee.
  • Client → server (requests): signed with HMAC-SHA256 under one of two secrets:
    • The bootstrap secret (shipped in source, one per product) signs only the very first /activate call. Because it ships with the product, it is necessarily extractable by anyone with the source — it's a deterrence measure, not a real security boundary.
    • The activation secret is generated by the server on successful activation, never shipped, and is the real security boundary for every call afterward (verify, heartbeat, deactivate).

This reflects the fundamental limit of licensing a product whose full source is handed to the customer: it deters casual bypass, it does not achieve DRM-level enforcement.

The local cache (EncryptedLocalCache) is encrypted with sodium_crypto_secretbox using a key derived from product_id + activation secret, so it fails closed if tampered with, copied to another installation, or read without the activation secret.

Usage

Activating an installation

php artisan license:activate
# or: php artisan license:activate LICENSE-KEY --domain=example.com --label="Production"

Falls back to LICENSE_KEY from config, then interactively prompts if neither the argument nor config value is set.

Checking license validity (recommended entry point)

use Finext\LicenseClient\Facades\License;

$result = License::check();
// [
//     'valid' => true,
//     'status' => 'active', // or grace_period, grace_period_expired, not_activated, unreachable_no_cache
//     'source' => 'live',    // or 'cache'
//     'days_remaining' => 29,
//     'data' => [...],       // full verified envelope data, when available
// ]

check() never throws. It verifies live against the server when reachable; if the server can't be reached, it falls back to the signed local cache and applies the offline grace period (grace_period_days from the server, or LICENSE_DEFAULT_GRACE_PERIOD_DAYS if no cache exists yet).

Gating routes with the middleware

Route::middleware('license.valid')->group(function () {
    // ...
});

Aborts with 403 and a support message when check()['valid'] is false.

Other operations

License::installationId();      // stable UUID for this installation
License::isActivated();         // whether an activation secret is stored
License::verify($domain);       // one-off live verify call
License::heartbeat($appVersion);
License::deactivate();          // deactivates server-side and clears local state
License::forget();              // clears local state only (secret + cache)

Testing this package in isolation

composer install
vendor/bin/phpunit

Tests run as plain PHPUnit (no Laravel container bootstrap) — all pure-logic classes (HmacSigner, ResponseVerifier, EncryptedLocalCache, GracePeriodPolicy) avoid Laravel facades so they can be exercised directly, including a golden-vector cross-check of the canonical JSON format against the license server's own implementation.

Compatibility

PHP ^8.2 with the sodium extension, Laravel (illuminate/support|http|console) ^10, ^11, ^12, or ^13.