Cloud WAF/Docs 中文 EN
Cloud WAF is an enterprise Web protection management platform. Go + Gin REST API backend, Vue 3 SPA frontend, with the zcloud CLI for automation.

This document belongs to Cloud WAF — Enterprise Web Protection Management Platform
CLI tool: zcloud · 5 modules: guard / sys / analytics / cli_release / auth
Full API index: /api/openapi.json · Sitemap: /sitemap.xml · AI Quick Read: /llms.txt


API Documentation

Production-grade REST API documentation · 57 endpoints · 80+ chart-key data interfaces · Dual-channel authentication
Audience: integration engineers, SREs, SaaS integrators, AI agents
Reading order: §0 Conventions§1 Authentication → jump to your business module


Table of Contents

Must Read

Public Endpoints (no auth)

Business Endpoints (dual-channel auth)

Plan Catalog

Node Operations

Analytics (74 chart-keys · single-shape contract)

Reference

Full OpenAPI: /api/openapi.json · AI Quick Read: /llms.txt · Error Codes: /docs/errors · CLI: /docs/cli · Permissions: /docs/permissions


§0 Overall Conventions

The Cloud WAF backend is a Gin-based RESTful service.

0.1 Basic Protocol

Item Value
Protocol HTTPS (recommended) / HTTP
Data format Requests and responses are application/json (exceptions: POST /api/guard/certs is still JSON with PEM as string fields; export/download endpoints return text/csv, application/pdf, etc.)
Charset UTF-8
Path prefix All business APIs live under /api/
Time format Unix milliseconds (int64), not ISO strings
i18n Accept-Language: zh-CN or en-US localizes error messages and permission names

0.2 Unified Response Envelope

Every JSON response follows this three-field shape:

{
  "code": 0,
  "message": "ok",
  "data": { /* business payload, type depends on the endpoint */ }
}
Field Type Meaning
code number 0 = success; non-zero = business error code
message string Error description (honors Accept-Language)
data any Payload; list endpoints use { list, total, page, size }

Exceptions: file-export endpoints (POST /api/analytics/overview/export, GET /api/analytics/reports/:id/download, POST /api/analytics/logs/export) return raw binary or CSV/JSON streams, not wrapped in the envelope.

0.3 HTTP Status Codes

Code When emitted
200 Business success (still inspect code)
201 Resource created
400 Bad request (missing param, invalid format, out-of-range)
401 Not logged in / session expired / API Key revoked
403 Authenticated but insufficient permission / cross-OEM forbidden (see Permission Matrix)
404 Resource not found or out of visible scope
429 Rate-limited (default 100 RPS per Key)
5xx Server error

0.4 Pagination Convention

All list endpoints use page + size (not page_size):

Field Type Default Range
page int 1 ≥ 1
size int 20 1 - 100

Response shape:

{
  "code": 0,
  "data": {
    "list": [ /* ... */ ],
    "total": 42,
    "page": 1,
    "size": 20
  }
}

0.5 Cross-Module Design Markers (D*)

A few D* markers in this document come from cross-module design decisions. They warn integrators not to use fields that do not exist or have different semantics:

Marker Meaning
D4 percentile / p50 / p95 / p99 are not exposed as common fields; only time windows ≤ 24h trigger real-time ES percentile calculation
D7 Report template names are a closed enum; templates outside this enum are not callable
D8 Cache-value fields use total_cache_*; single-field names such as cache_count / cache_bytes / cache_hit do not exist
D10 Alert/risk closure uses process_uid / process_time / status / level; old field names handle_user / handle_time / risk_score / alert_status are not supported

0.6 Three Audiences

Audience Entry Prefer
Human integrators This doc + Quickstart curl / Postman with single endpoints
Scripts / CI / third-party systems This doc + API Key Management API Key + narrowed scopes
Machines / AI agents /api/openapi.json / /llms.txt / /llms-full.txt OpenAPI v3 schema

§1 Authentication (read first)

Cloud WAF supports two authentication channels. Pick exactly one per request:

Scenario Header Best for Notes
Human login / Web Console / interactive CLI login Authorization: Bearer <token> People Token is issued by POST /api/auth/login; expires; best for short-lived sessions
Scripts / CI / third-party integrations Authorization: ApiKey zck_<prefix>.<secret> Machine calls The plaintext API Key is returned only once at issuance; best for long-running automation

Public endpoints (no auth) — only 5:

Every other endpoint must include one of the headers above. Examples in this doc default to Bearer; switch to API Key by replacing the header with Authorization: ApiKey zck_<prefix>.<secret> and ensure the key scopes cover the required permission.

1.1 API Key Permission Rule

effective_perms = user.RBAC ∩ key.scope

An API Key can never exceed the issuing user's current RBAC; scope can only narrow, not expand. After authentication, middleware injects the same user_id / role_id context as the session channel, so downstream RBAC/OEM isolation works identically.

Security guarantees:

Full API Key management endpoints are documented in §4.2.


§2 CLI Release (public endpoints)

Used for zcloud CLI self-update and one-line install. No authentication required.

GET /api/cli/version — Query latest CLI version

Purpose: clients self-check on startup; the install script /api/cli/install.sh calls this internally to decide which binary to download.

Authentication: none (public)

Input parameters: none

Output fields:

Field Type Description
data.version string Like v0.1.0-31, aligned with git tag
data.binaries[] array Download URLs for the four os/arch combinations
data.binaries[].os string linux / darwin
data.binaries[].arch string amd64 / arm64
data.binaries[].download_url string Append to your service URL to download

Visualization recommendation: plain text (version badge); not suitable for charts. Frontend may use this on a "System Settings - CLI version" page as a KPI card.

Example response:

{
  "code": 0,
  "message": "ok",
  "data": {
    "version": "v0.1.0-31",
    "binaries": [
      { "os": "linux",  "arch": "amd64", "download_url": "/api/cli/download/linux-amd64" },
      { "os": "linux",  "arch": "arm64", "download_url": "/api/cli/download/linux-arm64" },
      { "os": "darwin", "arch": "amd64", "download_url": "/api/cli/download/darwin-amd64" },
      { "os": "darwin", "arch": "arm64", "download_url": "/api/cli/download/darwin-arm64" }
    ]
  }
}

GET /api/cli/install.sh — One-line installer script

Purpose: install CLI on Linux/macOS in one command. Returns text/x-shellscript, suitable for curl ... | sh.

Authentication: none (public)

Input parameters: none

Output fields: a raw shell script (text); not wrapped in JSON envelope.

Visualization recommendation: not a chart; render as a code snippet.

Example:

curl -fsSL https://waf.example.com/api/cli/install.sh | sh

GET /api/cli/download/{filename} — Download a specific binary

Purpose: fetch the zcloud binary (signed) for a specific platform. filename matches the last segment of download_url returned by /api/cli/version (e.g. linux-amd64).

Authentication: none (public)

Input parameters:

Field Type Required Description
filename path yes one of linux-amd64 / linux-arm64 / darwin-amd64 / darwin-arm64

Output fields: binary stream (application/octet-stream).

Visualization recommendation: not a chart.


GET /api/cli/checksums.txt — Binary checksums

Purpose: SHA256 integrity check for /api/cli/download/*. The install script fetches this before downloading the binary.

Authentication: none (public)

Input parameters: none

Output fields: plain text/plain with one <sha256> <filename> per line.

Visualization recommendation: not a chart.


§3 User Authentication

POST /api/auth/login — Login

Purpose: exchange username/password for a session token. Used by Web Console, interactive CLI login, mobile clients.

Authentication: none (public)

Input parameters (request body):

Field Type Required Description
username string yes Login name
password string yes Password is base64-encoded by the frontend before sending; the backend decodes then bcrypt-compares (legacy compatibility)

Output fields:

Field Type Description
data.token string 32-char session token; use as Authorization: Bearer <token>
data.user_id string Unique user ID
data.user_name string Login name
data.nick_name string Display name
data.need_change_password bool true means first-login forced password change required

Visualization recommendation: login response is not charted; if need_change_password=true, the frontend should redirect to the change-password page.

Example request:

curl -X POST https://waf.example.com/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"'$(echo -n 'your_password' | base64)'"}'

Example response:

{
  "code": 0,
  "message": "ok",
  "data": {
    "token": "550e8400-e29b-41d4-a716-446655440000",
    "user_id": "u-admin",
    "user_name": "admin",
    "nick_name": "System Administrator",
    "need_change_password": false
  }
}

Common pitfalls:


POST /api/auth/logout — Logout

Purpose: actively invalidate the current Bearer token; subsequent requests with that token return 401.

Authentication: Bearer <token> (API Key has no logout concept; revoke via DELETE /api/sys/api-keys/:id)

Input parameters: none

Output fields: data is null.

Visualization recommendation: not a chart.

Example request:

curl -X POST https://waf.example.com/api/auth/logout \
  -H "Authorization: Bearer $TOKEN"

§4 System Management

4.1 User Management

Use case: CRUD users in the "System Settings - Users" page. All user endpoints are OEM-scoped; cross-OEM operations return 403.

GET /api/sys/users — User list (paged)

Purpose: render the user table on the "Users" page with keyword search and pagination.

Authentication: sys.user.list

Input parameters:

Field Type Required Example Description
page int no 1 Page number (1-based)
size int no 20 Per-page count, 1-100
keyword string no admin Fuzzy search on username / nick_name

Output fields (data.list[]):

Field Type Description
user_id string Unique user ID
user_name string Login name
nick_name string Display name
email string Email
mobile string Phone
locked int 0 = normal, non-zero = locked
role_ids int64[] Role ID array (multi-role)
roles[] array Role brief {role_id, name, level}
ctime int64 Creation time, Unix ms

Visualization recommendation: table. locked column uses badges (green/red); roles rendered as chips.

Example request:

curl -H "Authorization: Bearer $TOKEN" \
  "https://waf.example.com/api/sys/users?page=1&size=20&keyword=admin"

Example response:

{
  "code": 0,
  "message": "ok",
  "data": {
    "list": [
      {
        "user_id": "u-001",
        "user_name": "admin",
        "nick_name": "System Administrator",
        "email": "admin@example.com",
        "mobile": "",
        "locked": 0,
        "role_ids": [1],
        "roles": [{ "role_id": 1, "name": "Super Administrator", "level": 1 }],
        "ctime": 1714521600000
      }
    ],
    "total": 42,
    "page": 1,
    "size": 20
  }
}

Common pitfalls:


POST /api/sys/users — Create user

Purpose: backend for the "Add User" form on the user management page.

Authentication: sys.user.create

Input parameters (request body):

Field Type Required Example Description
user_name string yes u1 2-255 chars
password string yes InitPassw0rd! 6-72 chars (bcrypt limit)
nick_name string no Operator A ≤ 100 chars
email string no u1@x.com RFC email format
mobile string no 13800138000 ≤ 20 chars
comment string no On-call colleague Note

Output fields: returns the new user object with the same shape as a list item.

Visualization recommendation: not a chart; one-shot write. Frontend should refresh the list after success.

Example request:

curl -X POST https://waf.example.com/api/sys/users \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"user_name":"u1","password":"InitPassw0rd!","nick_name":"Operator A","email":"u1@example.com"}'

DELETE /api/sys/users/{id} — Delete user

Purpose: backend for the "Delete" button on user management. Deletion cascades to sessions, API Keys, and role bindings.

Authentication: sys.user.delete

Input parameters:

Field Type Required Description
id path yes User user_id

Output fields: data is null.

Visualization recommendation: not a chart.


PUT /api/sys/users/{id}/password — Reset password

Purpose: an admin resets a user's password. The user is forced to change the password on next login.

Authentication: sys.user.resetpwd

Input parameters:

Field Type Required Description
id path yes User user_id
password string yes New password (plaintext, 6-72 chars; backend bcrypts)

Output fields: data is null.

Visualization recommendation: not a chart.

Example request:

curl -X PUT https://waf.example.com/api/sys/users/u-001/password \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"password":"NewPassw0rd!"}'

Common pitfalls:


4.2 API Key Management

Use case: issue/revoke machine credentials in the "System Settings - API Keys" page. Pair with /api/sys/api-keys/:id/logs|stats|audit-actions for call auditing.

POST /api/sys/api-keys — Issue a new API Key

Purpose: issue a machine credential for scripts/CI/third-party systems. The plaintext api_key field is returned exactly once; the frontend must let the user copy and store it immediately.

Authentication: sys.apikey.create

Input parameters (request body):

Field Type Required Example Description
name string yes prod-integration ≤ 100 chars; for audit identification
scopes string[] no ["guard.domain.list"] Permission full key list; empty = full inheritance
expires_in_days int no 90 Default 90, max 365
allowed_ip_cidrs string[] no ["203.0.113.0/24"] E14 IP allowlist CIDRs; empty means no IP restriction

Output fields:

Field Type Description
data.key_id string Unique API Key ID (used for revoke / log lookup)
data.name string Same as request
data.api_key string Full plaintext prefix.secret, returned exactly once
data.prefix string Like zck_abc12345, safe to log
data.last4 string Last 4 chars of secret, frontend uses to identify "the one I just created"
data.expires_at int64 Expiration time, Unix ms

Error codes:

Visualization recommendation: issuance is a one-shot write; frontend should use a modal with a one-time copy button + masked display (industry pattern: GitHub/Stripe).

Example request:

curl -X POST https://waf.example.com/api/sys/api-keys \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name":"prod-integration","scopes":["guard.domain.list"],"expires_in_days":90}'

Example response:

{
  "code": 0,
  "data": {
    "key_id": "8f21c0c5-55ae-4cbd-a60a-8e64a6e2b1d0",
    "name": "prod-integration",
    "api_key": "zck_abc12345.A1b2C3d4E5f6G7h8I9j0K1L2M3n4O5p6Q7r8S9t0",
    "prefix": "zck_abc12345",
    "last4": "5t0",
    "expires_at": 1732982400000
  }
}

Common pitfalls:


GET /api/sys/api-keys — List API Keys

Purpose: render the API Keys list page with status for the current user / OEM.

Authentication: sys.apikey.list

Input parameters:

Field Type Required Description
page int no Default 1
size int no Default 20, max 100

Output fields (data.list[]):

Field Type Description
key_id string Unique API Key ID
name string Name
prefix string zck_xxx prefix
last4 string Last 4 of secret
user_id string Owner
oem_id string OEM scope boundary
scopes string[] Narrowed permission list
allowed_ip_cidrs string[] E14 IP allowlist
status int 1 = active, 2 = revoked
expires_at int64 Expiration time
last_used_at int64 Last call time; 0 if never used
last_used_ip string Last call IP; "" if never used
ctime int64 Issuance time

Visible scope:

Visualization recommendation: table. status rendered as a badge (green=active / gray=revoked); highlight rows where expires_at is < 7d. Combined with /stats, you can plot a "Top 5 Keys by call volume" bar chart.

Example request:

curl -H "Authorization: Bearer $TOKEN" \
  "https://waf.example.com/api/sys/api-keys?page=1&size=20"

DELETE /api/sys/api-keys/{id} — Revoke an API Key

Purpose: soft-revoke (statusrevoked); audit trail is preserved. Middleware rejects subsequent requests where status != active.

Authentication: sys.apikey.delete

Input parameters:

Field Type Required Description
id path yes API Key key_id

Output fields: data is null.

Idempotency: re-revoking returns 200; the frontend can call repeatedly without errors.

Operable scope:

Error code: 1051 API Key not found or out of visible scope.

Visualization recommendation: not a chart; trigger from a "Revoke" button with a confirmation dialog.

Example request (using API Key as caller):

curl -X DELETE https://waf.example.com/api/sys/api-keys/8f21c0c5-55ae-4cbd-a60a-8e64a6e2b1d0 \
  -H "Authorization: ApiKey zck_abc12345.A1b2C3d4..."

GET /api/sys/api-keys/{id}/logs — Call audit log for a Key (E12)

Purpose: audit the call history of an API Key. Returns events from the audit table api_key_audit_logs.

Authentication: sys.apikey.logs

Input parameters:

Field Type Required Description
id path yes API Key key_id
event string no call (default, call audit) / manage (management actions)
page int no Default 1
size int no Default 20, max 100

Output fields (per record):

Field Type Description
id int Auto-increment primary key
event_type string call / manage
key_id string Associated API Key ID
user_id string Subject (call = key holder; manage = operator)
auth_mode string apikey / session; the channel the request came through
action string call = <METHOD> <PATH>; manage = create / revoke / renew / revoke-all
status_code int HTTP response status (call type only)
biz_code int Business code (0 = success; call type only)
client_ip string Client IP
user_agent string UA (≤ 255 chars, truncated)
extra string JSON string with renew / batch action structured extensions
ctime int Unix ms

Visualization recommendation:

Visible scope:

Error code: 1051 API Key not found or out of visible scope.


GET /api/sys/api-keys/{id}/stats — Aggregated statistics for a Key

Purpose: on the API Key detail page, show aggregate KPIs (total calls, success rate, QPS, top endpoints). Based only on event_type=call records.

Authentication: sys.apikey.stats

Input parameters:

Field Type Required Description
id path yes API Key key_id
since string/int no Aggregation window start; supports 24h / 7d / 30m relative or pure integer ms timestamp; empty = all history

Output fields:

Field Type Description
total_calls int Total calls in window
success int Count of 200 ≤ status_code < 400
client_err int Count of 400 ≤ status_code < 500
server_err int Count of status_code ≥ 500
top_endpoints array Top 5 actions by count ({action, count}, descending)
last_1h_qps float Last 1h QPS (count / 3600)

Visualization recommendation:

Error codes: 400 since parse failed; 1051 API Key not found or out of visible scope.

Example request:

curl -H "Authorization: Bearer $TOKEN" \
  "https://waf.example.com/api/sys/api-keys/8f21c0c5-55ae-4cbd-a60a-8e64a6e2b1d0/stats?since=24h"

GET /api/sys/api-keys/audit-actions — Management action audit log (E13)

Purpose: cross-Key audit view; returns the management actions (create / revoke / renew / revoke-all) audit stream.

Authentication: sys.apikey.audit

Input parameters: page / size, standard pagination.

Output fields: same as GET /api/sys/api-keys/{id}/logs; event_type is always manage.

Visible scope:

Visualization recommendation:


4.3 Permission Tree

GET /api/sys/permissions/tree — Permission tree

Purpose: returns the full permission tree (module / resource / action three layers + i18n names) for the current OEM. The frontend "Role Permissions" page renders the checkbox tree from this; the API Key scope picker also uses the same tree.

Authentication: authenticated session (no specific permission required)

Input parameters: optionally append Accept-Language: en-US to localize permission names.

Output fields (excerpt, data[]):

Field Type Description
module string Module, e.g. guard / sys / analytics
resources[] array Resources under this module
resources[].prefix string Resource prefix, e.g. guard.domain
resources[].name string Resource i18n display name
resources[].actions[] array Actions under this resource
resources[].actions[].key string Short action key, e.g. list / create
resources[].actions[].name string Action i18n display name
resources[].actions[].full_key string Full permission key, e.g. guard.domain.list

Visualization recommendation:

Example response (excerpt):

{
  "code": 0,
  "data": [
    {
      "module": "guard",
      "resources": [
        {
          "prefix": "guard.domain",
          "name": "Domains",
          "actions": [
            { "key": "list",   "name": "List domains",  "full_key": "guard.domain.list" },
            { "key": "view",   "name": "View domain",   "full_key": "guard.domain.view" },
            { "key": "create", "name": "Create domain", "full_key": "guard.domain.create" }
          ]
        }
      ]
    }
  ]
}

§5 Guard Resource Management

📦 Guard Resource Management · 30 endpoints · used to configure protection objects (domains/certs/policies/CC&ACL rules/bwlist/forwards/schedules/WAF rules); the "configuration plane" of WAF protection.
Full schemas in /api/openapi.json. This section gives the most critical fields, enums, and common pitfalls for integration.

5.1 Domains /api/guard/domains

The domain is the core resource of Guard — every protection policy, certificate binding, and analytics aggregation anchors on domain_id.

GET /api/guard/domains — Domain list

Purpose: render the domain table on the "Protection - Domains" page; supports filtering by audit_status and keyword search.

Authentication: guard.domain.list

Input parameters:

Field Type Required Example Description
page int no 1 Page number
size int no 20 Per-page count, 1-100 (not page_size)
keyword string no api.example Fuzzy search on domain / asset_name
audit_status int no 4 1 = unaudited, 2 = auditing, 3 = rejected, 4 = approved

Output fields (data.list[] = DomainVO):

Field Type Description
domain_id string Unique domain ID
domain string The domain itself, e.g. api.example.com
asset_name string Asset note / alias
user_id string Owner UUID (kept for legacy platform compatibility)
user_name string Owner display name (P1.3 added, sourced from cloud sys.users)
policy_id string Bound policy ID
cname string CNAME assigned by the backend
auto_cert bool Whether auto-issuance of certificates is enabled
mode int32 Onboarding mode (1 = reverse-proxy, etc.; see ops doc)
audit_status int32 1=unaudited 2=auditing 3=rejected 4=approved
switches map<string,int32> Protection switches; keys from waf/cc/acl/bot/cache; 1=on 0=off
ctime / utime int64 Creation / update time

Visualization recommendation:

Example request:

curl -H "Authorization: ApiKey $ZCLOUD_API_KEY" \
  "https://waf.example.com/api/guard/domains?page=1&size=20&audit_status=4"

Example response:

{
  "code": 0,
  "data": {
    "list": [
      {
        "domain_id": "d_8a3b1c",
        "domain": "api.example.com",
        "asset_name": "Production API gateway",
        "user_id": "u_abc",
        "policy_id": "p_default",
        "cname": "api.example.com.cname.zcloud.io",
        "auto_cert": false,
        "mode": 1,
        "audit_status": 4,
        "switches": { "waf": 1, "cc": 1, "acl": 1, "bot": 0, "cache": 1 },
        "ctime": 1714521600000,
        "utime": 1714608000000
      }
    ],
    "total": 8,
    "page": 1,
    "size": 20
  }
}

Common pitfalls:


POST /api/guard/domains — Create domain

Purpose: backend for the "Add Domain" form, creating a new protected domain.

Authentication: guard.domain.create

Input parameters (request body DomainCreateReq):

Field Type Required Description
domain string yes The domain itself, e.g. api.example.com
asset_name string no Asset note
policy_id string no Bind a policy; default if not given

Important: domain is the only required field. Certificate binding goes through POST /api/guard/certs/:id/bind; do not set it via create-domain. Fields origin_addr / port / cert_id do not exist.

Output fields: returns DomainVO (same as a list item) with the assigned domain_id and cname.

Visualization recommendation: not a chart. After success, jump to the domain detail page or refresh the list.


GET /api/guard/domains/{id} — Domain detail

Purpose: fetch a single domain when entering the detail page.

Authentication: guard.domain.view

Input parameters: path id = domain_id.

Output fields: DomainVO, identical to a list item.

Visualization recommendation: form display. switches rendered as a switch group; audit_status as a badge.


PUT /api/guard/domains/{id} — Update domain

Purpose: edit mutable fields like asset_name, policy_id, auto_cert.

Authentication: guard.domain.edit

Input parameters (request body DomainUpdateReq):

Field Type Required Description
asset_name string no Asset note
policy_id string no Switch policy
auto_cert bool no Toggle auto-issuance
mode int32 no Onboarding mode

Output fields: returns the updated DomainVO.

Visualization recommendation: not a chart.


DELETE /api/guard/domains/{id} — Delete domain

Purpose: remove the domain from the protection list. Deletion cascades to settings, cert bindings, analytics snapshots.

Authentication: guard.domain.delete

Input parameters: path id = domain_id.

Output fields: data is null.

Visualization recommendation: not a chart. Confirm twice in the UI; warn that "analytics and bindings will be cleaned up".


GET /api/guard/domains/{id}/settings — Get domain settings

Purpose: render the "Advanced Settings" tab on the domain detail page; shows current effective settings (protection module switches, cache policy, CC limits).

Authentication: guard.domain.view

Input parameters: path id = domain_id.

Output fields:

Field Type Description
data.settings map<string,string> keys taken from the backend settings.* dictionary, typically waf/cc/acl/bot/cache; values are stringified config JSON

Visualization recommendation: form display; one config card per key.


PUT /api/guard/domains/{id}/settings — Update domain settings

Purpose: modify the settings map for a domain.

Authentication: guard.domain.edit

Input parameters (request body DomainSettingsUpdateReq):

Field Type Required Description
settings map<string,string> yes Keys must come from settings.* returned by GET; unknown keys rejected

Output fields: data is null; the caller should issue a GET to fetch the new state.

Visualization recommendation: not a chart.

Common pitfalls:


5.2 Certificates /api/guard/certs

Certificate flow is a two-step process: "upload PEM → bind to domain". Content-Type is application/json, not multipart/form-data.

GET /api/guard/certs — Cert list

Purpose: table on the "Protection - Certificates" page.

Authentication: guard.cert.list

Input parameters: page / size / keyword (search name/common_name).

Output fields (data.list[] = CertVO):

Field Type Description
id uint64 Unique cert ID
name string Custom name
user_name string Owner display name (P1.3 replaces user_id, sourced from cloud sys.users)
certificate_type int32 Cert type enum
common_name string Cert CN
issuer string Issuer
expired_at int64 Expiry, Unix ms
auto_cert bool Auto-renewal
ctime / utime int64 Creation / update time

Field bound_domains does not exist; query bound domains via GET /api/guard/certs/{id}/domains.

Visualization recommendation:


POST /api/guard/certs — Upload certificate

Purpose: upload one PEM cert. The most common integration pitfall is using multipart/form-data; please use JSON.

Authentication: guard.cert.create

Security note: direct integrations still pass cert / key as JSON PEM strings. Do not put private keys in AI chats, tickets, logs, or telemetry. When operating through the Aegeon Cloud conversational assistant, prefer its secure certificate attachment flow: the browser uploads the cert/key to Aegeon and the chat only contains an attachment reference; the private key is not stored in the AI message.

Input parameters (request body CertUploadReq, application/json):

Field Type Required Description
name string yes Cert name
cert string yes PEM text (with -----BEGIN CERTIFICATE----- markers)
key string yes Private key PEM text
sign_cert string no SM2 signing cert PEM (optional)
sign_key string no SM2 signing private key PEM (optional)

Output fields: returns the new CertVO.

Visualization recommendation: not a chart. The frontend upload component should support both "paste PEM" and "read local file".

Example request:

curl -X POST https://waf.example.com/api/guard/certs \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name":"prod-2026","cert":"-----BEGIN CERTIFICATE-----\nMII...\n-----END CERTIFICATE-----","key":"-----BEGIN PRIVATE KEY-----\nMII...\n-----END PRIVATE KEY-----"}'

Common pitfalls:


GET /api/guard/certs/{id} — Cert detail

Purpose: detail page; includes full PEM text.

Authentication: guard.cert.view

Input parameters: path id.

Output fields: CertDetailVO = CertVO + cert + sign_cert (PEM text, useful for download).

Visualization recommendation: form display. Add a "Download cert" button (frontend assembles PEM into a download).


PUT /api/guard/certs/{id} — Update cert

Purpose: replace PEM directly (no need to delete-then-create).

Authentication: guard.cert.edit

Input parameters (request body CertUpdateReq, same fields as upload but all optional): name / cert / key / sign_cert / sign_key.

Output fields: returns the updated CertVO.


DELETE /api/guard/certs/{id} — Delete cert

Authentication: guard.cert.delete

Input parameters: path id.

Output fields: data is null.

Side effect: deletion auto-unbinds the cert from all bound domains.


GET /api/guard/certs/{id}/domains — List domains bound to a cert

Purpose: on the cert detail page, show "which domains is this cert protecting".

Authentication: guard.cert.view

Input parameters: path id.

Output fields (data[] = CertDomainVO[]):

Field Type Description
domain_id string Domain ID
domain string Domain
cert_id uint64 Cert ID (= path id)
ctime int64 Bind time

Visualization recommendation: table.


POST /api/guard/certs/{id}/bind — Bind cert to domain

Purpose: bind a specific cert to a domain.

Authentication: guard.cert.edit

Input parameters (request body CertBindReq):

Field Type Required Description
domain_id string yes Target domain ID

Output fields: data is null.

Visualization recommendation: not a chart.


DELETE /api/guard/certs/{id}/bind/{domainId} — Unbind cert from domain

Purpose: break the binding between a cert and a domain.

Authentication: guard.cert.edit

Input parameters: path id = cert ID; path domainId = domain ID.

Output fields: data is null.

Path: it is DELETE /bind/{domainId}, not POST /unbind.


5.3 Policies /api/guard/policies

A policy is a rule container: CC / ACL / bwlist all attach to a policy. One domain binds one policy.

GET /api/guard/policies — Policy list

Authentication: guard.policy.list

Input parameters: page / size / keyword.

Output fields (data.list[] = PolicyVO):

Field Type Description
policy_id string Policy ID
name string Policy name
comment string Note (not remark)
user_id string Owner UUID (kept for legacy platform compatibility)
user_name string Owner display name (P1.3 added, sourced from cloud sys.users)
default_main_rule_version string Default main rule version
is_default bool Whether this is the system default
schema_id int64 Schema version
cc_rule_count int32 CC rule count under this policy
bwl_rule_count int32 bwlist rule count
acl_rule_count int32 ACL rule count
ctime / utime int64 Time

Visualization recommendation: table + three chips (cc/bwl/acl counts).


POST /api/guard/policies — Create policy

Authentication: guard.policy.create

Input parameters (PolicyCreateReq): name (required) / comment.

Output fields: returns the new PolicyVO.


GET /api/guard/policies/{id} — Policy detail

Authentication: guard.policy.view

Input parameters: path id.

Output fields: PolicyVO.


PUT /api/guard/policies/{id} — Update policy

Authentication: guard.policy.edit

Input parameters (PolicyUpdateReq): name / comment.

Output fields: returns the updated PolicyVO.


DELETE /api/guard/policies/{id} — Delete policy

Authentication: guard.policy.delete

Input parameters: path id.

Output fields: data is null. Ensure no domain references this policy before deleting.


5.4 CC Rules /api/guard/policies/{id}/cc/rules

CC = HTTP rate limiting (Connection / Concurrency Control). Lives under a policy and is isolated by policy_id.

GET /api/guard/policies/{id}/cc/rules — CC rule list

Authentication: guard.cc.list

Input parameters: path id = policy_id; query page / size.

Output fields (data.list[] = CcRuleVO):

Field Type Description
rule_id int64 Rule ID
name string Rule name
describe string Description
matches[] array Match conditions (path / method / headers etc.)
stats object Aggregation dimensions
limit object Rate limit threshold
action object Hit action (block / captcha / throttle etc.)
stime / etime int64 Effective start/end
status int32 1 = enabled, 2 = disabled
ctime / utime int64 Creation / update time

Visualization recommendation: table + status badge. matches is complex; collapse it under a "Detail" button modal.


POST /api/guard/policies/{id}/cc/rules — Create CC rule

Authentication: guard.cc.create

Input parameters (CcRuleCreateReq):

Field Type Required Description
name string yes Rule name
describe string no Description
matches[] array yes At least 1 match condition
stats object no Aggregation dimensions (IP / URI / UA etc.)
limit object yes Rate limit
action object yes Hit action
stime / etime int64 no Effective time window

Complex shapes: list one rule first as a template.

Output fields: returns the new CcRuleVO.


GET /api/guard/policies/{id}/cc/rules/{rid} — CC rule detail

Authentication: guard.cc.view

Input parameters: path id = policy_id, rid = rule_id. The backend verifies that rule_id belongs to the current policy_id; cross-policy reads return NotFound.

Output fields: CcRuleVO.


PUT /api/guard/policies/{id}/cc/rules/{rid} — Update CC rule

Authentication: guard.cc.edit

Input parameters: path id + rid, body same as Create.

Output fields: returns the updated CcRuleVO.


DELETE /api/guard/policies/{id}/cc/rules/{rid} — Delete CC rule

Authentication: guard.cc.delete

Input parameters: path id + rid.

Output fields: data is null.


PUT /api/guard/policies/{id}/cc/rules/{rid}/status — Toggle CC rule status

Purpose: enable/disable a rule from the list page using a switch component.

Authentication: guard.cc.edit

Input parameters (CcRuleStatusReq):

Field Type Required Values
status int32 yes 1 = enabled, 2 = disabled

Important: status is an int32 number, not the string "enabled" / "disabled".

Output fields: data is null.

Example request:

curl -X PUT https://waf.example.com/api/guard/policies/p_default/cc/rules/12345/status \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"status":1}'

5.5 ACL Rules /api/guard/policies/{id}/acl/rules

ACL = Access Control List (allow/block by IP / Header / URI). Similar shape to CC rules but no priority field.

GET /api/guard/policies/{id}/acl/rules — ACL rule list

Authentication: guard.acl.list

Input parameters: path id, query page / size.

Output fields (data.list[] = AclRuleVO):

Field Type Description
rule_id int64 Rule ID
name string Rule name
describe string Description
matches[] array Match conditions
action object Hit action (block / page / pass)
stime / etime int64 Effective start/end
status int32 1 = enabled, 2 = disabled
ctime / utime int64 Time

Visualization recommendation: table + status badge + action.type chip color (block red / pass green / page blue).


POST /api/guard/policies/{id}/acl/rules — Create ACL rule

Authentication: guard.acl.create

Input parameters (AclRuleCreateReq): name / describe / matches[] (≥1) / action / stime / etime.

When action.type is block or page, the content field is base64-encoded server-side; the caller passes plaintext.

Output fields: returns the new AclRuleVO.


GET /api/guard/policies/{id}/acl/rules/{rid} — ACL rule detail

Authentication: guard.acl.view

Input parameters: path id + rid.

Output fields: AclRuleVO.


PUT /api/guard/policies/{id}/acl/rules/{rid} — Update ACL rule

Authentication: guard.acl.edit

Input parameters: path id + rid, body same as Create.

Output fields: returns the updated AclRuleVO.


DELETE /api/guard/policies/{id}/acl/rules/{rid} — Delete ACL rule

Authentication: guard.acl.delete

Input parameters: path id + rid.

Output fields: data is null.


PUT /api/guard/policies/{id}/acl/rules/{rid}/status — Toggle ACL rule status

Authentication: guard.acl.edit

Input parameters (AclRuleStatusReq): status int32 (1=enabled 2=disabled, number).

Output fields: data is null.


5.6 BW List /api/guard/bwlist

The bwlist has two layers: a set (logical container, black/white/grey-black/grey-white) and IPs (individual entries within a set). Grey-black and grey-white sets are delivered as blacklist and whitelist rules respectively during config generation.

GET /api/guard/bwlist/sets — Set list

Authentication: guard.bwlist.list

Input parameters: page / size / keyword.

Output fields (data.list[] = IPSetVO):

Field Type Description
id uint64 Set ID
user_name string Owner display name (P1.3 replaces user_id, sourced from cloud sys.users)
name string Set name
policy_id string Bound policy
ip_set_type int32 1 = blacklist, 2 = whitelist, 3 = grey-black, 4 = grey-white (numeric enum, not string)
status int32 1 = disabled, 2 = enabled
count int64 Total entries
enable_count int64 Enabled entries
unable_count int64 Disabled entries
describe string Description (not remark)
is_default bool Whether default set
is_private bool Whether private
private_domain_id string Domain associated with private set
ctime / utime int64 Time

Visualization recommendation:


POST /api/guard/bwlist/sets — Create set

Authentication: guard.bwlist.create

Input parameters (BWListSetCreateReq):

Field Type Required Description
name string yes Set name
ip_set_type int32 yes 1 = blacklist, 2 = whitelist, 3 = grey-black, 4 = grey-white
policy_id string no Bind policy
status int32 no 1 = disabled, 2 = enabled, default 2
describe string no Description

Output fields: returns the new IPSetVO.

When policy_id is set, the backend mirrors the set into the policy and domain-level bwlist config; subsequent add, bulk-add, or delete IP operations trigger delivery.


PUT /api/guard/bwlist/sets/{id} — Update set

Authentication: guard.bwlist.edit

Input parameters (BWListSetUpdateReq): name / status / describe (all optional).

Output fields: returns the updated IPSetVO.

Updating status syncs policy/domain config and triggers delivery: enabled sets enter black/white lists, disabled sets enter the disabled list.


DELETE /api/guard/bwlist/sets/{id} — Delete set

Authentication: guard.bwlist.delete

Input parameters: path id.

Output fields: data is null. All IPs in the set are deleted as a side effect.

Deleting a set also unbinds it from policy/domain bwlist config and triggers redelivery for affected domains.


GET /api/guard/bwlist/sets/{id}/ips — List IPs in a set

Authentication: guard.bwlist.ip_list

Input parameters: path id = set ID; query page / size.

Output fields (data.list[] = IPVO):

Field Type Description
id uint64 Entry ID
ipset_id uint64 Owning set
ip_addr string IP or CIDR (not ip)
status bool true=enabled false=disabled, default true
ctime / utime int64 Time

Fields expires_at / remark do not exist.

Visualization recommendation: table.


POST /api/guard/bwlist/sets/{id}/ips — Add a single IP

Authentication: guard.bwlist.ip_add

Input parameters (BWListIPAddReq):

Field Type Required Description
ip_addr string yes IP or CIDR
status bool no default true

Output fields: returns the new IPVO.


POST /api/guard/bwlist/sets/{id}/ips/batch — Bulk add IPs

Purpose: import hundreds of IPs in one call (e.g. threat-intel feeds), reducing round trips.

Authentication: guard.bwlist.ip_add

Input parameters (BWListIPBatchAddReq):

Field Type Required Description
ips[] array yes At least 1 item; each {ip_addr, status?}

Output fields: data is null or returns the new ID list (implementation-dependent).


POST /api/guard/bwlist/sets/{id}/ips/batch/delete — Bulk delete IPs

Purpose: delete multiple IPs from a set in one call (e.g. clearing expired bans), atomically in a single transaction — avoids the partial-failure and audit-noise of deleting one by one.

Authentication: guard.bwlist.ip_delete

Input parameters (BWListIPBatchDeleteReq):

Field Type Required Description
ip_ids[] array<int64> yes At least 1 item; IP entry IDs to delete. Scoped to set {id}; IDs not belonging to this set are ignored

Output fields: data.deleted is the number of entries actually deleted. Returns 404 if no IP in the set matches.


DELETE /api/guard/bwlist/ips/{id} — Delete a single IP

Important: the path is the top-level DELETE /api/guard/bwlist/ips/{id}, not nested under sets like DELETE /sets/{sid}/ips/{id}.

Authentication: guard.bwlist.ip_delete

Input parameters: path id = entry ID.

Output fields: data is null.


5.7 IP Forward /api/guard/forwards

This is TCP/UDP port forwarding (L4), not path forwarding / reverse proxy (L7).

GET /api/guard/forwards — Forward list

Authentication: guard.forward.list

Input parameters:

Field Type Description
page / size int Standard pagination
user_id string Filter by owner
domain_id string Filter by domain
status int32 1 = disabled, 2 = enabled
keyword string Search domain / describe

Output fields (data.list[] = ForwardVO):

Field Type Description
id uint64 Forward ID
user_id string Owner UUID (kept for legacy platform compatibility)
user_name string Owner display name (P1.3 added, sourced from cloud sys.users)
domain string Forwarded domain/IP
domain_id string Associated domain ID
schema int32 3 = TCP, 4 = UDP, default 3
port int32 Port 1-65535
node_ipaddrs string Comma-separated source IPs
describe string Description
status int32 1 = disabled, 2 = enabled
src_setting json Source setting raw JSON
adv_settings json Advanced setting raw JSON
node_setting json Node setting raw JSON
dev_setting string Device setting
ctime / utime int64 Time

Visualization recommendation:


POST /api/guard/forwards — Create forward

Authentication: guard.forward.create

Input parameters (ForwardCreateReq):

Field Type Required Example Description
domain string yes *.example.com Forward domain
domain_id string yes d_8a3b1c Associated domain
port int32 yes 443 1-65535
schema int32 no 3 3=TCP (default) / 4=UDP
node_ipaddrs string no 10.0.0.1,10.0.0.2 Source IP list
describe string no Description
status int32 no 2 1=disabled 2=enabled
src_setting / adv_settings / node_setting json no Raw JSON config
dev_setting string no Device config

Fields source_path / target do not exist.

Output fields: returns the new ForwardVO.


GET /api/guard/forwards/{id} — Forward detail

Authentication: guard.forward.view

Input parameters: path id.

Output fields: ForwardVO.


PUT /api/guard/forwards/{id} — Update forward

Authentication: guard.forward.edit

Input parameters: path id, body same as Create (all fields optional).

Output fields: returns the updated ForwardVO.


DELETE /api/guard/forwards/{id} — Delete forward

Authentication: guard.forward.delete

Input parameters: path id.

Output fields: data is null.


5.8 DNS Scheduling /api/guard/schedules

This module only covers DNS parsing scheduling (mode switch between SRC/NODE and batch record enable/disable). The legacy cron-driven schedule endpoints have been retired.

⚠️ Asynchronous semantics: none of these endpoints calls any third-party DNS API directly. Every write persists to the legacy guard_db.dns_records + guard_db.dns_affairs tables and publishes an NSQ message on topic dns; the legacy zdns service consumes it and applies the change. Callers must poll the affairs endpoint to obtain the final status (initial AffairsStatus_StartAffairsStatus_Succeed / AffairsStatus_Faild).

⚠️ Source of truth: dns_records.group_type is the truth for the current mode, while guard_configs.parsing_state is synced asynchronously; both live in the legacy guard_db and may lag by up to 30s. The VO returns both; render a "syncing" badge whenever they disagree.

5.8.1 Domain Scheduling

GET /api/guard/schedules/domains — List domains

Authentication: guard.schedule.list

Input parameters:

Field Type Description
page / size int Pagination (size cap 100)
keyword string Fuzzy match against domain name
user_id string Filter by owner (super-admin / agency only)
mode int32 0=all / 1=SRC / 2=NODE

Output fields (data = ScheduleDomainListResp):

Field Type Description
list[].domain_id string Domain ID (guard_configs.domain_id)
list[].domain_name string Domain name
list[].user_id / user_name string Owner
list[].parsing_state int32 guard_configs.parsing_state (1=SRC / 2=NODE)
list[].dns_group_type int32 Majority vote of dns_records.group_type (source of truth, 1=SRC / 2=NODE)
list[].src_count int Records with group_type=1
list[].node_count int Records with group_type=2
list[].src_records[] array Source record summaries: subdomain / record_type / record_line / value / status
list[].node_records[] array Node record summaries: subdomain / record_type / record_line / value / status
list[].last_affair_status string Latest affair status (AffairsStatus_Start / AffairsStatus_Succeed / AffairsStatus_Faild)
list[].last_affair_ctime int64 Latest affair creation time (ms)
list[].last_affair_message string Latest affair message (HTML fragment)
total int64 Total

POST /api/guard/schedules/domains/{id}/switch-mode — Switch SRC/NODE

Authentication: guard.schedule.switch

Input parameters:

Field Type Required Description
path id string yes domain_id
body target_mode int32 yes 1=SRC / 2=NODE
body comment string no Affair note (written to dns_affairs.message)

Implementation notes: a single guard_db transaction SELECT FOR UPDATEs the domain's dns_records → updates group_type and switch_state=2 (switching) → inserts a dns_affairs row with status=AffairsStatus_Start. After commit, publishes NSQ Cmd=0.

Output fields: returns the newly created ScheduleAffairVO; the frontend should pin affairs_id for polling.


POST /api/guard/schedules/domains/{id}/init — Initialize parsing

Authentication: guard.schedule.init

Input parameters: path id (domain_id); optional body comment.

Implementation notes: reads source/dispatch settings from guard_domain_settings and node bindings from domain_node_ships, locks existing dns_records, deletes and recreates source/node records, then inserts a dns_affairs row and publishes NSQ Cmd=0 so zdns syncs once.

Output fields: returns the newly created ScheduleAffairVO.


POST /api/guard/schedules/domains/{id}/reset — Reset parsing

Authentication: guard.schedule.reset

Input parameters: path id (domain_id); optional body comment.

Implementation notes: every dns_records.switch_state is restored to 1 and status is rolled back to last_status; an affair is created and NSQ Cmd=0 is published.

Output fields: returns the newly created ScheduleAffairVO.


GET /api/guard/schedules/domains/{id}/records — List DNS records of a domain

Authentication: guard.schedule.records

Input parameters:

Field Type Description
path id string domain_id
group_type int32 1=SRC / 2=NODE
status int32 1=disabled / 2=enabled
page / size int Pagination

Output fields (data = ScheduleRecordsResp):

Field Type Description
list[] DnsRecordVO DNS record view
list[].record_id string Primary key
list[].associated_id string Upstream DNS provider record ID used to verify that the record is rebuilt or linked on the ZDNS/provider side
list[].domain / subdomain / value string Domain / subdomain / parsing value
list[].record_type int32 Internal DNS record type code
list[].record_line int32 Parsing line
list[].ttl int64 TTL (seconds)
list[].status / last_status int32 1=disabled / 2=enabled
list[].group_type int32 1=SRC / 2=NODE
list[].switch_state int32 1=ready / 2=switching
list[].ctime / utime int64 Timestamps (ms)

Read-only query against zdns_db.dns_records; no affair is created.


5.8.2 Record Scheduling

POST /api/guard/schedules/records/batch-status — Batch enable/disable records

Authentication: guard.schedule.batch

Input parameters (ScheduleBatchStatusReq):

Field Type Required Description
record_ids string[] yes Target record_id set
status int32 yes 1=disabled / 2=enabled
comment string no Affair note

Implementation notes: UPDATE dns_records SET last_status=status, status=? WHERE record_id IN (?); an affair is created and NSQ Cmd=0 is published.

Output fields: returns the newly created ScheduleAffairVO.


5.8.3 Affair Records

GET /api/guard/schedules/affairs — Affair list

Authentication: guard.schedule.affairs

Input parameters:

Field Type Description
page / size int Pagination
user_id string Filter by owner
status string AffairsStatus_Start / AffairsStatus_Succeed / AffairsStatus_Faild
ctime_from / ctime_to int64 Time range (ms)
domain_id string Filter by affected domain (implemented as content LIKE)

Output fields (data = ScheduleAffairListResp): see ScheduleAffairVO below.


GET /api/guard/schedules/affairs/{id} — Affair detail

Authentication: guard.schedule.affairs

Input parameters: path id (affairs_id).

Output fields (data = ScheduleAffairVO):

Field Type Description
affairs_id string Affair ID, format {ts}_{rand10}
user_id / user_name string Operator
status string AffairsStatus_Start / AffairsStatus_Succeed / AffairsStatus_Faild (string enum, matches the legacy MarshalJSON behaviour)
message string Affair message, may contain HTML snippets (e.g. <br>)
content string Affected domain_ids, comma separated
json_content object Extension JSON; carries outbox retry counters, etc.
affairs_oper string AffairsOperType_Page / AffairsOperType_Cron / AffairsOperType_Cli
ctime / utime int64 Created / updated (ms)

Polling tip: after triggering a write, poll this endpoint every 2-5s until status becomes Succeed or Faild; if it stays at Start for an extended period, the outbox is still retrying — render a "syncing" indicator.


5.9 WAF Rules /api/guard/waf/rules

A WAF rule group is mounted under a policy (policy_id). Each group contains a set of sub-rules (rules[]). The gateway matches a request against the zone / pattern fields, and on hit applies action (block / log / captcha).

⚠️ Status convention: status is unified as 1 = disabled, 2 = enabled (the S-6 fix aligned this with forwards / bwlist / schedules). The POST /status endpoint validates oneof=1 2.

⚠️ Path ID is tag: in PUT/DELETE /api/guard/waf/rules/{id}, {id} is the rule group's tag (uint64 primary key) — NOT rule_id (the business code). The frontend reads tag from the list response.

GET /api/guard/waf/rules — WAF rule group list

Authentication: guard.waf.list

Input parameters:

Field Type Description
page / size int Standard pagination
policy_id string Filter by policy (omit to return all rule groups visible to the current user)

Output fields (data.list[] = WafGroupVO):

Field Type Description
tag uint64 Rule group primary key (used as path {id})
rule_id int64 Business rule code
name string Rule group name
describe string Description
waf_type int32 WAF type internal code
sub_rule_condition int32 Sub-rule combination logic (0 = AND / 1 = OR, per model implementation)
scope string Scope (domain / path / empty = all)
action int32 1 = block, 2 = log, 3 = captcha
status int32 1 = disabled, 2 = enabled
policy_id string Owning policy ID
rules[] WafRuleVO Sub-rule list (each has zone / pattern / pattern_type / is_not)
ctime / utime int64 Timestamps (ms)

WafRuleVO (sub-rule) fields:

Field Type Description
tag uint64 Sub-rule primary key
rule_id int64 Business rule code
group_id int64 Owning rule group (joins WafGroupVO.tag / rule_id)
zone string Match zone (e.g. URL / ARGS / HEADER / BODY)
sub_field string Sub-field inside the zone (e.g. header name)
pattern_type int32 Match type internal code (exact / regex / contains, etc.)
pattern string Match expression
describe string Description
is_not bool true = negated match
ctime / utime int64 Timestamps (ms)

Visualization recommendation:


POST /api/guard/waf/rules — Create WAF rule group

Authentication: guard.waf.create

Input parameters (WafGroupCreateReq):

Field Type Required Example Description
name string yes "SQL injection guard" Rule group name
policy_id string yes "1" Owning policy
rule_id int64 no Business code; generated server-side if omitted
describe string no Description
waf_type int32 no WAF type internal code
sub_rule_condition int32 no Sub-rule combination logic
scope string no "*.example.com" Scope
action int32 no 1 1 = block / 2 = log / 3 = captcha
status int32 no 2 1 = disabled, 2 = enabled
rules[] object[] no Sub-rules (each = WafRuleReq, see below)

WafRuleReq (sub-rule body):

Field Type Description
rule_id int64 Business code; generated server-side if omitted
zone string Match zone
sub_field string Sub-field inside the zone
pattern_type int32 Match type
pattern string Match expression
describe string Description
is_not bool true = negated

Example:

curl -X POST https://waf.example.com/api/guard/waf/rules \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "SQL injection guard",
    "policy_id": "1",
    "describe": "OWASP Top 10 SQLi blacklist",
    "action": 1,
    "status": 2,
    "rules": [
      { "zone": "ARGS", "pattern_type": 2, "pattern": "union\\s+select", "describe": "SQLi: union select" }
    ]
  }'

Output fields: the new WafGroupVO (including the assigned tag and the persisted rules[]).

Fields that do not exist: a boolean enabled (use integer status) / description (use describe) / domains[] (scope is the scope string).


PUT /api/guard/waf/rules/{id} — Update WAF rule group

Authentication: guard.waf.edit

Input parameters (path id = rule group tag; body WafGroupUpdateReq, all fields optional, applied incrementally; rules[] is a full replace):

Field Type Description
name string Rule group name
describe string Description
waf_type *int32 Pointer type; omit to keep unchanged
sub_rule_condition *int32 Pointer type
scope string Scope
action *int32 Pointer type
rules[] object[] Full replace of sub-rules

This endpoint does not update status — use the dedicated PUT /api/guard/waf/rules/{id}/status for enable / disable.

Output fields: the updated WafGroupVO.


DELETE /api/guard/waf/rules/{id} — Delete WAF rule group

Authentication: guard.waf.delete

Input parameters: path id (rule group tag).

Implementation note: cascades to sub-rules (waf_rules.group_id = tag).

Output fields: data is null.


PUT /api/guard/waf/rules/{id}/status — Toggle WAF rule group enable / disable

Authentication: guard.waf.status

Input parameters (path id = rule group tag; body WafStatusReq):

Field Type Required Value Description
status int32 yes 1 / 2 1 = disabled, 2 = enabled; validated by oneof=1 2

Example:

curl -X PUT https://waf.example.com/api/guard/waf/rules/42/status \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "status": 1 }'

Output fields: data is null.


§6 Analytics

📊 Analytics · 18 paths (covering 80+ chart-key single-chart endpoints) · used to build WAF monitoring dashboards, operational reports, closed-loop alert handling
All /api/analytics/* paths are public; fields can only be added, paths cannot be modified or removed.
Each chart-key has a clearly recommended chart in the per-page sections below.

6.0 Common Calling Contract

Most Analytics endpoints are homogeneous chart queries. This chapter uses "common contract + index tables + special endpoint expansions".

Authentication & Permissions

Header: Authorization: Bearer <token> or Authorization: ApiKey zck_..., optionally with Accept-Language: zh-CN / en-US.

Permissions fall into three classes:

Class How decided Example
Page-level read-only analytics.<page>.view GET /api/analytics/overview/kpi requires analytics.overview.view
Special action Called out per row / sub-section POST /api/analytics/overview/export requires analytics.overview.export
Authenticated only No specific business permission GET /api/analytics/glossary

When using an API Key, the key's scopes must cover the required permission. For example, calling GET /api/analytics/access/status requires analytics.access.view in scope.

Single-chart GET template

curl -sS 'https://waf.example.com/api/analytics/access/status?window=last_24h&site_id=site-001' \
  -H "Authorization: ApiKey $ZCLOUD_API_KEY" \
  -H 'Accept-Language: en-US'

Returns the unified envelope. Chart data always uses the Chart Unified Contract (docs/specs/chart-contract.md). This is the only public contract for the new system; historical PostgreSQL tables, aggregate tables, and ES indexes are internal data sources only and do not affect request parameters or response shape.

Chart data always contains exactly these 5 fields; series / totals / kpis / points / list are not public top-level fields:

Field Type Description
chart_key string Identical to the requested <chart>
render_hint enum One of 8 vocab words: kpi / categorical_distribution / categorical_distribution_over_time / time_series_single / time_series_multi / topn / geo / table; the frontend selects its renderer accordingly
schema object Column metadata {dimensions:[{name,type,unit?,values?}], measures:[{name,type,unit?,format?}]}
rows array Tidy long table — one observation per row (no wide-pivoted columns); empty data returns []
meta object Debug fields {source, cache, latency_ms, partial?, available?, ...}

window.granularity is not a request parameter: clients pass window=last_24h; the backend automatically picks the 5m/1h/1d aggregation table based on the window size and echoes the chosen granularity in the response.

Batch template

For POST /api/analytics/batch and POST /api/analytics/<page>/batch.

curl -sS -X POST https://waf.example.com/api/analytics/overview/batch \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
        "time_window": "last_24h",
        "site_id": "",
        "domain_id": "",
        "compare": false,
        "charts": [
          { "key": "kpi" },
          { "key": "bandwidth" }
        ]
      }'

The batch response's data.data is keyed by chart-key; data.meta gives total elapsed, cache hit ratio, failed chart count, and effective window. If a single chart fails, inspect the partial / error fields on its node.


6.1 Glossary

GET /api/analytics/glossary — Statistics glossary

Purpose: returns the analytics glossary; the frontend renders tooltips ("what is QPS / block rate") from this map.

Authentication: authenticated session

Input parameters: none.

Output fields:

Field Type Description
data.terms map<string,string> key = term short name, value = localized explanation

Visualization recommendation:

Example response:

{
  "code": 0,
  "data": {
    "terms": {
      "qps": "Queries per second",
      "block_rate": "Block rate"
    }
  }
}

6.2 Cross-page Batch

POST /api/analytics/batch — Cross-page batch query

Purpose: load multiple chart-keys in one round-trip when initializing a dashboard, avoiding N single-chart GETs. The backend runs subqueries in parallel and merges responses.

Authentication: maps to analytics.<page>.view based on the page field.

Input parameters (request body):

Field Type Required Example Description
page string yes overview Must be one of: overview / access / protect / ai / bot / alert / health / ops / closure / cache
time_window string no last_24h Same as window
stime / etime int64 no 1746748800000 Custom timestamps
site_id string no Site filter
domain_id string no Domain filter
target_user_id string no Tenant-level switch viewed user
compare bool no false Enable previous-period comparison
charts[] array yes [{key:"kpi"}] At least 1 chart-key

logs (Phase 1 raw logs) and reports (Phase 4 report center) are independent groups and do not support batch.

Output fields:

Field Type Description
data.data map key = chart-key; value is always the 5-field Chart Unified Contract: {chart_key, render_hint, schema, rows, meta}
data.meta.elapsed_ms int Total elapsed
data.meta.cache_hit_ratio float Cache hit ratio (0-1)
data.meta.total_charts int Total charts requested
data.meta.failed_charts int Failed charts
data.meta.window object Effective time window

Visualization recommendation:

Example request/response:

// Request
{
  "page": "overview",
  "time_window": "last_24h",
  "compare": false,
  "charts": [
    { "key": "kpi" },
    { "key": "bandwidth" }
  ]
}
// Response
{
  "code": 0,
  "data": {
    "data": {
      "kpi": {
        "chart_key": "overview/kpi",
        "render_hint": "kpi",
        "schema": {
          "dimensions": [],
          "measures": [
            { "name": "domain_count", "type": "integer", "unit": "" },
            { "name": "requests",     "type": "integer", "unit": "requests" },
            { "name": "blocked",      "type": "integer", "unit": "events" },
            { "name": "block_rate",   "type": "percent", "unit": "%" },
            { "name": "qps",          "type": "float",   "unit": "qps" },
            { "name": "ai_detect",    "type": "integer", "unit": "events" }
          ]
        },
        "rows": [
          { "domain_count": 8, "requests": 12345, "blocked": 678, "block_rate": 5.49, "qps": 0.143, "ai_detect": 0 }
        ],
        "meta": { "source": "postgres", "cache": "miss", "latency_ms": 10 }
      },
      "event-type": {
        "chart_key": "overview/event-type",
        "render_hint": "categorical_distribution",
        "schema": {
          "dimensions": [{ "name": "event_type", "type": "string" }],
          "measures": [{ "name": "count", "type": "integer", "unit": "events" }]
        },
        "rows": [
          { "event_type": "sql_injection", "count": 1234 },
          { "event_type": "xss", "count": 567 }
        ],
        "meta": { "source": "elasticsearch", "cache": "miss", "latency_ms": 15, "partial": false }
      }
    },
    "meta": {
      "elapsed_ms": 22,
      "cache_hit_ratio": 0,
      "total_charts": 2,
      "failed_charts": 0,
      "window": { "stime": 1746662400000, "etime": 1746748800000, "granularity": "1h" }
    }
  }
}

Note: in the example above, kpi's measure names requests / blocked are the public contract truth names, aligned with the pkg/chart/contract source-of-truth structs. Underlying database field names are not exposed; the frontend reads values strictly by schema.measures[].name.


POST /api/analytics/{page}/batch — Page-level batch

Purpose: equivalent to POST /api/analytics/batch, but page is fixed by URL (more direct for fixed-page frontends).

Authentication: maps to analytics.<page>.view based on {page}.

Supported pages:

URL Permission
POST /api/analytics/overview/batch analytics.overview.view
POST /api/analytics/access/batch analytics.access.view
POST /api/analytics/protect/batch analytics.protect.view
POST /api/analytics/ai/batch analytics.ai.view
POST /api/analytics/bot/batch analytics.bot.view
POST /api/analytics/alert/batch analytics.alert.view
POST /api/analytics/health/batch analytics.health.view
POST /api/analytics/ops/batch analytics.ops.view
POST /api/analytics/closure/batch analytics.closure.view
POST /api/analytics/cache/batch analytics.cache.view

Input/Output: identical to POST /api/analytics/batch. Callers don't need to send page in the body; if they do, the path's page name wins.

Visualization recommendation: same as above.


6.3 Single-chart GET entry

GET /api/analytics/{page}/{chart} — Single-chart common entry

Purpose: fetch a single chart-key's data. {page} follows the batch enum; {chart} follows the chart-key index per page section.

Authentication: maps to analytics.<page>.view (a few special charts use independent permissions; see per-section).

Input parameters: see §A Common Query Parameters.

Output fields: unified envelope + data:

Visualization recommendation: the frontend dispatches each chart by render_hint; complex chart columns are defined by schema.


6.4 Overview page

Use case: WAF protection monitoring dashboard home — KPIs + trends + rankings + map.

All GET endpoints below use the §6.0 Single-chart GET template, §A Common Query Parameters, and the Chart Unified Contract response shape.

API chart-key render_hint Recommended chart Description
GET /api/analytics/overview/kpi kpi kpi KpiGroupCard (6 measures) Site count / requests / blocked / block_rate / qps / ai_detect
GET /api/analytics/overview/bandwidth bandwidth time_series_multi Line chart (dual Y-axis) Total bandwidth + origin bandwidth time-series
GET /api/analytics/overview/request-attack request-attack time_series_multi Line chart (two series) Requests vs attacks
GET /api/analytics/overview/event-type event-type categorical_distribution Pie / donut Event type distribution (dim=event_type, measure=count)
GET /api/analytics/overview/waf-type waf-type categorical_distribution Pie / donut WAF hit type distribution
GET /api/analytics/overview/geo geo geo Map heatmap (CN/world) Attack geo distribution
GET /api/analytics/overview/top-domains top-domains topn Horizontal bar / table Top 5 attacked domains
GET /api/analytics/overview/recent-events recent-events table Timeline / table (50 rows) Recent WAF events

chart-key data shape

kpi (render_hint = kpi)

schema.measures contains 6 entries:

measure name type unit Description
domain_count integer (empty) Site count
requests integer requests Total requests in window
blocked integer events Total blocked events in window
block_rate percent % block_rate = blocked / requests
qps float qps QPS = requests / window seconds
ai_detect integer events AI detection count (currently fixed 0; later connected to ES real data)

rows (single row): [ { domain_count, requests, blocked, block_rate, qps, ai_detect } ].

Public field names: requests / blocked are API contract field names. If the underlying database still uses historical fields such as request_today / attack_today, the backend converts them in the service layer and never exposes them to callers.

Recommended chart: KpiGroupCard (6 KPI cards). Render block_rate as a percentage with a progress bar; qps with a mini sparkline.


bandwidth / request-attack — Time-series two-series

Output: [{ctime: int64ms, bandwidth: float, origin_bandwidth: float}, ...] or [{ctime, requests, attacks}, ...].

Recommended chart: line chart with ctime on X-axis and two-series Y-axis.


event-type (render_hint = categorical_distribution)

schema part Content
dimensions [{ name: "event_type", type: "string" }]
measures [{ name: "count", type: "integer", unit: "events" }]

rows (long table): [ { event_type: "sql_injection", count: 1234 }, { event_type: "xss", count: 567 }, ... ].

Recommended chart: PieCard (≤8 categories — auto pie) / BarCard (>8 categories — auto horizontal bar); the frontend dispatches by the categorical_distribution vocab word.


waf-type — Dimension distribution

Output: [{key: string, count: int}, ...].

Recommended chart: pie (≤ 8 categories) or donut.


geo — Geo heatmap

Output: [{region: string, count: int}, ...]. region is country/province name.

Recommended chart: map heatmap (CN map + world map).


top-domains — Domain ranking

Output: [{host: string, attack_count: int}, ...], ordered by attack_count DESC, ≤ 5.

Recommended chart: horizontal bar.


recent-events — Recent events stream

Output: [{ctime, domain, attack_type, severity, ...}, ...], ≤ 50.

Recommended chart: timeline / table (sorted by ctime desc), clickable for detail.


POST /api/analytics/overview/export — Overview export

Purpose: export KPIs and chart snapshots as CSV or JSON for offline analysis or reporting. Returns raw file stream, not wrapped in envelope.

Authentication: analytics.overview.export

Input parameters (request body):

Field Type Required Example Description
format string yes csv / json Export format
window string no last_24h Time window
charts[] array yes [{key:"kpi"}] Chart-keys to export

Output: returns the file stream directly with Content-Type: text/csv or application/json and Content-Disposition: attachment.

Visualization recommendation: not a chart; triggers a download.


6.5 Access page

Use case: traffic and quality dashboard — requests, traffic, cache hit, status codes, latency distribution, ISP, top IP/URL, geo.

API chart-key render_hint Recommended chart Description
GET /api/analytics/access/request-hm request-hm time_series_single (no compare) / time_series_multi (compare) LineCard Requests trend; with compare=true uses sub-shape B (period enum dim distinguishes current/previous)
GET /api/analytics/access/flow-hm flow-hm time_series_multi LineCard multi-series 5 measures: total_bytes / request_bytes / response_bytes / upstream_send / upstream_receive
GET /api/analytics/access/cache-hm cache-hm time_series_multi Line chart (dual Y-axis) Cache hit count + cache bytes trend
GET /api/analytics/access/bandwidth bandwidth time_series_multi LineCard multi-series 4 measures: bandwidth / origin_bandwidth / up_bandwidth / down_bandwidth
GET /api/analytics/access/status status categorical_distribution_over_time StackedBarCard 4 HTTP status classes (dim=status_class enum["2xx","3xx","4xx","5xx"] + time) stacked over time
GET /api/analytics/access/flow-duration flow-duration time_series_multi Line chart (3 percentiles) Request latency P50/P95/P99 (D4: real-time only when window ≤ 24h)
GET /api/analytics/access/isp isp categorical_distribution Pie ISP distribution (mobile/unicom/telecom/other)
GET /api/analytics/access/top-ip top-ip topn Table / horizontal bar (with geo) Top access IPs (default 10, configurable via top)
GET /api/analytics/access/top-url top-url topn Table / horizontal bar Top URLs; sortable via order=bytes_desc/cache_desc
GET /api/analytics/access/geo geo geo Map heatmap Access geo distribution

chart-key data shape

request-hm

Without compare (render_hint = time_series_single):

schema part Content
dimensions [{ name: "time", type: "timestamp", unit: "ms" }]
measures [{ name: "requests", type: "integer", unit: "requests" }]

rows: [ { time: 1746748800000, requests: 1234 }, ... ].

With compare (render_hint = time_series_multi sub-shape B: 1 categorical + 1 ts + 1 measure):

schema part Content
dimensions [{ name: "period", type: "enum", values: ["current","previous"] }, { name: "time", type: "timestamp", unit: "ms" }]
measures [{ name: "requests", type: "integer", unit: "requests" }]

rows: [ { period: "current", time: ..., requests: ... }, { period: "previous", time: ..., requests: ... }, ... ] long table; pivots into 2 series by period.

Recommended chart: LineCard; in compare mode the frontend pivots by period and renders solid + dashed lines automatically.


flow-hm (render_hint = time_series_multi)

schema part Content
dimensions [{ name: "time", type: "timestamp", unit: "ms" }]
measures 5 entries: total_bytes / request_bytes / response_bytes / upstream_send / upstream_receive (type=integer, unit=bytes, format=iec)

rows: [ { time: ..., total_bytes: ..., request_bytes: ..., response_bytes: ..., upstream_send: ..., upstream_receive: ... }, ... ].

Recommended chart: LineCard multi-series (5 lines) / stacked area.


cache-hm — Cache trend

Output: [{ctime, cache_count, cache_bytes, cache_response}, ...].

Truth fields (D8): underlying data uses total_cache_count / total_cache_bytes / total_cache_response_bytes; the response maps these to cache_count / cache_bytes / cache_response.

Recommended chart: dual Y-axis line (left axis count, right axis bytes).


bandwidth (render_hint = time_series_multi)

schema part Content
dimensions [{ name: "time", type: "timestamp", unit: "ms" }]
measures 4 entries: bandwidth / origin_bandwidth / up_bandwidth / down_bandwidth (type=float, unit=bps)

rows: [ { time: ..., bandwidth: ..., origin_bandwidth: ..., up_bandwidth: ..., down_bandwidth: ... }, ... ].

Recommended chart: LineCard multi-series (4 lines).


status (render_hint = categorical_distribution_over_time)

schema part Content
dimensions [{ name: "status_class", type: "enum", values: ["2xx","3xx","4xx","5xx"] }, { name: "time", type: "timestamp", unit: "ms" }]
measures [{ name: "count", type: "integer", unit: "requests" }]

rows tidy long table (no wide-pivoted c2xx/c3xx/c4xx/c5xx columns):

[
  { "status_class": "2xx", "time": 1746748800000, "count": 1200 },
  { "status_class": "3xx", "time": 1746748800000, "count": 30 },
  { "status_class": "4xx", "time": 1746748800000, "count": 8 },
  { "status_class": "5xx", "time": 1746748800000, "count": 0 },
  { "status_class": "2xx", "time": 1746752400000, "count": 1340 },
  ...
]

Each timestamp must cover all 4 status_class values (fill missing with count: 0 — the stacked bar chart depends on a complete categorical grid).

Recommended chart: StackedBarCard (stacked time-series by status_class). The frontend dispatches by the categorical_distribution_over_time vocab word.


flow-duration — Latency percentiles

Output: {p50, p95, p99} or time-series array.

Truth boundary (D4): percentile fields are computed by ES in real time only when the window ≤ 24h; longer windows return available:false.

Recommended chart: line chart (3 percentile lines) or 3 KPI cards.


isp — ISP distribution

Output: {mobile, unicom, telecom, other}.

Recommended chart: pie (4 slices).


top-ip — Top IPs

Output: [{remote_addr, count, country, region, isp}, ...], ≤ top.

Recommended chart: table (with country flag + region + ISP); or horizontal bar.


top-url — Top URLs

Output: [{url, request_count, request_bytes, cache_bytes}, ...].

Recommended chart: table by default; or horizontal bar (switchable by bytes/cache).


geo — Geo heatmap

Output: [{region, count}, ...].

Recommended chart: map heatmap.


6.6 Protect page

Use case: hit analysis for the three protection engines — WAF / CC / DDoS.

API chart-key render_hint Recommended chart Description
GET /api/analytics/protect/overview overview kpi Multi-KPI cards WAF/CC/DDoS totals summary (time-series provided by independent statistics charts)
GET /api/analytics/protect/waf/statistics waf/statistics time_series_multi Line WAF hit trend (waf hits + total attacks)
GET /api/analytics/protect/waf/types waf/types categorical_distribution Pie / horizontal bar WAF hit type distribution (SQL injection / XSS / scanner...)
GET /api/analytics/protect/waf/top-ip waf/top-ip topn RankingCard / horizontal bar dim=ip(string), measure=count(events); country/province go to meta.row_extras
GET /api/analytics/protect/waf/geo waf/geo geo GeoHeatmapCard dim=country(geo), measure=count(events); provinces temporarily stored in meta.provinces
GET /api/analytics/protect/cc/statistics cc/statistics time_series_single Line CC hit trend
GET /api/analytics/protect/cc/top-ip cc/top-ip topn Table / horizontal bar Top CC attack IPs
GET /api/analytics/protect/cc/geo cc/geo geo Map heatmap CC attack geo
GET /api/analytics/protect/cc/top-url cc/top-url topn Table Top CC attack URLs
GET /api/analytics/protect/ddos/statistics ddos/statistics time_series_multi Line (dual Y-axis) DDoS event count + peak bandwidth time-series
GET /api/analytics/protect/ddos/types ddos/types categorical_distribution Pie / horizontal bar DDoS attack type distribution (syn flood / udp flood / ...)
GET /api/analytics/protect/ddos/top-ip ddos/top-ip topn Table Top DDoS source IPs (with peak bandwidth)

chart-key data shapes (key differences)

overview: render_hint = kpi, rows = [{waf, cc, ddos_bytes}] (single-row summary, dimensions=[], measures=waf/cc/ddos_bytes). Recommend 3 KPI cards; time-series is provided by the independent protect/waf/statistics and protect/ddos/statistics charts.

waf/statistics: render_hint = time_series_multi, rows = [{time, waf, attack_count}, ...] tidy long table (time=ms timestamp, two measures for two lines). Recommend line chart (two series).

waf/types / ddos/types: render_hint = categorical_distribution, rows = [{waf_type, count}, ...] / [{ddos_type, count}, ...] tidy long table, pie or horizontal bar.

waf/top-ip (render_hint = topn)

schema part Content
dimensions [{ name: "ip", type: "string" }]
measures [{ name: "count", type: "integer", unit: "events" }]

rows: [ { ip: "1.2.3.4", count: 1234 }, { ip: "5.6.7.8", count: 567 }, ... ], sorted DESC by count. Extra columns like country / province go into meta.row_extras (the frontend reads on demand; not declared as schema columns).

Recommended chart: RankingCard / horizontal bar.


ddos/top-ip: top IPs with geo, recommend tables with country flag.

waf/geo (render_hint = geo)

schema part Content
dimensions [{ name: "country", type: "geo" }]
measures [{ name: "count", type: "integer", unit: "events" }]

rows: [ { country: "China", count: 1234 }, { country: "US", count: 567 }, ... ].

Province data is temporarily stored in meta.provinces (shape [{province, count}]). The frontend GeoHeatmapCard uses rows for the main map and meta.provinces for province-level drill-down; if province-level analytics needs its own chart later, add a dedicated protect/waf/geo-provinces chart-key.

Recommended chart: GeoHeatmapCard (CN / world map heatmap).


cc/geo: render_hint = geo, rows = [{country, count}, ...], map heatmap.

ddos/statistics: render_hint = time_series_multi, rows = [{time, events, bandwidth}, ...] tidy long table (events=integer events, bandwidth=float bytes/iec). Recommend dual Y-axis line.


6.7 AI page

AI page paths are public. /logs uses an independent permission analytics.ai.logs; others use analytics.ai.view.
Some charts return a BatchChartResult placeholder (rows: [], meta.available = false, meta.reason explains why). Paths and contracts are stable; the backend may enable real data later without changing paths.

API chart-key Recommended chart Description
GET /api/analytics/ai/attack-trend attack-trend Line AI attack trend
GET /api/analytics/ai/top-ip top-ip Table / horizontal bar Top AI hit IPs
GET /api/analytics/ai/top-url top-url Table Top AI hit URLs
GET /api/analytics/ai/detection detection KPI cards / radar AI detection capability panel
GET /api/analytics/ai/test-results test-results Table / bar AI test results
GET /api/analytics/ai/logs logs Table (paged) AI hit logs (independent permission analytics.ai.logs)

Placeholder responses: not-yet-upgraded charts return a standard BatchChartResult (all 5 fields present, rows: [], meta.available = false, meta.reason explains why); the frontend should render an "empty" placeholder card, not fake data.


6.8 Bot page

Use case: identify and analyze bot/crawler traffic.

API chart-key Recommended chart Description
GET /api/analytics/bot/statistics statistics KPI (6) Bot requests / sessions / IPs / known/unknown bot overview (trend line is served by an independent chart; not rendered until split)
GET /api/analytics/bot/advance-warn advance-warn Table Bot warning list
GET /api/analytics/bot/browser browser Pie Browser distribution (chrome/safari/firefox/edge/wechat/other)
GET /api/analytics/bot/operating operating Pie OS distribution (android/ios/windows/mac/other)
GET /api/analytics/bot/geo geo Map heatmap Bot geo
GET /api/analytics/bot/top-agent top-agent Table / horizontal bar Top User-Agents
GET /api/analytics/bot/top-ip top-ip Table (with geo) Top bot IPs
GET /api/analytics/bot/scatter scatter Scatter Bot warning scatter (X=ctime / Y=top_visit_count, size=top_ip_count)
GET /api/analytics/bot/sessions sessions Table (paged) Bot session list (independent permission analytics.bot.session)
GET /api/analytics/bot/sessions/{sid} - Timeline Single bot session timeline detail (independent permission analytics.bot.session)

chart-key data shapes

statistics: render_hint = kpi, rows = [{requests, sessions, ips, known_bot, unknown_bot, req_per_session}] (single-row summary, 6 measures). Recommend 6 KPI cards; requests / sessions trend is served by an independent chart (currently not split; add bot/sessions-trend when needed).

browser: render_hint = categorical_distribution, rows = [{browser, count}, ...] (6 enum rows: chrome/safari/firefox/edge/wechat/other), pie.

operating: render_hint = categorical_distribution, rows = [{os, count}, ...] (5 enum rows: android/ios/windows/mac/other), pie.

scatter: render_hint = table (no native scatter hint; table fallback), rows = [{time, session_id, top_visit_count, top_ip_count, top_ua_count}, ...]; the frontend interprets X / Y / size to render the scatter chart.

sessions/{sid}: render_hint = table, rows = [{time, uri, remote_addr, method, status}, ...] (5 trimmed fields), the access timeline of that session; returns empty when no session_id is passed (via the order query parameter).


6.9 Alert page

Use case: alert overview + list + detail + acknowledgment.

API Method chart-key Recommended chart Description
GET /api/analytics/alert/total GET total KPI card Total alerts
GET /api/analytics/alert/hm GET hm Line / heatmap Alert trend (count aggregated by ctime)
GET /api/analytics/alert/types GET types Pie Alert type distribution (by policy_type)
GET /api/analytics/alert/domains GET domains Horizontal bar / table Alert domain ranking
GET /api/analytics/alert/list GET list Table (paged) Alert list
GET /api/analytics/alert/{id} GET - Detail card Single alert detail (with closure metadata)
PATCH /api/analytics/alert/{id}/ack PATCH - Not a chart Acknowledge specified alert

GET /api/analytics/alert/{id} output fields

Field Type Description
id int Alert ID
uuid string Alert UUID
policy_type string Alert type
title / body string Alert title / body
domain / domain_id string Associated domain
status int Alert status (0/1/2/3, see D10)
ctime int64 Creation time
last_update_timestamp int64 Last update time
process_uid string Closer (D10 truth field)
process_time int64 Closure time (D10 truth field)
user_id string Owner

Non-super-admin is filtered by user_id; cross-tenant reads return 404. Permission analytics.alert.view.

PATCH /api/analytics/alert/{id}/ack request

Authentication: analytics.alert.ack

Input: path id; body may be empty.

Output: data is null.


6.10 Health (Phase 3)

Use case: business availability + origin quality + slow URI + geo quality.

API chart-key Recommended chart Description
GET /api/analytics/health/summary summary KPI cards (6) c2xx/c3xx/c4xx/c5xx/n4xx/n5xx totals
GET /api/analytics/health/status-breakdown status-breakdown Stacked time-series bar Three-layer c/n/a status code breakdown (c=client, n=gateway, a=app)
GET /api/analytics/health/origin-errors origin-errors Table / horizontal bar Origin error ranking (ES upstream_addr terms, only upstream_status >= 500)
GET /api/analytics/health/origin-latency origin-latency Line (3 series) Origin latency (mobile/unicom/telecom average)
GET /api/analytics/health/slow-uri slow-uri Table Slow URI Top (initially ranked by hit count; slow ranking will use real latency data after it is connected)
GET /api/analytics/health/availability availability KPI / multi-line HTTP/Ping/DNS/TCP/Page/IPv6 availability + downtime
GET /api/analytics/health/geo-isp-quality geo-isp-quality Table / map Geo / ISP quality

Truth boundary: percentile / p50 / p95 / p99 fields exist in neither chart nor statistic packages; only windows ≤ 24h get real-time ES percentile (D4). The availability table m_ava_domain.*AvailableDomain keys on domain_or_ip (not domain_id).


6.11 Ops (Phase 5)

Use case: platform-level operational view — high-traffic / high-error users and domains, origin errors, node capacity.

Permission boundary: analytics.ops.view is read-only across the platform; does not allow passing target_user_id to switch view; only analytics.ops.admin can. Regular tenant accounts cannot access ops even with view.

API chart-key Recommended chart Description
GET /api/analytics/ops/summary summary KPI cards (6) Platform capacity overview (requests/bytes/peak bandwidth/origin bandwidth/active domains/active users)
GET /api/analytics/ops/traffic-users traffic-users Horizontal bar / table Top traffic users (by total_bytes DESC)
GET /api/analytics/ops/traffic-domains traffic-domains Horizontal bar / table Top traffic domains
GET /api/analytics/ops/error-users error-users Table Top error users (by c5xx DESC, with c4xx/n5xx)
GET /api/analytics/ops/error-domains error-domains Table Top error domains
GET /api/analytics/ops/origin-errors origin-errors Table Origin error ranking
GET /api/analytics/ops/nodes nodes Table / topology Node / data-center view (RPC GetWafIpWithMachineRoom + ES server_addr/bind_addr)
GET /api/analytics/ops/query-pressure query-pressure Line (placeholder) ES query pressure (depends on instrumentation; returns available:false until metrics are connected)

Truth boundary: node system metrics (CPU/memory/disk) charts do not exist; use external Zabbix. Not implemented this iteration. Fields node_id / server_node are forbidden.


6.12 Closure (Phase 6)

Use case: handle alerts and risk queues on one page; bulk acknowledgment supported.

Truth fields (D10): process_uid / process_time / status / level. Forbidden: handle_user / handle_time / risk_score / alert_status. AlertRecord.status (0/1/2/3) and RiskRecord.status (1/2) have different semantics — frontend i18n keys must not be shared.

API Method chart-key Recommended chart Description
GET /api/analytics/closure/summary GET summary KPI cards (5) Pending alerts/risks + handled today + average handle time (ms)
GET /api/analytics/closure/alerts GET alerts Table (paged) Pending alert queue (status=0)
GET /api/analytics/closure/risks GET risks Table (paged) Pending risk queue (connects to RiskRecord later; returns an empty list when no data is available)
GET /api/analytics/closure/trend GET trend Line / stacked bar Closure history trend (group by ctime + status)
POST /api/analytics/closure/alerts/confirm POST - Not a chart Bulk acknowledge alerts (proxies /api/alert/records/confirm)
POST /api/analytics/closure/risks/confirm POST - Not a chart Bulk acknowledge risks (proxies /api/chart/risk/events/:event_id/confirm)

summary output

{
  "alerts_pending": 12,
  "alerts_handled_today": 8,
  "risks_pending": 0,
  "risks_handled_today": 0,
  "avg_handle_time_ms": 0
}

confirm request body

{
  "ids": ["a1", "a2", "a3"],
  "remark": "added to blacklist"
}

Authentication: analytics.closure.confirm

Output: data is null.


6.13 Cache (Phase 6)

Use case: analyze the value of CDN/edge cache in saving origin bandwidth.

Truth fields (D8): total_cache_count / total_cache_bytes / total_cache_response_bytes. Forbidden: single-field names cache_count / cache_bytes / cache_hit (do not exist).

API chart-key Recommended chart Description
GET /api/analytics/cache/summary summary KPI cards (4) Hit rate / saved origin bytes / hit count / average cache object size
GET /api/analytics/cache/trend trend Line (dual Y-axis) Hit rate trend (request count vs cache hit count)
GET /api/analytics/cache/top-uri top-uri Table / horizontal bar URI Top (hit count / hit rate / saved bytes)
GET /api/analytics/cache/content-types content-types Pie Content type distribution (response_content_type aggregation)

summary output

Field Type Description
hit_rate float Cache hit rate = total_cache_count / request_count
saved_response_bytes int Saved origin bandwidth (bytes the origin would have served if not cached)
total_cache_count int Hit count
total_cache_bytes int Cached bytes
avg_object_bytes float Average cache object size = total_cache_bytes / total_cache_count
request_count int Total requests

Business metrics:


6.14 Raw Logs (Phase 1)

Raw logs are detail queries, not single-chart endpoints; they share the same authentication, but requests/responses are list / detail / export.

API Method Permission Recommended chart Description
GET /api/analytics/logs GET analytics.logs.view Table (paged + 12-field filters) Paged query of raw access/attack logs
GET /api/analytics/logs/{uuid} GET analytics.logs.view Detail card (7 sections) Single log detail (basic / request / response / upstream / protection / waf_detail / ai_detail)
POST /api/analytics/logs/export POST analytics.logs.export Not a chart Field-whitelist export (csv/json, size ≤ 10000; larger goes async)

GET /api/analytics/logs filter fields

12 filters: uuid / session_id / remote_addr / host / uri / method / status / z_final_action / z_final_type / z_final_mod / z_final_action_type / z_white.

Truth boundary: z_final_action is an integer enum 0=pass / 1=block / 2=captcha; z_white is an independent bool (whitelist hit), not part of the action enum. Forbidden: match_content / match_area / hit_rule / rule_desc (exist in neither chart nor statistic packages).

Fallback response

The stub returns:

{ "available": false, "reason": "Raw log ES query pending..." }

The list / detail / export contracts are stable; ES zcloud-access-* will be connected without changing the public contract.


6.15 Reports (Phase 4)

The report center is for templates / generation / download; it does not use the single-chart response shape. List and detail still use the unified envelope; download returns a file stream.

API Method Permission Recommended chart Description
GET /api/analytics/reports/templates GET analytics.reports.view Table / card grid Template list (with platform_only flag)
GET /api/analytics/reports GET analytics.reports.view Table (paged) Report history list
GET /api/analytics/reports/{id} GET analytics.reports.view Detail card Single report detail (status, params, artifact URL)
POST /api/analytics/reports/generate POST analytics.reports.generate Not a chart Trigger generation (platform-summary requires analytics.reports.platform)
GET /api/analytics/reports/{id}/download GET analytics.reports.download Not a chart Download artifact (pdf/csv/json/html)

Template enum (D7): protection-value / asset-risk / attack-source / business-health / platform-summary (platform ops / super-admin only) / raw-log-export.

Async threshold: estimated rows ≤ 100k goes synchronous; larger forces async with a task_id; the frontend polls /reports/:id for status + download URL. Sync generation times out after 30s and then degrades to async.

generate request body

{
  "template": "protection-value",
  "format": "pdf",
  "window": "last_30d",
  "stime": 1735660800000,
  "etime": 1738339200000,
  "filters": {}
}
Field Type Required Description
template string yes Template name (D7 closed enum)
format string yes pdf / csv / json / html
window string no Time window alias
stime / etime int64 no Custom timestamps
filters object no Template-specific filters

6.16 CLI Mapping

All Analytics APIs are wrapped by zcloud analytics commands:

# Original 6 pages
zcloud analytics overview kpi --format json
zcloud analytics access status --window last_24h --format json
zcloud analytics protect waf/types --format json
zcloud analytics ai logs --page 1 --size 20 --format json
zcloud analytics bot session <session-id> --format json
zcloud analytics alert ack <alert-id>

# 2026-04-30 chart-rebuild 6 phase extension (13 new commands)
zcloud analytics health summary --window last_24h --format json
zcloud analytics ops traffic-users --top 20 --format json
zcloud analytics closure summary --format json
zcloud analytics cache summary --format json
zcloud analytics logs list --window last_24h --status 403 --format json
zcloud analytics logs detail req-abc123 --format json
zcloud analytics logs export --format csv --fields ctime,uuid,host,uri,status > logs.csv
zcloud analytics closure alerts confirm --ids a1,a2,a3
zcloud analytics closure risks confirm --ids ev_001,ev_002
zcloud analytics reports templates --format json
zcloud analytics reports list --format json
zcloud analytics reports describe r-001 --format json
zcloud analytics reports generate --template protection-value --window last_30d --format pdf
zcloud analytics reports download r-001 --output report.pdf

When new Analytics APIs are added, the CLI mapping must be added or confirmed in sync; for new chart-keys, at least the CLI chart-key list and docs must be updated.


§7 Plan Catalog

External exposure scope: Only the two read-only endpoints below are exposed for external integration. Plan create/edit/delete, user assignment, subscription queries, and all order lifecycle operations (renew / change / refund / audit — /api/plan/orders/*) are platform-console admin operations that write the live shared billing tables — they are NOT part of the external API/CLI surface (platform ops only, via console + RBAC).

GET /api/plan/plans — Plan list (paginated)

Query the plan catalog with optional product-type and keyword filters, paginated.

Required permission: plan.plan.list

Query parameters

Parameter Type Required Default Description
page int No 1 Page number (1-based)
size int No 20 Page size
prod_type int No 0 (all) Product type filter: 2=WAF · 4=Monitor · 32=GFIP
keyword string No Fuzzy match on plan name

Response data shape

{
  "list": [ { "plan_id": "...", "name": "Basic", "prod_type": 2, "price": 99.00, "valid": 365, "level": 1, "open_status": true, ... } ],
  "total": 10,
  "page": 1,
  "size": 20
}

Examples

# Bearer Session
curl -H "Authorization: Bearer $TOKEN" \
  "$API/api/plan/plans?prod_type=2&keyword=basic&page=1&size=20"

# API Key
curl -H "Authorization: ApiKey zck_prefix.secret" \
  "$API/api/plan/plans?prod_type=2"

GET /api/plan/plans/{id} — Plan detail

Retrieve a single plan's full details by ID (including the content quota JSON).

Required permission: plan.plan.view

Path parameters

Parameter Type Required Description
id string Yes Plan ID (UUID)

Response data fields (PlanVO)

Field Type Description
plan_id string Plan unique ID (UUID)
name string Plan name
content object Quota JSON (fields differ per prod_type)
price float Price (2 decimal places)
scene string Applicable scenario description
comment string Remark
open_status bool Whether publicly listed for purchase
valid int64 Validity period (days)
effect int32 Effect mode
level int64 Plan level
creator_id string Creator user ID
ctime int64 Creation time (Unix ms)
utime int64 Update time (Unix ms)
version string Plan source version (cloud / zmod)
prod_type int32 Product type (2=WAF 4=Monitor 32=GFIP)

Example

curl -H "Authorization: Bearer $TOKEN" \
  "$API/api/plan/plans/550e8400-e29b-41d4-a716-446655440000"

Equivalent CLI commands

zcloud plan list --prod-type 2 --keyword basic
zcloud plan describe <plan_id>

§8 Node Install / Upgrade

Purpose: one-line install / upgrade of skynet-node on a protection-node host. Two sides: the management plane (platform session + RBAC) registers packages, mints one-time commands, queries jobs, and revokes tokens; the installer side (install-token only) fetches the script, downloads package/env, and reports results.
Backend: src/backend/internal/node/{handler,service,repo}/install.go, routes in src/backend/internal/node/route.go, reaper in src/backend/internal/app/install_reaper.go.

8.0 Auth & status codes

Dimension Management endpoints Installer-side endpoints
Paths /install/artifacts /commands /upgrades /jobs /tokens/:id/revoke /install/script /package /env /report
Auth Platform session (Bearer Session / API Key) + RBAC Only Authorization: Bearer <install_token>
RBAC action (perms.NodeNode) artifact / install / upgrade / job / revoke none (token self-authenticates)
Auth failure 401 / 403 (platform envelope) 401 with WWW-Authenticate: Bearer realm="node-install" (challenge)

Key points:

8.1 POST /api/node/install/artifacts — register a local package + precheck

Registers a package already present on the backend host, scans it, computes sha256, and runs prechecks.

curl -sS -X POST https://<cloud>/api/node/install/artifacts \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"skynet-node-1.0.0.tar.gz","version":"1.0.0","package_path":"/data/artifacts/skynet-node-1.0.0.tar.gz"}'

Constraints:

Response (excerpt — package_path returns the file name only, never the backend absolute path):

{"code":0,"data":{
  "artifact_id":"8f1c…","name":"skynet-node-1.0.0.tar.gz","version":"1.0.0",
  "sha256":"…64hex…","status":"ready",
  "precheck":{"passed":true,"checks":[
    {"key":"required_env","severity":"error","passed":true,"message":"required env keys found"},
    {"key":"binary_version_drift","severity":"warning","passed":true,"message":"…"}
  ]}
}}

GET /api/node/install/artifacts lists registered packages; package_path there is also the file name only, and frontends/examples must not show the real backend absolute path.

8.2 POST /api/node/install/commands · POST /api/node/install/upgrades · POST /api/node/install/uninstalls — mint a one-time command

curl -sS -X POST https://<cloud>/api/node/install/commands \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
        "artifact_id":"8f1c…",
        "node_id":"optional-target-node-UUID",
        "server_url":"https://<cloud>",
        "ttl_seconds":3600,
        "max_uses":20,
        "env":{
          "ZCLOUD_NGX_ACCESS_TOPIC":"cloud/ngx",
          "ZCLOUD_CC_TOPIC":"cc/sync",
          "ZCLOUD_SYNC_TOPIC":"zcloud-sync",
          "ZCLOUD_DELTA_TOPIC":"zcloud-delta",
          "ZCLOUD_BLOCK_TOPIC":"zcloud-block"
        }
      }'

The upgrade command uses /upgrades, equivalent to action=upgrade, with the same body.

The uninstall command uses /uninstalls, equivalent to action=uninstall, with the same body (same as install/upgrade: artifact_id required, server_url required, env/ttl_seconds/max_uses optional). Like upgrade, it targets an existing node and node_id is required (omitting it → 400 "卸载必须指定目标节点"). The response is likewise a one-time command (curl … | sudo bash one-liner; the plaintext token appears only once). Permission: node.node.uninstall.

⚠️ Destructive, irreversible: the bootstrap decides which package script to run from the server-authoritative X-Install-Action response header returned by GET /api/node/install/package (its value is the action bound to the token's job — here uninstall), running the package's uninstall.sh instead of install.sh and auto-answering its interactive [y/N] confirm via a here-string. uninstall.sh stops and removes all of the node's on-host protection services (nginx / agent / waf-spoa, etc.) and their data directories; the operation cannot be undone.

Scope: uninstall only removes the node's on-host services; it does not delete the node record from the cloud node list. To also drop the node record, the operator separately calls DELETE /api/node/nodes/:id.

Response:

{"code":0,"data":{
  "job_id":"…","token_id":"…","token_prefix":"nit_xxxxxxx",
  "expires_at":1735900000000,
  "command":"curl -fsSL --connect-timeout 10 --max-time 60 -H 'Authorization: Bearer nit_…' 'https://<cloud>/api/node/install/script' | sudo bash -s -- --token 'nit_…' --server 'https://<cloud>'"
}}

Constraints / behavior:

8.3 Installer side: script / package / env / report

The script fetched & run by command (GET /install/script) does: set -euo pipefail + temp-dir cleanup on exit → download package & env with bounded --connect-timeout/--max-time/--retry → verify X-Artifact-SHA256 matches local sha256sum (mismatch → report failed and exit) → overwrite the package's env.conf with the cloud env → if the registration env contains AGENT_PORT, inject it into the extracted install*.sh (changing the agent listen/register port from the default 33020; only the extracted copy is patched, never the verified package) → run the package's install.sh → report running / success / failed via /report.

Token quota semantics (important):

Endpoint Auth Consumes max_uses (use_count)? Counter / side effect
GET /install/script validate token validity No (ValidateBearerNoUse) no consumption; the script can be re-fetched
GET /install/package validate + consume Yes, use_count+1 headers X-Artifact-SHA256 / X-Install-Job-ID; job → running
GET /install/env validate + consume Yes, use_count+1 returns env text; 403 if env payload unavailable
POST /install/report ValidateBearerForReport No independent report_count+1; does not consume max_uses

Manual installer-side testing:

TOKEN=nit_xxx; BASE=https://<cloud>
curl -fsSL -H "Authorization: Bearer $TOKEN" "$BASE/api/node/install/script"
curl -fsSL -D - -o pkg.tar.gz -H "Authorization: Bearer $TOKEN" "$BASE/api/node/install/package"
curl -fsSL -H "Authorization: Bearer $TOKEN" "$BASE/api/node/install/env"
curl -fsS -X POST "$BASE/api/node/install/report" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"status":"success","message":"done","hostname":"node-1","node_version":"1.0.0"}'

8.4 GET /api/node/install/jobs · /jobs/:id — query jobs

curl -sS -H "Authorization: Bearer $TOKEN" https://<cloud>/api/node/install/jobs        # latest 50
curl -sS -H "Authorization: Bearer $TOKEN" https://<cloud>/api/node/install/jobs/<job_id>  # includes reports[]

status: pendingrunningsuccess / failed. started_at / finished_at are millisecond timestamps, 0 meaning not yet occurred. The detail endpoint attaches recent reports (up to 20) and recent_report.

8.5 POST /api/node/install/tokens/:id/revoke — revoke a token

curl -sS -X POST -H "Authorization: Bearer $TOKEN" \
  https://<cloud>/api/node/install/tokens/<token_id>/revoke

8.6 Stale-job reaper

A single-process background reaper (install_reaper.go) runs once at startup, then every 10m. It atomically marks jobs with ctime older than now - 24h (StaleJobMaxAge) still in pending/running as failed (finished_at=now, message="install job timed out without report"), and revokes any still-active tokens pointing at those jobs in the same transaction — preventing a killed installer from later reusing its bearer token to resurrect a closed job. Terminal jobs are never rewritten; across replicas the transactional WHERE clause keeps it idempotent (only the first process matches; others get RowsAffected=0 and exit silently).

8.7 Frontend display conventions

8.8 POST /api/node/reg — node agent self-registration

Node agent self-registration endpoint. Public endpoint: no RBAC, no Bearer auth — same category as POST /api/node/install/report (agent infrastructure, no zcloud CLI command). The only access gate is the shared token dummy_token in the request body, which must equal the agent's hardcoded constant value; a mismatch is rejected.

Backend: src/backend/internal/node/{handler,service}/reg.go, route src/backend/internal/node/route.go (rg.POST("/reg", h.RegNode)).

Request body fields (note manger_addr is the agent's existing misspelling, missing an a — it must not be renamed to manager_addr):

Field Type Required Description
node_id string no Empty on a fresh install; non-empty means the agent already holds a node ID (an existing node is reused, not re-created)
manger_addr string yes Management address host:port, auto-detected by the agent via ip route get and reported. Falls back to default port 33020 when the port is missing
node_type string no Proto enum name string, e.g. "NODE_1_WAF". Only WAF defense nodes are supported (empty / "NODE_1_WAF" / "waf" / "1" all map to WAF; any other value returns a non-zero code)
extend_config string no url-escaped extended config; not consumed by self-registration yet
dummy_token string yes Shared token; must equal the agent's hardcoded constant value, otherwise rejected
plugin string no Plugin list, e.g. "waf,detect,agent,ebpf"
only_acl bool no ACL-only mode flag; self-registered nodes do not take this path
ip string no only_acl companion parameter; empty for standard registration
acl_tags string no only_acl companion parameter
ip_groups string no only_acl companion parameter
curl -sS -X POST https://<cloud>/api/node/reg \
  -H 'Content-Type: application/json' \
  -d '{
        "node_id":"",
        "manger_addr":"192.168.14.171:33020",
        "node_type":"NODE_1_WAF",
        "dummy_token":"<agent hardcoded shared token>",
        "plugin":"waf,detect,agent,ebpf"
      }'

Response: standard envelope {code, message, data}, where data is RegNodeResponse:

{"code":0,"message":"success","data":{
  "node_id":"3f2c…",
  "listen_addr":":33020",
  "tls":false,
  "cert":"",
  "key":"",
  "settings":{},
  "plugin":{}
}}
Response field Type Description
node_id string Node ID. Returns the existing ID when an existing node is matched idempotently; returns a newly created ID on first registration
listen_addr string Listen address, e.g. ":33020", taken from the management address port (falls back to default 33020)
tls bool Fixed false on the new (NSQ) platform (no longer distributing per-node certs via etcd)
cert string Empty string on the new platform
key string Empty string on the new platform
settings object Pushed config. The new platform distributes config over NSQ pub/sub, so this is always an empty map {}
plugin object Plugin config. Always an empty map {} on the new platform

Semantics:


§A Analytics Common Query Parameters

These apply to all single-chart GET endpoints (GET /api/analytics/<page>/<chart>). Defaults apply if not specified. Passing an unauthorized target_user_id or cross-OEM resource returns 403.

Parameter Type Required Example Description
window string no last_1h / last_24h / last_7d Time window alias; default last_24h
stime int64 no 1746748800000 Custom start time (Unix ms; paired with etime; takes precedence over window)
etime int64 no 1746835200000 Custom end time (Unix ms)
site_id string no site-001 Site filter
domain_id string no d_8a3b1c Domain filter
target_user_id string no u-tenant-001 Tenant-level switch viewed user; backend enforces cross-tenant authorization
compare bool no false Enable previous-period comparison (only some charts support it)
top int no 10 TopN, default 10, max 100
order string no bytes_desc Sorting; chart-defined (e.g. top-url supports request_count_desc/bytes_desc/cache_desc)
page int no 1 Pagination for list-style charts
size int no 20 Per-page count, max 100

Single-chart response skeleton:

{
  "code": 0,
  "message": "ok",
  "data": {
    "chart_key": "access/status",
    "render_hint": "categorical_distribution_over_time",
    "schema": {
      "dimensions": [
        { "name": "status_class", "type": "enum", "values": ["2xx", "3xx", "4xx", "5xx"] },
        { "name": "time", "type": "timestamp", "unit": "ms" }
      ],
      "measures": [
        { "name": "count", "type": "integer", "unit": "requests" }
      ]
    },
    "rows": [],
    "meta": {
      "cache": "miss",
      "source": "postgres",
      "latency_ms": 12,
      "window": {
        "stime": 1777526400000,
        "etime": 1777530000000,
        "granularity": "5m",
        "bucket_table": "tfs_flow_domains"
      }
    }
  }
}

window.granularity is a response field describing the actual aggregation granularity (5m / 1h / 1d), not a request parameter. The client passes window=last_24h, and the backend automatically picks the table.


§B Visualization Recommendations

B.1 Chart Unified Contract — render_hint lookup

All chart-keys are dispatched automatically by the frontend to one of 6 chart components based on render_hint. This is the contract truth (docs/specs/chart-contract.md §2) — no custom vocab words allowed.

render_hint schema shape Recommended frontend component Typical use
kpi 0~1 dim + 1+ measure KpiGroupCard Multi-metric KPI card group (e.g. overview/kpi with 6 measures)
categorical_distribution 1 categorical dim + 1 measure PieCard (≤8 cats) / BarCard (>8 cats) One-dimension share (e.g. overview/event-type)
categorical_distribution_over_time 1 categorical + 1 timestamp + 1 measure StackedBarCard / LineCard multi-series Multi-class stacked over time (e.g. access/status)
time_series_single 1 timestamp + 1 measure LineCard Single-measure time series (e.g. access/request-hm without compare)
time_series_multi 1 timestamp + ≥2 measures, or 1 categorical + 1 timestamp + 1 measure LineCard multi-series Multi-measure time series (e.g. access/flow-hm with 5 measures); compare uses sub-shape B
topn 1 string dim + 1 measure BarCard horizontal / RankingCard Pre-sorted TOP-N (e.g. protect/waf/top-ip)
geo 1 geo dim + 1 measure GeoHeatmapCard Geographic distribution (e.g. protect/waf/geo)
table any TableCard Fallback for non-visualizable data

Adding a new hint vocab word requires bilateral review (cloud + Aegeon); never extend the vocab unilaterally.

B.2 Data shape lookup

The table maps "output data shape → recommended chart" — handy as a quick lookup when wiring up the frontend. Actual responses still follow the Chart Unified Contract and are defined by schema + rows.

Data shape Typical fields Recommended chart Avoid
Single value (scalar) {count: 12345} KPI card Line / pie
Multi-KPI (4-6 scalars) {domain_count, requests, blocked, block_rate, qps} KPI card grid / radar Single pie
Time-series single [{ctime, value}, ...] Line / area Pie
Time-series multi [{ctime, requests, attacks}, ...] Multi-line / stacked area Pie
Time-series compare {current:[...], previous:[...]} Two-line solid+dashed Single line
Dimension distribution (≤ 8) [{key, count}, ...] Pie / donut Table
Dimension distribution (> 8) [{key, count}, ...] Horizontal bar / column Pie (fragmented)
Geo distribution [{region, count}, ...] Map heatmap Table
Top ranking [{key, count, ...}, ...] Horizontal bar / table Line / pie
2D matrix [[v11, v12], [v21, v22]] Heatmap Line
Scatter [{x, y, size, ...}, ...] Scatter / bubble Pie
Paged list {list, total, page, size} Table (paged) Any chart
Timeline detail {items: [{ctime, ...}]} Vertical timeline Pie
Placeholder {available: false, ...} Render n-empty placeholder, not a chart Fake data

Color suggestions:



Full API Index

The complete REST set is exported as OpenAPI v3:

GET /api/openapi.json

Importable into Postman / Insomnia / Swagger UI. All routes include full schema, parameters, response samples, and required permission keys.


Cloud WAF · REST API Documentation · the source of truth is /api/openapi.json