# Plugin deployment Uploading a bundle plugin, enabling it, and managing permissions. ## Upload — `POST /api/plugin/:pluginId/upload-bundle` Upload the built bundle to the host. Uses `multipart/form-data`. The `pluginId` in the URL must match the `id` in `plugin.json`. Preferred (handles entry + chunks + maps + css automatically): ```bash TEABLE_ENDPOINT=http://localhost:3003 TEABLE_PAT= \ node /root/deployments/inkreach-teable/packages/plugin-cli/dist/index.js deploy -d . ``` Raw curl equivalent (legacy single-file form still works): ```bash PLUGIN_ID=$(python3 -c "import json; print(json.load(open('plugin.json'))['id'])") curl -s -H "Authorization: Bearer $TEABLE_PAT" \ -F "js=@dist/main.js" \ -F "css=@dist/styles.css" \ -F "manifest=@plugin.json" \ "http://localhost:3003/api/plugin/$PLUGIN_ID/upload-bundle" ``` **Auth note**: Supports both session cookies (`auth_session`) and Personal Access Tokens (`Authorization: Bearer `). Runtime asset GETs (`/api/plugin/:id/runtime/*`) are session-only (PAT gets 403 by design) — verify those with a signed cookie, not a PAT. **Fields** (v2 multi-asset, since 2026-10): | Field | Required | Max count | Max size | Description | |---|---|---|---|---| | `js` | Yes | 20 | 5 MB each | Entry + lazy chunks. Entry = the file whose name matches `manifest.entry` (a lone differently-named js file still works — legacy fallback) | | `css` | No | 5 | 1 MB each | Stylesheets, concatenated under the `manifest.style` name | | `map` | No | 20 | 5 MB each | Source maps, served later as `application/json` | | `manifest` | Yes | 1 | 10 KB | `plugin.json` manifest | - **Total cap 15 MB** across all assets. - Chunk filenames must be **FLAT** (`x-.chunk.js`) — the multipart transport strips directory parts from filenames. - The bundle dir on disk (`/.assets/plugins/bundle//`) is **wiped on every upload** — stale chunks are removed, and hand-patched files on disk are discarded. **Returns** `{ pluginId, hash, size, assets }` — `assets` is the stored file count (1 for legacy single-file uploads). **Registering a NEW plugin id**: `POST /api/plugin {"id": "", ...}` — PAT-accessible (`@TokenAccess()` since 2026-10) and **custom ids require the `plugin_developer` whitelist**. Without an explicit `id` the server mints a `plgXXX` id, which CANNOT receive bundles (manifest id must equal the DB id). Deleting a plugin (`DELETE /api/plugin/:id`) remains admin-only. **Security** (7-layer validation): 1. Authentication — valid session cookie or PAT required (401 if missing) 2. Developer whitelist — `plugin_developer` table lookup (403 if not whitelisted) 3. Rate limit — 10 uploads/min per user (429 if exceeded) 4. Size limits — JS/map ≤ 5 MB each, CSS ≤ 1 MB each, manifest ≤ 10 KB, 15 MB total 5. Manifest validation — required fields, `id` regex `[a-z0-9-]+`, `minAppVersion` ≤ host version 6. Content validation — the ENTRY must be IIFE format (prefix `!function` / `(function` / `(() =>` / `var =`); chunks get the forbidden-pattern check only (they are not IIFEs); JS rejects `