Files
inkreach-teable-cli/skills/inkreach-teable-plugin/dynamic-plugins/deployment.md
T

236 lines
9.3 KiB
Markdown

# 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=<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 <PAT>`). 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-<hash>.chunk.js`) — the multipart
transport strips directory parts from filenames.
- The bundle dir on disk (`<backend-cwd>/.assets/plugins/bundle/<pluginId>/`)
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": "<kebab-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 <name>=<iife>`); chunks get the
forbidden-pattern check only (they are not IIFEs); JS rejects
`<script>`/`document.write`/`javascript:`; CSS strips `expression()`/
`@import`/`javascript:` URIs; maps are name/size checked only (they embed
original source text — pattern checks would false-positive)
7. Integrity — entry SHA-256 stored in DB for audit + PluginAuditLog row
(uploader/IP/UA); note the loader does NOT re-verify the hash at load time
## Enable — `PUT /api/base/:baseId/plugin-config/:pluginId`
The plugin must be enabled before it can be installed to any position.
```bash
curl -s -b "auth_session=$SESSION" -X PUT \
"http://localhost:3003/api/base/$BASE_ID/plugin-config/$PLUGIN_ID" \
-H 'Content-Type: application/json' \
-d '{"enabled":true,"tableIds":[]}'
```
- `tableIds: []` — enabled base-wide (all tables)
- `tableIds: ["tblX", "tblY"]` — enabled only for specific tables
## Install by position
### ContextMenu
```bash
curl -s -b "auth_session=$SESSION" -X POST \
"http://localhost:3003/api/table/$TABLE_ID/plugin-context-menu/install" \
-H 'Content-Type: application/json' \
-d '{"pluginId":"$PLUGIN_ID","name":"My Plugin"}'
```
Body: `pluginId` (required), `name` (optional display name).
### Panel
Panel install requires an existing panel. First create one, then install to it:
```bash
# Create a panel (if none exists)
PANEL_ID=$(curl -s -b "auth_session=$SESSION" -X POST \
"http://localhost:3003/api/table/$TABLE_ID/plugin-panel" \
-H 'Content-Type: application/json' \
-d '{"name":"My Panel"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
# Install plugin to the panel
curl -s -b "auth_session=$SESSION" -X POST \
"http://localhost:3003/api/table/$TABLE_ID/plugin-panel/$PANEL_ID/install" \
-H 'Content-Type: application/json' \
-d '{"pluginId":"$PLUGIN_ID","name":"My Plugin"}'
```
The `plg` prefixed ID is the panel's UUID, not a plugin ID. Create a panel first
if you don't already have one.
### Dashboard
```bash
curl -s -b "auth_session=$SESSION" -X POST \
"http://localhost:3003/api/base/$BASE_ID/dashboard/$DASHBOARD_ID/plugin" \
-H 'Content-Type: application/json' \
-d '{"pluginId":"$PLUGIN_ID","name":"My Plugin"}'
```
### View
```bash
curl -s -b "auth_session=$SESSION" -X POST \
"http://localhost:3003/api/table/$TABLE_ID/view/plugin" \
-H 'Content-Type: application/json' \
-d '{"pluginId":"$PLUGIN_ID","name":"My Plugin"}'
```
Returns `{ pluginId, pluginInstallId, name, viewId }`. Creates a new view
of type `plugin` in the table's view list. The plugin's `definePlugin.onLoad`
must call `ctx.addView(id, component)` to render the component as a view.
Authority Matrix entries are auto-created for the new install (default
denied for non-owner roles).
### Route (full-page app, since 2026-10)
The host renders a dedicated page for the plugin at
`/base/<baseId>/p/<pluginId>` — the whole main-area viewport inside the base
layout. The plugin registers via `ctx.addRoute(id, component)` and typically
brings its own router (HashRouter recommended, so Next.js routing is
untouched).
```bash
curl -s -H "Authorization: Bearer $TEABLE_PAT" -X POST \
"http://localhost:3003/api/base/$BASE_ID/plugin-installs" \
-H 'Content-Type: application/json' \
-d '{"pluginId":"$PLUGIN_ID","name":"My App","position":"route"}'
```
Notes (verified 2026-10 on plugin-showcase):
- `manifest.positions` must include `"route"`.
- The page itself does not gate on plugin-config enablement — the URL is the
access point. Follow the standard enable+install flow anyway so the plugin
shows up in management UIs and future permission layers behave uniformly.
- Host SDK hooks (e.g. `host.SDK.useBaseId()`) work on the route page because
it renders inside BaseLayout's provider stack.
## Authority Matrix (plugin permissions)
Plugins integrate with the 4-layer permission system. Three control layers:
| Layer | What it controls | Mechanism |
|---|---|---|
| **Plugin visible** | User sees plugin in context menu / panel / dashboard | `AuthorityMatrixRolePluginInstall` per-role per-install |
| **Plugin executable** | User can click and run the plugin | Checked at `PluginSlot` render |
| **Data operations** | Plugin's CRUD respects user's permissions | **Already protected** — plugins call `@teable/openapi` which passes through existing AM enforcement |
Default behavior:
- New installs default to disabled for non-owner roles
- Base owner always has full access
- Admin enables per role through the RoleEditor UI
## Developer whitelist
Only users in the `plugin_developer` table can upload bundles. To add a developer:
```sql
INSERT INTO plugin_developer (id, user_id, email, created_by)
VALUES (gen_random_uuid(), '<userId>', '<email>', '<adminUserId>');
```
## Getting a session cookie
Prefer a PAT (upload-bundle accepts `Authorization: Bearer <PAT>`). For a
session cookie, sign in with any real account created for the purpose
(`POST /api/auth/signup`), then add its `plugin_developer` row before uploading:
```bash
curl -s -c /tmp/cookie.txt http://localhost:3003/api/auth/signin \
-X POST -H 'Content-Type: application/json' \
-d '{"email":"<real-account>","password":"<password>"}'
SESSION=$(grep auth_session /tmp/cookie.txt | awk '{print $NF}')
```
## Full deploy script (copy-paste)
```bash
#!/bin/bash
set -e
DIR="${1:-.}"
UPLOAD_URL="${UPLOAD_URL:-http://localhost:3003}"
BASE_ID="${BASE_ID:?set to a throwaway sandbox base, never a production base}"
# Read plugin ID from manifest
PLUGIN_ID=$(python3 -c "import json; print(json.load(open('$DIR/plugin.json'))['id'])")
# Upload: teable-plugin deploy handles entry + chunks + maps + css.
# Raw-curl equivalent for a single-file bundle:
# curl -s -H "Authorization: Bearer $TEABLE_PAT" \
# -F "js=@$DIR/dist/main.js" -F "css=@$DIR/dist/styles.css" \
# -F "manifest=@$DIR/plugin.json" \
# "$UPLOAD_URL/api/plugin/$PLUGIN_ID/upload-bundle"
TEABLE_ENDPOINT="$UPLOAD_URL" TEABLE_PAT="${TEABLE_PAT:?set PAT env}" \
node /root/deployments/inkreach-teable/packages/plugin-cli/dist/index.js deploy -d "$DIR"
echo "Uploaded: $PLUGIN_ID"
# Enable
curl -s -H "Authorization: Bearer $TEABLE_PAT" -X PUT \
"$UPLOAD_URL/api/base/$BASE_ID/plugin-config/$PLUGIN_ID" \
-H 'Content-Type: application/json' \
-d '{"enabled":true,"tableIds":[]}' > /dev/null
echo "Enabled: $PLUGIN_ID"
```