Skip to content

Plugin Hub — author & ops manual ​

For plugin authors (what a package must look like, how to publish) and operators (how to publish, revoke, and where the keys live). The full client behaviour and API contract live in design/plugin-marketplace.md (Chinese); this page is the practical version. The Chinese page 官网插件工坊(作者/运维手册) has the same content in Chinese.


1. Two distribution channels ​

Steam WorkshopOfficial Plugin Hub
Who uses itSteam buildSelf-distributed build (TRANSBOX_DISTRIBUTION=selfpub)
DeliveryValve CDN/api/workshop/* + private R2 bucket
LoginSteam accountTransBox account (device-code login in the client)
Package.zip (root mod.json).tbmod (same zip, plus a signature)
TrustSteam moderationEd25519 signature verified against a key baked into the client

The same plugin can ship on both channels. Load priority is fixed: local > steam > web (only one copy per id).


2. Package format: .tbmod ​

A plain zip with mod.json at the root (no wrapping folder, and manifest.json is not accepted).

my_mod.tbmod
├── mod.json              required: id / name / version / type / **target_app_version**; optional: min_app_version …
├── terms.json            glossary (type=glossary)
├── provider.py           engine implementation (type=mt/asr/tts…)
└── locales/zh-CN.json    plugin-local strings (recommended)

Rules (violations are rejected automatically, and the client refuses to install):

  • id needs at least two segments using [A-Za-z0-9_-]; reserved org segments (transbox, steam, community, …) are admin-only;
  • version must be strict semver and greater than the latest published version. Both the in-app publish form (“Plugin Hub → Publish”) and the web author centre (/workshop-author) have a required Version field (defaults to mod.json): type a higher version and submit — packaging writes that value into the package's mod.json (only the version key). After an in-app publish the value is also synced back into your local mod.json (formatting preserved);
  • target_app_version is required (write "0.2"): it is how the client tells an "0.2-ready" plugin from a 0.1-era one. A package without it is refused at install time (InstallError), and dropping the folder into extensions/ will not get it recognised either — the point is to stop old plugins from being auto-installed into a new client and blowing up. See Compatibility & version gates;
  • min_app_version is optional: omitted (or empty) = 1.0.0 = “no restriction” (same convention as the client and the CLI packer); if you do declare it, it must be x.y.z or the publish is rejected. The in-app publish flow (“Plugin Hub → Publish”) fills in the default for you;
  • POSIX relative paths only: no .., absolute paths, symlinks, or duplicate entries;
  • .workshop/ (client-side receipts), __pycache__/, *.pyc are forbidden;
  • nested archives (.zip/.7z/.rar/.tbmod) are forbidden;
  • files referenced by entrypoint / icon / preview_image must exist in the package.

Size limits (enforced by the server, then written into the signed manifest):

data tierscript tier
Archive≤ 5 MB≤ 50 MB (format limit)
Unpacked≤ 50 MB (ratio ≤ 20)≤ 200 MB
Files≤ 500≤ 2000
Single file≤ 5 MB≤ 50 MB

⚠️ Uploads from the app or the web author centre are hard-capped at 20 MB (stricter than the format limits above): the hub runs on a Cloudflare Pages Function, and packages from ~45 MB up get cut off mid-upload — the client only sees a write failure with no useful message. Contact us for larger packages, or move the big files into model_artifacts.

Do not bundle big models or big native runtimes — declare them with model_artifacts (HuggingFace / ModelScope); the client downloads them into its data directory, and this works for DLLs / .exe runtimes too.

Need another plugin installed first? Declare it with requires_plugins (mirrors Steam Workshop Required Items), up to 8 entries:

json
"requires_plugins": [
  { "id": "workshop.amd_llama", "min_version": "1.0.0", "reason": "provides the llama.cpp runtime" }
]

Client behaviour: if the prerequisite is missing / disabled / too old, the plugin cannot be enabled — the card shows “Requires plugins: …” with a one-click install (code-bearing plugins are never installed silently). Locate files inside a prerequisite with host.plugin_dir(id) — never hard-code a folder name (workshop installs use the plugin id as the folder). See the client's docs/WORKSHOP_GUIDE.md §3.11.


3. Tier: data vs script ​

Derived from the package contents, strictly:

  • contains .py/.qml/.js/.dll/.exe/..., declares permissions, declares ui_extensions, or its type is an engine type → script;
  • everything else (plain terms.json glossaries, language packs, QML-free themes) → data.

Client behaviour:

After installUpdatesNew device sync
datafollows the user's toggletakes effect after restartinstalled normally
scriptdisabled by default; confirmation dialog on first enabletakes effect after restart (new permissions ask again)installed, not enabled

Script plugins run with the same privileges as the host, unsandboxed. Declare what you use in mod.json → permissions (net / clipboard / hotkey / window / audio / fs_read / fs_write / process); reviewers and users both read it.


4. Signing: workshop.json + envelope ​

Generated and signed by the server after review — authors do nothing:

jsonc
{
  "schema": 1,
  "id": "web.alice.my_glossary",
  "version": "1.2.0",
  "sha256": "…",              // sha256 of the whole .tbmod
  "size": 12345,
  "unpacked_size": 40960,
  "max_file_size": 20480,
  "min_app_version": "1.0.0",
  "permissions": ["net"],
  "category_tier": "data",
  "files": [{ "path": "mod.json", "sha256": "…" }],
  "signed_by": "wk-2026-1",   // must equal the envelope key_id
  "created_at": 1790999253
}
jsonc
{ "payload": "<raw text above>", "signature": "<Ed25519 base64>", "key_id": "wk-2026-1" }

Client verification (all must pass): key_id is in the baked-in allow-list → signature verifies over the payload’s raw UTF-8 bytes → signed_by == key_id → byte count and sha256 match → after unpacking, every declared file matches and one extra file voids the install.

Key management: private key lives in a Cloudflare Pages Secret (WORKSHOP_PKG_SIGNING_KEY), the matching public key is compiled into the client (plugins/workshop/signing.py). Rotation: ship a client that trusts the new key first, then switch the server key_id.


5. Where the client puts things ​

  • Install dir: <install>/data/workshop/<id>/ (separate from the shipped extensions/, survives app updates);
  • Each plugin carries a client-side receipt (.workshop/receipt.json) plus the signed manifest (.workshop/workshop.json[.sig]) for offline re-verification;
  • Installs are atomic (stage → verify → swap), the previous version is kept in .previous/;
  • Updates take effect after a restart;
  • Auto-sync (web channel): 10 s after start, then every 24 h — revocations (ETag + monotonic seq), subscription sync (cursor + tombstones), update check.

6. Publishing ​

6.1 Official plugins ​

bash
# run inside the trans-box-website repo; the key file is the same one stored as a Cloudflare Secret
node scripts/workshop-publish.mjs \
  --dir ../trans-box/extensions/community_theme_glassmorphism \
  --official --changelog "First release" \
  --private-key-file <path-to-private-key-backup>

# --all-official scans every non-example_* folder; --dry only packs and validates

The script packs the .tbmod, validates it locally with the same server code (server/workshop-zip.js), then uploads straight to production R2 + D1 through wrangler. Proxy variables are stripped in child processes (bucket and database are in APAC).

6.2 Community uploads (available) ​

Open /workshop-author in a browser and sign in with your site account:

  1. Pick a package: a .tbmod/.zip file, or a whole plugin folder (root mod.json required — the server packs it for you). The server recomputes hashes and re-validates everything on upload; bad packages are rejected immediately.
  2. Fill in the listing: title, summary, description, English slot, changelog, visibility (public / unlisted / private), licence.
  3. Submit for review:
    • pure-data plugins (glossaries, language packs, QML-free themes) can auto-publish;
    • anything containing code goes to the review queue — an admin approves (signs and publishes) or rejects it in /admin/workshop (the rejection reason is shown to the author).
  4. Manage published plugins: publish a new version (version must increase), edit metadata/visibility, or unpublish (soft delete — already-installed users are unaffected).

Quota: at most 5 versions waiting for review, at most 10 versions per author per day. You can also publish from the client: in the official build open Workshop → Plugin Hub → Publish and pick the plugin folder (it uses the workshop:publish scope of the workshop token; the server validates exactly the same way). Managing published plugins happens in the web author centre.


7. API quick reference ​

Prefix /api/workshop; all timestamps are Unix seconds; pages start at 1; size is bytes.

EndpointAuthNotes
GET /items?page&page_size&q&tag&type&sort&lang&min_app&officialanonymouscatalogue (sort: updated/recent/popular/top)
GET /items/{id}anonymousdetail (versions, permissions, i18n slots)
GET /items/{id}/download?app_version=anonymousreturns {url, expires_at, size, sha256, signed}; 409 incompatible / 410 revoked
GET /files/{token}tokenstreams the .tbmod, supports Range (10 min TTL)
POST /updates/checkanonymousbatch update/revocation check, app_version required
GET /revocationsanonymoussigned revocation list, ETag/304
GET /keysanonymouspublic key info (never used to add trust)
GET /me/subscriptions?since=workshop token or site cookiemy subscriptions (incremental + tombstones + cursor)
PUT / DELETE /me/subscriptions/{id}same (workshop:subscribe)subscribe / unsubscribe (idempotent)
GET /me/publishedsamemy items (with per-version review status and rejection reason)
POST /uploadsauthor (cookie or workshop:publish)upload a package (archive, or files[]+paths[] for a folder); returns upload_id + validation summary
POST /items / POST /items/{id}/versionssamecreate an item / submit a new version from an upload_id
PATCH / DELETE /items/{id}sameedit metadata/visibility, unpublish
POST /items/{id}/reportsigned-in userreport (malware/copyright/illegal/spam/other)
POST /admin/publish …Cloudflare Accessadmin: publish, visibility, review queue, approve/reject, revoke

Client headers: Authorization: Bearer <TBW1 token>, User-Agent: TransBox/<version> (+site), X-TransBox-App-Version, X-TransBox-Distribution. A proper User-Agent is mandatory — Cloudflare answers 403 (error 1010) to the default Python-urllib.


8. Common rejections ​

SymptomCause
bad_package: mod.json must be at package rootwrapping folder, or manifest.json instead
bad_package: forbidden path.workshop/, __pycache__/, .pyc shipped
reserved_idorg segment uses a reserved name
409 conflict: version must be greaterversion not bumped
413 too_largetier limits exceeded — use model_artifacts for models
Client refuses to installkey_id not trusted / bad signature / size or sha256 mismatch / extra files

TransBox — empowering borderless cross-language communication for everyone