Giao thức CLV1 đã công khaiĐọc đặc tả
Mục lục thư viện

Thư viện tài liệuBắt đầu theo ngôn ngữ

Tự viết client

Khi ngôn ngữ của bạn không có SDK, không gọi được hàm C và không chạy được công cụ dòng lệnh, bạn có thể tự viết client theo giao thức CLV1.1. Bài này đi qua những gì client phải làm, với một client mẫu viết bằng Python thuần chỉ từ đặc tả, chạy với API thật trong CI, và bộ test vector để bạn tự kiểm client của mình.

5 phút đọc

Có thật cần không?

Hãy cân nhắc lối khác trước: thư viện C gọi được từ hầu hết ngôn ngữ, và công cụ dòng lệnh dùng được ở bất cứ đâu chạy được một chương trình. Tự viết client nghĩa là tự giữ đúng phần ký và kiểm chữ ký; sai một chi tiết là phần mềm từ chối mọi câu trả lời, hoặc tệ hơn, tin một câu trả lời giả.

Những gì client phải làm

  1. Giữ một khoá thiết bị Ed25519 trên máy, tạo một lần và dùng lại mỗi lần chạy.
  2. Ký mọi yêu cầu bằng khoá thiết bị, kèm nonce dùng một lần và dấu thời gian.
  3. Kiểm mọi câu trả lời, kể cả câu báo lỗi, bằng khoá công khai của ứng dụng: chữ ký phải gắn đúng với yêu cầu vừa gửi và lệch không quá 60 giây.
  4. Gửi heartbeat khi phần mềm chạy, theo nhịp server ký trong nextCheckSeconds, đăng xuất khi đóng.
  5. Chỉ thử lại khi an toàn, như phần câu hỏi bên dưới giải thích.

Đặc tả đầy đủ (header, chuỗi ký, endpoint, token offline, mã lỗi) ở Đặc tả giao thức CLV1. Phần dưới cho thấy từng bước bằng mã đã chạy được.

1. Khoá

Khoá công khai của ứng dụng lấy ở trang Tích hợp › Kết nối phần mềm. kid của nó là 16 ký tự hex đầu của SHA-256 trên 32 byte khoá; câu trả lời ký bằng khoá có kid khác bị từ chối. Khoá thiết bị được tạo một lần rồi giữ lại: phiên và token offline gắn với khoá này, còn suất trên license đi theo các định danh đã băm của máy; dấu nhận diện máy được băm ngay trên máy:

# The app's public key, from the dashboard. Its kid is the first 16 hex characters of sha256 of its 32 bytes.
APP_KEY_BYTES = from_b64url(os.environ["CLAVIS_APP_KEY"])
APP_KEY = Ed25519PublicKey.from_public_bytes(APP_KEY_BYTES)
APP_KID = sha256_hex(APP_KEY_BYTES)[:16]


def device_key() -> Ed25519PrivateKey:
    """Made once and kept: the same key every run, so sessions and offline tokens stay with this machine."""
    path = STATE / "raw-device.key"
    if path.exists():
        return Ed25519PrivateKey.from_private_bytes(bytes.fromhex(path.read_text()))
    key = Ed25519PrivateKey.generate()
    STATE.mkdir(parents=True, exist_ok=True)
    path.write_text(key.private_bytes_raw().hex())
    return key


DEVICE_KEY = device_key()


def fingerprint() -> str:
    """Hashed on the machine: sha256(lowercase(trim(value))), first 32 hex characters. Raw values never leave."""
    machine_id = Path("/etc/machine-id")
    value = machine_id.read_text() if machine_id.exists() else platform.node()
    return sha256_hex(value.strip().lower().encode())[:32]

2. Ký yêu cầu

Mỗi yêu cầu là một POST tới /v1/client/<endpoint> với thân JSON. Chuỗi được ký gồm tám dòng: CLV1-REQUEST, App ID, phương thức, đường dẫn, SHA-256 của đúng các byte thân, dấu thời gian, nonce và mã phiên (hoặc -). Header x-clavis-proto: 1.1 xin câu trả lời theo CLV1.1. Header x-clavis-sdk không được ký, chỉ để trang Kết nối phần mềm hiện tên client của bạn:

def call(endpoint: str, body: dict, session: str = None, retry: bool = True):
    """POST one signed request; returns (HTTP status, answer) once the answer's signature is checked."""
    global clock_offset
    raw = json.dumps(body, separators=(",", ":")).encode()
    path = "/v1/client/" + endpoint
    timestamp = str(int(time.time()) + clock_offset)
    nonce = secrets.token_urlsafe(18)
    signed = "\n".join(["CLV1-REQUEST", APP_ID, "POST", path, sha256_hex(raw), timestamp, nonce, session or "-"])
    headers = {
        "content-type": "application/json",
        "x-clv-app": APP_ID,
        "x-clv-time": timestamp,
        "x-clv-nonce": nonce,
        "x-clv-key": b64url(DEVICE_KEY.public_key().public_bytes_raw()),
        "x-clv-signature": b64url(DEVICE_KEY.sign(signed.encode())),
        "x-clavis-proto": "1.1",
        "x-clavis-sdk": "raw-python/1.0.0 (%s)" % platform.system().lower(),
    }
    if session:
        headers["x-clv-session"] = session
    request = urllib.request.Request(BASE_URL + path, data=raw, headers=headers, method="POST")
    try:
        with urllib.request.urlopen(request, timeout=15) as response:
            status, answer, answer_headers = response.status, response.read(), response.headers
    except urllib.error.HTTPError as err:
        status, answer, answer_headers = err.code, err.read(), err.headers
    except OSError:
        fail("network_error")
    verify(status, path, nonce, answer, answer_headers)
    result = json.loads(answer)
    # A signed clock_skew says this machine's clock is off: correct it and try once more.
    if result.get("error", {}).get("code") == "clock_skew" and retry:
        clock_offset = result["serverTime"] - int(time.time())
        return call(endpoint, body, session, retry=False)
    return status, result

3. Kiểm câu trả lời

Câu trả lời CLV1.1 mang ba header x-clavis-kid, x-clavis-timestamp, x-clavis-signature. Chuỗi được ký gồm tám dòng: CLV1.1, kid, mã trạng thái HTTP, phương thức, đường dẫn, nonce của yêu cầu, dấu thời gian và SHA-256 của thân. Thiếu header, sai kid, sai chữ ký hay lệch quá 60 giây: coi như không có câu trả lời.

def verify(status: int, path: str, nonce: str, body: bytes, headers) -> None:
    """The verification contract: every answer, error answers included, or it is no answer at all."""
    kid = headers.get("x-clavis-kid")
    timestamp = headers.get("x-clavis-timestamp")
    signature = headers.get("x-clavis-signature")
    if not (kid and timestamp and signature):
        fail("unsigned_response")
    if kid != APP_KID:
        fail("unknown_key")
    signed = "\n".join(["CLV1.1", kid, str(status), "POST", path, nonce, timestamp, sha256_hex(body)])
    try:
        APP_KEY.verify(from_b64url(signature), signed.encode())
    except InvalidSignature:
        fail("invalid_response_signature")
    if abs(int(timestamp) - (time.time() + clock_offset)) > 60:
        fail("stale_response")

Mỗi điều kiện có một mã riêng (unsigned_response, unknown_key, invalid_response_signature, stale_response), giống hệt các SDK chính thức, xem Mã lỗi.

4. K1 từ đầu tới cuối

init, rồi activate với key và mô tả máy, heartbeat với mã phiên vừa nhận, cuối cùng logout:

call("init", {})
status, grant = call("activate", {"key": os.environ["CLAVIS_LICENSE"], "device": {"fingerprint": fingerprint()}})
if status != 200:
    fail(grant["error"]["code"])
session = grant["session"]["id"]
# Every few minutes while the software runs; sessions end after an hour without one.
status, beat = call("heartbeat", {}, session)
if status != 200:
    fail(beat["error"]["code"])
call("logout", {}, session)
print("OK k1", grant["license"]["status"])

Nguyên file chỉ cần Python 3.9 trở lên và gói cryptography:

"""K1 without an SDK: a client written from /docs/protocol alone, in plain Python with the
`cryptography` package for Ed25519. It shows what a client in any language must do: sign every request, check every
answer, keep the device key.

Settings: CLAVIS_BASE_URL, CLAVIS_APP_ID, CLAVIS_APP_KEY, CLAVIS_LICENSE, CLAVIS_STATE_DIR.
Ends with "OK k1 active", or "FAIL <code>".
"""

import base64
import hashlib
import json
import os
import platform
import secrets
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey

BASE_URL = os.environ["CLAVIS_BASE_URL"].rstrip("/")
APP_ID = os.environ["CLAVIS_APP_ID"]
STATE = Path(os.environ.get("CLAVIS_STATE_DIR", ".clavis"))


def b64url(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode()


def from_b64url(text: str) -> bytes:
    return base64.urlsafe_b64decode(text + "=" * (-len(text) % 4))


def sha256_hex(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


def fail(code: str) -> None:
    print("FAIL", code)
    sys.exit(1)


# The app's public key, from the dashboard. Its kid is the first 16 hex characters of sha256 of its 32 bytes.
APP_KEY_BYTES = from_b64url(os.environ["CLAVIS_APP_KEY"])
APP_KEY = Ed25519PublicKey.from_public_bytes(APP_KEY_BYTES)
APP_KID = sha256_hex(APP_KEY_BYTES)[:16]


def device_key() -> Ed25519PrivateKey:
    """Made once and kept: the same key every run, so sessions and offline tokens stay with this machine."""
    path = STATE / "raw-device.key"
    if path.exists():
        return Ed25519PrivateKey.from_private_bytes(bytes.fromhex(path.read_text()))
    key = Ed25519PrivateKey.generate()
    STATE.mkdir(parents=True, exist_ok=True)
    path.write_text(key.private_bytes_raw().hex())
    return key


DEVICE_KEY = device_key()


def fingerprint() -> str:
    """Hashed on the machine: sha256(lowercase(trim(value))), first 32 hex characters. Raw values never leave."""
    machine_id = Path("/etc/machine-id")
    value = machine_id.read_text() if machine_id.exists() else platform.node()
    return sha256_hex(value.strip().lower().encode())[:32]



clock_offset = 0


def call(endpoint: str, body: dict, session: str = None, retry: bool = True):
    """POST one signed request; returns (HTTP status, answer) once the answer's signature is checked."""
    global clock_offset
    raw = json.dumps(body, separators=(",", ":")).encode()
    path = "/v1/client/" + endpoint
    timestamp = str(int(time.time()) + clock_offset)
    nonce = secrets.token_urlsafe(18)
    signed = "\n".join(["CLV1-REQUEST", APP_ID, "POST", path, sha256_hex(raw), timestamp, nonce, session or "-"])
    headers = {
        "content-type": "application/json",
        "x-clv-app": APP_ID,
        "x-clv-time": timestamp,
        "x-clv-nonce": nonce,
        "x-clv-key": b64url(DEVICE_KEY.public_key().public_bytes_raw()),
        "x-clv-signature": b64url(DEVICE_KEY.sign(signed.encode())),
        "x-clavis-proto": "1.1",
        "x-clavis-sdk": "raw-python/1.0.0 (%s)" % platform.system().lower(),
    }
    if session:
        headers["x-clv-session"] = session
    request = urllib.request.Request(BASE_URL + path, data=raw, headers=headers, method="POST")
    try:
        with urllib.request.urlopen(request, timeout=15) as response:
            status, answer, answer_headers = response.status, response.read(), response.headers
    except urllib.error.HTTPError as err:
        status, answer, answer_headers = err.code, err.read(), err.headers
    except OSError:
        fail("network_error")
    verify(status, path, nonce, answer, answer_headers)
    result = json.loads(answer)
    # A signed clock_skew says this machine's clock is off: correct it and try once more.
    if result.get("error", {}).get("code") == "clock_skew" and retry:
        clock_offset = result["serverTime"] - int(time.time())
        return call(endpoint, body, session, retry=False)
    return status, result




def verify(status: int, path: str, nonce: str, body: bytes, headers) -> None:
    """The verification contract: every answer, error answers included, or it is no answer at all."""
    kid = headers.get("x-clavis-kid")
    timestamp = headers.get("x-clavis-timestamp")
    signature = headers.get("x-clavis-signature")
    if not (kid and timestamp and signature):
        fail("unsigned_response")
    if kid != APP_KID:
        fail("unknown_key")
    signed = "\n".join(["CLV1.1", kid, str(status), "POST", path, nonce, timestamp, sha256_hex(body)])
    try:
        APP_KEY.verify(from_b64url(signature), signed.encode())
    except InvalidSignature:
        fail("invalid_response_signature")
    if abs(int(timestamp) - (time.time() + clock_offset)) > 60:
        fail("stale_response")



call("init", {})
status, grant = call("activate", {"key": os.environ["CLAVIS_LICENSE"], "device": {"fingerprint": fingerprint()}})
if status != 200:
    fail(grant["error"]["code"])
session = grant["session"]["id"]
# Every few minutes while the software runs; sessions end after an hour without one.
status, beat = call("heartbeat", {}, session)
if status != 200:
    fail(beat["error"]["code"])
call("logout", {}, session)
print("OK k1", grant["license"]["status"])

Chạy với một key thử nghiệm (nút Tạo key thử nghiệm):

pip install cryptography
export CLAVIS_BASE_URL="<địa chỉ API>"
export CLAVIS_APP_ID="<App ID>"
export CLAVIS_APP_KEY="<khoá công khai>"
export CLAVIS_LICENSE="<key thử nghiệm>"
python k1_raw.py

Dòng cuối in ra OK k1 active, và trang Kết nối phần mềm hiện Đã kết nối kèm raw-python/1.0.0. CI chạy file này với API thật trên Windows và Linux: kích hoạt được, key không dùng được nhận FAIL invalid_credentials, và khi cấu hình nhầm khoá công khai của một ứng dụng khác, client từ chối câu trả lời với FAIL unknown_key.

5. Tự kiểm bằng test vector

Tải clv1.1-test-vectors.json ở trang Tải về. File chứa khoá, chuỗi ký, chữ ký, một token offline và một manifest bản phát hành, cùng kết quả mà server và mọi SDK chính thức đều ra đúng. Viết lại các phép kiểm dưới đây bằng mã của client bạn; khi tất cả qua, client ký và kiểm giống hệt server. Bản Python của các phép kiểm:

Mã hoá base64url không đệm, SHA-256 hex chữ thường, và cách băm định danh phần cứng:

# base64url without padding, SHA-256 in lowercase hex, and hardware identifiers hashed before they leave.
for v in vectors["base64url"]:
    check("base64url " + v["hex"], b64url(bytes.fromhex(v["hex"])) == v["b64"])
for v in vectors["sha256"]:
    check("sha256 " + v["hex"], sha256_hex(v["text"].encode()) == v["hex"])
for v in vectors["hashComponent"]:
    check("hashComponent", sha256_hex(v["input"].strip().lower().encode())[:32] == v["output"])

Khoá từ seed, kid và mã băm khoá thiết bị:

keys = vectors["keys"]
app_key = Ed25519PrivateKey.from_private_bytes(bytes.fromhex(keys["appSeedHex"])).public_key()
device_key = Ed25519PrivateKey.from_private_bytes(bytes.fromhex(keys["deviceSeedHex"])).public_key()
check("app public key from its seed", b64url(app_key.public_bytes_raw()) == keys["appPublicKey"])
check("kid", sha256_hex(app_key.public_bytes_raw())[:16] == keys["kid"])
check("device public key from its seed", b64url(device_key.public_bytes_raw()) == keys["devicePublicKey"])
check("device key hash", sha256_hex(device_key.public_bytes_raw()) == keys["deviceKeyHash"])

Chuỗi ký và chữ ký của một yêu cầu:

# The string the device signs, and the signature the server expects for it.
r = vectors["request"]
lines = ["CLV1-REQUEST", r["appId"], r["method"], r["path"], sha256_hex(r["body"].encode())]
signed = "\n".join(lines + [str(r["timestamp"]), r["nonce"], r["sessionId"] or "-"])
check("request string", signed == r["canonical"])
device = Ed25519PrivateKey.from_private_bytes(bytes.fromhex(keys["deviceSeedHex"]))
check("request signature", b64url(device.sign(signed.encode())) == r["signature"])

Chuỗi ký và chữ ký của một câu trả lời:

# The string the server signs for a 1.1 client: verify it with the app's public key.
s = vectors["response"]
lines = ["CLV1.1", s["kid"], str(s["status"]), s["method"], s["path"], s["requestNonce"], str(s["timestamp"])]
signed = "\n".join(lines + [sha256_hex(s["body"].encode())])
check("response string", signed == s["canonical"])
app_public = Ed25519PublicKey.from_public_bytes(from_b64url(keys["appPublicKey"]))
check("response signature", signed_by(app_public, s["signature"], signed))

Token offline: clv1.<payload>.<chữ ký>, ứng dụng ký clv1.<payload>; token chỉ hợp lệ trên máy có mã băm khoá khớp dev, trong khoảng nbf tới exp, và bị từ chối khi đồng hồ máy bị lùi:

# clv1.<payload>.<signature>: the app key signs "clv1.<payload>"; the payload names the device by its key hash.
o = vectors["offline"]
prefix, payload, signature = o["token"].split(".")
check("offline signature", signed_by(app_key, signature, prefix + "." + payload))
claims = json.loads(from_b64url(payload))
check("offline payload", claims == o["payload"] and claims["dev"] == keys["deviceKeyHash"])
check("offline valid", claims["nbf"] <= o["validAt"] < claims["exp"])
check("offline expired", o["expiredAt"] >= claims["exp"])
check("offline clock turned back", o["clockBackAt"] < claims["iat"])

Manifest bản phát hành: ứng dụng ký CLV1-MANIFEST cộng xuống dòng cộng nguyên văn manifest nhận được:

# A release manifest: the app key signs "CLV1-MANIFEST\n" followed by the manifest text exactly as received.
m = vectors["manifest"]
check("manifest signature", signed_by(app_key, m["signature"], "CLV1-MANIFEST\n" + m["manifest"]))
manifest = json.loads(m["manifest"])
check("manifest kid", manifest["kid"] == m["kid"])
check("manifest valid", manifest["issuedAt"] <= m["validAt"] < manifest["expiresAt"] <= m["expiredAt"])

Nguyên file Python, kèm vectors nó nạp và hai hàm phụ check, signed_by:

"""The compatibility test for your own client: every check of clv1.1-test-vectors.json (the
Downloads page), done here in plain Python with `cryptography` for Ed25519. Write the same checks with your own
client's code; when they all pass, it signs and verifies exactly as the server and every official SDK do.

Run: python check_vectors.py clv1.1-test-vectors.json      Ends with "OK vectors", or the first check that failed.
"""

import base64
import hashlib
import json
import sys

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey


def b64url(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode()


def from_b64url(text: str) -> bytes:
    return base64.urlsafe_b64decode(text + "=" * (-len(text) % 4))


def sha256_hex(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


def signed_by(key: Ed25519PublicKey, signature: str, data: str) -> bool:
    try:
        key.verify(from_b64url(signature), data.encode())
        return True
    except InvalidSignature:
        return False


def check(name: str, ok: bool) -> None:
    if not ok:
        print("FAIL", name)
        sys.exit(1)


vectors = json.load(open(sys.argv[1] if len(sys.argv) > 1 else "clv1.1-test-vectors.json", encoding="utf-8"))

# base64url without padding, SHA-256 in lowercase hex, and hardware identifiers hashed before they leave.
for v in vectors["base64url"]:
    check("base64url " + v["hex"], b64url(bytes.fromhex(v["hex"])) == v["b64"])
for v in vectors["sha256"]:
    check("sha256 " + v["hex"], sha256_hex(v["text"].encode()) == v["hex"])
for v in vectors["hashComponent"]:
    check("hashComponent", sha256_hex(v["input"].strip().lower().encode())[:32] == v["output"])

keys = vectors["keys"]
app_key = Ed25519PrivateKey.from_private_bytes(bytes.fromhex(keys["appSeedHex"])).public_key()
device_key = Ed25519PrivateKey.from_private_bytes(bytes.fromhex(keys["deviceSeedHex"])).public_key()
check("app public key from its seed", b64url(app_key.public_bytes_raw()) == keys["appPublicKey"])
check("kid", sha256_hex(app_key.public_bytes_raw())[:16] == keys["kid"])
check("device public key from its seed", b64url(device_key.public_bytes_raw()) == keys["devicePublicKey"])
check("device key hash", sha256_hex(device_key.public_bytes_raw()) == keys["deviceKeyHash"])

# The string the device signs, and the signature the server expects for it.
r = vectors["request"]
lines = ["CLV1-REQUEST", r["appId"], r["method"], r["path"], sha256_hex(r["body"].encode())]
signed = "\n".join(lines + [str(r["timestamp"]), r["nonce"], r["sessionId"] or "-"])
check("request string", signed == r["canonical"])
device = Ed25519PrivateKey.from_private_bytes(bytes.fromhex(keys["deviceSeedHex"]))
check("request signature", b64url(device.sign(signed.encode())) == r["signature"])

# The string the server signs for a 1.1 client: verify it with the app's public key.
s = vectors["response"]
lines = ["CLV1.1", s["kid"], str(s["status"]), s["method"], s["path"], s["requestNonce"], str(s["timestamp"])]
signed = "\n".join(lines + [sha256_hex(s["body"].encode())])
check("response string", signed == s["canonical"])
app_public = Ed25519PublicKey.from_public_bytes(from_b64url(keys["appPublicKey"]))
check("response signature", signed_by(app_public, s["signature"], signed))

# clv1.<payload>.<signature>: the app key signs "clv1.<payload>"; the payload names the device by its key hash.
o = vectors["offline"]
prefix, payload, signature = o["token"].split(".")
check("offline signature", signed_by(app_key, signature, prefix + "." + payload))
claims = json.loads(from_b64url(payload))
check("offline payload", claims == o["payload"] and claims["dev"] == keys["deviceKeyHash"])
check("offline valid", claims["nbf"] <= o["validAt"] < claims["exp"])
check("offline expired", o["expiredAt"] >= claims["exp"])
check("offline clock turned back", o["clockBackAt"] < claims["iat"])

# A release manifest: the app key signs "CLV1-MANIFEST\n" followed by the manifest text exactly as received.
m = vectors["manifest"]
check("manifest signature", signed_by(app_key, m["signature"], "CLV1-MANIFEST\n" + m["manifest"]))
manifest = json.loads(m["manifest"])
check("manifest kid", manifest["kid"] == m["kid"])
check("manifest valid", manifest["issuedAt"] <= m["validAt"] < manifest["expiresAt"] <= m["expiredAt"])

print("OK vectors")

Chạy bằng python check_vectors.py clv1.1-test-vectors.json (cần pip install cryptography); dòng cuối là OK vectors. Sửa một ký tự của bất kỳ chữ ký nào trong file, nó dừng ở phép kiểm đó.

Câu hỏi thường gặp

**Có cần gửi x-clavis-proto: 1.1 không?** Có. Không có header đó, server trả lời theo CLV1 cũ, ký trên ít trường hơn; client mới nên luôn dùng 1.1.

Đồng hồ máy khách sai thì sao? Server trả clock_skew (có ký) kèm serverTime; client tính độ lệch, cộng vào dấu thời gian và gửi lại đúng một lần, như mẫu ở bước 2.

Khi nào client nên thử lại? Không có trả lời hoặc nhận trang lỗi 5xx không ký thì chỉ gửi lại init, activate, login, heartbeat, variables, updates, logout và report. Không bao giờ gửi lại trial, register, redeem hay transfer sau khi mất câu trả lời: có thể chúng đã chạy xong. rate_limited có ký, hoặc 429, 503 không ký, thì lượt gọi nào cũng gửi lại được, sau khoảng chờ trong header Retry-After. Giữa các lần thử, chờ một khoảng tăng dần và ngẫu nhiên, và ký lại mỗi lần với nonce mới. Chỉ câu trả lời đã xác minh mới đổi được trạng thái license.

Bao lâu gửi heartbeat một lần? Câu trả lời đăng nhập và heartbeat mang trường có ký nextCheckSeconds. Chờ đúng khoảng đó, cộng thêm phần chênh tối đa một phần năm, cố định theo từng máy, để hàng nghìn bản không gọi cùng lúc.

Còn dùng thử, tài khoản, biến, cập nhật? Cùng cách ký và kiểm, khác endpoint và thân yêu cầu: bảng endpoint ở Đặc tả giao thức CLV1, các lượt gọi của từng kịch bản ở Hệ thống hoạt động thế nào.

Liên quan

Bạn cần giúp gì?

Câu hỏi thường gặp

ClavisAUH là gì?

Dịch vụ bản quyền và xác thực cho phần mềm desktop: license key, khoá thiết bị, đăng nhập người dùng và phản hồi có chữ ký. Tất cả quản lý trên một dashboard

Vì sao kẻ crack không làm giả được server?

SDK chỉ tin câu trả lời ký bằng khoá bí mật của ứng dụng. Khoá đó không bao giờ rời server của chúng tôi, nên server giả không ký được và bị từ chối

Trong file chạy của tôi có gì?

Mã ứng dụng và khoá công khai. Không có bí mật nào, vì mỗi thiết bị tự tạo cặp khoá riêng ở lần chạy đầu

Có chạy được khi offline không?

Có. Mọi gói đều có token offline có chữ ký dùng được 7 ngày, ứng dụng tự kiểm tra tại máy. Premium cho bạn chọn bao lâu. License đã thu hồi vẫn có thể chạy offline trong khoảng đó, trừ khi Chế độ chỉ online đang bật

Hỗ trợ những ngôn ngữ nào?

Hiện có JavaScript và TypeScript. Tiếp theo là C++, C# và Python, cùng một giao thức công khai

Gói Basic có miễn phí thật không?

Có. Không giới hạn thời gian, không cần thẻ. Cần nhiều hơn thì lên Premium

Liên hệ