125 lines
5.3 KiB
Markdown
125 lines
5.3 KiB
Markdown
# 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
|
|
|
|
```sh
|
|
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
|
|
|
|
```text
|
|
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):
|
|
|
|
```json
|
|
{
|
|
"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.
|