MallaDocs

Plugin SDK

definePlugin: connections, secrets, operations and the outbound fetch policy.

Developer guide

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

ts
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>; definePlugin checks it.
  • The input schema becomes the editor’s form (as JSON Schema) and validates resolved inputs; output drives autocomplete.
  • connection: null means no integration row is needed, like the built-in CMS.
  • icon sets the editor icon; fetchTimeoutMs raises the fetch timeout for slow upstreams such as model APIs (at most 120 s).

The plugin context

ctx.configThe integration’s settings, parsed by your connection schema.
ctx.secretsDecrypted secrets that have a value. Never sent to the browser.
ctx.fetchPolicy-enforcing fetch: allowed hosts, HTTPS, no private addresses, timeouts, size cap.
ctx.publishedTrue on the live site (live data), false in the editor (working data).
ctx.cmsFirst-party only: list and get CMS items.
ctx.postgresFirst-party only: one TLS connection per call, with the same address checks as fetch.
ctx.signalAborts when the caller cancels.

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.