ডেভেলপার গাইড

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

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

Markdown এ খুলুন

এআই এজেন্টকে এই লিংক দিলে পুরো গাইড একবারে পড়তে পারবে।

API
POST https://qvixgroup.com/api/licence
পরিচয়
শুধু লাইসেন্স কী
সাইন
Ed25519
ইন্টারনেট ছাড়া
৭ দিন পর্যন্ত

লাইসেন্স কী সহ বিক্রি হওয়া প্রতিটা 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: প্রতিটা রিকোয়েস্টে নতুন একটা এলোমেলো লেখা, ৬৪ অক্ষর পর্যন্ত। এটা সাইন করা উত্তরের ভেতরে ফেরত আসে, তাই পুরনো উত্তর আবার চালিয়ে ফাঁকি দেওয়া যায় না।

উত্তর

{
  "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
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>';
        }
    }
}

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

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

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

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 লেখা (বাদ দিতে হবে)।
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

ফ্রি কোটেশন নিন

এক মিনিটের কাজ। এক কর্মদিবসের মধ্যে ফোন দিয়ে কোটেশন জানাবো।

কী বানাতে চান?

একটা বেছে নিন। এরপর শুধু নাম আর নম্বর।

এই নম্বরে ফোন দিয়ে কোটেশন বুঝিয়ে বলবো।

আরো কিছু জানাতে চান? (অপশনাল)
আনুমানিক বাজেট

কোনো চার্জ নেই। নম্বর কারো সাথে শেয়ার করি না।