Skip to content

Plugin services

A plugin builds its services once per process with services. The result is its context’s services wherever the plugin runs code: procedures, server routes, jobs, data providers and hooks.

import { auditor, type PluginServicesContext, pluginError, type Scope } from '@manablox/core';
import type {} from '@manablox/server';
export function notesServices(context: PluginServicesContext) {
const repos = () => notesRepositories(context.repos);
return {
notes: {
list: (scope: Scope) => repos().notes.list(scope),
create: (scope: Scope, text: string) =>
context.repos.transaction(async (tx) => {
const row = await notesRepositories(tx).notes.create(scope, text);
await auditor(tx, 'notes.note', (note: { text: string }) => note.text).record(
'notes.note.create',
row,
);
return row;
}),
},
};
}
export type NotesServices = ReturnType<typeof notesServices>;

definePlugin infers the services’ type, so plugin.services is typed in the plugin’s own jobs, maintenance, start, stop and server routes and middleware.

What services receives

FieldWhat it is
pluginThe plugin’s context, below, without services
manabloxThe instance: config, hooks, registries, controls, instance.id
loggerThe instance logger bound to the plugin
reposThe core repositories
dbThe database context for the plugin’s own repositories, see Tables and migrations
cacheThe tagged cache

The types of repos, db, cache and the context’s core come with @manablox/server; a plugin that uses them imports it for types (import type {} from '@manablox/server').

The plugin context

Every process builds the services after the core services and before it starts. The context (manablox.plugin(name)) carries id, name, manablox (with manablox.instance.id, the instance’s id, below), logger, controls, repos, db, cache, jobs, core (the core services: content, contentTypes, spaces, environments, users, menus, tags, roles, audit, media, storage, realtime, hostVerifier, and on management instances credentials, mailer and pusher) and services, plugins (other plugins’ services and flags, see Dependencies), contributions(point) (see Extension points) and channel(name) (see Lifecycle and channels). Reading services before they are built, for example in an after:init hook, throws.

The instance id

manablox.instance.id is the instance’s identity: a uuid v7 made at the first boot and kept in the core instance_meta table, the same in every process of the instance. It is read-only. A backup and its restore keep it; creating or promoting an environment and a snapshot restore leave it alone. It is loaded before the plugins’ services are built.

Audit entries

A plugin declares the target kinds it records under audit, each <pluginId>.<entity>. Entries are recorded as <pluginId>.<entity>.<verb> with any one-word verb, and the activity log’s filters list the kinds. A kind outside the plugin’s namespace is refused at start (plugin.key.invalid). The server keeps no labels: the plugin’s admin bundle labels its kinds and actions, and the admin shows the key where none is given.

audit: { entities: ['notes.note'] },
// ...
await auditor(tx, 'notes.note', (note: { text: string }) => note.text).record('notes.note.create', note);

Work the plugin does on its own, not for a person or an API key, can be recorded under an actor kind of its own, declared in audit.actorKinds as the plugin id or <pluginId>.<name> (audit: { entities: [...], actorKinds: ['notes'] }), then passed as { actor: { kind: 'notes', id, label } }. The activity log filters by it and shows the key.

Error keys

A plugin declares its error keys under errors, each plugins.<pluginId>.<name> with an English sentence and a kind (bad_request by default), which sets the HTTP status. pluginError(key, params) builds the error; its transport message is the sentence with {param} filled in, so the admin and API clients can show it.

errors: {
'plugins.notes.locked': { kind: 'locked', message: 'Note {id} is locked.' },
},
// ...
throw pluginError('plugins.notes.locked', { id: note.id });

A key outside the plugin’s namespace is refused at start (plugin.key.invalid). instance.plugins of the management API lists each plugin with its permission, control and error labels and its audit kinds for the admin.

Signed tokens

@manablox/core/node exports the signing helpers core uses for its own tokens: signingKey(secret, purpose) derives a key for one purpose from the instance secret, signFor(secret, purpose, data) is the base64url HMAC-SHA256 of data under it, and sameSignature(expected, actual) compares two signatures in constant time. Pass manablox.config.auth.secret and a purpose string of the plugin’s own, so its tokens never verify anywhere else. The website plugin signs its share links and site password cookies this way.