Tài liệu tích hợp · UTF-8

Hướng dẫn tích hợp Shield Cloud

Tài liệu dành cho mọi đội phát triển — dù bạn dùng JavaScript, Python, PHP, Go, Ruby, Java, C# hay chỉ cần cURL. Shield tự nhận domain sau khi bạn thêm website trong portal; không bắt buộc cấu hình biến môi trường site key.

Hướng dẫn khách hàng đầy đủ (PDF-style / Markdown)

Quy trình đăng ký → embed → verify (Node/PHP) → score/risk → Mode C. File: docs/HUONG-DAN-KHACH-HANG.md

  1. Portal → đăng ký → thêm domain → copy embed (challenge.js + data-mode=auto)
  2. Backend: Node @kimidevvn/shield-cloud-node hoặc PHP cURL POST /v1/verify + sk_live
  3. Kiểm tra cookie shield-token; (khuyến nghị) reject nếu token.score risk cao
  4. Gọi API Shield qua ShieldSDK.fetch (token one-time / rotate)
  5. (Tuỳ chọn) Mode C Worker / custom domain — không bật sẵn trên localhost

Bắt đầu nhanh (3 bước)

1
1

Đăng ký tenant

Vào Portal, tạo tài khoản. Bạn nhận trial Pro 14 ngày ngay sau khi đăng ký.

2
2

Thêm domain website

Trong dashboard, thêm domain chính của hệ thống. Shield tự động hỗ trợ localhost.

3
3

Copy mã nhúng SDK

Chọn site, copy mã embed và dán vào <head> của web. Hoàn tất!

3 cách tích hợp (A / B / C)

Chọn theo độ sâu bảo vệ. MVP cho thuê = A + B; C là full WAF-as-a-service.

A. SDK-only

Embed script auto hoặc gọi ShieldSDK.getToken() trước login/API. Phù hợp SPA, form.

B. Middleware API

Shield gate /v1/login và /v1/encrypt — thiếu shield-token → 403 + x-shield-action: challenge.

Mode C — Cloudflare Worker / Nginx

Chặn cả site ở edge: cookie shield-token hợp lệ thì proxy origin; không có thì hiện interstitial challenge rồi reload. Toàn bộ code mẫu nằm ngay bên dưới — không cần clone repo.

Risk score — bảng thiết lập code

token.score = điểm rủi ro (càng cao càng nghi bot), nhúng lúc PoW. Backend đọc sau /v1/verify: if (score > maxRisk) reject. Chi tiết: docs/HUONG-DAN-KHACH-HANG.md §3.1.

1) Khuyến nghị maxRisk trên app của bạn

Loại APImaxRiskReject khiGhi chú
Public / browse60score > 60Lỏng
Login / Register40score > 40Mặc định khuyến nghị
OTP / reset password35score > 35Hơi chặt
Checkout / thanh toán25score > 25Chặt
Admin / hủy tiền15score > 15Rất chặt

2) Điểm cộng (signals lúc mint)

Tín hiệuĐiểm
navigator.webdriver+80
Headless UA+50
document webdriver+40
Automation flags+20 mỗi (max +60)
Giải PoW < 5ms+25
No plugins (desktop)+20
No languages+15
Chrome runtime missing+15
Canvas fail+10

3) Ngưỡng Shield khi cấp token (theo gói)

GóiCaptcha từBlock từCaptcha fallback
Starter5070Không (block cứng)
Pro45100
Business40120
// Login — khuyến nghị
if (visitor.token.score > 40) {
  return res.status(403).json({ error: "high_risk_score", score: visitor.token.score });
}

// PHP
// define('SHIELD_MAX_RISK_AUTH', 40);
// shield_require(SHIELD_MAX_RISK_AUTH);

Thêm domain — Shield tự nhận

Đây là cách mặc định và khuyến nghị. Khi bạn thêm website trong portal, Shield sẽ:

  • Tự tìm site key theo hostname người đang truy cập
  • Khớp cả www và không www
  • Kiểm tra allowed_origins đã cấu hình sẵn khi tạo site

Không cần đặt biến môi trường ở Frontend

Chỉ cần thêm đúng domain trong portal. API trả về mode: auto khi tìm thấy site. Chỉ đặt SHIELD_SELF_SITE_KEY nếu bạn muốn ghi đè thủ công.

Nhúng SDK (Trình duyệt)

Trên website/web app, dán một dòng script. Production dùng challenge.js (obfuscated); dev có thể dùng shield-sdk.js.

HTML
<!-- Production: challenge.js (obfuscated). Dev: shield-sdk.js -->
<script src="https://shield-cloud.vercel.app/v1/challenge.js"
        data-site-key="pk_live_SITE_KEY_CUA_BAN"
        data-api="https://shield-cloud.vercel.app"
        data-mode="auto"></script>
<!-- data-mode: auto | manual | off
     data-difficulty: optional leading-zero bits (min = plan)
     Custom domain: data-api="https://token.yourdomain.com"
     Flow: PoW interstitial → cookie shield-token → gated APIs -->

Sau khi load, window.ShieldSDK sẵn sàng. data-mode=auto sẽ chạy interstitial PoW và set cookie shield-token trước khi site hiển thị.

JS Challenge (PoW)

Interstitial kiểu WAF: thu thập signals, giải SHA-256 hashcash hoặc bandwidth, set cookie shield-token.

data-mode

  • auto — interstitial mỗi page (mặc định cho thuê)
  • manual — gọi getToken() / runChallenge()
  • off — tắt challenge

Loại challenge

  • sha256 — hashcash (mọi plan)
  • bandwidth — encode payload (Pro/Business)
  • captcha — fallback khi signals cao (Pro/Business)

Cấu hình mode, difficulty, types trong Portal → chọn site → JS Challenge.

Events: shield:ready, shield:error. Cookie: shield-token (TTL ~4 ngày).

Plan & difficulty

Difficulty PoW và tính năng challenge theo gói:

  • Starter — PoW bits 8, SHA-256 only, block sớm khi signals cao
  • Pro — bits 10, bandwidth + captcha fallback
  • Business — bits 12, bandwidth + captcha + hạn mức cao
PoW API / SDK
// 1) Issue
const issue = await fetch("https://shield-cloud.vercel.app/v1/pow/issue", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Shield-Site-Key": "pk_live_SITE_KEY_CUA_BAN",
  },
  credentials: "include",
  body: JSON.stringify({ device_id: crypto.randomUUID() }),
});
// 2) Client solves SHA-256 or bandwidth (SDK handles this)
// 3) Verify → shield-token cookie
// Or simply:
const token = await window.ShieldSDK.getToken({ baseUrl: "https://shield-cloud.vercel.app" });
// 4) Call protected API
await window.ShieldSDK.fetch("https://shield-cloud.vercel.app/v1/login", { method: "POST", body: "..." });

Bảo vệ Form Login

Luồng chuẩn: có shield-token (auto challenge) rồi mới init session + encrypt form.

  1. Đảm bảo script data-mode=auto đã pass (event shield:ready) hoặc gọi getToken().
  2. Gọi trackFormFocus() + initSession(api).
  3. Nếu needs_challenge (captcha layer-2) — gọi POST /v1/challenge.
  4. Gắn X-Shield-Token và dùng encryptedFetch gửi login.
JavaScript (Client-side)
// Trình duyệt — dùng Shield SDK (bắt buộc cho web)
// data-mode="auto" tự chạy interstitial PoW.
// Gated APIs: token one-time (jti) — luôn đọc lại getStoredToken() sau init.
async function shieldLogin(email, password) {
  const api = "https://shield-cloud.vercel.app";
  const siteKey = "pk_live_SITE_KEY_CUA_BAN";

  await window.ShieldSDK.getToken({ baseUrl: api });
  window.ShieldSDK.trackFormFocus();

  // initSession is gated → burns jti and rotates shield-token
  const init = await window.ShieldSDK.initSession(api);
  if (!init.ok) throw new Error("Shield init thất bại");

  // NEVER reuse token string from before init
  const shieldToken = window.ShieldSDK.getStoredToken();
  const extra = shieldToken ? { "X-Shield-Token": shieldToken } : {};

  if (init.needs_challenge) {
    const ch = await fetch(api + "/v1/challenge", {
      method: "POST",
      headers: {
        "Content-Type": "application/json; charset=utf-8",
        "X-Shield-Site-Key": siteKey,
      },
      credentials: "include",
      body: JSON.stringify({
        session_id: init.session_id,
        captcha_token: "human-verified",
      }),
    });
    const data = await ch.json();
    if (!ch.ok) throw new Error("Challenge thất bại");
    extra["X-Behavior-Token"] = data.behavior_token;
  }

  const res = await window.ShieldSDK.encryptedFetch(
    api,
    "/login",
    { username: email, password, time: Math.floor(Date.now() / 1000) },
    String(init.session_id),
    extra
  );
  return res.json();
}

// Prefer ShieldSDK.fetch for your own APIs (queue + rotate + retry)
// await window.ShieldSDK.fetch("/api/private", { credentials: "include" });

Package npm @kimidevvn/shield-cloud-node

SDK server publish riêng: verify shield-token bằng secret_key qua POST /v1/verify. Hỗ trợ Express, Next.js, Edge, Worker.

Cài đặt

shell
npm install @kimidevvn/shield-cloud-node

Biến môi trường: SHIELD_SECRET_KEY (sk_live_...) và SHIELD_API_BASE (origin Shield hoặc token.customer.com).

TypeScript / Node
// npm install @kimidevvn/shield-cloud-node
import {
  ShieldClient,
  shieldMiddleware,
  assertShield,
  ShieldAuthError,
} from "@kimidevvn/shield-cloud-node";

// Env: SHIELD_SECRET_KEY=sk_live_...  SHIELD_API_BASE=https://shield-cloud.vercel.app
const opts = {
  secretKey: process.env.SHIELD_SECRET_KEY,
  apiBase: process.env.SHIELD_API_BASE || "https://shield-cloud.vercel.app",
};

// --- Express ---
// app.use(shieldMiddleware(opts));
// app.get("/api/me", (req, res) => res.json({ visitor: req.shield }));

// --- Next.js App Router ---
export async function GET(req) {
  try {
    const visitor = await assertShield(req, opts);
    return Response.json({
      site: visitor.site,
      sid: visitor.token.sid,
      score: visitor.token.score,
    });
  } catch (e) {
    if (e instanceof ShieldAuthError) return e.toResponse();
    throw e;
  }
}

// --- One-shot ---
const shield = new ShieldClient(opts);
const result = await shield.verify({ token: cookieOrHeader });
// result.token.score = risk (higher = more bot-like); optional: reject if > 40
if (!result.ok) throw new Error(result.error);

Custom domain (token.customer.com)

Gắn subdomain API riêng cho site: CNAME về host Shield Cloud. Embed dùng data-api=https://token.yourdomain.com.

  • Portal → site → JS Challenge → Custom token domain
  • DNS: token.yourdomain.com CNAME → host Shield (cname_target trong API)
  • API resolve site theo Host — có thể bỏ X-Shield-Site-Key khi gọi qua custom domain
DNS + embed
# DNS
token.yourdomain.com.  CNAME  shield-cloud.vercel.app.

# Portal → site → JS Challenge → Custom token domain = token.yourdomain.com

# Embed (portal auto-generates this)
<script src="https://token.yourdomain.com/v1/challenge.js"
        data-site-key="pk_live_SITE_KEY_CUA_BAN"
        data-api="https://token.yourdomain.com"
        data-mode="auto"></script>

# API resolves site by Host header on token.yourdomain.com
# GET/PATCH /saas/sites/:id → { challenge.tokenDomain, api_base, cname_target }

Mode C — Cloudflare Worker / Nginx

Chặn cả site ở edge: cookie shield-token hợp lệ thì proxy origin; không có thì hiện interstitial challenge rồi reload. Toàn bộ code mẫu nằm ngay bên dưới — không cần clone repo.

Bước: (1) Tạo Worker / sidecar với secret sk_live + pk_live + API base + ORIGIN. (2) Gắn route domain. (3) Origin chỉ nhận traffic đã qua gate.

Secrets cần chuẩn bị

  • SHIELD_SECRET_KEY — sk_live_... (portal, không public)
  • SHIELD_SITE_KEY — pk_live_... (embed interstitial)
  • SHIELD_API_BASE https://shield-cloud.vercel.app
  • ORIGIN — origin thật, vd. https://origin.yourdomain.com

Cloudflare Worker (copy & deploy)

Tạo file shield-gate.js, dán code, set secrets, deploy, gắn route domain.

shield-gate.js (Cloudflare Worker)
// shield-gate.js — Cloudflare Worker (Mode C)
// Secrets: wrangler secret put SHIELD_SECRET_KEY | SHIELD_SITE_KEY | SHIELD_API_BASE | ORIGIN
// SHIELD_API_BASE example: https://shield-cloud.vercel.app
// ORIGIN example: https://origin.yourdomain.com
// Deploy: npx wrangler deploy  →  attach route *yourdomain.com/*
// Flow: valid cookie shield-token → proxy ORIGIN; else interstitial → reload

const COOKIE = "shield-token";

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const token = readCookie(request, COOKIE);
    if (token && (await verifyToken(env, token))) {
      return proxyOrigin(request, env);
    }
    const accept = request.headers.get("Accept") || "";
    if (request.method === "GET" && accept.includes("text/html")) {
      return challengePage(env, url);
    }
    return new Response(
      JSON.stringify({ ok: false, code: 1124, error: "shield_token_required" }),
      {
        status: 403,
        headers: { "Content-Type": "application/json", "x-shield-action": "challenge" },
      }
    );
  },
};

function readCookie(request, name) {
  const raw = request.headers.get("Cookie") || "";
  const parts = ("; " + raw).split("; " + name + "=");
  if (parts.length < 2) return null;
  return decodeURIComponent(parts.pop().split(";").shift() || "");
}

async function verifyToken(env, token) {
  const api = String(env.SHIELD_API_BASE || "").replace(/\/$/, "");
  const secret = env.SHIELD_SECRET_KEY || "";
  if (!api || !secret) return false;
  try {
    const res = await fetch(api + "/v1/verify", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ secret_key: secret, token }),
    });
    if (!res.ok) return false;
    const data = await res.json();
    return Boolean(data.ok && data.valid);
  } catch {
    return false;
  }
}

async function proxyOrigin(request, env) {
  const originBase = String(env.ORIGIN || "").replace(/\/$/, "");
  if (!originBase) return new Response('ORIGIN secret not set', { status: 500 });
  const url = new URL(request.url);
  const target = new URL(url.pathname + url.search, originBase);
  const headers = new Headers(request.headers);
  headers.set("X-Forwarded-Host", url.host);
  headers.set("X-Shield-Gate", "1");
  return fetch(new Request(target, {
    method: request.method,
    headers,
    body: request.method === "GET" || request.method === "HEAD" ? null : request.body,
    redirect: "manual",
  }));
}

function challengePage(env, url) {
  const api = String(env.SHIELD_API_BASE || "").replace(/\/$/, "");
  const siteKey = env.SHIELD_SITE_KEY || "";
  const next = url.pathname + url.search;
  const html = `<!DOCTYPE html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Security Check</title>
<style>body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;
background:#060a12;color:#e8eef7;font-family:system-ui,sans-serif}</style></head><body>
<p>Shield Security Check…</p>
<script src="${api}/v1/challenge.js" data-site-key="${siteKey}" data-api="${api}" data-mode="auto"></script>
<script>document.addEventListener("shield:ready",function(){location.replace(${JSON.stringify(next)});});</script>
</body></html>`;
  return new Response(html, {
    headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" },
  });
}
wrangler.toml + deploy
# wrangler.toml (tối giản)
name = "shield-gate"
main = "shield-gate.js"
compatibility_date = "2024-11-01"

# Sau đó:
# npx wrangler secret put SHIELD_SECRET_KEY
# npx wrangler secret put SHIELD_SITE_KEY
# npx wrangler secret put SHIELD_API_BASE
# npx wrangler secret put ORIGIN
# npx wrangler deploy

Nginx + Node auth sidecar (copy & chạy)

Chạy sidecar Node cạnh Nginx; dùng auth_request. Code đầy đủ bên dưới.

auth-sidecar.mjs + nginx fragment
# === auth-sidecar.mjs (Node 18+) — chạy cạnh Nginx ===
# env:
#   SHIELD_SECRET_KEY=sk_live_...
#   SHIELD_API_BASE=https://shield-cloud.vercel.app
#   PORT=9090

import http from "node:http";

const PORT = Number(process.env.PORT || 9090);
const API = String(process.env.SHIELD_API_BASE || "").replace(/\/$/, "");
const SECRET = process.env.SHIELD_SECRET_KEY || "";

function readCookie(header, name) {
  if (!header) return null;
  const parts = ("; " + header).split("; " + name + "=");
  if (parts.length < 2) return null;
  return decodeURIComponent(parts.pop().split(";").shift() || "");
}

async function verify(token) {
  if (!token || !API || !SECRET) return false;
  const res = await fetch(API + "/v1/verify", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ secret_key: SECRET, token }),
  });
  if (!res.ok) return false;
  const data = await res.json();
  return Boolean(data.ok && data.valid);
}

http.createServer(async (req, res) => {
  if (!req.url || !req.url.startsWith("/auth")) {
    res.writeHead(404);
    return res.end("not found");
  }
  const token = req.headers["x-shield-token"] || readCookie(req.headers.cookie, "shield-token");
  const ok = await verify(String(token || ""));
  res.writeHead(ok ? 200 : 401, {
    "x-shield-action": ok ? "allow" : "challenge",
  });
  res.end(ok ? "ok" : "challenge");
}).listen(PORT);

# Chạy: node auth-sidecar.mjs

# === nginx (fragment) ===
# location = /__shield_auth {
#   internal;
#   proxy_pass_request_body off;
#   proxy_set_header Content-Length "";
#   proxy_set_header Cookie $http_cookie;
#   proxy_pass http://127.0.0.1:9090/auth;
# }
# location / {
#   auth_request /__shield_auth;
#   error_page 401 403 = @shield_challenge;
#   proxy_pass http://origin_upstream;
# }
# location @shield_challenge {
#   default_type text/html;
#   # interstitial: script src=https://shield-cloud.vercel.app/v1/challenge.js data-site-key=pk_live_XXX data-api=https://shield-cloud.vercel.app data-mode=auto
#   # on shield:ready → location.reload()
# }

Tích hợp Backend đa ngôn ngữ

Kiến trúc hệ thống

  • Frontend: nhúng challenge.js — PoW + fingerprint + cookie shield-token.
  • Backend: npm @kimidevvn/shield-cloud-node hoặc POST /v1/verify với secret_key + token.

Xác thực Secret Key (Server)

Dùng sk_live_... + shield-token trên server. Không bao giờ đưa secret ra frontend. Khuyến nghị: npm i @kimidevvn/shield-cloud-node.

POST /v1/verify
// npm install @kimidevvn/shield-cloud-node
import { ShieldClient, assertShield, shieldMiddleware } from "@kimidevvn/shield-cloud-node";

const shield = new ShieldClient({
  secretKey: process.env.SHIELD_SECRET_KEY, // sk_live_...
  apiBase: process.env.SHIELD_API_BASE || "https://shield-cloud.vercel.app",
});

// Express
// app.use(shieldMiddleware({ secretKey, apiBase }));

// Next.js Route Handler / Edge
export async function GET(req) {
  const visitor = await assertShield(req, {
    secretKey: process.env.SHIELD_SECRET_KEY,
    apiBase: process.env.SHIELD_API_BASE || "https://shield-cloud.vercel.app",
  });
  return Response.json({ site: visitor.site, sid: visitor.token.sid });
}

// Manual
const result = await shield.verifyNodeRequest(req);
if (!result.ok) throw new Error(result.error);

Lấy thông tin Tenant

Dùng Bearer Token để tự động hóa usage data, quản lý sites từ server của bạn.

GET /saas/me
curl "https://shield-cloud.vercel.app/saas/me" \
  -H "Authorization: Bearer tn_API_TOKEN_CUA_BAN" \
  -H "Accept: application/json; charset=utf-8"

Captcha layer-2 (Risk cao)

Sau PoW, nếu signals nghi ngờ bot (Pro/Business): captcha fallback. Starter có thể block sớm. Còn Turnstile/demo captcha khi needs_challenge:

  1. Hiển thị captcha (Turnstile hoặc demo token).
  2. Gửi captcha_token lên POST /v1/challenge hoặc /v1/pow/captcha (pending_grant).
  3. Nhận behavior_token / shield-token và gắn header request tiếp theo.
POST /v1/challenge
curl -X POST "https://shield-cloud.vercel.app/v1/challenge" \
  -H "Content-Type: application/json; charset=utf-8" \
  -H "X-Shield-Site-Key: pk_live_SITE_KEY_CUA_BAN" \
  -d '{"session_id":"SESSION_ID","captcha_token":"human-verified"}'

Phân biệt Site Key và Secret Key

Site Key

pk_live_...

Frontend/Trình duyệt (pk_live_...). An toàn khi public trên HTML.

Secret Key

sk_live_...

Backend only (sk_live_...). Verify token, Worker, reverse-proxy. Không đưa ra client.

API Reference

JSON UTF-8. Content-Type: application/json. Header site: X-Shield-Site-Key.

EndpointDescription
POST /v1/pow/issueCấp PoW challenge (sha256 / bandwidth)
POST /v1/pow/verifyVerify solution + signals → shield-token
POST /v1/pow/captchaCaptcha fallback (pending_grant) → token
POST /v1/pow/statusKiểm tra shield-token còn hạn
POST /v1/initSession + risk; có thể kèm challenge package
POST /v1/encryptMã hóa E2E payload (cần shield-token)
POST /v1/loginDemo login được gate (cần shield-token)
POST /v1/challengeCaptcha layer-2 → behavior_token
POST /v1/verifyServer verify: secret_key + token
GET /v1/challenge.jsSDK obfuscated (production)
GET /v1/shield-sdk.jsSDK source (dev/debug)
GET/PATCH /saas/sites/:idSite + JS Challenge config + tokenDomain
GET /saas/meTenant + usage
GET /api/shield/configAuto config theo domain

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

Q: Shield chưa báo Active trên portal?

Thêm đúng domain (localhost cho dev). Script load xong → badge Active.

Q: Lỗi origin_not_allowed?

Domain hiện tại không nằm trong allowed_origins của site. Kiểm tra portal.

Q: 403 shield_token_required / x-shield-action: challenge?

API login/encrypt yêu cầu cookie hoặc header shield-token. Dùng data-mode=auto hoặc getToken() trước khi gọi API.

Q: token_replay / nonce_replay là gì?

API Shield gated dùng token one-time (jti) + X-Nonce một lần. Copy curl chỉ dùng được 1 lần. Browser: dùng ShieldSDK.fetch — SDK tự rotate token từ header X-Shield-Token.

Q: token.score nghĩa là gì? Có chặn bot theo điểm không?

score = risk (càng cao càng nghi bot). webdriver +80, headless +50, solve quá nhanh +25… Ngưỡng app gợi ý: public 60 · login 40 · payment 25 · admin 15. Shield Starter block mint ≥70. Bảng đầy đủ: docs/HUONG-DAN-KHACH-HANG.md §3.1.

Q: Nên set maxRisk bao nhiêu trong code?

Login/register: if (score > 40) reject. Checkout: > 25. Admin/hủy tiền: > 15. Public: > 60 hoặc bỏ qua. Đừng đặt maxRisk = 0 (user sạch vẫn có thể 10–20).

Q: getToken() public — attacker có mint token giả không?

Không. getToken chỉ chạy challenge; server ký HMAC. Deobfuscate challenge.js không lộ secret. Playwright full browser vẫn lấy token thật (trả cost PoW) — cần rate limit + captcha + auth app.

Q: Tích hợp PHP thế nào (không npm)?

Embed challenge.js + POST /v1/verify bằng cURL với sk_live và cookie shield-token. Helper đầy đủ trong docs/HUONG-DAN-KHACH-HANG.md §4.5.

Q: Localhost đã bật Mode C chưa?

Chưa. Local = SelfWaf (Mode A interstitial) + middleware gate API. Mode C cần Cloudflare Worker hoặc Nginx sidecar (integrations/).

Q: Khác nhau challenge.js và shield-sdk.js?

challenge.js là bản obfuscate (production). shield-sdk.js là source dễ debug. API giống nhau (window.ShieldSDK).

Q: Publish @kimidevvn/shield-cloud-node thế nào?

npm login (granular token + 2FA), rồi npm run publish:node-sdk. Khách: npm i @kimidevvn/shield-cloud-node.

Q: Mode C Worker cần secret nào?

SHIELD_SECRET_KEY, SHIELD_SITE_KEY, SHIELD_API_BASE, ORIGIN — xem integrations/cloudflare-worker.

Q: Custom domain token.* không resolve?

CNAME đúng cname_target; lưu tokenDomain trong Portal; đợi DNS. Gọi API qua https://token.domain.com.

Q: Vượt quota request?

Request mới bị từ chối. Xem Usage dashboard và nâng Pro/Business.

Sẵn sàng tích hợp?

Portal → thêm domain → cấu hình JS Challenge → copy embed (hoặc deploy Worker Mode C).