Integrations are plugins. The built-in REST, GraphQL and CMS connectors are written with the same @malla/plugin-sdk any plugin uses: a connection (settings, secret fields, a test) and operations (queries and mutations) validated with Zod.
Define a plugin
import { definePlugin, defineOperation } from '@malla/plugin-sdk'
import { z } from 'zod'
const config = z.object({
baseUrl: z.url().describe('API base URL'),
})
export default definePlugin({
id: 'weather',
name: 'Weather',
version: '1.0.0',
description: 'Current conditions by city.',
connection: {
config,
secrets: { apiKey: { label: 'API key' } },
secretBoundFields: ['baseUrl'], // changing it clears the stored key
hosts: (c) => [new URL(c.baseUrl).host], // the only hosts ctx.fetch may reach
test: async (ctx) => {
const res = await ctx.fetch(`${ctx.config.baseUrl}/ping`)
if (!res.ok) throw new Error('Weather API unreachable')
},
},
operations: {
'weather.current': defineOperation({
kind: 'query',
name: 'Current weather',
input: z.object({ city: z.string().min(1) }),
output: z.object({ tempC: z.number(), summary: z.string() }),
run: async (ctx, input) => {
const url = `${ctx.config.baseUrl}/current?city=${encodeURIComponent(input.city)}`
const res = await ctx.fetch(url, { headers: { 'x-api-key': ctx.secrets.apiKey ?? '' } })
return res.json()
},
}),
},
})
- Operation ids are
<plugin id>.<name>;definePluginchecks it. - The
inputschema becomes the editor’s form (as JSON Schema) and validates resolved inputs;outputdrives autocomplete. connection: nullmeans no integration row is needed, like the built-in CMS.iconsets the editor icon;fetchTimeoutMsraises the fetch timeout for slow upstreams such as model APIs (at most 120 s).
The plugin context
Outbound policy
ctx.fetch only reaches the hosts the connection declares, over HTTPS. It resolves hostnames (DNS over HTTPS) before each request and redirect and blocks private, loopback and link-local answers, follows at most 5 redirects on the same host, times out after 10 seconds and caps responses at 2 MB. ALLOW_LOCAL_INTEGRATIONS=1 lifts the address checks for local development only.
Secrets
Secret values are encrypted at rest and never returned by the API; the editor only learns which ones are set. Changing a secretBoundFields setting or the host set clears stored secrets unless new values come with the same update, so a token can never be silently redirected to another host.
The plugin catalog
Plugins are published with a manifest that declares their permissions, go through review, and are installed per site at a pinned version. Updates are opt-in.