Apps (plugins): reference
Manifest (deplao-plugin.json)
| Field | Required | Rules |
|---|---|---|
id |
yes | lowercase reverse-DNS (vn.acme.spa); the last segment (spa) is the node prefix and must not clash with built-in nodes (zalo, fb, trigger…) |
name, version |
yes | version follows semver; an update installs only if it is higher than the installed version |
apiVersion |
yes | 1 |
runtime |
yes | web, local (needs backend.entry), remote (needs an https remote.baseUrl) |
permissions |
yes | API scopes (see "Authentication & permissions") + the plugin permissions below. May not request admin, plugins:manage, employees:manage, settings:write |
ui.entry |
when there is a UI | an .html file inside the plugin; remote: a URL on the same origin as remote.baseUrl |
ui.connectSrc |
no | https origins the UI may fetch directly (requires the network permission) |
ui.imgSrc |
no | external image sources the UI may display, https://<domain> or https://*.<domain> (e.g. Zalo avatars https://*.zadn.vn; requires the network permission) |
minDeplaoVersion |
no | semver |
contributes |
no | the extension points below |
Plugin permissions
| Permission | Allows |
|---|---|
storage |
deplao.storage (max 1 MB per value, 20 MB per plugin) and a private data folder |
ui:pages |
pages in the navigation bar |
ui:chat-panel |
a panel on the right of the chat screen that knows which conversation is open |
ui:composer |
buttons in the message composer, deplao.ui.insertComposerText |
ui:microphone |
the microphone inside the app's UI (the frame gets allow="microphone"; without this permission the browser blocks getUserMedia) |
workflow:nodes |
workflow nodes and triggers, deplao.workflow.trigger |
events:emit |
deplao.events.emit (names declared in contributes.events) |
network |
deplao.http.fetch in the backend, ui.connectSrc |
schedules |
contributes.schedules (cron) |
cloudflare:workers |
deplao.cloudflare in the backend (runtime local): create / update Workers and D1 databases named <prefix> or <prefix>-… (prefix = the last segment of the id) on the Cloudflare account connected in Deplao; the Cloudflare token is never handed to the app |
Extension points (contributes)
| Point | Declaration | Notes |
|---|---|---|
pages |
{ id, title, path } |
path is a hash route inside ui.entry |
chatPanels |
{ id, title, path, width } |
receives deplao.ui.context() and onContext |
composerButtons |
{ id, title, icon, target: 'ui' | 'backend' } |
backend: commands[id](context) returns { insertText } or { toast } |
settings.schema |
JSON Schema (string, number, integer, boolean, enum; x-secret, x-multiline) |
auto-generated form on the Apps, API & Webhook page (Ứng dụng, API & Webhook): click the app tile → Settings (Cài đặt); secrets are encrypted |
workflowNodes |
{ type: '<prefix>.<name>', label, configSchema, outputs } |
backend nodes[type](cfg, run, ctx); x-widget: account|thread|label|assistant (assistant = picks a Deplao AI assistant and stores its id; includes a "Default" (Mặc định) option = left empty), x-template |
triggers |
{ type: '<prefix>.trigger.<name>', label, configSchema } |
fired with deplao.workflow.trigger(type, payload); payload fields are used as {{ $trigger.<field> }} |
routes |
{ method, path, scope } |
/api/v1/plugins/<id><path>; the caller needs scope; body ≤ 1 MB |
schedules |
{ id, cron, handler } |
calls handlers[handler]; if the previous run hasn't finished, the next one is skipped |
events |
['booking.created'] |
emitted as plugin.<id>.booking.created |
SDK deplao.*
| Group | Functions |
|---|---|
| API v1 | deplao.<group>.<operationId>(params), deplao.call(operationId, params), deplao.api.request(method, path, { query, body }). Functions that return files (downloadMessageMedia, downloadLibraryItem, downloadLibraryThumb) return { name, mime, size, base64 } (≤ 25 MB, requires media:read; Zalo voice messages not yet on the machine are downloaded by the app automatically) |
storage |
get, set, delete, list(prefix); backend: dataDir() |
settings |
get(), onChange(cb) |
events |
on(type, handler) (requires events:subscribe), emit(name, data) |
workflow |
trigger(type, payload) |
media |
upload(file) → an upload_id to use in attachments when sending a message (≤ 25 MB); the Media group's operationIds can be called like any other group |
log |
debug/info/warn/error → plugin log (Apps, API & Webhook page → click the app tile → Log, Nhật ký) |
system |
info() |
ui (UI only) |
context, onContext, theme, openConversation, insertComposerText, toast, navigate(pageId), openExternal(url), onCommand |
backend (UI only) |
call(name, params) → commands[name] in the plugin's own backend |
http (backend only) |
fetch(url, init): requires network, blocks the local network, ≤ 5 MB; returns text or base64 depending on the Content-Type (if it is missing, the content is inspected), init.responseType = 'text' / 'base64' to force one |
cloudflare (backend only) |
status(), d1.ensure(name), d1.migrate(name, dir), d1.query(name, sql, params), workers.get(name), workers.deployBundle({ name, bundleDir, bindings, secrets, workersDev }): requires cloudflare:workers; every call counts toward the app's budget of 100 requests / 5 minutes |
Errors come back as an Error with a code (the same error codes as the API: FORBIDDEN_SCOPE, FORBIDDEN_ACCOUNT, VALIDATION_ERROR…) and a status. deplao.cloudflare adds PLUGIN_PERMISSION, CF_NOT_CONNECTED, CF_PERMISSION, CF_NAME_FORBIDDEN, CF_BUNDLE_INVALID, CF_API.
Backend (runtime local)
js
module.exports = {
async activate({ deplao, settings, pluginId, dataDir }) {},
async deactivate() {}, // 5 seconds max
async onUpgrade(prevVersion) {},
async onSettingsChanged(settings) {},
nodes: { 'shop.lookup': async (cfg, run, ctx) => ({ found: true }) }, // 60 seconds
routes: { 'GET /orders/:code': async (req, ctx) => ({ status: 200, body: {} }) }, // 30 seconds
handlers: { remind: async (ctx) => {} }, // schedules, 120 seconds
commands: { book: async (params, ctx) => ({}) }, // UI backend.call, composer buttons
};- Runs in a separate process (utilityProcess, 256 MB heap) and only receives a few environment variables;
require('electron')andprocess.exitare blocked;console.*goes to the plugin log. - After a crash it restarts automatically after 2 s, 10 s, 60 s; on the 4th crash the plugin is turned off ("Disabled due to error",
Tắt do lỗi). If it fails to answer 3 pings in a row, it is restarted. - A plugin is still Node code running on the machine: Deplao only hands out data according to the approved permissions, but it does not isolate the process's file system or network access. Only install plugins from trusted sources.
Runtime remote
- Deplao calls
POST {remote.baseUrl}/rpcwith the body{ id, method, params }and the headersX-Deplao-Plugin,X-Deplao-Timestamp,X-Deplao-Signature: v1=HMAC-SHA256(secret, timestamp + "." + body). Respond with{ result }or{ error: { code, message } }. method:activate,deactivate,ping,node.execute,route.handle,schedule.run,composer.command,settings.changed.- On install, Deplao creates a secret and an API key (bound to the plugin, revoked when it is uninstalled) and shows them once. Your server uses the key to call API v1 and to register webhooks for receiving events.
baseUrlmust be https (http is allowed only for127.0.0.1during development).
Security
- The plugin UI runs on its own origin
deplao-plugin://<id>in a sandboxed iframe (no popups, no navigating the app's pages, no access to the app's DOM, store or IPC); the CSP blocks external scripts, outgoing connections exceptui.connectSrc, and external images exceptui.imgSrc. - Every data call goes through API v1 with the principal
plugin:<id>: limited to the scopes and accounts granted at install time, with requests logged just like for API keys. - Zip packages are checked: max 50 MB, max 200 MB unpacked, no absolute paths or
.., no symlinks, matching CRCs. - The plugin management IPC channels (
plugins:*) cannot be called through the API or from a plugin. The same applies to the Cloudflare channels (cloudflare:*), exceptcloudflare:workers:*when the app has thecloudflare:workerspermission: Deplao checks the permission and the name prefix before making the call.
Didn't find your answer?
Message us on Telegram