按类型开发指南

服务商接口(provider)开发指南

provider 是跑在你自己服务器上的 HTTPS 服务,实现 cdnca.provider/v1 协议,为主控提供 DNS、短信、邮件、备案查询或支付通道。主控只按协议签名调用你,你的代码不进主控进程。

← 全部开发指南

快速开始

  1. 新建插件,类型选 provider。
  2. 生成一对 Ed25519 响应签名密钥,公钥(base64url,无填充,43 字符)写进 provider_response_key;私钥只留在你的服务里。
  3. 在 base_url 下实现 /cdnca-provider/v1/{kind}/{action},先实现 verify,再实现本类型的动作。
  4. 沙箱模式(请求体 sandbox=true)只用测试数据回答,不碰客户账户。
  5. 签名 manifest,上传版本(带至少一个平台包)。客户安装后,主控先对你的沙箱跑一遍安装契约,全部通过管理员才能启用。

当前开放:dns、sms、mail、icp、payment。通知渠道(notify)即将开放:manifest 里可以声明、市场审核也接受,但主控目前不会启用 notify,声明后该协议会被忽略;第三方 CDN(cdn)只限官方。

签名与公钥登记见插件签名工具页。 签名工具与步骤 →

manifest 示例与字段

"spec": {
  "categories": ["dns_provider"],
  "compatibility": {"control_api": ">=1.0.35 <2.0.0"},
  "entrypoint": {
    "type": "external_https",
    "base_url": "https://dns.acme.example/api",
    "health_path": "/healthz",
    "provider_response_key": "<响应签名公钥,43 字符 base64url>"
  },
  "provides": [{"kind": "dns", "protocol_version": "1"}],
  "permissions": ["providers.dns.provide"],
  "configuration_schema": {"path": "schemas/config.schema.json", "sha256": "<SHA-256>"},
  "supply_chain": {"sbom_sha256": "<SHA-256>", "provenance_sha256": "<SHA-256>"}
}
字段要求
spec.entrypoint.typeexternal_https
spec.entrypoint.base_urlhttps、DNS 主机名(不能是 IP 或 localhost)、默认端口,可带路径,不能有查询串、片段或结尾 /
spec.entrypoint.health_path以 / 开头,最多 512 字节
spec.entrypoint.provider_response_key响应签名公钥,无填充 base64url 43 字符;声明 dns / sms / icp / mail / payment 时必填
spec.provides[]1–8 项,kind 不重复,protocol_version 当前为 "1";provider 插件必须至少声明一项,其他 kind 不能声明
spec.permissions每个 kind 必须带 providers.<kind>.provide(如 providers.dns.provide),缺了整个 manifest 被拒绝
spec.packages列出每个上传的平台包与 SHA-256(当前为试运行规则)

协议与接口

每次调用都是 POST {base_url}/cdnca-provider/v1/{kind}/{action},请求体 JSON 带 "protocol": "cdnca.provider/v1" 和 "sandbox",写操作带 idempotency_key(同一个 key 重复调用必须返回第一次的结果、不能重复执行)。请求体最大 256 KiB,应答最大 1 MiB,超时 15 秒。主控只连公网地址、不走代理、不跟随重定向。

请求头内容
X-Cdnca-Provider-Installation主控安装 ID
X-Cdnca-Provider-TimestampUnix 秒;与你的时钟相差超过 300 秒必须拒绝
X-Cdnca-Provider-Nonce32 位十六进制;见过的 nonce 必须拒绝
X-Cdnca-Provider-Signature安装私钥对请求消息的 Ed25519 签名,无填充 base64url
X-Cdnca-Provider-Response-Signature(应答头)你的响应私钥对应答消息的签名
request  = "cdnca-provider-request/v1\n" + installation_id + "\n" + plugin_id + "\n" + kind + "\n" + action + "\n" + timestamp + "\n" + nonce + "\n" + hex(sha256(body))
response = "cdnca-provider-response/v1\n" + plugin_id + "\n" + kind + "\n" + action + "\n" + nonce + "\n" + hex(sha256(response_body))

签名、时间戳或 nonce 不对时返回 HTTP 401。其余情况一律 HTTP 200 + 信封 {"protocol":"cdnca.provider/v1","ok":true,"data":{…}},失败时 {"ok":false,"error":{"code":"…"}};code 取 invalid_request、unauthorized、not_found、conflict、zone_not_found、rate_limited、quota_exceeded、unavailable、unsupported、provider_error、invalid_signature,其他值按 provider_error 处理。信封不允许多余字段。应答签名绑定了请求 nonce,抓到的应答不能用于别的调用。安装公钥由客户主控管理员在插件详情“安装签名公钥”处复制给你。

kind动作请求与应答要点
dnsverify, list_records, create_record, update_record, delete_record记录 {id, name, type, value, ttl, line},name 为不带结尾点的完整域名,line 为统一线路名(默认/电信/联通/移动/境外);list 按 zone(可选 name)返回 records;create 返回 id;未知 zone 返回 zone_not_found,删不存在的记录返回 not_found
smsverify, send{to(E.164), template_id, params, body, idempotency_key} → {delivery_id};非 E.164 号码返回 invalid_request
mailverify, health, send{to(单个地址,不带显示名), subject, text, html?, reply_to?, idempotency_key} → {message_id};发件人由你的配置决定;health 表示上游账户当前能发信
icpverify, query{domain(小写、punycode 的可注册域名)} → {status: filed | unfiled, licence?, subject?}
paymentverify, create_checkout, query_payment, verify_callback, refund, reconcile, sandbox_settle插件只上报支付事实;主控用自己的订单号、商户、整数金额(amount_minor)、币种绑定每个事实,收到通知后先 query_payment 向你确认才入账。create_checkout 返回 action(redirect 或 qr_code)与 url / qr_content;verify_callback 收到主控转来的原始通知字节;reconcile 返回一个 UTC 日的对账单;sandbox_settle 只在沙箱用,模拟网关回调
沙箱固定值值
SandboxZonecontract.cdnca-sandbox.test
SandboxPhone+8613800000000
SandboxMailboxcontract@cdnca-sandbox.test
SandboxFiledDomain / SandboxUnfiledDomainfiled.cdnca-sandbox.test / unfiled.cdnca-sandbox.test
SandboxMerchantcdnca-sandbox-merchant

打包与上传字段

表单字段必填内容与校验
manifest是签名后的 manifest,1 字节到 256 KiB;身份必须是本插件 ID、本命名空间,integrity.key_id 必须是该命名空间 active 的公钥并验签通过
configuration_schema是配置 JSON Schema 文件,1 字节到 64 KiB;SHA-256 必须等于 spec.configuration_schema.sha256
changelog_md否更新说明 Markdown,最多 65536 字节
channel否stable(默认)或 beta
auto_publish否true / false:审核通过后自动上架
package:<platform>是,1–16 个字段名形如 package:linux-amd64,平台名匹配 ^[a-z0-9]+-[a-z0-9_]+$ 且不重复;每个包默认最大 64 MiB,格式为 zip、tar.gz 或 tar
content不能有external_https 版本没有数据包文件

接口:POST /seller/v1/plugins/{id}/versions,Content-Type: multipart/form-data,必须带一个 1–128 字符的 Idempotency-Key 请求头;同一 key 重试返回同一结果。请求体默认上限 200 MiB。除上表与本类型专属字段外,出现任何其他字段名都返回 400 invalid_upload。

同一插件的版本号不能重复,且必须高于任何已审核过的版本(已通过、已上架、已隐藏或已下架)。上传成功后版本进入“预检中”,卖家站版本详情显示预检报告。

审核规则

上传后审核 worker 先跑“校验”阶段(manifest、入口、权限、本类型专属检查),出现任何拒绝项就不再扫描;再跑“扫描”阶段。结论:有拒绝项 → 需修改(修好后点“重新提交”,每个版本最多 10 次扫描);有隔离项 → 待人工审核;都没有 → 视下列条件直接通过或进入人工审核。

  • 首个版本(该插件还没有任何已审核版本)
  • 权限与上一个已审核版本不同(增加或减少)
  • 高风险类型:module、engine_module、provider
  • 入驻未满 90 天的卖家(市场默认值,运营可调)
  • 扫描连续 3 次出错
  • provider 是高风险类型,每个版本都人工审核

满足任一条即进入人工审核。完整规则表(规则 ID、级别、拒绝或隔离、是否试运行)见审核规则页。

  • entrypoint.provides_invalid:provides 格式错、重复或协议版本不对(拒绝);entrypoint.provides_official_only:声明了 cdn(拒绝)。
  • package.*:压缩包损坏、zip 炸弹、路径穿越、符号链接、超出条目数(10000)或解压大小(单文件 32 MiB、合计 128 MiB)上限等(拒绝;setuid 位为隔离)。
  • secrets.*:包里有私钥、云厂商或代码托管平台的令牌(拒绝);JWT 与通用凭据赋值目前是试运行。
  • malware.detected、dependencies.malicious:病毒或已知恶意依赖(拒绝)。

审核接受 notify,但主控暂不启用(见快速开始的说明)。

本类型相关的规则见审核规则清单: metadata.* entrypoint.* permissions.* package.* secrets.* dependencies.* malware.* diff.*

版本与兼容

metadata.version 是规范 SemVer(不带前导 v),每次上传必须比已审核版本高;同一版本号不能重复上传。

spec.compatibility.control_api 是 1–8 个以空格分隔的比较式(>=、<=、=、>、<,后接规范 SemVer),总长不超过 128;全部满足才算兼容。不满足的主控不能安装或升级到该版本,已安装的保持最近兼容版本。

"compatibility": {"control_api": ">=1.0.35 <2.0.0"}

新版本增加权限时,客户管理员在主控里要重新确认;权限变化的版本一律人工审核。

provides[].protocol_version 当前只有 "1"(cdnca.provider/v1)。安装契约版本是 cdnca.provider-contract/v1,契约版本变化后主控会重新跑契约。

本地调试

仓库里的 libs/entitlement/pluginprovider/fake.go(FakePlugin)就是协议服务端的参考实现:验签、时间窗、nonce、幂等、签名应答和沙箱行为都在里面。上面的协议节示例由它改写:

func (p *Plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// {base_url}/cdnca-provider/v1/{kind}/{action}
	i := strings.Index(r.URL.Path, "/cdnca-provider/v1/")
	kind, action, _ := strings.Cut(r.URL.Path[i+len("/cdnca-provider/v1/"):], "/")
	nonce := r.Header.Get("X-Cdnca-Provider-Nonce")
	body, _ := io.ReadAll(io.LimitReader(r.Body, 256<<10))
	ts := r.Header.Get("X-Cdnca-Provider-Timestamp")
	unix, err := strconv.ParseInt(ts, 10, 64)
	sig, _ := base64.RawURLEncoding.DecodeString(r.Header.Get("X-Cdnca-Provider-Signature"))
	skew := time.Now().Unix() - unix
	msg := "cdnca-provider-request/v1\n" + r.Header.Get("X-Cdnca-Provider-Installation") + "\n" + p.PluginID +
		"\n" + kind + "\n" + action + "\n" + ts + "\n" + nonce + "\n" + sha256hex(body)
	if i < 0 || err != nil || len(nonce) != 32 || skew > 300 || skew < -300 || p.seen(nonce) ||
		!ed25519.Verify(p.InstallationKey, []byte(msg), sig) {
		p.reply(w, kind, action, nonce, 401, "unauthorized", nil)
		return
	}
	// … 按 kind/action 处理,写操作按 idempotency_key 去重 …
	p.reply(w, kind, action, nonce, 200, "", map[string]any{})
}

func (p *Plugin) reply(w http.ResponseWriter, kind, action, nonce string, status int, code string, data any) {
	env := map[string]any{"protocol": "cdnca.provider/v1", "ok": code == ""}
	if code != "" {
		env["error"] = map[string]string{"code": code}
	}
	if data != nil {
		env["data"] = data
	}
	body, _ := json.Marshal(env)
	msg := "cdnca-provider-response/v1\n" + p.PluginID + "\n" + kind + "\n" + action + "\n" + nonce + "\n" + sha256hex(body)
	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("X-Cdnca-Provider-Response-Signature",
		base64.RawURLEncoding.EncodeToString(ed25519.Sign(p.ResponseKey, []byte(msg))))
	w.WriteHeader(status)
	w.Write(body)
}

主控安装时跑的契约用例(RunContract,contract.go):所有 kind 都有 verify、rejects_bad_signature、rejects_tampered_body、rejects_stale_timestamp、rejects_replayed_nonce;dns 另有 create_a、list_created、create_idempotent、update_a、delete_a、delete_missing_not_found、txt_line、unknown_zone;sms 有 send、send_idempotent、invalid_number;mail 有 health、send、send_idempotent、invalid_recipient;icp 有 filed、unfiled、invalid_domain;payment 有 create_checkout、query_unpaid、sandbox_settle、query_paid、verify_callback、rejects_tampered_callback、refund_idempotent、refund_over_limit、reconcile、unknown_order。本地可以仿照 pluginprovider_test.go 的 newHarness,把 pluginprovider.NewClient 指向你的服务后调用 pluginprovider.RunContract,看到的结果与客户主控一致。