Skip to content

server Module & Lifecycle Hooks

Plugins get their bridge to the host via require("server"). This native module is injected by the plugin system and provides routing, hooks, RPC invocation, and configuration access.

js
const server = require("server");
MethodDescriptionPermission
server.route(method, path, handler)Register an HTTP route on the host engineallowRoutes
server.hook(kind, fn)Register request/response hooksallowHooks
server.call(method, params...)Call system RPC with admin authorityallowSystemRPC
server.registerRPC(method, handler)Register a plugin-owned RPC methodalways granted
server.getConfig()Read configuration (merged with defaults)always granted

Missing allowRoutes / allowHooks throws TypeError at load time (plugin load fails); missing allowSystemRPC rejects the Promise returned by server.call.

Lifecycle Hooks

The entry script's top-level code runs immediately at load. Additionally, two optional global functions may be defined:

js
function load() {
  // Called every time the plugin is enabled/started (including startup recovery)
}

function unload() {
  // Called on disable, uninstall, or server shutdown
}
  • A load() error (or a top-level script error) → the plugin is auto-disabled and the error is written to last_error.
  • unload() errors are recorded but do not block unloading.
  • Omitting load() is valid, but it is recommended to put route/hook/RPC registration inside load().
  • During unload() the plugin is already removed from the instance registry, so server.call rejects with "not loaded".

server.route

Registers an HTTP route on the host gin engine:

js
server.route("GET", "/plug", async (req, res) => {
  res.setHeader("Content-Type", "application/json");
  res.end(JSON.stringify({ ok: true }));
});
ArgTypeDescription
methodstringHTTP method, uppercased automatically; must not be empty
pathstringPath; must start with /
handler(req, res) => voidHandler; must call res.end() to finish
  • Requires allowRoutes, otherwise a TypeError is thrown at load time.
  • Route slots survive unload (they return 404) and are restored on reload; re-registering the same route within one load is a no-op.

Request object req

FieldTypeDescription
methodstringRequest method, e.g. "GET"
urlstringRaw request URL (with query string), e.g. "/plug?x=7"
headersobjectRequest headers; keys are lower-cased, single values are strings, multi-values are arrays
queryobjectQuery parameters; multi-values joined with ,
bodystringRequest body (fully read, limited by maxHTTPBodyBytes)
contextobjectRequest context, see below

context carries identity and origin information:

js
{
  principal: {           // only when the identity middleware ran
    type: "agent" | "user" | "api_key" | "anonymous",
    roles: [...],
    user_uuid,
    client_uuid,
    is_api_key
  },
  role,                  // user role
  user_uuid,             // user UUID
  client_uuid,           // client UUID (agent requests)
  remote_ip,             // client IP
  user_agent             // User-Agent
}

Response object res

MemberTypeDescription
statusCodenumberStatus code, default 200, writable
statusMessagestringStatus text
streamingbooleanSet true to enable streaming: every write() is flushed immediately
isAborted()() => booleanReturns true after the client disconnects (or streaming idle timeout)
setHeader(name, value)fnSet a header (value can be a string or array)
getHeader(name)fnRead a header (string / array / undefined)
removeHeader(name)fnRemove a header
write(data)(Buffer | string) => booleanWrite data; buffered until end() unless streaming
end([data])fnMust be called to finish the response

Must call end()

If the handler never calls res.end() (and is not streaming), the client receives 504 plugin route handler timed out after timeout.

Streaming responses (SSE)

js
server.route("GET", "/stream", async (req, res) => {
  res.streaming = true;
  res.setHeader("Content-Type", "text/event-stream");
  while (!res.isAborted()) {
    const data = ...; // fetch one frame
    res.write("data: " + data + "\n\n");
    await new Promise((resolve) => setTimeout(resolve, 50));
  }
});
  • With res.streaming = true, each res.write() is sent to the client and flushed immediately.
  • When the client disconnects, isAborted() returns true; the script should exit its loop and return (returning ends the stream).
  • Streaming idle timeout beyond timeout also aborts and closes the stream.

server.hook

Registers request or response hooks on the host HTTP chain, affecting all HTTP requests entering/leaving the server (except WebSocket upgrade requests, which pass through untouched):

js
server.hook("request", (req) => {
  req.headers["x-hooked"] = "yes";
});

server.hook("response", (req, res) => {
  res.statusCode = 201;
  res.body = res.body + "|hooked";
});
ArgTypeDescription
kind"request" | "response"Hook type
fnfunctionRequest hook fn(req); response hook fn(req, res)

Requires allowHooks, otherwise a TypeError is thrown at load time.

Request hook req

js
{
  method,                 // mutable: applied to the real request
  url,                    // mutable: applied to the real request
  headers,                // mutable: replaces the real request headers
  query,                  // query params (read-only snapshot)
  body,                   // mutable: replaces the request body
  context: { remote_ip, user_agent }   // no identity (hooks run before the identity middleware)
}
  • Request bodies are read up to 32 MiB (maxHTTPBodyBytes).
  • Hooks run in registration order; a hook error → client receives 500 plugin request hook failed and remaining hooks are skipped.
  • Hook execution is bounded by timeout; a timeout is treated as a failure.

Response hook res

js
{
  statusCode,             // mutable
  statusMessage,          // mutable
  headers,                // mutable: replaces the real response headers
  body                    // mutable: replaces the response body
}
  • Responses are buffered (up to 32 MiB) so hooks can rewrite them.
  • Streaming responses (SSE, after the first Flush()) or responses larger than 32 MiB pass through untouched; hooks can no longer rewrite them (logged).
  • Hook errors are logged only and do not block the response.

server.call

Invokes any registered system RPC method with admin authority:

js
const result = await server.call("common:getNodes");
const status = await server.call("common:getNodesLatestStatus", { uuid: "..." });
ArgDescription
methodRPC method name, e.g. common:getNodes, admin:getTasks
params...Params. 0 argsnull; 1 arg → passed as-is; N args → marshalled into a positional array

Returns a Promise:

  • Success → resolves to the RPC result.
  • Failure → rejects an Error carrying JSON-RPC error fields:
    • err.code (integer)
    • err.message
    • err.data (optional)

Admin authority

server.call runs with RoleAdmin, equivalent to an admin operating the panel — including sensitive operations such as admin:exec. Requires allowSystemRPC.

See RPC Methods for the full method inventory.

server.registerRPC

Registers a plugin-owned RPC method, callable by the frontend or by other plugins (via server.call):

js
server.registerRPC("plugin:greet", (params) => {
  return { echo: params, from: "example" };
});

server.registerRPC("plugin:fail", () => {
  const err = new Error("boom");
  err.code = -32045;
  err.data = { detail: "x" };
  throw err;
});
ArgDescription
methodMethod name; must not be empty and must not start with rpc. (reserved prefix)
handler(params) => result; return a result or throw an error
  • Always granted, no permission declaration needed.
  • Re-registering the same method within one load is a no-op; methods are unregistered on unload.
  • Handlers run on the plugin event loop; thrown JS Errors map to JSON-RPC errors (err.code / err.message / err.data are propagated).
  • Prefer the plugin:<name>:<action> naming convention to avoid clashes with system methods.

server.getConfig

Reads the plugin's saved configuration (merged with manifest defaults):

js
const config = await server.getConfig();
console.log(config.interval); // defaults already merged

Released under the MIT license.