Where the page runs
Pages are served from your own https origin; the control plane loads them at fixed mount points in <iframe sandbox="allow-scripts allow-forms"> (no allow-same-origin). The frame origin is the opaque null: no access to the control plane’s cookies, localStorage, sessionStorage or IndexedDB, its DOM, or top-level navigation. Only enabled ui_page plugin origins enter the control plane’s frame-src; disabling or uninstalling removes them on the next sync.
Handshake and messages
- The page reads the nonce from the URL fragment #cdnca-nonce=<nonce> and posts {protocol, type: "cdnca.hello", nonce} to parent.
- The host accepts only this iframe’s window and nonce, answers {type: "cdnca.ready"} and hands over a MessagePort.
- From then on every request uses that port: {protocol, nonce, id, method, params}, answered with {protocol, nonce, id, ok, result | error}.
| Method | Meaning |
|---|---|
| context.get | User ID / display name / role, tenant scope, site ID, locale, light or dark theme; no credentials |
| navigate | {path}: an in-app relative path; sign-in, password recovery, API keys, profile and similar pages are refused |
| api.request | {method, path, query?, body?}: call your api_base_url through the control plane proxy |
Any other message answers message_not_allowed.
Restricted API and backend verification
api.request goes to the control plane’s POST /v1/plugin-ui/{plugin_id}/proxy, which forwards it to api_base_url: public addresses only, no redirects, JSON only, 120 per user per plugin per minute. The control plane never forwards the user’s session or token; it adds three headers instead:
| Header | Content |
|---|---|
| X-Cdnca-Plugin-Context | base64url JSON (cdnca.plugin-ui-context/v1), valid 60 s, with a nonce |
| X-Cdnca-Plugin-Signature | Ed25519 signature by the installation key (base64url) |
| X-Cdnca-Plugin-Key-Id | Installation key ID |
"cdnca-plugin-ui-proxy/v1\n" + METHOD + "\n" + path?query + "\n" + hex(sha256(body)) + "\n" + contextpath is relative to the path part of api_base_url. Your backend verifies with the installation public key, checks validity and remembers the nonce. The customer’s administrator copies the installation public key to you from “Plugin market → plugin detail → Installation signing key” (GET /v1/plugin-ui/signing-key; key_id is X-Cdnca-Plugin-Key-Id). Writes and refused calls are audited as plugin.ui_page.proxy.
Installation confirmation
When the customer’s administrator confirms an install or upgrade, the dialog checks the version on the plugin market by the manifest fingerprint the licence service signed and lists the menus, page origins and plugin backend it adds; if the fingerprint does not match or the market is unreachable it only asks to verify with the platform and never shows another version’s declaration.
SDK files and self-test
- cdnca-plugin-ui.mjs: the in-frame SDK, no dependencies. connect() does the handshake and returns context() / navigate(path) / request({method, path, query, body}); errors are PluginUIError with a code.
- index.html: a sample page.
- verify-backend.mjs: Node.js backend verifier (signature, key_id, validity, nonce replay).
- proxy-vector.json: the golden vector the control plane test TestDeveloperSDKProxyVector produces.
The files and the self-test are in docs/developer/examples/plugin-ui-sdk/ of the developer kit cdnca-dev-kit.tar.gz (downloaded from the same site as the signing tool, released with each control plane version); the self-test needs Node.js 20 or later. Download, check the hash and unpack:
CDNCA_KIT='https://developer.cdnca.com/downloads/dev-kit'
curl --fail --location --show-error --output cdnca-dev-kit.tar.gz "$CDNCA_KIT/cdnca-dev-kit.tar.gz" &&
curl --fail --location --show-error --output dev-kit.manifest.json "$CDNCA_KIT/dev-kit.manifest.json" &&
curl --fail --location --show-error --output SHA256SUMS "$CDNCA_KIT/SHA256SUMS" &&
sha256sum -c SHA256SUMS &&
tar -xzf cdnca-dev-kit.tar.gz &&
cd cdnca-dev-kit && cat VERSION SOURCEnode --test docs/developer/examples/plugin-ui-sdk/sdk.test.mjs