Skip to content

Plugin Development Guide ​

Introduction ​

Komari supports extending the server with JavaScript plugins. A plugin is a ZIP package containing a manifest (komari-plugin.json) and an entry script (default script.js). Plugins run inside the server process in their own MoeJS JavaScript runtime, and can register HTTP routes, intercept HTTP requests and responses, call system RPC methods, register their own RPC methods, declare configuration items, and inject admin pages.

Security

Plugins inherit Komari's system privileges and may request sensitive capabilities such as filesystem access, child process execution, or port listening. Only install plugins you trust, and review the declared permissions carefully before enabling a third-party plugin.

Administrator API examples in this guide use Bearer authentication:

http
Authorization: Bearer <api-key>
bash
export KOMARI_API_KEY="<api-key>"

Table of Contents ​

1. Quick Start ​

1.1 Using npm create komari-plugin ​

In addition to writing a plugin manually, you can create a project with the official scaffold:

sh
npm create komari-plugin

The template provides these common development features:

  • Plugin hot reload: npm run dev watches the source files and manifest, then automatically builds, packages, uploads, and re-enables the plugin. Changes are applied without repeating the installation manually.
  • Log tracking: the development command continuously prints plugin runtime logs, making load results and runtime errors easy to inspect.
  • Type hints: the template uses TypeScript and @komari-monitor/plugin-sdk, providing API types, parameter hints, and manifest field completion.
  • Local development configuration: the development server URL and API key are stored in komari.local.json, which is ignored by Git by default. Do not commit this file.

The template requires Node.js 20 or later, a reachable Komari development server, and an administrator API key.

The generated project looks like this:

text
hello/
├── src/plugin.ts          # TypeScript plugin source
├── komari-plugin.json     # Plugin manifest
├── komari.local.json      # Local server URL and API key; do not commit
├── package.json
└── tsconfig.json

SDK example:

ts
import { definePlugin, jsonResponse, server } from "@komari-monitor/plugin-sdk";

definePlugin({
  load() {
    server.route("GET", "/hello", (_req, res) => {
      jsonResponse(res, { ok: true });
    });
  },
});

1.2 Writing a Minimal Plugin Manually ​

A plugin package is a ZIP file. Its root must contain komari-plugin.json, and the default entry script is script.js:

text
my-plugin.zip
├── komari-plugin.json
└── script.js

komari-plugin.json:

json
{
  "name": "Hello Plugin",
  "short": "hello-plugin",
  "description": "Minimal Komari plugin",
  "author": "Example",
  "version": "1.0.0",
  "entry": "script.js",
  "permissions": {
    "timeout": 30
  }
}

script.js:

js
const server = require("server");

function load() {
  console.log("hello plugin loaded");
}

function unload() {
  console.log("hello plugin unloaded");
}

2. Plugin Package and Manifest ​

2.1 Package Structure ​

text
<plugin>.zip
├── komari-plugin.json            # Required and must be in the ZIP root
├── script.js                     # Default entry
├── pages/                        # Optional iframe pages and assets
│   └── admin.html
└── assets/

Archive limits:

LimitCurrent value
Maximum number of files10000
Maximum uncompressed file size128 MiB
Maximum total uncompressed size512 MiB
Maximum manifest size1 MiB

2.2 komari-plugin.json Fields ​

FieldTypeRequiredDefaultDescription
namestring | Record<string,string>Yes-Plugin name. Accepts a plain string or an i18n object such as {"zh_CN":"Example","en":"Example"}.
shortstringYes-Plugin short name. Only letters, digits, _, and - are allowed. It cannot be default and is also used as the directory name.
descriptionstring | Record<string,string>No-Plugin description.
authorstring | Record<string,string>No-Author.
versionstringNo-Plugin version. Market installation compares it with the catalog version.
urlstringNo-Project homepage or repository URL.
iconstringNo-Plugin icon. Must be a relative path inside the plugin directory.
komaristringNo-Komari version constraint, such as >=0.0.1 or 1.2.3.
entrystringNoscript.jsEntry script. Must be a relative path inside the plugin directory.
permissionsPluginPermissionsNoZero valuesRuntime permissions and limits.
configurationConfigurationNo-Configuration item declarations, using the same shape as themes.
pagesPluginPage[]No-Plugin page declarations.

Supported komari constraints:

SyntaxMeaning
Empty stringNo restriction
x.y.zExact match
>=x.y.zGreater than or equal to
>x.y.zGreater than
<=x.y.zLess than or equal to
<x.y.zLess than

The version may have a leading v and contains at most three numeric components.

2.3 permissions Fields ​

FieldTypeDefaultRequires approvalDescription
nodebooleanfalseNoEnables the Node.js compatibility modules.
allowSystemRPCbooleanfalseYesAllows server.call() to invoke system RPC methods with administrator authority.
allowRoutesbooleanfalseYesAllows server.route() and server.static().
allowHooksbooleanfalseYesAllows HTTP and WebSocket hooks.
allowHTMLInjectbooleanfalseYesAllows server.injectHTML() to inject fragments into HTML responses.
allowExecbooleanfalseYesAllows child_process to execute child processes.
allowListenbooleanfalseYesAllows net and http servers to listen on local ports.
allowAllFileAccessbooleanfalseYesAllows access to files outside the plugin directory.
maxHTTPBodyBytesinteger33554432NoBuffer limit for fetch response bodies, HTTP server request bodies, and route request bodies.
maxChildOutputBytesinteger1048576NoBuffer limit for each stdout or stderr stream from a child process.
timeoutinteger30NoPer-turn execution timeout in seconds.

Changing permissions invalidates the stored approval hash, so the plugin must be approved again before it can be enabled.

2.4 configuration Field ​

See Managed Configuration.

To read the saved managed configuration, call server.getConfig().

Example:

json
{
  "type": "managed",
  "data": [
    {
      "key": "endpoint",
      "name": "Endpoint",
      "required": true,
      "type": "string",
      "default": "https://example.com"
    },
    {
      "key": "enabled",
      "name": "Enabled",
      "type": "switch",
      "default": true
    }
  ]
}

2.5 pages Field ​

FieldTypeRequiredDefaultDescription
filestringRequired for iframe-Relative HTML file inside the plugin directory. Not used by redirect.
titlestring | Record<string,string>Yes-Page title.
iconstringNo-Page icon. Relative path inside the plugin directory.
type"iframe" | "redirect"No"iframe"How the page is presented.
urlstringRequired for redirect-Internal absolute path. It must start with / and cannot contain //, a backslash, or ...
visibility"admin" | "public"No"admin"Page access scope.

visibility: "public" only applies to iframe pages. Public pages are served by /api/plugin/:short/*filepath without authentication, and only files in the directory of the declared public page and its subdirectories are accessible.

See Plugin Pages for details.

2.6 Complete Manifest Example ​

json
{
  "name": {
    "zh_CN": "Status Extension",
    "en": "Status Extension"
  },
  "short": "status-extension",
  "description": "Adds a status endpoint and an admin page",
  "author": "Example",
  "version": "1.2.0",
  "url": "https://example.com/status-extension",
  "icon": "assets/icon.png",
  "komari": ">=0.0.1",
  "entry": "script.js",
  "permissions": {
    "node": true,
    "allowRoutes": true,
    "allowHooks": true,
    "allowHTMLInject": true,
    "allowSystemRPC": true,
    "timeout": 30,
    "maxHTTPBodyBytes": 33554432
  },
  "configuration": {
    "type": "managed",
    "data": [
      {
        "key": "message",
        "name": "Message",
        "type": "string",
        "default": "hello"
      }
    ]
  },
  "pages": [
    {
      "file": "pages/admin.html",
      "title": "Status",
      "icon": "assets/icon.png",
      "type": "iframe",
      "visibility": "admin"
    },
    {
      "file": "pages/public.html",
      "title": "Public status",
      "type": "iframe",
      "visibility": "public"
    }
  ]
}

3. Lifecycle Interfaces ​

The plugin entry script is not a CommonJS wrapper. Its top-level code runs directly. The runtime recognizes only the optional global functions load and unload.

3.1 load() ​

Description: Called once after the plugin is loaded. Use it to register routes, hooks, HTML injections, cron jobs, and RPC methods, and to initialize plugin resources.

Parameters: None.

Return value: No value is required. It can return undefined or a Promise. When it returns a Promise, the runtime waits for it to settle.

Caller: The Komari plugin manager.

Example:

js
const server = require("server");

function load() {
  console.log("plugin loaded");
  server.registerRPC("plugin:hello", () => ({ ok: true }));
}

Async example:

js
const server = require("server");

async function load() {
  await new Promise((resolve) => setTimeout(resolve, 100));
  server.registerRPC("plugin:hello", () => ({ ok: true }));
}

If load() throws or its Promise rejects, plugin loading fails, the plugin is disabled automatically, and the error is stored as last_error.

3.2 unload() ​

Description: Called once before the plugin is unloaded. Use it to release resources created by the plugin. Komari automatically unregisters the plugin's hooks, HTML injections, cron jobs, and RPC methods.

Parameters: None.

Return value: No value is required. It can return undefined or a Promise. When it returns a Promise, the runtime waits for it to settle.

Caller: The Komari plugin manager.

Example:

js
let timer = null;

function load() {
  timer = setInterval(() => console.log("tick"), 1000);
}

async function unload() {
  if (timer !== null) {
    clearInterval(timer);
    timer = null;
  }
}

If unload() throws or its Promise rejects, the error is reported, but the manager still completes its cleanup.

4. JavaScript Runtime and Compatibility Modules ​

Plugins run in an independent MoeJS JavaScript runtime with CommonJS require(), Promise and async/await support, an event loop, and common web APIs. Compatibility modules are provided by Komari. This is neither a browser nor a complete Node.js implementation; same-named APIs do not necessarily behave identically.

The following are available without any additional permission:

CategoryAPIs
Base interfacesconsole, setTimeout/setInterval/setImmediate and their clear functions, queueMicrotask
Module loadingCommonJS require() for plugin files and the server module
HTTP clientsfetch, XMLHttpRequest
Web compatibility objectsURL, URLSearchParams, Headers, Request, Response, Blob, File, FormData, AbortController/AbortSignal, EventTarget/Event, etc.
Always-available modulesbuffer, url, util (also with the node: prefix)

Without Node compatibility mode, use const { Buffer } = require("buffer"); a global Buffer is not injected. The local buffer implementation includes common methods such as from/alloc/concat/isBuffer/byteLength. util provides basic format/inspect/promisify/callbackify/inherits/types functionality, not full Node.js compatibility.

When permissions.node: true is set in the manifest, the runtime also injects Buffer, process, global, __dirname, __filename, and __storageDir__, and provides the following modules. Both plain names and node:-prefixed names are supported.

ModuleDescription
events, stream, stream/promises, path, os, processEvents, Node-style streams, paths, host information, and process interfaces. These streams are not Web Streams.
fsFile access restricted to the plugin code directory and __storageDir__ by default; escaping those roots requires allowAllFileAccess.
child_processChild process execution; requires allowExec.
net, httpTCP/HTTP. Server listening requires allowListen and binds to 127.0.0.1 by default. Outbound connections do not require that permission.
tlsPartial client implementation: connect(options, callback?) / createConnection with host/port/servername/ca/socket. No TLS server.
cryptoHashes, random values, key derivation, AES/ChaCha20-Poly1305, and common signing and verification operations.

The tls client verifies certificates and requires TLS 1.2 or newer. A PEM string in ca can specify trusted certificates; socket can upgrade an existing net.Socket (a server name is required). Do not assume all Node.js TLS options work: for example, rejectUnauthorized: false does not disable verification. For HTTP/HTTPS requests, normally use fetch; no built-in https module is registered.

Module loading and packaging:

  • The entry runs as a plain script with global lifecycle functions. It is not a CommonJS wrapper and does not receive module / exports. Files loaded through require() receive exports/require/module/__filename/__dirname.
  • Top-level require("./lib.js") resolves from the plugin code directory, even when the manifest entry is in a subdirectory; relative paths inside a module resolve from that module's directory. The loader supports node_modules, .js / .json, package.json main, directory index.js/index.json fallbacks, and module caching.
  • Path traversal and symlinks are checked by default. require() stays within the code directory: the existence of __storageDir__ does not permit loading modules from persistent storage. In Node mode, fs can read and write within both confined roots. Escaping directory restrictions requires allowAllFileAccess.
  • Package resolution does not support package.json exports/imports/type rules or Node native extensions (.node). The plugin loading path does not provide an ESM import/export loader or execute TypeScript directly. Compile to a plain JavaScript entry and bundle dependencies or include CommonJS-loadable files in the ZIP. Host APIs required by npm packages must still fall within the compatibility surface above.

Compatibility limitations:

Plugins may extend built-in objects such as Reflect and their prototypes. Changes are local to the current runtime and are not shared with other plugins.

  • JavaScript callbacks in one plugin execute serially on its event loop. Promise/async-await does not parallelize synchronous CPU work; synchronous infinite loops remain subject to execution timeout interruption.
  • Timers return numeric handles without Node's ref/unref/refresh/hasRef methods. setImmediate uses a zero-delay timer and does not guarantee Node.js phase ordering.
  • fetch buffers request/response bodies and is subject to the HTTP body limit. Response.body is null; Blob.stream() is not a standard Web ReadableStream. Synchronous XHR blocks the plugin event loop.
  • There is no browser DOM, global WebSocket constructor, EventSource, or Web Streams. To accept WebSocket connections, use req.upgradeWebSocket() below. Core modules such as dns, zlib, and worker_threads are not registered as built-ins.
  • process.memoryUsage() and cpuUsage() describe the whole Komari process, not an individual plugin. An independent VM and filesystem restrictions do not provide OS-level process isolation.

5. server Module ​

js
const server = require("server");

5.1 server.route(method, path, handler) ​

Description: Registers a route on the Komari HTTP engine.

Permission: allowRoutes.

Parameters:

ParameterTypeRequiredDescription
methodstringYesHTTP method. It is converted to uppercase and cannot be empty.
pathstringYesRoute path. It must start with /.
handler(req, res) => void | Promise<void>YesRequest handler. It may return a Promise. Complete an HTTP response with res.end(), or upgrade the connection with req.upgradeWebSocket().

Return value: undefined.

req object:

FieldTypeDescription
methodstringRequest method.
urlstringRequest URI, including the query string.
headersRecord<string,string | string[]>Request headers with lowercase keys.
queryRecord<string,string>Query parameters. Multiple values with the same name are joined with commas.
bodystringRequest body text.
contextRequestContextCaller and network information.
upgradeWebSocket()() => Promise<RouteWebSocket>Upgrades the current GET request to WebSocket. See below.

req.context object:

FieldTypeDescription
principalobjectThe resolved caller identity.
principal.type"anonymous" | "agent" | "user" | "api_key"Identity type.
principal.rolesstring[]Role list.
principal.user_uuidstringUser UUID, or an empty string when absent.
principal.client_uuidstringClient UUID, or an empty string when absent.
principal.is_api_keybooleanWhether the request uses an API key.
rolestringCurrent request role. Omitted when absent.
user_uuidstringCurrent user UUID. Omitted when absent.
client_uuidstringCurrent client UUID. Omitted when absent.
remote_ipstringRemote IP address.
user_agentstringUser-Agent header.

res object:

MemberTypeDescription
statusCodenumberResponse status code, default 200.
statusMessagestringStatus text field. It is currently exposed only as a writable field.
streamingbooleanWhen true, write() pushes data immediately.
setHeader(name, value)(string, string | string[]) => resSets a response header.
getHeader(name)(string) => string | string[] | undefinedReads a response header.
removeHeader(name)(string) => voidRemoves a response header.
write(data)(string | Buffer | ArrayBuffer | Uint8Array) => booleanWrites response data. In streaming mode it sends data immediately.
end(data?)(string?) => resEnds the response and optionally appends text.
isAborted()() => booleanReports whether the client disconnected or the stream was aborted.

Request example:

js
const server = require("server");

function load() {
  server.route("GET", "/status", (req, res) => {
    res.setHeader("Content-Type", "application/json; charset=utf-8");
    res.end(JSON.stringify({
      message: "ok",
      viewer: req.context.principal.type,
      query: req.query
    }));
  });
}

Async and error handling example:

js
const server = require("server");

function load() {
  server.route("POST", "/echo", async (req, res) => {
    try {
      await new Promise((resolve) => setTimeout(resolve, 10));
      res.setHeader("Content-Type", "application/json");
      res.statusCode = 201;
      res.end(JSON.stringify({ body: req.body }));
    } catch (error) {
      res.statusCode = 500;
      res.end(error.message);
    }
  });
}

Streaming example:

js
const server = require("server");

function load() {
  server.route("GET", "/stream", (req, res) => {
    res.streaming = true;
    res.setHeader("Content-Type", "text/plain; charset=utf-8");
    let count = 0;
    const timer = setInterval(() => {
      if (res.isAborted() || count >= 5) {
        clearInterval(timer);
        res.end();
        return;
      }
      res.write(`chunk-${count++}\n`);
    }, 100);
  });
}

Route slots remain after the plugin is unloaded, but requests return 404. A non-streaming handler that has not upgraded to WebSocket times out after permissions.timeout seconds and returns 504.

5.1.1 req.upgradeWebSocket() ​

Description: Upgrades the current GET request to WebSocket using Komari's existing HTTP listener. Do not register a WS method.

Permission: allowRoutes. It does not require node, allowListen, or allowHooks.

Parameters: None.

Return value: Promise<RouteWebSocket>. The connection is already open when the Promise resolves; failures reject it. There is no need to wait for onopen.

Connection object:

MemberTypeDescription
send(data)(string | ArrayBuffer | ArrayBufferView) => voidQueues a message. Strings become text frames; binary data becomes binary frames.
close(code?, reason?)(number?, string?) => voidStarts closing, with defaults of 1000 and an empty string. Repeated calls do not close again.
readyStatenumber, read-only1: open; 2: closing; 3: closed.
onmessage(event) => voidevent.data is a string for text or an ArrayBuffer for binary data.
onerror(event) => voidevent.message describes the error. A fatal connection error closes the socket.
onclose(event) => voidevent.code, event.reason, and event.wasClean describe the close code, reason, and whether the close handshake completed cleanly.

Example:

js
const server = require("server");

function load() {
  server.route("GET", "/socket", async (req, res) => {
    // Check req.context.principal before upgrading if authorization is needed.
    const ws = await req.upgradeWebSocket();
    ws.onmessage = ({ data }) => {
      ws.send(data);
    };
    ws.onerror = ({ message }) => {
      console.error("WebSocket error:", message);
    };
    ws.onclose = ({ code, reason, wasClean }) => {
      console.log("WebSocket closed:", code, reason, wasClean);
    };
    ws.send("connected");
    ws.send(new Uint8Array([1, 2, 3]));
  });
}

Upgrade and response behavior:

  • Only GET WebSocket handshake requests can be upgraded. Upgrade before calling res.write(), res.end(), or setting res.streaming = true. A request cannot be upgraded twice.
  • After a successful upgrade, do not call res.end(), write an HTTP body, or change response headers. HTTP response headers set before upgrading are not forwarded as custom handshake response headers.
  • Local validation failures, such as a non-WebSocket request, can be caught while the HTTP response remains writable. An actual handshake failure may already have sent an error response or taken over the connection; do not attempt to append another HTTP response.
  • The existing server Origin policy applies, but Origin validation is not authentication. Check identity and roles through req.context.principal before upgrading, and return an HTTP error if access is denied. A path under /api/admin does not automatically enforce an admin role.

Messages and lifecycle:

  • Assign callback properties instead of using .on(). This is not the complete browser WebSocket or Node.js ws interface, and it does not provide outbound client connections.
  • send() accepts ArrayBuffer, TypedArray, DataView, and Buffer when available. Binary data is copied on admission, using only a view's byte range. Later source changes do not affect queued messages.
  • send() returns undefined, not a Promise or a peer acknowledgement. Invalid data, a non-open connection, an oversized message, or a full queue throws synchronously. Network errors trigger onerror, followed by a single onclose. A throwing or timed-out callback also closes the connection.
  • close() accepts only code 1000 or 3000–4999. The reason must be valid UTF-8 and at most 123 bytes. Invalid arguments throw.
  • Each sent or received message is limited to 8 MiB. Outstanding sends are limited to 64 messages and 16 MiB total, including the message being written. A rejected send throws without closing the connection.
  • An upgraded connection is not subject to the HTTP response waiting timeout. JavaScript callbacks remain subject to permissions.timeout. Disabling, unloading, or reloading the plugin closes its connections. onclose is not guaranteed during unload, so essential cleanup still belongs in unload().

5.2 server.static(mount, dir, options?) ​

Description: Mounts a static directory from the plugin directory on an HTTP path.

Permission: allowRoutes.

Parameters:

ParameterTypeRequiredDescription
mountstringYesMount path. It must start with / and cannot be /.
dirstringYesRelative directory path inside the plugin directory.
optionsobjectNo{ spa?: boolean }. With spa: true, unmatched paths fall back to index.html.

Return value: undefined.

Static file behavior:

  • Registers both GET and HEAD.
  • The mount root resolves to index.html.
  • A subdirectory resolves to that directory's index.html.
  • In non-SPA mode, a missing file returns 404.
  • In SPA mode, failed resolution falls back to the mount directory's index.html.

Example:

js
const server = require("server");

function load() {
  server.static("/panel", "dist", { spa: true });
}

In this example, files are served from data/plugin/<short>/dist.

5.3 server.call(method, params?) ​

Description: Calls a registered Komari RPC method with administrator authority.

Permission: allowSystemRPC.

Parameters:

ParameterTypeRequiredDescription
methodstringYesRPC method name, such as common:getVersion.
paramsanyNoRPC parameters. A single value is passed directly; multiple values are passed positionally.

Return value: Promise<any>. It resolves to the RPC result. On failure it rejects with an Error carrying code, message, and optional data.

Basic calls:

js
const version = await server.call("common:getVersion");
const result = await server.call("plugin:echo", { text: "hello" });

Example:

js
const server = require("server");

function load() {
  server.route("GET", "/version", async (req, res) => {
    try {
      const version = await server.call("common:getVersion");
      res.setHeader("Content-Type", "application/json");
      res.end(JSON.stringify(version));
    } catch (error) {
      res.statusCode = 502;
      res.end(JSON.stringify({
        code: error.code,
        message: error.message,
        data: error.data
      }));
    }
  });
}

For the parameters, return values, and errors of individual RPC methods, see the RPC documentation.

5.4 server.hook(kind, fn) / server.hook(kind, matcher, fn) ​

Description: Registers HTTP request or response hooks, or WebSocket connection and frame hooks.

Permission: allowHooks.

Parameters:

ParameterTypeRequiredDescription
kindstringYesrequest, response, wsConnect, wsMessage, wsSend, or wsClose.
matcherstringNoHTTP hooks support "METHOD /path", "/path", and "/path/*". WebSocket hooks accept only path matchers.
fnfunctionYesHook callback.

Return value: undefined.

Callback signatures:

kindCallback signatureReturn value
request(req) => voidNone
response(req, res) => voidNone
wsConnect(ctx) => object | void{ deny?: boolean, reason?: string }
wsMessage(ctx, msg) => object | void{ type?: number, data?: any, drop?: boolean }
wsSend(ctx, msg) => object | void{ type?: number, data?: any, drop?: boolean }
wsClose(ctx) => voidNone

Example:

js
const server = require("server");

function load() {
  server.hook("request", "/api/*", (req) => {
    req.headers["x-plugin"] = "status-extension";
  });

  server.hook("response", "GET /api/version", (req, res) => {
    res.headers["x-version-hooked"] = "1";
  });
}

The HTTP hook matcher, execution order, and error behavior, as well as the WebSocket hook context and frame handling rules, are described below.

5.4.1 request Hook ​

Description: Modifies a request before the business handler runs.

Registration: server.hook("request", fn) or server.hook("request", matcher, fn).

Parameters:

ParameterTypeDescription
reqRequestMutable request object.

Request fields:

FieldTypeWritableDescription
methodstringYesRequest method.
urlstringYesRequest URI, including the query string.
headersRecord<string,string | string[]>YesRequest headers with lowercase keys.
queryRecord<string,string>NoParsed query parameters. Changing this object does not write back to the URL.
bodystringYesRequest body text.
contextHookContextNoNetwork information.

HookContext fields:

FieldTypeDescription
remote_ipstringUses the first X-Forwarded-For entry, then X-Real-IP, then the host from RemoteAddr.
user_agentstringUser-Agent header.

Return value: None. Direct changes to req.method, req.url, req.headers, and req.body take effect.

Example:

js
const server = require("server");

function load() {
  server.hook("request", "POST /api/items/*", (req) => {
    req.headers["x-plugin-request"] = "1";
    req.url = req.url.replace("old=1", "old=0");
    req.body = req.body.replaceAll("foo", "bar");
  });
}

When a request matches the matcher, the request body is read and buffered. Requests that do not match skip both the hook and buffering. If the request body exceeds permissions.maxHTTPBodyBytes, the server returns 413.

5.4.2 response Hook ​

Description: Modifies response status, headers, and body after the business handler returns.

Registration: server.hook("response", fn) or server.hook("response", matcher, fn).

Parameters:

ParameterTypeDescription
reqRequestSame request object as in 5.4.1. In a response hook, body is "".
resResponseMutable response object.

Response fields:

FieldTypeWritableDescription
statusCodenumberYesHTTP status code.
statusMessagestringYesCurrently always an empty string.
headersRecord<string,string | string[]>YesResponse headers with lowercase keys.
bodystringYesResponse body text.

Return value: None. Direct changes to res take effect.

Example:

js
const server = require("server");

function load() {
  server.hook("response", "/api/version", (req, res) => {
    res.statusCode = 200;
    res.headers["x-plugin-response"] = "1";
    res.body = res.body.replace("Komari", "Komari + Plugin");
  });
}

If the body changes, the original Content-Length is removed. Once the internal response buffer limit is reached, the response passes through directly and response hooks can no longer rewrite it. A streaming response enters pass-through mode after the first Flush(), after which hooks no longer rewrite it.

5.4.3 Hook Matcher ​

Formats:

text
"METHOD /path"
"/path"
"/path/*"

Rules:

RuleDescription
Matcher omittedMatches every request.
"METHOD /path"Matches the specified HTTP method and exact path.
"/path"Matches the exact path for any HTTP method.
"/path/*"Matches /path and every descendant path.
"/*"Matches every path.

Path comparison is case-insensitive. HTTP methods must be GET, HEAD, POST, PUT, PATCH, DELETE, or OPTIONS. If the method in "METHOD..." is not recognized, the whole string is treated as a path and fails because it does not start with /.

5.4.4 HTTP Hook Execution and Errors ​

  • Multiple request hooks run in registration order, and each hook sees the request modifications made by the previous hook.
  • Multiple response hooks run in registration order, and each hook sees the response modifications made by the previous hook.
  • When a request hook reaches timeout, the request returns 500. When a response hook reaches timeout, the error is logged and the original response is sent. Either hook may continue running after the timeout, and later side effects are not guaranteed to be rolled back.
  • If a request hook throws, the request returns 500 plugin request hook failed. If a response hook throws, the error is logged and the original response is sent.
  • WebSocket upgrade requests bypass request and response hooks and use the connection and frame hooks beginning in 5.4.5.

5.4.5 WebSocket Hooks ​

WebSocket hooks share the allowHooks permission with HTTP hooks. They observe or modify existing connections and messages; they do not provide a connection object for sending messages. To accept a plugin-owned WebSocket connection, use req.upgradeWebSocket() in 5.1.1. Those connections also pass through WebSocket hooks, but accepting a connection does not require allowHooks. Every upgrade is a GET request, so WebSocket matchers accept only paths:

js
server.hook("wsMessage", "/api/clients/v2/rpc", (ctx, msg) => {
  return { data: "replaced" };
});

5.4.6 Common ctx Object ​

FieldTypeDescription
pathstringWebSocket endpoint path.
connIdnumberConnection ID.
remoteIpstringRemote IP address.
userAgentstringUser-Agent header.
clientUuidstringClient UUID. Omitted when empty.

5.4.7 wsConnect ​

Callback: (ctx) => object | void

Return value:

FieldTypeDescription
denybooleanWhen true, reject the connection.
reasonstringRejection reason.

Example:

js
const server = require("server");

function load() {
  server.hook("wsConnect", "/api/clients/v2/rpc", (ctx) => {
    if (ctx.remoteIp === "203.0.113.9") {
      return {
        deny: true,
        reason: "blocked by plugin"
      };
    }
  });
}

The first hook returning deny: true rejects the connection. Returning undefined, null, or an object without deny allows the connection.

5.4.8 wsMessage ​

Callback: (ctx, msg) => object | void

Inbound msg:

FieldTypeDescription
typenumberWebSocket frame type.
datastring | ArrayBufferString for a text frame, ArrayBuffer for a binary frame.
connIdnumberConnection ID.
pathstringEndpoint path.

Return value:

FieldTypeDescription
typenumberReplacement frame type.
datastring | ArrayBuffer | Buffer | Uint8ArrayReplacement frame payload.
dropbooleanWhen true, drop the frame. This takes precedence over type and data.

Example:

js
const server = require("server");

function load() {
  server.hook("wsMessage", "/api/rpc2", (ctx, msg) => {
    if (typeof msg.data === "string" && msg.data.includes('"blocked"')) {
      return { drop: true };
    }
    return { data: msg.data };
  });
}

Multiple wsMessage hooks run in registration order as a chain. Each hook sees the frame produced by the previous hook. Oversized frames pass through directly.

5.4.9 wsSend ​

Callback: (ctx, msg) => object | void

Its parameters, return shape, and chaining rules are the same as wsMessage, but it applies to frames sent from the server to the client.

Example:

js
const server = require("server");

function load() {
  server.hook("wsSend", "/api/clients/v2/rpc", (ctx, msg) => {
    if (typeof msg.data === "string") {
      return { data: msg.data.replace("old", "new") };
    }
  });
}

5.4.10 wsClose ​

Callback: (ctx) => void

Called once when the connection ends. Its return value is ignored.

Example:

js
const server = require("server");

function load() {
  server.hook("wsClose", "/api/clients/v2/rpc", (ctx) => {
    console.log("websocket closed: " + ctx.connId);
  });
}

5.4.11 WebSocket Hook Limits ​

  • wsMessage and wsSend allow at most 8 MiB per frame. Larger frames skip every hook and pass through unchanged.
  • Each frame-level hook waits at most one second. After a timeout, the event is logged and the frame passes through unchanged.
  • Connection-level hooks use permissions.timeout.
  • After 16 consecutive dropped frames, the read loop ends with an error.
  • If the runtime closes, a hook times out, or a hook throws, the connection or frame continues with safe pass-through behavior.

5.5 server.injectHTML(head, body) ​

Description: Injects HTML fragments into every text/html response. head is inserted before </head>, and body is inserted before </body>.

Permission: allowHTMLInject.

Parameters:

ParameterTypeRequiredDescription
headstringYesFragment inserted before </head>. Pass an empty string when unused.
bodystringYesFragment inserted before </body>. Pass an empty string when unused.

Return value: undefined.

Example:

js
const server = require("server");

function load() {
  server.injectHTML(
    '<link rel="stylesheet" href="/api/plugin/status-extension/assets/panel.css">',
    '<script src="/api/plugin/status-extension/assets/panel.js"></script>'
  );
}

Injection applies to every HTML page, including admin and terminal pages. Responses larger than the internal HTML buffer limit are not injected.

5.6 server.cron(expr, fn) ​

Description: Runs a callback on the plugin event loop according to a cron expression.

Permission: Granted by default; no manifest declaration is required.

Parameters:

ParameterTypeRequiredDescription
exprstringYesA 5-field or 6-field cron expression, or @every <duration>.
fn() => voidYesScheduled callback. Its return value does not affect scheduling.

Return value: undefined.

Expression formats:

FormatFields
5 fieldsminute hour day-of-month month day-of-week
6 fieldssecond minute hour day-of-month month day-of-week
Interval@every 30s, @every 1m, @every 1h

Fields support *, */n, a-b, a-b/n, comma-separated lists, and specific numbers. In day-of-week, 7 is equivalent to 0.

Example:

js
const server = require("server");

function load() {
  server.cron("*/5 * * * *", () => {
    console.log("every five minutes");
  });

  server.cron("@every 30s", () => {
    console.log("every thirty seconds");
  });
}

Cron jobs registered by a plugin are cancelled when the plugin is unloaded.

5.7 server.registerRPC(method, handler) ​

Description: Registers a plugin-owned RPC method.

Permission: Granted by default; no manifest declaration is required.

Parameters:

ParameterTypeRequiredDescription
methodstringYesRPC method name. It cannot be empty or start with rpc..
handler(params) => anyYesRPC handler. It receives the raw params and returns a synchronously JSON-serializable value.

Return value: undefined.

Example:

js
const server = require("server");

function load() {
  server.registerRPC("plugin:statusEcho", (params) => {
    return {
      echo: params,
      at: new Date().toISOString()
    };
  });
}

A normal Error thrown by the handler becomes JSON-RPC -32603. To return a business error code, attach code and data to the Error:

js
const server = require("server");

function load() {
  server.registerRPC("plugin:statusFail", () => {
    const error = new Error("status unavailable");
    error.code = -32051;
    error.data = { retryable: true };
    throw error;
  });
}

See section 10 for the invocation entry point and permission rules.

5.8 server.getConfig() ​

Description: Reads the saved plugin configuration and merges it with defaults declared in the manifest.

Permission: Granted by default; no manifest declaration is required.

Parameters: None.

Return value: Promise<Record<string, any>>.

Example:

js
const server = require("server");

function load() {
  server.route("GET", "/config", async (req, res) => {
    const config = await server.getConfig();
    res.setHeader("Content-Type", "application/json");
    res.end(JSON.stringify(config));
  });
}

For example, if the configuration contains message and enabled, the return shape is:

json
{
  "message": "hello",
  "enabled": true
}

6. Plugin Pages ​

6.1 Admin iframe Pages ​

Description: An iframe page with visibility: "admin" is loaded by the admin navigation.

File route: GET /api/admin/plugin/:short/*filepath

Authentication: Administrator.

Path parameters:

ParameterTypeDescription
shortstringPlugin short name.
filepathstringFile path inside the plugin directory.

Return value: File contents. Returns 404 if the file is missing, the path is invalid, or the plugin is not installed.

Example:

bash
curl -s "$BASE/api/admin/plugin/status-extension/pages/admin.html" \
  -H "Authorization: Bearer $KOMARI_API_KEY"

Resources from the same origin in pages/admin.html can use the same prefix:

html
<link rel="stylesheet" href="/api/admin/plugin/status-extension/pages/admin.css">
<script src="/api/admin/plugin/status-extension/pages/admin.js"></script>

6.2 Public iframe Pages ​

Description: A page with visibility: "public" and type: "iframe" can be accessed without authentication.

File route: GET /api/plugin/:short/*filepath

Authentication: None.

Path parameters:

ParameterTypeDescription
shortstringPlugin short name.
filepathstringFile path inside the directory of the public page.

Return value: File contents. Returns 404 if the plugin is disabled, the file is outside the public page directory, or the path is invalid.

Example:

bash
curl -s "http://127.0.0.1:8080/api/plugin/status-extension/pages/public.html"

If the public page is pages/public.html, relative resources such as pages/public.js and pages/public.css in the same directory are also accessible. Files outside that directory, such as script.js or an admin page, and paths containing ../ are not accessible.

6.3 redirect Pages ​

A page with type: "redirect" is not served from the plugin's static file route. The admin UI navigates to the internal path specified by url. The URL must start with /, must not start with //, and cannot contain backslashes, a URL scheme, or a .. path segment.

Example:

json
{
  "title": "Open dashboard",
  "type": "redirect",
  "url": "/admin/dashboard",
  "visibility": "admin"
}

7. Plugin Configuration ​

7.1 Reading Configuration in a Plugin ​

Use server.getConfig() inside the plugin. See 5.8. The result merges saved values with manifest defaults.

7.2 Reading Configuration Declarations and Values as an Administrator ​

Interface: GET /api/admin/plugin/configuration?short=<short>

Authentication: Administrator.

Query parameters:

ParameterTypeRequiredDescription
shortstringYesPlugin short name.

Return value: Standard envelope. data.configuration is the manifest configuration declaration, and data.data is the resolved saved values.

Example:

bash
curl -s "$BASE/api/admin/plugin/configuration?short=status-extension" \
  -H "Authorization: Bearer $KOMARI_API_KEY"

Example response:

json
{
  "status": "success",
  "data": {
    "configuration": {
      "type": "managed",
      "data": [
        {
          "key": "message",
          "name": "Message",
          "type": "string",
          "default": "hello"
        }
      ]
    },
    "data": {
      "message": "hello"
    }
  }
}

7.3 Saving Configuration as an Administrator ​

Interface: POST /api/admin/plugin/configuration

Authentication: Administrator.

Request body:

FieldTypeRequiredDescription
shortstringYesPlugin short name.
dataobjectNoComplete configuration value object. When omitted or null, an empty object is saved.

Return value: Standard success envelope. If the plugin is enabled, it is reloaded immediately after saving.

Example:

bash
curl -s -X POST "$BASE/api/admin/plugin/configuration" \
  -H "Authorization: Bearer $KOMARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "short": "status-extension",
    "data": {
      "message": "from-admin",
      "enabled": true
    }
  }'

If saving succeeds but the reload fails, the configuration has already been written and the call returns an error containing plugin configuration saved but reload failed.

8. Plugin-owned RPC ​

8.1 Registration ​

Plugins register methods through server.registerRPC(method, handler). See 5.7.

8.2 Invocation ​

Registered methods enter the Komari RPC registry. They can be called through the existing /api/rpc2 endpoint or by another plugin through server.call(). This document does not repeat the RPC request envelope, error codes, or authentication details; see the RPC documentation.

HTTP example:

bash
curl -s -X POST "http://127.0.0.1:8080/api/rpc2" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "plugin:statusEcho",
    "params": {
      "text": "hello"
    },
    "id": 1
  }'

Calling another plugin's method:

js
const result = await server.call("plugin:statusEcho", {
  text: "hello"
});

8.3 Rules ​

  • The method name cannot be empty and cannot start with rpc..
  • Registering the same method more than once during a single load of the same plugin is ignored.
  • The handler receives one raw params value and does not automatically unwrap named parameters.
  • The handler is called synchronously. Returning a Promise does not wait for its asynchronous result.
  • The return value must be JSON-serializable. Export failures such as circular references return -32603.
  • The handler times out after permissions.timeout seconds and returns -32011.
  • When the plugin unloads, its methods are unregistered. Later calls return -32601.
  • If a method name conflicts with an existing method, registration fails and plugin loading fails.
  • Plugin methods still go through the unified ACL. Registering a plugin:* method without an additional permission declaration requires the administrator role.

9. Plugin Management HTTP Interfaces ​

This section documents the HTTP routes exposed by plugin management. Except for chunked uploads and the market, most routes bridge to admin:* RPC methods. Their response shape depends on the target RPC result, which is not repeated here.

All interfaces require administrator authentication. "Standard envelope" means a successful response shaped like { "status": "success", "message": "...", "data": ... }, while errors use { "status": "error", "message": "..." } with an appropriate HTTP status.

9.1 Plugin List ​

Interface: GET /api/admin/plugin/list

Path parameters: None.

Query parameters: None.

Request body: None.

Return value: Standard envelope whose data is PluginInfo[].

PluginInfo:

FieldTypeDescription
namestring | Record<string,string>Manifest name.
shortstringPlugin short name.
descriptionstring | Record<string,string>Manifest description.
authorstring | Record<string,string>Manifest author.
versionstringVersion.
urlstringPlugin URL.
iconstringRelative icon path.
komaristringServer version constraint.
entrystringEntry script.
permissionsPluginPermissionsPermission object.
configurationConfigurationConfiguration declaration.
pagesPluginPage[]Page declarations. Omitted when absent.
enabledbooleanWhether the plugin is persistently enabled.
runningbooleanWhether the plugin is currently running.
last_errorstringLatest load error, or an empty string.

Example:

bash
curl -s "$BASE/api/admin/plugin/list" \
  -H "Authorization: Bearer $KOMARI_API_KEY"

9.2 Enabling or Disabling a Plugin ​

Interface: POST /api/admin/plugin/enabled

Authentication: Administrator.

Request body:

FieldTypeRequiredDescription
shortstringYesPlugin short name.
enabledbooleanNotrue enables and false disables. Omitted values are treated as false.
approvedbooleanNoWhether to approve the current permission set. It is false when omitted and approval is required.

Return value: Standard success envelope. When permissions are not approved, data is { "requires_approval": true }. After approving, retry with approved: true.

Example:

bash
curl -s -X POST "$BASE/api/admin/plugin/enabled" \
  -H "Authorization: Bearer $KOMARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "short": "status-extension",
    "enabled": true,
    "approved": true
  }'

9.3 Getting Plugin Logs ​

Interface: GET /api/admin/plugin/logs?short=<short>

Authentication: Administrator.

Query parameters:

ParameterTypeRequiredDescription
shortstringYesPlugin short name.

Return value: Standard envelope whose data.logs is the log string.

Example:

bash
curl -s "$BASE/api/admin/plugin/logs?short=status-extension" \
  -H "Authorization: Bearer $KOMARI_API_KEY"

Example response:

json
{
  "status": "success",
  "data": {
    "logs": "[plugin] loading status-extension\n[plugin] loaded status-extension\n"
  }
}

9.4 Deleting a Plugin ​

Interface: POST /api/admin/plugin/delete

Authentication: Administrator.

Request body:

FieldTypeRequiredDescription
shortstringYesPlugin short name.

Return value: Standard success envelope. Returns an error when the plugin is not installed.

Example:

bash
curl -s -X POST "$BASE/api/admin/plugin/delete" \
  -H "Authorization: Bearer $KOMARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"short":"status-extension"}'

Deletion unloads the runtime and removes data/plugin/<short>, data/plugin-data/<short>, and the persisted state.

9.5 Installing a Plugin with Chunked Upload ​

Plugin installation uses the shared archive upload interface with purpose set to plugin.

9.5.1 Initialization ​

Interface: POST /api/admin/upload/init

Request body:

FieldTypeRequiredDescription
purposestringYesMust be "plugin".
filenamestringNoZIP filename. The plugin installation flow does not depend on this field.
sizeintegerYesTotal file size in bytes. It must be greater than 0 and no larger than the backup archive limit.

Return value: Standard envelope. data.upload_id is a UUID and data.chunk_size is 5 MiB.

Example:

bash
curl -s -X POST "$BASE/api/admin/upload/init" \
  -H "Authorization: Bearer $KOMARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "purpose": "plugin",
    "filename": "status-extension.zip",
    "size": 123456
  }'

9.5.2 Uploading a Chunk ​

Interface: POST /api/admin/upload/chunk

Content-Type: multipart/form-data

Form fields:

FieldTypeRequiredDescription
upload_idstringYesUUID returned by initialization.
chunk_indexintegerYesZero-based chunk index.
chunk_datafileYesChunk bytes. Every chunk must be exactly 5 MiB except the final chunk.

Return value: Standard envelope. data contains received: true and chunk_index.

Example:

bash
curl -s -X POST "$BASE/api/admin/upload/chunk" \
  -H "Authorization: Bearer $KOMARI_API_KEY" \
  -F "upload_id=<upload-id>" \
  -F "chunk_index=0" \
  -F "chunk_data=@chunk-0.bin"

9.5.3 Merging and Installing ​

Interface: POST /api/admin/upload/merge

Request body:

FieldTypeRequiredDescription
upload_idstringYesUUID returned by initialization.

Return value: Standard envelope. The server currently returns 插件上传成功 as message (Chinese for "plugin uploaded successfully"), and data is the installed Plugin manifest.

Example:

bash
curl -s -X POST "$BASE/api/admin/upload/merge" \
  -H "Authorization: Bearer $KOMARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"upload_id":"<upload-id>"}'

9.5.4 Cancelling an Upload ​

Interface: POST /api/admin/upload/cancel

Request body:

FieldTypeRequiredDescription
upload_idstringYesUUID returned by initialization.

Return value: Standard success envelope.

Example:

bash
curl -s -X POST "$BASE/api/admin/upload/cancel" \
  -H "Authorization: Bearer $KOMARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"upload_id":"<upload-id>"}'

9.6 Plugin Market Sources ​

9.6.1 Listing Sources ​

Interface: GET /api/admin/plugin/market/sources

Request body: None.

Return value: Standard envelope whose data is PluginMarketSource[].

Example:

bash
curl -s "$BASE/api/admin/plugin/market/sources" \
  -H "Authorization: Bearer $KOMARI_API_KEY"

9.6.2 Creating a Source ​

Interface: POST /api/admin/plugin/market/sources

Request body:

FieldTypeRequiredDescription
namestringYesSource name.
urlstringYesHTTP or HTTPS catalog URL.
enabledbooleanNoWhether the source is enabled. Defaults to false.
idstringNoIgnored. The server generates a new ID.

Return value: Standard success envelope whose data is the created PluginMarketSource.

Example:

bash
curl -s -X POST "$BASE/api/admin/plugin/market/sources" \
  -H "Authorization: Bearer $KOMARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My catalog",
    "url": "https://example.com/plugins/v1.json",
    "enabled": true
  }'

9.6.3 Updating a Source ​

Interface: PUT /api/admin/plugin/market/sources/:id

Path parameters:

ParameterTypeDescription
idstringSource ID.

Request body: Same as creating a source. The path parameter overrides id in the request body.

Return value: Standard success envelope whose data is the updated source.

Example:

bash
curl -s -X PUT "$BASE/api/admin/plugin/market/sources/<source-id>" \
  -H "Authorization: Bearer $KOMARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Updated catalog",
    "url": "https://example.com/plugins/v2.json",
    "enabled": true
  }'

9.6.4 Deleting a Source ​

Interface: DELETE /api/admin/plugin/market/sources/:id

Path parameters:

ParameterTypeDescription
idstringSource ID.

Return value: Standard success envelope.

Example:

bash
curl -s -X DELETE "$BASE/api/admin/plugin/market/sources/<source-id>" \
  -H "Authorization: Bearer $KOMARI_API_KEY"

9.7 Plugin Market Catalog ​

Interface: GET /api/admin/plugin/market/catalog

Query parameters:

ParameterTypeRequiredDefaultDescription
refreshbooleanNofalseWhen true, bypass the cache.

Return value: Standard envelope. data.plugins is the merged plugin list and data.sources is the status of each source.

PluginMarketPlugin:

FieldTypeDescription
namestring | Record<string,string>Plugin name.
shortstringPlugin short name.
descriptionstring | Record<string,string>Description.
versionstringVersion.
authorstring | Record<string,string>Author.
urlstringPlugin page URL.
downloadstringZIP download URL, or an empty string when no package is available.
sha256stringZIP SHA-256, or an empty string when no package is available.
komaristringServer version constraint.
installablebooleanWhether an installable package exists and the version constraint is satisfied.
source_idstringSource ID.
source_namestringSource name.

sources[]:

FieldTypeDescription
idstringSource ID.
namestringSource name.
urlstringSource URL.
countintegerNumber of plugins read from this source.
errorstringSource read error. Omitted on success.

Example:

bash
curl -s "$BASE/api/admin/plugin/market/catalog?refresh=true" \
  -H "Authorization: Bearer $KOMARI_API_KEY"

9.8 Installing a Plugin from the Market ​

Interface: POST /api/admin/plugin/market/install

Request body:

FieldTypeRequiredDescription
source_idstringYesID of an enabled market source.
shortstringYesShort name of the plugin to install.

Return value: Standard success envelope whose data is the installed manifest.

Example:

bash
curl -s -X POST "$BASE/api/admin/plugin/market/install" \
  -H "Authorization: Bearer $KOMARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source_id": "official",
    "short": "status-extension"
  }'

The server fetches the catalog again, downloads the ZIP, validates its SHA-256, and confirms that the installed short and version match the catalog.

10. Permissions, Limits, and Errors ​

10.1 Permission Matrix ​

InterfacePermission
server.route()allowRoutes
req.upgradeWebSocket()allowRoutes; Node compatibility is not required
server.static()allowRoutes
server.hook()allowHooks
server.injectHTML()allowHTMLInject
server.call()allowSystemRPC
server.registerRPC()Granted by default
server.getConfig()Granted by default
server.cron()Granted by default
CommonJS loading inside the plugin code directoryGranted by default; does not require node: true
fs access inside the code directory and __storageDir__node: true
File access outside these rootsallowAllFileAccess; fs also requires node: true
child_processallowExec and node: true
Listening on a local portallowListen and node: true

10.2 Time and Capacity Limits ​

The execution timeout applies to script initialization, function calls, and asynchronous callbacks, not the total lifetime of a plugin. The default HTTP client uses the same timeout. Configure it through permissions.timeout in the manifest.

ItemCurrent value
Default execution timeout30 seconds
Default HTTP body limit32 MiB
Default child process output limit1 MiB
HTTP hook response buffer limit32 MiB
HTML injection buffer limit32 MiB
WebSocket frame hook limit8 MiB
WebSocket frame hook timeout1 second
Consecutive WebSocket frame drops16 frames
Route WebSocket message limit8 MiB
Route WebSocket outstanding sends64 messages, 16 MiB total, including the message being written
Route WebSocket close reason limit123 UTF-8 bytes

10.3 Error Objects ​

server.call() rejection:

js
{
  name: "Error",
  message: "method not found",
  code: -32601,
  data: "optional detail"
}

Error thrown by a server.registerRPC() handler:

js
const error = new Error("boom");
error.code = -32045;
error.data = { detail: "x" };
throw error;

Common JSON-RPC error codes:

CodeMeaning
-32700Parse error
-32600Invalid request
-32601Method not found
-32602Invalid params
-32603Internal error
-32011Deadline exceeded
-32040Unauthenticated
-32041Permission denied
-32044Not found
-32045Already exists
-32051Unavailable

10.4 Lifecycle Cleanup ​

ResourceBehavior when the plugin unloads
Route slot registered by server.route()Retained; requests return 404.
Connections accepted by req.upgradeWebSocket()Closed; JavaScript onclose delivery is not guaranteed.
Static route slot registered by server.static()Retained; requests return 404.
HTTP request and response hooksRemoved.
WebSocket hooksRemoved.
Fragments registered by server.injectHTML()Removed.
Jobs registered by server.cron()Cancelled and removed.
Methods registered by server.registerRPC()Unregistered.
Plugin-created timers, listeners, and processesThe runtime closes registered resources. The script should release them explicitly in unload().

Released under the MIT license.