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

Thư viện tài liệuTích hợp

SDK JavaScript

`@clavisauh/sdk` là SDK chính thức để phần mềm của bạn làm việc với Clavis Authentication Engine: kích hoạt key, đăng nhập, giữ phiên, đọc biến, nhận bản cập nhật có ký và chạy offline. Mọi yêu cầu đều được ký trên máy, mọi câu trả lời đều được xác minh trước khi bạn dùng tới.

15 phút đọc

SDK làm gì cho bạn

  • Ký từng yêu cầu bằng khoá thiết bị Ed25519, kèm nonce dùng một lần và dấu thời gian, nên một yêu cầu không thể bị gửi lại lần hai.
  • Xác minh từng câu trả lời theo giao thức CLV1.1: chữ ký phải đến từ khoá công khai của ứng dụng, gắn đúng với yêu cầu vừa gửi (mã trạng thái HTTP, endpoint, nonce) và lệch không quá 60 giây. Không endpoint nào được miễn, kể cả câu trả lời báo lỗi.
  • Tự chỉnh đồng hồ khi server trả clock_skew (câu trả lời có ký) rồi gửi lại đúng một lần.
  • Tự thử lại khi không gọi được server hoặc server bảo chờ, và chỉ khi gửi lại là an toàn.
  • Kiểm tra token offline ngay trên máy, không cần server; kiểm tra chữ ký manifest cập nhật và checksum file tải về trước khi trả cho bạn.

SDK dùng được trong Node.js, Electron hay trình duyệt, miễn là môi trường có fetch (hoặc bạn truyền fetch riêng). Gói là JavaScript ES module thuần, kèm khai báo kiểu TypeScript; các kiểu như Grant và ClavisOptions cũng được export. Phần mềm viết bằng ngôn ngữ khác có SDK riêng (Python, C#, C++, Go, Rust, Java) hoặc thư viện C, cùng giao thức và cùng cách kiểm chữ ký: xem Tải về và các bài Bắt đầu với …. Các lớp bảo vệ cần mã native như ghim chứng chỉ TLS hay khoá phần cứng (TPM, Secure Enclave) sắp có trong các SDK đó.

Cài đặt

Tải clavisauh-sdk-js-0.1.0.tgz ở trang Tải về, kiểm SHA-256, rồi cài file vào dự án:

npm install ./clavisauh-sdk-js-0.1.0.tgz

SDK chưa có trên npm. Từng bước từ cài đặt tới phát hành: Bắt đầu với Node.js và Electron.

Lấy baseUrl, App ID và khoá công khai từ dashboard:

  1. Mở ứng dụng, vào trang Tổng quan của ứng dụng. Thẻ Khoá công khai hiển thị App ID (app_…) và khoá công khai.
  2. Vào Tích hợp › Kết nối phần mềm và chọn Node.js. Bước Mã của bạn có sẵn địa chỉ API, App ID và khoá công khai. Hãy chép đúng địa chỉ API ở đây làm baseUrl.
  3. Muốn giao việc tích hợp cho AI, bấm Sao chép prompt cho AI agent ở đầu trang.

Khoá công khai chỉ dùng để xác minh, không ký được gì, nên nhúng vào phần mềm là an toàn. Đừng bao giờ nhúng một khoá bí mật nào.

Khởi tạo ClavisClient

import { ClavisClient, hashComponent } from "@clavisauh/sdk";

const clavis = new ClavisClient({
  baseUrl: "https://api.example.com",
  appId: "app_…",
  appPublicKey: "<khoá công khai của ứng dụng>",
  device: {
    fingerprint: hashComponent(machineId),
    components: { cpu: hashComponent(cpuId), disk: hashComponent(diskSerial) },
    platform: "windows",
  },
  deviceKey: await loadDeviceKey(),
  appVersion: "1.4.0",
});
Tuỳ chọnBắt buộcÝ nghĩa
baseUrlCóĐịa chỉ API; SDK nối thêm /v1/client/<endpoint>
appIdCóId ứng dụng app_…
appPublicKeyMột trong haiKhoá công khai (base64url). Câu trả lời không ký bằng khoá này bị từ chối
appPublicKeysMột trong haiDanh sách { kid?, key } mọi khoá bản build tin, dùng khi xoay khoá. Thiếu kid thì SDK tự tính từ khoá
deviceCóMô tả máy, xem bên dưới
deviceKeyKhôngCặp khoá thiết bị { secretKey, publicKey }. Bỏ trống thì SDK sinh cặp mới
appVersionKhôngPhiên bản đang chạy, dạng semver, tối đa 32 ký tự
buildHashKhôngSHA-256 của file đang chạy, đúng 64 ký tự hex chữ thường
securityKhôngmaxOfflineSec, heartbeatSec, reportTamperSignals của riêng bản build
timeoutMsKhôngSố mili giây chờ câu trả lời trước khi báo timeout, mặc định 15 000
maxRetriesKhôngSố lần thử thêm sau lần đầu khi không gọi được server hoặc server bảo chờ. Mặc định 2, 0 là tắt
fallbackUrlsKhôngCác địa chỉ khác của cùng server, thử lần lượt. An toàn vì câu trả lời chỉ được tính khi khoá của ứng dụng đã ký
fetchKhôngHàm fetch thay thế

Thiếu cả appPublicKey lẫn appPublicKeys, constructor ném Error ngay. Truyền cả hai thì SDK tin mọi khoá trong cả hai. Hàm fetch bạn truyền vào cũng được download() dùng, nên timeout quá ngắn có thể cắt ngang file cập nhật lớn.

Thông tin thiết bị và hashComponent

  • fingerprint (bắt buộc): hash ổn định đại diện cho cả máy, 16 tới 128 ký tự chữ, số, _ hoặc -.
  • components: tối đa 12 linh kiện đã băm, tên tối đa 32 ký tự, giá trị tối đa 128 ký tự. Nhờ đó server nhận ra máy cũ sau khi cài lại hệ điều hành hay thay một linh kiện.
  • name, platform: tối đa 64 ký tự, hiển thị trong dashboard.

hashComponent(value) cắt khoảng trắng hai đầu, chuyển chữ thường, băm SHA-256 rồi giữ 32 ký tự hex đầu, nên " ABC " và "abc" cho cùng một kết quả. Hãy băm mọi định danh phần cứng bằng hàm này: giá trị thô không bao giờ rời khỏi máy. Cách server so khớp máy có trong Thiết bị, HWID và chuyển máy.

Giữ khoá thiết bị qua các lần chạy

deviceKey ký từng yêu cầu, phiên gắn với nó và token offline chỉ hợp lệ với nó. Nếu mỗi lần chạy một khoá mới, máy vẫn được nhận ra nhờ fingerprint, nhưng mọi token offline đã lưu bị từ chối. Đọc cặp khoá vừa sinh qua clavis.deviceKey và cất vào keychain, DPAPI hoặc kho bí mật của hệ điều hành:

const toB64 = (b: Uint8Array) => Buffer.from(b).toString("base64");
const fromB64 = (s: string) => new Uint8Array(Buffer.from(s, "base64"));

async function loadDeviceKey() {
  const saved = await readSecret("clavis.deviceKey"); // hàm của bạn
  if (!saved) return undefined;
  const { s, p } = JSON.parse(saved) as { s: string; p: string };
  return { secretKey: fromB64(s), publicKey: fromB64(p) };
}

async function saveDeviceKey(key: { secretKey: Uint8Array; publicKey: Uint8Array }) {
  await writeSecret("clavis.deviceKey", JSON.stringify({ s: toB64(key.secretKey), p: toB64(key.publicKey) }));
}

Khoá công khai khi xoay khoá ký

Ở Bảo vệ Advanced, mục Khoá ký trong Bảo mật ứng dụng › Bảo vệ, tab Phương pháp luôn có một khối appPublicKeys dưới dòng Danh sách khoá cho bản build kế tiếp, dán vào ClavisClient, gồm mọi khoá hiện hành (hiện tại, kế tiếp, trước đó). Khi xoay khoá: bấm Chuẩn bị khoá mới, chép khối đó vào bản build, phát hành, rồi mới bấm Kích hoạt khoá mới. Bản build chỉ biết khoá cũ sẽ từ chối câu trả lời ký bằng khoá mới với mã unknown_key. Quy trình đầy đủ ở Khoá ký và xoay vòng khoá.

appVersion và buildHash

Hai giá trị này đi kèm init() và mọi lần đăng nhập. Server dùng chúng để từ chối bản thấp hơn phiên bản tối thiểu (update_required), bản thuộc một bản phát hành đã chặn (build_blocked) và, khi Kiểm tra toàn vẹn build đặt ở Chặn, bản build không khớp bản phát hành nào (unknown_build). Không gửi appVersion thì bước phiên bản tối thiểu bị bỏ qua. Xem Chế độ đăng nhập và dùng thử.

Vòng đời một phiên

SDK chỉ giữ phiên trong bộ nhớ. Mỗi lần mở phần mềm, bạn gọi lại activate(key) hoặc login(...); trên cùng một máy, server dùng lại suất cũ nên không tốn thêm suất. Trình tự thường gặp:

  1. init(): đọc cấu hình ứng dụng và áp gợi ý bảo mật.
  2. Một lời gọi đăng nhập theo chế độ của ứng dụng.
  3. heartbeat() theo lịch để giữ phiên. Phiên hết hạn sau 60 phút không có heartbeat, hoặc sau khoảng thời gian gấp ba Nhịp báo khi Session lease ngắn hạn bật. SDK không tự gửi heartbeat.
  4. logout() khi đóng phần mềm, để trả chỗ cho giới hạn phiên đồng thời.

Các phương thức

Phương thứcCần phiênTrả về
init()Không{ app, securityHints, serverTime }
activate(key)KhôngGrant
trial()KhôngGrant kèm trial: { endsAt, fallback }
register(key, username, password, email?)KhôngGrant
login(username, password)KhôngGrant
redeem(key)Có, phiên của tài khoảnGrant
heartbeat()Có{ session, license, entitlements, offlineToken }
variables()CóCác biến theo tên, { [key]: value }
transfer()Có{ ok, remaining }
checkUpdates({ channel?, platform? })CóManifest đã xác minh, hoặc null
download(file)Link còn hạnUint8Array
logout()CóKhông có gì
verifyOffline(token?)Không, chạy cục bộPayload token, hoặc null
nextCheckIn()Không, chạy cục bộSố giây cần chờ trước heartbeat kế tiếp

Gọi một phương thức cần phiên khi chưa đăng nhập, bạn nhận session_required.

init()

Trả về app với: name, version (bản mới nhất của kênh stable, hoặc Phiên bản hiện tại trong Tích hợp › Cài đặt ứng dụng khi chưa có bản phát hành), forceUpdate, downloadUrl và notice (các ô Bắt buộc cập nhật, Link tải, Thông báo cho người dùng của trang đó), loginMode (key, account hoặc account_key), trial (có dùng thử hay không), maintenance (lời nhắn khi đang bảo trì, hoặc null), minVersion và update. Khi bạn truyền appVersion, update có required: true nếu bản đang chạy phải cập nhật mới đăng nhập được, hoặc required: false kèm latest nếu chỉ là đã có bản mới hơn. SDK chỉ chuyển tiếp forceUpdate, downloadUrl và notice; hiển thị thế nào là việc của phần mềm.

init() không bắt buộc, nhưng gợi ý bảo mật của server chỉ có hiệu lực sau khi nó thành công. Hãy gọi nó đầu tiên mỗi lần khởi động.

activate, trial, register, login, redeem

Đây là năm cách mở phiên, mỗi cách thuộc một chế độ đăng nhập. Gọi sai chế độ, server trả mode_not_allowed.

  • activate(key) (chế độ License key): kích hoạt key trên máy này. Key không phân biệt hoa thường, khoảng trắng bị bỏ qua, độ dài hợp lệ từ 10 tới 80 ký tự.
  • trial() (chế độ License key): dùng thử gắn với máy, mỗi máy một lần cho mỗi ứng dụng.
  • register(key, username, password, email?) (chế độ Tài khoản và Tài khoản + license key): truyền null cho key để tạo tài khoản trên gói mặc định.
  • login(username, password): đăng nhập tài khoản đã có.
  • redeem(key) (chế độ Tài khoản + license key): nâng cấp tài khoản đang đăng nhập bằng một key, giữ nguyên tên, mật khẩu và máy.

Mọi cách đều trả về một Grant:

TrườngNội dung
sessionid, expiresAt, và heartbeatSeconds khi Session lease ngắn hạn bật
licenseid, status, state (active, expiring, grace), expiresAt (null là vĩnh viễn), maxDevices, metadata, mark (watermark người mua, khi Forensic Watermarking bật)
entitlementsDanh sách quyền tính năng của gói license
deviceid của máy trên license
variablesBiến ứng dụng dành cho phiên này
offlineTokenToken offline, trên mọi gói. Chỉ là null khi Chế độ chỉ online đang bật

state là expiring khi license sắp hết hạn trong số ngày Báo trước khi hết (ngày) của gói (mặc định 7), và grace khi đã quá hạn nhưng còn trong Ân hạn sau hạn (ngày). Dùng nó để nhắc người dùng gia hạn trước khi phần mềm dừng.

heartbeat() và variables()

heartbeat() gia hạn phiên và trả trạng thái license mới nhất. SDK cập nhật danh sách quyền tính năng theo câu trả lời, và giữ offlineToken mới nếu có. Hãy đặt lịch lần kế tiếp bằng clavis.nextCheckIn(): số giây cần chờ, lấy từ nextCheckSeconds có ký trong câu trả lời đăng nhập hoặc heartbeat gần nhất, 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.

variables() đọc lại biến ứng dụng giữa phiên, ví dụ sau khi bạn đổi giá trị trong dashboard. Biến dành cho một người dùng hoặc một license ghi đè biến chung cùng tên. Xem Biến ứng dụng.

transfer() và logout()

transfer() trả suất của máy này để người mua chuyển sang máy khác, tính vào giới hạn chuyển máy của gói. Kết quả có remaining, số lượt còn lại. Bị từ chối, bạn nhận HTTP 429 với transfer_limited hoặc transfer_not_allowed. Sau khi thành công, SDK bỏ phiên.

logout() kết thúc phiên trên server và trong SDK.

checkUpdates() và download()

checkUpdates() hỏi bản phát hành mới nhất của kênh mà gói license cho phép. Kênh bạn xin không nằm trong danh sách được phép thì server dùng kênh đầu tiên được phép (mặc định stable). platform (ví dụ win-x64) lọc file theo nền tảng, file any luôn có mặt. Không có bản phát hành nào, kết quả là null.

SDK chỉ trả manifest khi nó được ký bằng một khoá tin cậy của chính ứng dụng và còn hạn; ngược lại ném ClavisVerificationError với mã invalid_manifest. Manifest sống 10 phút, link tải trong đó sống 5 phút và gắn với phiên đã xin. SDK không so phiên bản giúp bạn: hãy so manifest.version với bản đang chạy.

download(file) tải một file của manifest về bộ nhớ và chỉ trả bytes khi SHA-256 khớp với manifest. Chi tiết ở Bản phát hành có ký và cập nhật.

const manifest = await clavis.checkUpdates({ platform: "win-x64" });
if (manifest && manifest.version !== currentVersion) {
  const file = manifest.files[0];
  if (file) await installUpdate(file.fileName, await clavis.download(file));
}

verifyOffline()

verifyOffline(token?) kiểm tra token offline ngay trên máy và trả payload (app, lic, dev, lexp, iat, exp, có thể kèm nbf, kid, wm) hoặc null. Không truyền token thì SDK dùng token mới nhất nó giữ trong bộ nhớ. Token bị từ chối khi sai khoá, sai máy, hết hạn, đồng hồ có dấu hiệu bị lùi, vượt maxOfflineSec, hoặc sau khi license đã bị báo thu hồi. Toàn bộ quy tắc ở License offline.

Token offline không mang danh sách quyền tính năng. Trong một tiến trình chưa nhận được câu trả lời online nào, has() luôn trả false, nên hãy quyết định trước bản offline được mở những tính năng nào.

Thuộc tính trạng thái

Thuộc tínhÝ nghĩa
has(name)true khi câu trả lời đã xác minh gần nhất có quyền tính năng name
entitlementsBản sao danh sách quyền tính năng hiện có
sessionId phiên hiện tại, hoặc null
securityCấu hình bảo mật đang áp dụng
revokedtrue khi một câu trả lời đã xác minh mang license_revoked hoặc license_suspended
deviceKeyCặp khoá thiết bị đang dùng

security gồm heartbeatSec, maxOfflineSec, reportTamperSignals, pinning, spkiPins và requireHardwareKey. Giá trị bạn đặt trong tuỳ chọn security chỉ có thể bị server siết thêm, không bao giờ nới ra: lấy số nhỏ hơn cho heartbeatSec và maxOfflineSec, và một cờ đã bật thì giữ bật. Vì vậy một server giả gửi gợi ý lỏng hơn cũng không thay đổi được gì. pinning, spkiPins và requireHardwareKey dành cho SDK native; SDK JavaScript không ghim được TLS.

revoked chỉ nằm trong bộ nhớ: khi thấy nó bật, hãy xoá token offline đã lưu.

Xử lý lỗi

Mọi từ chối từ server và mọi lỗi xác minh đều là ClavisError, với status (HTTP), code và message. ClavisVerificationError là lớp con của nó: câu trả lời không chứng minh được là đến từ server của ứng dụng cho đúng yêu cầu này, nên hãy coi như không có câu trả lời. Kiểm tra lớp con trước.

Mã xác minhKhi nào
unsigned_responseCâu trả lời thiếu chữ ký, thiếu kid hoặc thiếu dấu thời gian
unknown_keyKý bằng một khoá mà bản build không tin
invalid_response_signatureChữ ký không khớp: khoá giả, câu trả lời của yêu cầu khác, mã trạng thái bị đổi, body bị sửa
stale_responseThời điểm ký lệch quá 60 giây so với đồng hồ đã chỉnh
invalid_manifestManifest cập nhật không hợp lệ (chỉ từ checkUpdates())

Với bốn mã đầu, ngay sau câu trả lời hợp lệ kế tiếp, SDK tự gửi nền một báo cáo kèm số lần thất bại. Báo cáo này hiện thành tín hiệu Chữ ký giả mạo trong Bảo mật ứng dụng › Bảo vệ.

SDK còn tự tạo vài mã, với status là 0 khi không nhận được câu trả lời: network_error (mất mạng, DNS, tường lửa, TLS), timeout (quá timeoutMs, mặc định 15 giây) và invalid_response (câu trả lời không phải JSON, thường là trang của proxy). Đó là lúc thích hợp để chuyển sang verifyOffline(). Khi tải file có thêm download_failed (status là mã HTTP của lần tải) và checksum_mismatch.

Mọi mã, từ server hay từ SDK, nằm trong một bảng chung cho mọi SDK chính thức: Mã lỗi. Kèm theo là hai hàm tiện:

  • err.userMessage("vi") (hoặc "en") trả một câu bạn có thể hiện cho người dùng phần mềm, cho mọi mã.
  • err.retryable cho biết thử lại sau, không đổi gì, có thể thành công không (mạng, giới hạn tần suất, bảo trì).
  • err.retryAfter là số giây server bảo chờ, khi server có bảo.

Muốn xem SDK gửi và nhận gì, tạo client với debug: true (hoặc một hàm nhận từng dòng). Key chỉ hiện 4 ký tự đầu và cuối, mật khẩu không bao giờ xuất hiện trong nhật ký đó.

Mã server hay gặp: invalid_credentials, too_many_attempts, device_limit, session_expired, license_expired, maintenance, update_required. SDK này nói CLV1.1, nên key không tồn tại, bị khoá, thu hồi hay hết hạn đều trả chung invalid_credentials, trừ khi chính máy này đang giữ suất trên license. Ý nghĩa và cách xử lý từng mã ở Mã lỗi.

Tự thử lại

SDK tự thử lại, tối đa maxRetries lần, với thời gian chờ tăng dần và ngẫu nhiên. Không lần thử nào bắt đầu quá 20 giây sau lần đầu.

  • network_error, timeout hoặc trang lỗi 5xx không ký: chỉ với các lượt gọi an toàn khi lặp: init, activate, login, heartbeat, variables, checkUpdates, logout và báo cáo. trial, register, redeem và transfer không bao giờ được gửi lại sau khi mất câu trả lời, vì có thể chúng đã chạy xong.
  • rate_limited có ký, hoặc 429, 503 không ký: với mọi lượt gọi, theo Retry-After tối đa 30 giây. Phải chờ lâu hơn thì SDK không thử lại: bạn nhận lỗi, số giây nằm ở err.retryAfter.

Mỗi lần thử được ký lại với nonce mới, và chuyển sang địa chỉ kế tiếp nếu bạn đặt fallbackUrls. Chỉ chữ ký đã xác minh mới đổi được trạng thái license, nên lần thử hỏng không đổi gì. Bạn chỉ nhận lỗi sau lần thử cuối.

Ví dụ hoàn chỉnh

import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { ClavisClient, ClavisError, ClavisVerificationError, hashComponent } from "@clavisauh/sdk";

const deviceKey = await loadDeviceKey();
const clavis = new ClavisClient({
  baseUrl: "https://api.example.com",
  appId: "app_…",
  appPublicKey: "<khoá công khai của ứng dụng>",
  device: {
    fingerprint: hashComponent(machineId),
    components: { cpu: hashComponent(cpuId), disk: hashComponent(diskSerial), board: hashComponent(boardSerial) },
    platform: process.platform,
  },
  deviceKey,
  appVersion: "1.4.0",
  // exePath: file thực thi của bạn
  buildHash: createHash("sha256").update(await readFile(exePath)).digest("hex"),
  fetch: (url, init) => fetch(url, { ...init, signal: AbortSignal.timeout(15_000) }),
});
if (!deviceKey) await saveDeviceKey(clavis.deviceKey);

export async function start(licenseKey: string) {
  try {
    const { app } = await clavis.init();
    if (app.maintenance) return showMessage(app.maintenance.message);
    if (app.update?.required) return showUpdate(app.update.minVersion);

    const grant = await clavis.activate(licenseKey);
    if (grant.offlineToken) await saveToken(grant.offlineToken);
    if (grant.license.state !== "active") warnRenewal(grant.license.expiresAt);
  } catch (e) {
    if (e instanceof ClavisVerificationError) return showMessage("Không xác minh được máy chủ");
    // status 0 nghĩa là không nhận được câu trả lời: network_error hoặc timeout
    if (e instanceof ClavisError && e.status !== 0) {
      if (clavis.revoked) await deleteToken();
      return showMessage(e.code);
    }
    // Lỗi mạng: chạy offline bằng token đã lưu, tới hạn exp của nó
    const offline = clavis.verifyOffline(await loadToken());
    return offline ? runOffline(offline.exp) : showMessage("Cần kết nối mạng");
  }

  if (clavis.has("export_pdf")) enableExport();

  const every = clavis.nextCheckIn() * 1000;
  setInterval(async () => {
    try {
      const hb = await clavis.heartbeat();
      if (hb.offlineToken) await saveToken(hb.offlineToken);
    } catch (e) {
      // Phiên bị kết thúc hoặc hết hạn: đăng nhập lại trên cùng máy
      if (e instanceof ClavisError && e.code === "session_expired") {
        await clavis.activate(licenseKey).catch(() => lockApp());
      }
    }
  }, every);

  onQuit(() => clavis.logout());
}

loadDeviceKey, saveDeviceKey, saveToken, loadToken, deleteToken, runOffline, các định danh phần cứng và các hàm giao diện là phần bạn tự viết cho nền tảng của mình. Key người dùng đã nhập cần được giữ lại (ví dụ trong keychain) để lần khởi động sau gọi lại activate.

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ệ