5.3 KiB
Sub2API Business Plugin Control Plane V1
This directory contains the independent administrator-only control plane for
Business Plugins. It is deliberately separate from Sub2API Core and from the
existing .s2plugin OpenAI OAuth transport runtime.
The control plane owns the installed-plugin registry, a constrained marketplace catalog, signed package verification, revision directories, lifecycle state, encrypted configuration metadata, menu preview/apply, and its own audit log. Core remains authoritative for users, administrator roles, balances, subscriptions, billing, and audit records. The service never connects to Core PostgreSQL/Redis and never sends a Core JWT or Admin Key to the browser.
Run locally
CORE_BASE_URL=http://127.0.0.1:8080 \
PLUGIN_HOST=127.0.0.1 PLUGIN_PORT=8090 \
PLUGIN_REGISTRY_DIR=./data \
PLUGIN_ENV=development \
PLUGIN_ALLOW_UNSIGNED=true \
PLUGIN_CONFIG_KEY=local-development-secret-at-least-32-chars \
go run .
PLUGIN_ALLOW_UNSIGNED=true is a development-only switch. Production
packages must contain signature.json, use Ed25519, and match a trusted key
from PLUGIN_TRUSTED_PUBLISHERS (a JSON object of key ID to base64 public
key). PLUGIN_CONFIG_KEY is required in every environment; use a randomly
generated secret in production and keep it stable across restarts so encrypted
plugin configuration remains decryptable.
Open /admin/ directly or expose the service through the reverse proxy in
deploy/. The first login is the existing Core administrator login; no plugin
user table is created. The control plane stores only a short-lived server-side
session and encrypted plugin configuration.
Control-plane endpoints
GET /healthz
GET /readyz
POST /login POST /login/2fa POST /logout
GET /api/marketplace POST /api/marketplace/install (JSON: plugin_id, version)
GET /api/me GET /api/plugins GET /api/plugins/{id}
POST /api/plugins/install (multipart field: package)
POST /api/plugins/{id}/install (multipart field: package)
POST /api/plugins/{id}/upgrade (multipart field: package)
POST /api/plugins/{id}/enable|disable|rollback|uninstall
DELETE /api/plugins/{id} (same delete operation as uninstall)
GET|PUT /api/plugins/{id}/config
POST /api/plugins/{id}/menu-preview|menu-apply
POST /api/menu-items/preview|apply (JSON: {"plugin_id":"..."})
GET /api/audit
Every mutation requires the plugin CSRF token and an Idempotency-Key. A
mutation returns an operation ID even when it completes synchronously. A local
upload or marketplace download only verifies and stages the package in the
registry (disabled / “已入库,待启用”); it never starts a process. The
administrator must configure the service and click enable. Only a
successful health and readiness check changes the plugin to healthy and
installs its runtime/menu. Failed installation and upgrade never replace the
active revision. Delete/uninstall is allowed only after disable and removes
plugin files, not Core data.
Marketplace catalog
The marketplace is server-side only. The browser receives metadata and sends a
plugin ID/version; it never receives an archive URL and cannot request an
arbitrary download. Set PLUGIN_MARKETPLACE_INDEX to a local JSON file (the
default) or an HTTPS index URL. Remote indexes and archives are restricted to
the exact hosts in PLUGIN_MARKETPLACE_ALLOWED_HOSTS; HTTP is accepted only
for loopback sources in PLUGIN_ENV=development. Redirects, credentials,
queries, fragments, oversized responses, path escapes, and hash mismatches are
rejected. The package must still pass the normal manifest signature, file hash,
and Core compatibility checks.
Catalog format (schema version 1):
{
"schema_version": 1,
"source": "internal-release-catalog",
"entries": [
{
"plugin_id": "example.plugin",
"name": "Example Plugin",
"version": "1.0.0",
"description": "Administrator extension",
"archive_url": "example.plugin-1.0.0.s2plugin",
"archive_sha256": "SHA256_OF_ARCHIVE",
"archive_size": 12345,
"publisher_key_id": "publisher-key-id",
"core_api_baseline": "sub2api-0.1.183",
"tested_core_versions": ["0.1.183"],
"capabilities": ["example.v1"]
}
]
}
An entry is never an implicit upgrade. If the plugin ID is already registered, use the existing upgrade flow, then enable it explicitly.
Plugin package
Packages are ZIP files with manifest.json, optional detached
signature.json, and declared ui/ files. A package may also include
service/ files when the control plane owns the plugin process; external
service packages omit backend.command and require a configured loopback
service_url before enabling. SHA-256 hashes in the manifest cover every
declared file. Absolute paths, traversal, duplicate entries, symlinks,
undeclared hashes, oversized files, unknown manifest fields, and untrusted
publishers are rejected before staging. Installation uses a per-plugin
revision directory and an atomic registry JSON update.
Deliberate V1 limits
The control plane does not register Core routes, change Core migrations, run arbitrary proxy URLs, provide transparent iframe SSO, or move billing and subscription hot-path logic out of Core. A subscription manager remains a separate business plugin that consumes its own typed Core API adapter.