External service and page plugin (external_service / ui_page) guide
An external service is an HTTPS service on your own server that subscribes to webhook events and calls the open API; a ui_page embeds pages from your own origin as sandboxed iframes in a control plane menu or a site’s “Extensions” tab. Neither runs code inside the control plane.
external_service: create a plugin of kind external_service with an external_https entrypoint; declare events in subscriptions and the matching events.*.read permissions, and upload 1–16 platform packages.
ui_page: first register and verify your origin (and the api_base_url origin) on the seller site’s “Page origins”: put cdnca-plugin-origin=<token> at https://<host>/.well-known/cdnca-plugin-origin.txt. Create a plugin of kind ui_page; its versions carry no package or file.
Calling the control plane: use an API key the tenant creates and hands you (Authorization: CdncaKey <key_id>.<secret>), limited to operations marked “API key”. Workload credentials for third parties are not published yet.
Up to 64 event types such as io.cdnca.site.v1.updated
webhooks[]
Up to 64 webhook capability codes, each approved per installation
origin
https://<DNS host>, default port, optional trailing /; must be a verified page origin of yours
api_base_url
Optional, https; the control plane proxies the page’s restricted API calls here; its origin must be verified too
pages[]
1–16 pages: id ^[a-z][a-z0-9-]{0,31}$, path (starts with /), mount (menu or site_tab), role (admin or user), title (zh-CN required, at most 32 characters)
menu_parent
Required for mount=menu: admin monitor/node/site/stream/config/package/finance/system, user monitor/site/stream/user-package/account; not allowed for site_tab
required_permission / required_module
Optional display gates (the permission cannot be platform.admin; modules look like module.xxx, pack.xxx, theme.xxx)
permissions
A ui_page must include platform.ui_pages.provide and declares no provides
Protocol and interfaces
Webhook events
Seven domain events: deployment status, site updates, certificate status, certificate expiry, node status, usage thresholds and security incidents, each needing the installation’s approved events.*.read permission. Delivered as POST with Content-Type: application/cloudevents+json, at least once; per-site filters, dead-letter redelivery and time-based replay are available.
Hash the raw body with SHA-256 and compare with X-CDNCA-Webhook-Content-Sha256 in constant time.
Pick the secret by X-CDNCA-Webhook-Key-Id (43-character base64url decoded to 32 bytes), rebuild the v2 HMAC and compare with X-CDNCA-Webhook-Signature.
Reject an X-CDNCA-Webhook-Timestamp more than 300 s from your clock.
Deduplicate by delivery ID, then parse JSON and use the event ID for business idempotency; return 2xx only after your transaction succeeds.
cdnca.webhook.signature/v2
POST
<canonical-escaped-request-path>
<timestamp>
<event-id>
<delivery-id>
<secret-version-id>
<lowercase-hex-sha256-of-raw-body>
ui_page protocol
Pages run in <iframe sandbox="allow-scripts allow-forms"> (no allow-same-origin) without the control plane’s cookies, storage or user credentials. The page reads the nonce from the URL fragment #cdnca-nonce=<nonce>, posts cdnca.hello to parent, and the host answers cdnca.ready with a MessagePort; after that only context.get, navigate and api.request exist, anything else answers message_not_allowed.
api.request goes through the control plane’s POST /v1/plugin-ui/{plugin_id}/proxy to api_base_url (public addresses only, no redirects, JSON only, 120 per user per plugin per minute). The control plane adds X-Cdnca-Plugin-Context (60 s, with a nonce), X-Cdnca-Plugin-Signature (Ed25519 by the installation key) and X-Cdnca-Plugin-Key-Id; your backend verifies with the installation public key and remembers the nonce. See the ui_page SDK page for the SDK and verifier.
Packaging and upload fields
Form field
Required
Content and checks
manifest
Yes
The signed manifest, 1 byte to 256 KiB; it must name this plugin and namespace, and integrity.key_id must be an active key of the namespace whose signature verifies
configuration_schema
Yes
The configuration JSON Schema file, 1 byte to 64 KiB; its SHA-256 must equal spec.configuration_schema.sha256
changelog_md
No
Release notes in Markdown, at most 65536 bytes
channel
No
stable (default) or beta
auto_publish
No
true / false: publish automatically once approved
package:<platform>
external_service: yes, 1–16; ui_page: must be absent
The platform matches ^[a-z0-9]+-[a-z0-9_]+$; each package defaults to at most 64 MiB, as zip, tar.gz or tar
content
Must be absent
Neither entrypoint has a data file
Endpoint: POST /seller/v1/plugins/{id}/versions, Content-Type: multipart/form-data, with exactly one Idempotency-Key header of 1–128 characters; a retry with the same key returns the same result. The body limit defaults to 200 MiB. Any field name other than those in the table and the kind-specific ones returns 400 invalid_upload.
A plugin version number cannot repeat and must be higher than every reviewed version (approved, published, hidden or delisted). After upload the version is “scanning”; the seller site’s version detail shows the scan report.
Review rules
After upload the review worker runs the validating phase (manifest, entrypoint, permissions, the kind’s own checks); any reject stops it before scanning, then the scanning phase runs. Outcome: a reject → changes requested (fix it and press “Resubmit”, at most 10 scans per version); a quarantine → human review; neither → approved directly or sent to human review under the conditions below.
The first version (the plugin has no reviewed version yet)
Permissions differ from the last reviewed version (added or removed)
Privileged kinds: module, engine_module, provider
Sellers who joined less than 90 days ago (market default, adjustable by the operator)
Three scan attempts in a row errored
Any one of them sends the version to human review. The full rule table (rule ID, severity, reject or quarantine, evaluation mode) is on the review rules page.
ui_page.declaration_invalid: origin, pages, mounts, menu parents, titles or gates are malformed; detail gives field and reason (reject).
ui_page.origin_unverified: origin or api_base_url is not a verified origin of yours (reject).
ui_page.permission_missing / ui_page.artifacts_present: platform.ui_pages.provide missing, or the version carries a package or file (reject).
external_service packages go through the same package, secrets, dependencies, malware and diff scans as providers.
metadata.version is canonical SemVer (no leading v); every upload must be higher than the reviewed versions, and a version number cannot be uploaded twice.
spec.compatibility.control_api is 1–8 space-separated comparators (>=, <=, =, >, < followed by canonical SemVer), at most 128 characters; all must hold. A control plane that does not satisfy it cannot install or upgrade to the version; an installed plugin keeps its last compatible version.
When a new version adds permissions, the customer’s administrator must confirm them again in the control plane; any version whose permissions change goes to human review.
The vN in event types evolves separately from the manifest api_version and the OpenAPI version; one event major version only adds optional fields.
Local debugging
Webhooks: the control plane’s plugin webhook subscription can send a test event (io.cdnca.webhook.v1.test) to exercise your verifier; docs/developer/examples/webhook-v2-receiver.md in the repository is a runnable reference receiver.
ui_page: docs/developer/examples/plugin-ui-sdk/ has the in-frame SDK, a sample page, a Node backend verifier and a golden vector; run it with node --test.
Manifest: docs/developer/examples/validate-plugin-manifest.mjs in the repository validates a manifest against the schema, extension catalog and compatibility matrix offline and checks its signature.