Guides by plugin kind

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.

← All guides

Quick start

  1. 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.
  2. 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.
  3. 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.

Signing and key registration are on the plugin signing page. Signing tool and steps →

Manifest example and fields

"entrypoint": {"type": "external_https", "base_url": "https://hooks.acme.example", "health_path": "/healthz"},
"permissions": ["events.site.read", "events.security.read"],
"subscriptions": ["io.cdnca.site.v1.updated", "io.cdnca.security.v1.incident_detected"]
"entrypoint": {
  "type": "ui_page",
  "origin": "https://ui.acme.example",
  "api_base_url": "https://api.acme.example/v1",
  "pages": [{
    "id": "report", "path": "/report", "mount": "menu", "menu_parent": "monitor",
    "role": "admin", "title": {"zh-CN": "报表", "en-US": "Reports"}, "required_permission": "sites.read"
  }]
},
"permissions": ["platform.ui_pages.provide"]
FieldRequirement
subscriptions[]Up to 64 event types such as io.cdnca.site.v1.updated
webhooks[]Up to 64 webhook capability codes, each approved per installation
originhttps://<DNS host>, default port, optional trailing /; must be a verified page origin of yours
api_base_urlOptional, 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_parentRequired 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_moduleOptional display gates (the permission cannot be platform.admin; modules look like module.xxx, pack.xxx, theme.xxx)
permissionsA 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.

  1. Hash the raw body with SHA-256 and compare with X-CDNCA-Webhook-Content-Sha256 in constant time.
  2. 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.
  3. Reject an X-CDNCA-Webhook-Timestamp more than 300 s from your clock.
  4. 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 fieldRequiredContent and checks
manifestYesThe 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_schemaYesThe configuration JSON Schema file, 1 byte to 64 KiB; its SHA-256 must equal spec.configuration_schema.sha256
changelog_mdNoRelease notes in Markdown, at most 65536 bytes
channelNostable (default) or beta
auto_publishNotrue / false: publish automatically once approved
package:<platform>external_service: yes, 1–16; ui_page: must be absentThe platform matches ^[a-z0-9]+-[a-z0-9_]+$; each package defaults to at most 64 MiB, as zip, tar.gz or tar
contentMust be absentNeither 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.

Rules for this kind are in the review rule list: metadata.* entrypoint.* permissions.* ui_page.* package.* secrets.* dependencies.* malware.* diff.*

Versions and compatibility

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.

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

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.
node --test docs/developer/examples/plugin-ui-sdk/sdk.test.mjs
node docs/developer/examples/validate-plugin-manifest.mjs --control-api-version 1.0.35 plugin.json publisher-public-key.json