# Qvix সফটওয়্যারে **লাইসেন্স** যাচাই

লাইসেন্স কী সহ বিক্রি হওয়া প্রতিটা Qvix প্লাগইন, থিম, ডেস্কটপ সফটওয়্যার বা অন্য যেকোনো সফটওয়্যার Qvix লাইসেন্স API দিয়ে কী যাচাই করে। কোনো ডেভেলপার বা এআই কোডিং এজেন্টের এই যাচাই যোগ করতে যা যা লাগে, সব এই গাইডে আছে। লেখা মতো করলে সফটওয়্যারটা বাকি সব Qvix প্রোডাক্টের মতোই কাজ করবে।

## কীভাবে কাজ করে

1. ক্রেতা https://qvixgroup.com এ প্রোডাক্টের পেমেন্ট করেন। সাথে সাথে `QVX-7K2M-9PQR-4XTB-H8WN` এর মতো একটা কী তৈরি হয়, ক্রেতা সেটা ইমেইলে, SMS এ আর অ্যাকাউন্টে পান।
2. ক্রেতা সফটওয়্যারে কী বসান। সফটওয়্যার কী আর কোথায় চলছে (ওয়েবসাইটের ডোমেইন, বা কম্পিউটারের একটা আইডি) দিয়ে `activate` পাঠায়।
3. প্রতিটা কী নির্দিষ্ট কয়টা জায়গায় চলে। সব জায়গা ভরে গেলে `activate` উত্তর দেয় `limit_reached`। তখন ক্রেতা অ্যাকাউন্ট (https://qvixgroup.com/account) থেকে পুরনো একটা জায়গা খালি করেন, অথবা সফটওয়্যার সরানোর সময় নিজেই `deactivate` পাঠায়।
4. দিনে একবার সফটওয়্যার `verify` পাঠায়। ইন্টারনেট না থাকলে শেষ ভালো উত্তর দিয়ে ৭ দিন পর্যন্ত চলে।
5. প্রতিটা উত্তর সাইন করা থাকে। সফটওয়্যার Qvix এর পাবলিক কী দিয়ে সাইন মিলিয়ে দেখে, তাই ভুয়া সার্ভার বানিয়ে কেউ চালু করতে পারে না।

## শুরুর আগে: Qvix অ্যাডমিনে প্রোডাক্ট

Admin > Products এ প্রোডাক্টটা খুলে এগুলো ঠিক করুন:

- **Comes with a licence key**: চালু।
- **Sites or computers per key**: একটা কী কয় জায়গায় চলবে (যেমন ১ বা ৩)।
- **Slug**: লিখে রাখুন (যেমন `wp-sms-otp`)। সফটওয়্যার মিলিয়ে দেখে কী টা এই slug এর কি না, তাই অন্য প্রোডাক্টের কী দিয়ে এটা চালু হয় না।

মাসিক বা বাৎসরিক প্রোডাক্ট: সাবস্ক্রিপশনের মেয়াদ শেষ হলে কী আর valid থাকে না, নবায়ন করলে আবার চলে। সফটওয়্যারে আলাদা কিছু লাগে না।

## সফটওয়্যারে যা লাগবে

এই তিনটা মান কোডে constant হিসেবে রাখুন। পাবলিক কী সার্ভার থেকে ডাউনলোড করবেন না।

| মান | কী দেবেন |
|---|---|
| API এর ঠিকানা | `https://qvixgroup.com/api/licence` |
| প্রোডাক্টের slug | Admin > Products এর slug |
| পাবলিক কী | `PUBLIC_KEY_NOT_SET` |

## API

প্রতিটা রিকোয়েস্ট API এর ঠিকানা আর endpoint এ `POST`, ফর্ম বা JSON দিয়ে। লগইন, কুকি বা API টোকেন লাগে না, লাইসেন্স কী টাই পরিচয়। প্রতি IP থেকে মিনিটে ৬০টা রিকোয়েস্ট পর্যন্ত।

| Endpoint | যা পাঠাবেন | কাজ |
|---|---|---|
| `/verify` | `key`, `domain` (ঐচ্ছিক), `nonce` (ঐচ্ছিক) | কী ঠিক আছে কি না। এই জায়গায় চালু থাকলে "শেষ চেক" এর তারিখ আপডেট হয়। |
| `/activate` | `key`, `domain`, `nonce` (ঐচ্ছিক) | জায়গা খালি থাকলে এখানে চালু করে। একই জায়গায় আবার চালু করলে সমস্যা নেই, নতুন জায়গা খরচ হয় না। |
| `/deactivate` | `key`, `domain`, `nonce` (ঐচ্ছিক) | এই জায়গা খালি করে, যাতে কী অন্য কোথাও চালানো যায়। |

- `key`: লাইসেন্স কী। ছোট বা বড় হাতের অক্ষর দুটোই চলে।
- `domain`: সফটওয়্যার কোথায় চলছে। ওয়েবসাইট হলে তার ডোমেইন। সার্ভার এটাকে ছোট হাতের অক্ষরে নেয়, URL থেকে শুধু host রাখে আর `www.` বাদ দেয়, তাই `https://www.Shop.com/admin` জমা হয় `shop.com` হিসেবে। ডেস্কটপ সফটওয়্যার হলে কম্পিউটারের একটা স্থির আইডি ("ডেস্কটপ ও অন্য সফটওয়্যার" দেখুন)। সর্বোচ্চ ১৯০ অক্ষর।
- `nonce`: প্রতিটা রিকোয়েস্টে নতুন একটা এলোমেলো লেখা, ৬৪ অক্ষর পর্যন্ত। এটা সাইন করা উত্তরের ভেতরে ফেরত আসে, তাই পুরনো উত্তর আবার চালিয়ে ফাঁকি দেওয়া যায় না।

### উত্তর

```json
{
  "valid": true,
  "status": "valid",
  "product": "wp-sms-otp",
  "expires_at": "2027-10-18T10:00:00+06:00",
  "activations": { "used": 1, "limit": 3 },
  "activated": true,
  "signed": "eyJ2YWxpZCI6dHJ1ZSwic3RhdHVzIjoidmFsaWQi...",
  "signature": "q8cJ3kP0...=="
}
```

| `status` | HTTP | মানে | সফটওয়্যার যা দেখাবে |
|---|---|---|---|
| `valid` | 200 | কী ঠিক আছে | চালু করুন |
| `invalid` | 404 | এমন কোনো কী নেই | "কী টা ঠিক নেই, আবার দেখে নিন" |
| `unpaid` | activate এ 403 | অর্ডারের পেমেন্ট এখনো হয়নি | "পেমেন্ট এখনো নিশ্চিত হয়নি" |
| `refunded` | activate এ 403 | টাকা ফেরত দেওয়া হয়েছে | "এই কী আর চালু নেই" |
| `expired` | activate এ 403 | মাসিক বা বাৎসরিক মেয়াদ শেষ | "চালিয়ে যেতে নবায়ন করুন" আর শপের লিংক |
| `revoked` | activate এ 403 | Qvix টিম বন্ধ করেছে | "Qvix সাপোর্টে যোগাযোগ করুন" |
| `limit_reached` | 409 | এই কী এর সব জায়গা ভরা | "Qvix অ্যাকাউন্ট থেকে পুরনো একটা সাইট বা কম্পিউটার সরিয়ে দিন", সাথে https://qvixgroup.com/account এর লিংক |

`expires_at` শুধু মাসিক বা বাৎসরিক প্রোডাক্টে থাকে। `activated` বলে এই জায়গায় চালু আছে কি না। ভুল ফর্ম পাঠালে (যেমন কী ছাড়া) 422 আসে, সাইন ছাড়া। শুধু সাইন করা অংশের ভেতরে `valid` সত্যি `true` হলেই চালু করবেন।

## সাইন মিলিয়ে দেখা

`signed` হলো base64 করা JSON। ভেতরে উপরের সব ঘর, সাথে `key` (বড় হাতের অক্ষরে), `domain` (সার্ভার যেভাবে জমা রাখে), `nonce` (যেটা পাঠিয়েছেন) আর `checked_at` (সার্ভারের সময়)। `signature` হলো হুবহু `signed` লেখাটার (ডিকোড করা JSON এর নয়) Ed25519 সাইন, base64 এ।

সফটওয়্যার যা করবে:

1. পাবলিক কী দিয়ে `signed` লেখার উপর `signature` মিলিয়ে দেখবে। না মিললে উত্তরটা বাদ।
2. `signed` কে base64 থেকে ডিকোড করে তার JSON পড়বে। শুধু এই ঘরগুলোই ব্যবহার করবে, পাশের সাইন ছাড়া কপি নয়।
3. মিলিয়ে দেখবে `key` এই কী, `domain` এই জায়গা (সার্ভারের নিয়মেই বানানো), `nonce` এইমাত্র পাঠানোটা, আর `product` নিজের slug।
4. `signed`, `signature` আর nonce সেভ করে রাখবে। চালু হওয়ার সময় সেভ করা উত্তর একইভাবে মিলিয়ে দেখবে, আর `checked_at` ৭ দিনের কম পুরনো হলে মেনে নেবে।

## সফটওয়্যারের নিয়ম

- ক্রেতা কী সেভ করলে `activate`, দিনে একবার (আর সেটিংস পেজ খুললে) `verify`, আর সফটওয়্যার বন্ধ বা আনইনস্টল করলে `deactivate`।
- নেটওয়ার্ক কল ১৫ সেকেন্ডের মতো পরে ছেড়ে দেবে, আর অপেক্ষার সময় ওয়েবসাইট বা অ্যাপ আটকে রাখবে না।
- নেটওয়ার্কের সমস্যা মানে ভুল কী নয়। সেভ করা উত্তর ৭ দিন পুরনো না হওয়া পর্যন্ত চালিয়ে যাবে।
- কী valid না হলে শুধু পেইড ফিচার বন্ধ করবে। ক্রেতার ওয়েবসাইট ভাঙবে না, ডেটা মুছবে না, ভয় দেখানো মেসেজ দেবে না।
- ক্রেতাকে `status` সহজ ভাষায় দেখাবে (উপরের টেবিল দেখুন), আর `activations` থেকে কয়টা জায়গা ব্যবহার হয়েছে।
- কী কোথাও খোলা জায়গায় রাখবে না, আর Qvix API ছাড়া অন্য কোথাও পাঠাবে না।
- সফটওয়্যারে বাংলা আর English দুটো ভাষা থাকলে ক্রেতার দেখা সব লেখা দুই ভাষাতেই রাখবে।

## WordPress প্লাগইন

এই class টা প্লাগইনে কপি করুন, যেমন `includes/class-qvix-licence.php` নামে, আর উপরের চারটা constant বদলে নিন। এটা Settings এর নিচে লাইসেন্সের পেজ, valid কী না থাকলে একটা নোটিশ, দিনে একবার চেক যোগ করে, আর প্লাগইন বন্ধ করলে জায়গা খালি করে দেয়। `sodium_crypto_sign_verify_detached` WordPress 5.2 থেকে নিজেই থাকে।

```php
<?php
if (! defined('ABSPATH')) exit;

class My_Plugin_Licence
{
    const PRODUCT    = 'wp-sms-otp';            // product slug in the Qvix admin
    const PUBLIC_KEY = 'PUBLIC_KEY_NOT_SET';
    const API        = 'https://qvixgroup.com/api/licence/';
    const PREFIX     = 'my_plugin';             // unique to this plugin: option, cron and page names

    public static function boot($plugin_file)
    {
        add_action('admin_menu', [__CLASS__, 'menu']);
        add_action('admin_notices', [__CLASS__, 'notice']);
        add_action(self::PREFIX . '_licence_check', [__CLASS__, 'refresh']);
        if (! wp_next_scheduled(self::PREFIX . '_licence_check')) {
            wp_schedule_event(time() + HOUR_IN_SECONDS, 'daily', self::PREFIX . '_licence_check');
        }
        register_deactivation_hook($plugin_file, [__CLASS__, 'release']);
    }

    /** Call this before every paid feature */
    public static function is_valid()
    {
        $proof = self::read_proof(get_option(self::PREFIX . '_licence', []));

        return $proof && $proof['valid'] === true && strtotime($proof['checked_at']) > time() - 7 * DAY_IN_SECONDS;
    }

    /** The signed answer, or null when there is none yet or it cannot be trusted */
    public static function proof()
    {
        return self::read_proof(get_option(self::PREFIX . '_licence', []));
    }

    private static function call($action, $key)
    {
        $nonce = wp_generate_password(20, false);
        $response = wp_remote_post(self::API . $action, [
            'timeout' => 15,
            'body'    => ['key' => $key, 'domain' => self::domain(), 'nonce' => $nonce],
        ]);
        if (is_wp_error($response)) {
            return null; // no internet: the saved answer keeps working
        }

        $body = json_decode(wp_remote_retrieve_body($response), true) ?: [];
        $data = ['key' => $key, 'nonce' => $nonce, 'signed' => $body['signed'] ?? '', 'signature' => $body['signature'] ?? ''];
        $proof = self::read_proof($data);
        if ($proof && $action !== 'deactivate') {
            update_option(self::PREFIX . '_licence', $data, false);
        }

        return $proof;
    }

    private static function read_proof($data)
    {
        if (empty($data['signed']) || empty($data['signature']) || empty($data['key'])) {
            return null;
        }
        $signature = base64_decode($data['signature'], true);
        $public    = base64_decode(self::PUBLIC_KEY, true);
        if (! $signature || ! $public || ! sodium_crypto_sign_verify_detached($signature, $data['signed'], $public)) {
            return null;
        }

        $proof = json_decode(base64_decode($data['signed']), true);
        $ours  = is_array($proof)
            && $proof['key'] === strtoupper(trim($data['key']))
            && $proof['domain'] === self::domain()
            && $proof['nonce'] === $data['nonce']
            && (empty($proof['product']) || $proof['product'] === self::PRODUCT);

        return $ours ? $proof : null;
    }

    public static function refresh()
    {
        $data = get_option(self::PREFIX . '_licence', []);
        if (! empty($data['key'])) {
            self::call('verify', $data['key']);
        }
    }

    public static function release()
    {
        $data = get_option(self::PREFIX . '_licence', []);
        if (! empty($data['key'])) {
            self::call('deactivate', $data['key']);
        }
        delete_option(self::PREFIX . '_licence');
        wp_clear_scheduled_hook(self::PREFIX . '_licence_check');
    }

    /** The site as the server stores it: lower case host, no "www." */
    private static function domain()
    {
        $host = strtolower((string) wp_parse_url(home_url(), PHP_URL_HOST));

        return preg_replace('/^www\./', '', $host);
    }

    public static function menu()
    {
        add_options_page('Licence', 'My Plugin licence', 'manage_options', self::PREFIX . '-licence', [__CLASS__, 'page']);
    }

    public static function page()
    {
        $message = '';
        if (isset($_POST['licence_key']) && check_admin_referer(self::PREFIX . '_licence')) {
            $proof = self::call('activate', sanitize_text_field(wp_unslash($_POST['licence_key'])));
            $message = ! $proof ? 'Could not reach the licence server. Please try again.'
                : ($proof['valid'] ? 'Licence activated.' : 'This key does not work here: ' . $proof['status']);
        }
        $data  = get_option(self::PREFIX . '_licence', []);
        $proof = self::proof();
        ?>
        <div class="wrap">
            <h1>Licence</h1>
            <?php if ($message) : ?><div class="notice notice-info"><p><?php echo esc_html($message); ?></p></div><?php endif; ?>
            <form method="post">
                <?php wp_nonce_field(self::PREFIX . '_licence'); ?>
                <p><label for="licence_key">Licence key</label><br>
                <input id="licence_key" name="licence_key" class="regular-text" placeholder="QVX-XXXX-XXXX-XXXX-XXXX"
                       value="<?php echo esc_attr($data['key'] ?? ''); ?>"></p>
                <?php submit_button('Activate licence'); ?>
            </form>
            <?php if ($proof) : ?>
                <p>Status: <strong><?php echo esc_html($proof['status']); ?></strong>,
                   places used: <?php echo (int) $proof['activations']['used']; ?> of <?php echo (int) $proof['activations']['limit']; ?></p>
            <?php endif; ?>
        </div>
        <?php
    }

    public static function notice()
    {
        if (current_user_can('manage_options') && ! self::is_valid()) {
            $url = admin_url('options-general.php?page=' . self::PREFIX . '-licence');
            echo '<div class="notice notice-warning"><p>Add your licence key to use every feature: <a href="' . esc_url($url) . '">Licence settings</a></p></div>';
        }
    }
}
```

প্লাগইনের মূল ফাইলে:

```php
require_once __DIR__ . '/includes/class-qvix-licence.php';
My_Plugin_Licence::boot(__FILE__);

// Before a paid feature
if (My_Plugin_Licence::is_valid()) {
    // paid feature
}
```

প্রতিটা প্লাগইনে class এর নাম আর `PREFIX` বদলে নিন, যাতে এক সাইটে দুটো Qvix প্লাগইন থাকলে গোলমাল না হয়।

## ডেস্কটপ ও অন্য সফটওয়্যার

যেকোনো ভাষায় চলে: ফর্ম `POST` পাঠান, JSON পড়ুন, Ed25519 সাইন মিলিয়ে দেখুন। `domain` এ কম্পিউটারের এমন একটা আইডি দিন যা রিস্টার্ট বা আপডেটে বদলায় না:

- Windows: `HKLM\SOFTWARE\Microsoft\Cryptography` এর `MachineGuid`।
- macOS: `ioreg -rd1 -c IOPlatformExpertDevice` এর `IOPlatformUUID`।
- Linux: `/etc/machine-id`।

আসল আইডি সরাসরি পাঠাবেন না। পাঠাবেন `sha256(আসল আইডি + ":" + প্রোডাক্টের slug)` এর প্রথম ৩২ অক্ষর, ছোট হাতের hex এ। এতে আসল আইডি গোপন থাকে, প্রতিবার একই আসে, আর শুধু অক্ষর ও সংখ্যা থাকায় সার্ভার হুবহু জমা রাখে। ক্রেতাকে এটা "কম্পিউটার আইডি" হিসেবে দেখান, কারণ অ্যাকাউন্ট পেজে এটাই দেখা যায়।

### Node.js বা Electron

```js
const crypto = require('crypto');

const API = 'https://qvixgroup.com/api/licence';
const PRODUCT = 'qvix-pos';
const PUBLIC_KEY = 'PUBLIC_KEY_NOT_SET';

// Ed25519 public key from its 32 raw bytes
const publicKey = crypto.createPublicKey({
  key: Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), Buffer.from(PUBLIC_KEY, 'base64')]),
  format: 'der',
  type: 'spki',
});

async function licence(action, key, place) {
  const nonce = crypto.randomBytes(16).toString('hex');
  const res = await fetch(`${API}/${action}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
    body: JSON.stringify({ key, domain: place, nonce }),
    signal: AbortSignal.timeout(15000),
  });
  const body = await res.json();
  return readProof({ key, place, nonce, signed: body.signed, signature: body.signature });
}

// Save the object passed in here; call it again on start-up with the saved copy
function readProof({ key, place, nonce, signed, signature }) {
  if (!signed || !signature) return null;
  const ok = crypto.verify(null, Buffer.from(signed), publicKey, Buffer.from(signature, 'base64'));
  if (!ok) return null;
  const proof = JSON.parse(Buffer.from(signed, 'base64').toString('utf8'));
  const ours = proof.key === key.trim().toUpperCase() && proof.domain === place && proof.nonce === nonce
    && (!proof.product || proof.product === PRODUCT);
  return ours ? proof : null;
}

function isValid(proof) {
  return !!proof && proof.valid === true && Date.now() - Date.parse(proof.checked_at) < 7 * 24 * 3600 * 1000;
}
```

### Python

```python
import base64, json, secrets, time
from datetime import datetime
import requests
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

API = "https://qvixgroup.com/api/licence"
PRODUCT = "qvix-pos"
PUBLIC_KEY = Ed25519PublicKey.from_public_bytes(base64.b64decode("PUBLIC_KEY_NOT_SET"))

def licence(action, key, place):
    nonce = secrets.token_hex(16)
    body = requests.post(f"{API}/{action}", data={"key": key, "domain": place, "nonce": nonce}, timeout=15).json()
    return read_proof(key, place, nonce, body.get("signed"), body.get("signature"))

def read_proof(key, place, nonce, signed, signature):
    if not signed or not signature:
        return None
    try:
        PUBLIC_KEY.verify(base64.b64decode(signature), signed.encode())
    except Exception:
        return None
    proof = json.loads(base64.b64decode(signed))
    ours = (proof["key"] == key.strip().upper() and proof["domain"] == place and proof["nonce"] == nonce
            and (not proof.get("product") or proof["product"] == PRODUCT))
    return proof if ours else None

def is_valid(proof):
    if not proof or proof["valid"] is not True:
        return False
    return time.time() - datetime.fromisoformat(proof["checked_at"]).timestamp() < 7 * 24 * 3600
```

## টেস্ট করা

- আসল কী পেতে লাইভ সাইটে একবার প্রোডাক্টটা কিনুন (অথবা Qvix টিমের কাছে একটা টেস্ট অর্ডার চান)।
- `localhost` আর টেস্ট সাইটও জায়গা হিসেবে গোনা হয়। টেস্ট শেষে অ্যাকাউন্ট (https://qvixgroup.com/account) থেকে সরিয়ে দিন, যাতে ক্রেতার জায়গা খালি থাকে।
- প্রতিটা অবস্থা চালিয়ে দেখুন: ভুল কী, একই সাইটে দুবার (একটাই জায়গা খরচ), লিমিটের চেয়ে একটা বেশি সাইট (`limit_reached`), ইন্টারনেট ছাড়া (সেভ করা উত্তরে চলে), আর বদলানো `signed` লেখা (বাদ দিতে হবে)।

```bash
curl -X POST https://qvixgroup.com/api/licence/verify -d key=QVX-XXXX-XXXX-XXXX-XXXX -d domain=example.com -d nonce=test123
```

এই গাইড সাধারণ Markdown এ, এআই এজেন্ট আর এডিটরের জন্য: https://qvixgroup.com/developers/licence.md
