Pular para o conteúdo

CPanel

Este conteúdo não está disponível em sua língua ainda.

React SPA that serves as the Buntime operator shell and bootstrap entrypoint. It hosts plugin UIs via iframes (web component <z-frame>), exposes first-class runtime sections (/cpanel/overview, /cpanel/keys, /cpanel/workers, /cpanel/plugins) for API-key/worker/plugin management without relying on the CLI/TUI, and discovers the real API path through /.well-known/buntime. The entire cpanel authenticates end-to-end against the core runtime via an HttpOnly session cookie issued by POST /api/admin/session.

The CPanel is distributed as a Buntime app (not a plugin): it has a manifest.yaml, is deployed as a static worker, and is accessible at /cpanel/. There is no /cpanel/admin subpath — everything is cpanel. Its surface is twofold:

RoleDescription
Runtime sectionsFirst-class routes under /cpanel/: overview (operator dashboard), keys (API keys CRUD), workers (deployed workers), plugins (installed plugins). All gated by the session cookie.
Micro-frontend shellRenders the unified sidebar and hosts plugin UIs in iframes via <z-frame> (@zomme/frame). Plugin paths are siblings of the runtime sections (e.g. /cpanel/gateway, /cpanel/metrics).
File browserWorkers + Plugins tabs use a shared <FileBrowser> over the runtime’s /api/{workers,plugins}/files/* endpoints — drag-drop upload, multi-select, rename/move/delete, recursive folder upload.

Both surfaces share a single unified sidebar and a single auth gate — the user signs in once with an API key and sees, in the same shell, a “Runtime” group (Overview/Keys/Workers/Plugins) and a “Plugins” group listing every installed plugin’s micro-frontend. The Runtime “Plugins” item (install/manage page) is distinct from the “Plugins” section heading (plugin-contributed menus) — hierarchy disambiguates. See Bootstrap independence.

The same cookie opens the operator endpoints of core plugins (Proxy, Gateway, Logs, Metrics). The runtime validates the cookie before any plugin onRequest hook runs.

LayerTechnology
FrameworkReact 19
RoutingTanStack Router (file-based, generated in routeTree.gen.ts)
Data fetchingTanStack Query
Web component frame@zomme/frame (<z-frame>)
FormsReact Hook Form + Zod (via @hookform/resolvers)
TablesTanStack Table
ComponentsRadix UI primitives + shadcn-style wrappers
EditorCodeMirror 6 (@uiw/react-codemirror + lang-json/html)
i18ni18next + @zomme/bun-plugin-i18next (pt, en)
StylingTailwind CSS v4
Buildbun scripts/build.ts (custom Bun bundler with TSR, Tailwind, Iconify, i18next plugins)

The final bundle is placed in dist/ with index.html as the entrypoint. Code splitting happens automatically via Bun, generating multiple chunk-*.js files.

apps/cpanel/manifest.yaml:

entrypoint: dist/index.html
visibility: protected
injectBase: true
publicRoutes:
GET:
- /**
FieldBehavior
entrypoint: dist/index.htmlBun serves the SPA from the bundled index.html
visibility: protectedCosmetic only — affects how the cpanel appears in the deployments UI (read-only), does not govern auth
injectBase: trueRuntime injects <base href="/cpanel/"> so TanStack Router can compute basepath correctly
publicRoutes.GET: /**The entire cpanel bypasses any installed authentication plugin. All GETs serve the static SPA bundle; writes happen against the core runtime API which validates the operator credential (cookie / header) before any plugin hook runs

The cpanel is the bootstrap entrypoint of the runtime — see Bootstrap independence. The login form POSTs the API key to /api/admin/session; the runtime sets the HttpOnly buntime_api_key cookie and the browser attaches it to every subsequent same-origin request (cpanel API calls plus plugin iframes). JavaScript never holds the credential. The runtime gate at apps/runtime/src/app.ts validates the cookie/header on each request and bypasses plugin onRequest hooks when valid.

The client-side basepath discovery reads the injected <base> tag:

apps/cpanel/src/index.tsx
function getBasePath(): string {
const base = document.querySelector("base");
if (base?.href) {
const url = new URL(base.href);
return url.pathname.replace(/\/$/, "") || "/";
}
return "/";
}

This is why the same bundle works with the /cpanel/ prefix (default), /admin-panel/, or wherever the operator mounts it.

apps/cpanel/
├── manifest.yaml
├── package.json
├── scripts/build.ts # custom Bun bundler
├── src/
│ ├── index.tsx # React entry
│ ├── index.html # HTML template
│ ├── index.css # tailwind base
│ ├── routeTree.gen.ts # generated by TSR plugin
│ ├── routes/ # file-based routes (TanStack Router)
│ │ ├── __root.tsx # global ApiKey auth gate + unified MainLayout
│ │ ├── index.tsx # redirects / → /overview
│ │ ├── overview.tsx # runtime dashboard
│ │ ├── keys.tsx # API keys CRUD
│ │ ├── apps.tsx # deployed apps
│ │ ├── plugins.tsx # installed plugins
│ │ ├── $.tsx # catch-all (plugin iframe host)
│ │ └── locales/ # pt.json, en.json
│ ├── components/
│ │ ├── auth/
│ │ │ ├── api-key-login.tsx
│ │ │ └── loading-splash.tsx
│ │ └── admin/ # internal namespace (no /admin URL)
│ │ ├── shared.tsx # helpers, atoms (SourceBadge, Section, ...)
│ │ └── tabs/
│ │ ├── overview.tsx
│ │ ├── keys.tsx
│ │ ├── apps.tsx
│ │ └── plugins.tsx
│ ├── contexts/
│ │ ├── api-key-auth-context.tsx # ApiKeyAuthProvider, useApiKey
│ │ └── header-context.tsx # HeaderProvider, useHeader (routes inject actions)
│ ├── helpers/
│ │ ├── api-client.ts # generic HTTP client (discovers /api via well-known)
│ │ ├── admin-api.ts # types + endpoints for the runtime API surface
│ │ ├── upload-validation.ts # app/plugin package validation
│ │ ├── i18n.ts
│ │ └── query-client.ts
│ ├── hooks/
│ ├── types/
│ └── utils/
└── dist/ # build output

A single HttpOnly session cookie covers the entire cpanel — every runtime section (/cpanel/overview, /cpanel/keys, /cpanel/workers, /cpanel/plugins) and every plugin UI mounted via <z-frame>. The root layout (apps/cpanel/src/routes/__root.tsx) reads useApiKey().status and renders the login form before any route component is mounted.

  • The entry form POSTs the API key to POST /api/admin/session. The runtime validates it against ApiKeyStore (or matches RUNTIME_ROOT_KEY) and replies with Set-Cookie: [REDACTED] HttpOnly; SameSite=Strict; Path=/.
  • The cookie is Secure when the request arrives via HTTPS; on plain http://localhost:8000 it’s marked non-secure so dev still works.
  • Lifetime is configurable via RUNTIME_CPANEL_SESSION_TTL (default 24h).
  • JavaScript never sees the key — sessionStorage is empty, XSS cannot exfiltrate the credential.
  • Initial probe: GET /api/admin/session — the browser auto-attaches the cookie. If the response is 200 the SPA renders; on 401 the login form is rendered.
  • Sign out: DELETE /api/admin/session clears the cookie (Max-Age=0).
  • The cpanel never sends Authorization; it has nothing to do with end-user authentication sessions.
POST /api/admin/session
Content-Type: application/json
{"key": "btk_..."}

The cookie travels automatically on same-origin requests including <iframe>-initiated fetches and <a download> URLs — that’s why the “Plugins” sidebar entries (Gateway, Proxy, KeyVal, etc.) load without any auth-related orchestration.

In environments with RUNTIME_API_PREFIX="/_" (e.g., Rancher), the real path is POST /_/api/admin/session. Note that the API endpoint still lives under /api/admin/session (it is the core runtime’s admin-session endpoint, not a cpanel path). The cpanel URL paths have no /admin segment.

The cpanel’s runtime sections are first-class routes under /cpanel/, each managing one slice of the runtime. Canonical reference for the underlying API at the Runtime API Reference; the summary below covers what belongs to the CPanel layer.

Real authorization remains on the runtime side. The frontend uses the capabilities returned by /api/admin/session only to show or hide actions.

ProfileExpected use
adminFull administration, including key creation and revocation
editorDeploy and remove apps/plugins, without managing keys
viewerRead access to apps, plugins, workers, and keys
customIndividually selected permissions

RUNTIME_ROOT_KEY appears as the synthetic principal root (isRoot: true, role: admin, full access). Recommendation: use the root key only for bootstrap and create a dedicated admin or editor key for browser use.

Defined in helpers/admin-api.ts:

workers:read · workers:install · workers:remove · workers:restart
plugins:read · plugins:install · plugins:remove · plugins:config
keys:read · keys:create · keys:revoke

A “worker” here is a deployed serverless artifact served by the WorkerPool — the runtime treats apps and workers as the same concept (pre-2026-05-19 the two vocabularies coexisted with apps:* for filesystem ops and workers:* for runtime ops; they collapsed into one set).

Orthogonal to permissions, each key carries a namespaces list that scopes which @scope workers/plugins it can see and manage — the key-create Sheet exposes a Namespaces field (default *). See namespace-scoped access control.

SectionPathOperations
Overview/cpanel/overviewOperator dashboard: principal info, capability matrix, counts (apps / plugins / keys / permissions)
API keys/cpanel/keysList (non-revoked), create by profile (admin/editor/viewer/custom), display the generated secret once, revoke (except the key in use)
Workers/cpanel/workersList workers in workerDirs, show built-in vs uploaded origin, upload (POST /api/workers/upload) .zip/.tgz/.tar.gz, remove an entire uploaded worker or a specific uploaded version
Plugins/cpanel/pluginsList installed/loaded plugins, show built-in vs uploaded origin, upload (POST /api/plugins/upload), POST /api/plugins/reload, remove uploaded plugins and reload

Client-side package validation lives in helpers/upload-validation.ts (extension, size, presence of manifest.yaml or package.json). The same package semantics apply to any automation hitting POST /api/workers/upload or POST /api/plugins/upload directly.

The UI treats removable=false as authoritative. Built-in rows remain visible for inspection but do not render delete actions; the runtime enforces the same rule server-side with 403 errors.

For plugins that expose a UI, the CPanel hosts each UI in an iframe managed by the <z-frame> web component (registered in index.tsx). The catch-all route routes/$.tsx resolves the plugin by path and renders the appropriate frame.

AspectDetail
Web component<z-frame> from the @zomme/frame package
Discovery/.well-known/buntime returns the apiPrefix and the UI catalog
Plugin auth (inside the iframe)A plugin’s own endpoints continue to be governed by its own auth model — the iframe is a separate origin context. The cpanel shell itself is not protected by a plugin.
Unified navigationThe cpanel sidebar lists runtime sections (group “Runtime”: Overview/Keys/Workers/Plugins) and plugin menus (group “Platform”) side-by-side, permission-filtered. Both share the breadcrumb header and the global logout button in the sidebar footer.

The HTTP client (helpers/api-client.ts) queries /.well-known/buntime to discover:

  • The real apiPrefix (/api by default; /_/api when RUNTIME_API_PREFIX="/_")
  • The list of available plugin UIs
  • Runtime metadata

This allows the same CPanel bundle to work under any prefix configured by the operator, without a rebuild.

The cpanel is intentionally the first thing that works in a fresh deploy. Because plugins are themselves installed and configured through the cpanel, the cpanel cannot afford to depend on any plugin for its own access control. The runtime ships RUNTIME_ROOT_KEY as the synthetic principal root (full access), which is enough to enter the cpanel on day zero and bootstrap everything else.

  1. Operator deploys the runtime with RUNTIME_ROOT_KEY set in the environment.
  2. Operator opens /cpanel/ in a browser → the cpanel calls GET /api/admin/session; no cookie yet → 401 → renders ApiKeyLogin (no plugin involved).
  3. Operator pastes the root key → the cpanel POSTs to /api/admin/session. The runtime validates and sets the buntime_api_key HttpOnly cookie. The SPA mounts the shell at /cpanel/overview.
  4. Operator goes to /cpanel/keys → creates a dedicated admin (or editor) API key for daily use and signs out + back in with that key (the new key replaces the root key in the cookie).
  5. Operator uploads/installs any plugin (and any provider config) via /cpanel/plugins → calls POST /api/plugins/reload.
  6. Other apps and plugin UIs (mounted under their own bases) become governed by their own auth from this point on. The cpanel itself remains outside that gate.
ConstraintImplication
The cpanel is where you install/configure pluginsIf the cpanel required a plugin to authenticate, day-zero access would be impossible
The cpanel is where you create API keysIf keys were governed by plugin sessions, key creation would chase its own tail
Distinct auth surfacePlugin auth uses sessions (cookies) for end users; the cpanel uses runtime-managed API keys for operators
Single source of truthA single cookie issued by the runtime gates the entire cpanel (same-origin SPA + iframes); CLI continues to use the X-API-Key header against the same backend

Other apps and plugin UIs continue to be governed by their own auth model. The two auth systems coexist without conflict and are orthogonal.

Terminal window
# dev (watch)
cd apps/cpanel
bun dev
# production
bun run build
# quality
bun run lint # biome + tsc --noEmit
bun test

Build artifacts go to dist/. In CI/CD, the CPanel package is generated with the same workflow as the other Buntime apps.

https://buntime.home/cpanel/ # redirects to /cpanel/overview
https://buntime.home/cpanel/overview # operator dashboard
https://buntime.home/cpanel/keys # API keys management
https://buntime.home/cpanel/workers # deployed workers
https://buntime.home/cpanel/plugins # installed plugins
https://buntime.home/cpanel/gateway # plugin UI (example — varies per installed plugin)
https://buntime.home/.well-known/buntime # discovery
https://buntime.home/_/api/admin/session # real endpoint (with RUNTIME_API_PREFIX=/_)
GuaranteeHow it is enforced
Secret never reaches JSHttpOnly cookie — document.cookie cannot read it; XSS cannot exfiltrate it
SameSite=StrictCookie is not sent on cross-site navigations or third-party iframes — only on first-party same-origin requests
Secure on HTTPSWhen the request arrives over TLS, the cookie is marked Secure so it never leaks over plaintext
Configurable lifetimeRUNTIME_CPANEL_SESSION_TTL (default 24h) — issue short-lived cookies for tighter control
Authorization on the runtimeFrontend only hides UI; backend validates every request via the ApiKeyStore (cookie/header alike)
Cpanel isolated from plugin auth end-to-endpublicRoutes: { GET: ["/**"] } keeps the SPA bundle reachable; the SPA itself enforces the API-key gate client-side
Plugin endpoints still protectedA plugin’s own auth continues to protect its endpoints and plugin UI hosts (the iframes are separate origins)

File-browser — two upload paths, two contracts

Section titled “File-browser — two upload paths, two contracts”

The Workers/Plugins tabs expose two distinct upload mechanisms that hit different runtime endpoints and follow different rules. Knowing which one you are using matters because they validate paths differently.

Path-agnostic install — <UploadArchiveButton>

Section titled “Path-agnostic install — <UploadArchiveButton>”

The explicit “Upload” button in the tab header sends the archive to POST /api/{workers,plugins}/upload. The server reads manifest.yaml / package.json from the archive and places the contents at the policy-derived install path, ignoring the FileBrowser’s current path. So uploading while browsing inside @scope/foo/1.0.0/ (workers) or @scope/foo/ (plugins) lands the new install at the right semver/plugin folder regardless of where you are in the tree.

Layout rules and full archive contract: see the Runtime API Reference — workers upload. Scoped names work end-to-end on this path.

Drag-drop into the current folder — /api/{workers,plugins}/files/upload

Section titled “Drag-drop into the current folder — /api/{workers,plugins}/files/upload”

Dropping files onto a folder in the FileBrowser (or using the “Upload here” context action) hits a different endpoint that respects the current path and is gated by a PathPolicy (workersPathPolicy or pluginsPathPolicy in apps/runtime/src/libs/fs/path-policies.ts).

Both policies are scope-aware. If the first segment starts with @, the next segment is treated as the second half of the unit name — @scope/name is recognised as one unit, not two folders:

PathWorkers policyPlugins policy
@scope/rejected (no name yet)rejected (not a plugin yet)
my-worker/1.0.0/unit root (writes allowed)n/a
my-worker/1.0.0/src/inside unit (writes allowed)n/a
@scope/my-worker/1.0.0/unit root (writes allowed)n/a
@scope/my-worker/1.0.0/src/inside unit (writes allowed)n/a
@scope/my-worker@1.0.0/unit root, flat variantn/a
my-plugin/n/aunit root
@scope/my-plugin/n/aunit root
@scope/my-plugin/dist/x.jsn/ainside unit (writes allowed)

Drag-drop and the explicit Upload button both work for scoped names. The @scope folder alone (no name segment) is treated as an “above any unit” location: writes are rejected; navigation is fine.