Add ability for hook mapping transform modules to export a verifyAuth function for custom webhook authentication (e.g., GitHub HMAC signatures). When a mapping's transform exports verifyAuth, it replaces standard token auth for that mapping. Returns true to allow, false to reject. Flow in server-http.ts: 1. Read raw body + parse JSON 2. findMapping() to match on path/source 3. authenticateHook() with matched transform 4. Route: wake / agent / applyMapping() Changes: - hooks.ts: Split readJsonBody into readRawBody + parseJsonBody; add authenticateHook() for custom or token auth - hooks-mapping.ts: Add verifyAuth types, loadVerifyAuth(), findMapping(), applyMapping(); CachedTransform for caching - server-http.ts: Linear flow using the above - Tests for authenticateHook and loadVerifyAuth - Document verifyAuth with GitHub HMAC example
11 KiB
| summary | read_when | ||
|---|---|---|---|
| Webhook ingress for wake and isolated agent runs |
|
Webhooks
Gateway can expose a small HTTP webhook endpoint for external triggers.
Enable
{
hooks: {
enabled: true,
token: "shared-secret",
path: "/hooks"
}
}
Notes:
hooks.tokenis required whenhooks.enabled=true.hooks.pathdefaults to/hooks.
Auth
Every request must include the hook token. Prefer headers:
Authorization: Bearer <token>(recommended)x-moltbot-token: <token>?token=<token>(deprecated; logs a warning and will be removed in a future major release)
Endpoints
POST /hooks/wake
Payload:
{ "text": "System line", "mode": "now" }
textrequired (string): The description of the event (e.g., "New email received").modeoptional (now|next-heartbeat): Whether to trigger an immediate heartbeat (defaultnow) or wait for the next periodic check.
Effect:
- Enqueues a system event for the main session
- If
mode=now, triggers an immediate heartbeat
POST /hooks/agent
Payload:
{
"message": "Run this",
"name": "Email",
"sessionKey": "hook:email:msg-123",
"wakeMode": "now",
"deliver": true,
"channel": "last",
"to": "+15551234567",
"model": "openai/gpt-5.2-mini",
"thinking": "low",
"timeoutSeconds": 120
}
messagerequired (string): The prompt or message for the agent to process.nameoptional (string): Human-readable name for the hook (e.g., "GitHub"), used as a prefix in session summaries.sessionKeyoptional (string): The key used to identify the agent's session. Defaults to a randomhook:<uuid>. Using a consistent key allows for a multi-turn conversation within the hook context.wakeModeoptional (now|next-heartbeat): Whether to trigger an immediate heartbeat (defaultnow) or wait for the next periodic check.deliveroptional (boolean): Iftrue, the agent's response will be sent to the messaging channel. Defaults totrue. Responses that are only heartbeat acknowledgments are automatically skipped.channeloptional (string): The messaging channel for delivery. One of:last,whatsapp,telegram,discord,slack,mattermost(plugin),signal,imessage,msteams. Defaults tolast.tooptional (string): The recipient identifier for the channel (e.g., phone number for WhatsApp/Signal, chat ID for Telegram, channel ID for Discord/Slack/Mattermost (plugin), conversation ID for MS Teams). Defaults to the last recipient in the main session.modeloptional (string): Model override (e.g.,anthropic/claude-3-5-sonnetor an alias). Must be in the allowed model list if restricted.thinkingoptional (string): Thinking level override (e.g.,low,medium,high).timeoutSecondsoptional (number): Maximum duration for the agent run in seconds.
Effect:
- Runs an isolated agent turn (own session key)
- Always posts a summary into the main session
- If
wakeMode=now, triggers an immediate heartbeat
POST /hooks/<name> (mapped)
Custom hook names are resolved via hooks.mappings (see configuration). A mapping can
turn arbitrary payloads into wake or agent actions, with optional templates or
code transforms.
Mapping options (summary):
hooks.presets: ["gmail"]enables the built-in Gmail mapping.hooks.mappingslets you definematch,action, and templates in config.hooks.transformsDir+transform.moduleloads a JS/TS module for custom logic.- Use
match.sourceto keep a generic ingest endpoint (payload-driven routing). - TS transforms require a TS loader (e.g.
bunortsx) or precompiled.jsat runtime. - Set
deliver: true+channel/toon mappings to route replies to a chat surface (channeldefaults tolastand falls back to WhatsApp). allowUnsafeExternalContent: truedisables the external content safety wrapper for that hook (dangerous; only for trusted internal sources).moltbot webhooks gmail setupwriteshooks.gmailconfig formoltbot webhooks gmail run. See Gmail Pub/Sub for the full Gmail watch flow.
Transform Functions
When templates aren't flexible enough, use a transform function to process webhook payloads with code.
Basic Example
hooks:
enabled: true
token: "secret"
transformsDir: ./hooks # relative to config file
mappings:
- id: github
match:
path: github
action: agent
transform:
module: github.js # resolves to ./hooks/github.js
export: handleWebhook # optional, defaults to "default" or "transform"
The transform function receives a context object:
type HookMappingContext = {
payload: Record<string, unknown>; // Parsed JSON body
headers: Record<string, string>; // Lowercase header names
url: URL; // Parsed request URL
path: string; // Subpath after /hooks/ (e.g., "github")
};
// hooks/github.js
export function handleWebhook(ctx) {
const event = ctx.headers["x-github-event"];
// Return null to skip this webhook entirely (no agent run)
if (event === "ping") return null;
// Return fields to override the mapping defaults
return {
message: `GitHub ${event}: ${ctx.payload.action} on ${ctx.payload.repository?.full_name}`,
name: "GitHub",
sessionKey: `github:${ctx.payload.repository?.id}`,
};
}
Return Values
Return an object with fields to override, or null to skip:
// For action: "agent" (default)
return {
message: string; // Required if not set in mapping
name?: string; // Display name for the hook
sessionKey?: string; // Session identifier
wakeMode?: "now" | "next-heartbeat";
deliver?: boolean; // Send response to chat
channel?: string; // Target channel
to?: string; // Recipient
model?: string; // Model override
thinking?: string; // Thinking level
timeoutSeconds?: number;
};
// For action: "wake"
return {
text: string; // Required
mode?: "now" | "next-heartbeat";
};
// Skip this webhook (no action taken, returns 204)
return null;
Async Transforms
Transforms can be async:
export default async function(ctx) {
const extra = await fetchAdditionalContext(ctx.payload.id);
return {
message: `Event: ${ctx.payload.type}\nContext: ${extra}`,
};
}
TypeScript
JavaScript (.js / .mjs) transforms work out of the box with no extra setup.
TypeScript transforms require a loader at runtime:
# Run gateway with tsx
npx tsx node_modules/.bin/moltbot gateway start
# Or use bun
bun run moltbot gateway start
# Or precompile to .js
tsc hooks/*.ts --outDir hooks-dist
# then reference hooks-dist/github.js in config
Responses
200for/hooks/wake202for/hooks/agent(async run started)401on auth failure400on invalid payload413on oversized payloads
Examples
curl -X POST http://127.0.0.1:18789/hooks/wake \
-H 'Authorization: Bearer SECRET' \
-H 'Content-Type: application/json' \
-d '{"text":"New email received","mode":"now"}'
curl -X POST http://127.0.0.1:18789/hooks/agent \
-H 'x-moltbot-token: SECRET' \
-H 'Content-Type: application/json' \
-d '{"message":"Summarize inbox","name":"Email","wakeMode":"next-heartbeat"}'
Use a different model
Add model to the agent payload (or mapping) to override the model for that run:
curl -X POST http://127.0.0.1:18789/hooks/agent \
-H 'x-moltbot-token: SECRET' \
-H 'Content-Type: application/json' \
-d '{"message":"Summarize inbox","name":"Email","model":"openai/gpt-5.2-mini"}'
If you enforce agents.defaults.models, make sure the override model is included there.
curl -X POST http://127.0.0.1:18789/hooks/gmail \
-H 'Authorization: Bearer SECRET' \
-H 'Content-Type: application/json' \
-d '{"source":"gmail","messages":[{"from":"Ada","subject":"Hello","snippet":"Hi"}]}'
Custom Authentication (verifyAuth)
External webhooks (GitHub, Stripe, Linear, etc.) often use their own authentication
schemes (e.g., HMAC signatures). Instead of using the standard bearer token, you can
export a verifyAuth function from your transform module to handle custom auth.
When a mapping's transform module exports verifyAuth, it runs before the standard
token check. If it returns true, the request is authorized; if false, a 401 is returned.
Example: GitHub Webhook Signature Verification
The verifyAuth function receives a context with the raw body for signature verification:
type HookVerifyAuthContext = {
headers: Record<string, string>; // Lowercase header names
url: URL; // Parsed request URL
path: string; // Subpath after /hooks/
rawBody: Buffer; // Raw request body for signature verification
};
// hooks/github-transform.js
import { createHmac, timingSafeEqual } from "crypto";
// Runs BEFORE token auth - return true to allow, false to reject
export function verifyAuth(ctx) {
const signature = ctx.headers["x-hub-signature-256"];
if (!signature) return false;
const secret = process.env.GITHUB_WEBHOOK_SECRET;
const expected = "sha256=" + createHmac("sha256", secret)
.update(ctx.rawBody)
.digest("hex");
try {
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
} catch {
return false;
}
}
// Transform the payload (runs after auth passes)
export default function transform(ctx) {
const event = ctx.headers["x-github-event"];
// Format based on event type
if (event === "push") {
return { message: `Push to ${ctx.payload.repository?.full_name}: ${ctx.payload.head_commit?.message}` };
}
if (event === "pull_request") {
return { message: `PR ${ctx.payload.action}: ${ctx.payload.pull_request?.title}` };
}
return { message: `GitHub ${event}: ${JSON.stringify(ctx.payload).slice(0, 200)}` };
}
Config:
hooks:
enabled: true
token: "regular-token" # Still needed for non-custom-auth hooks
mappings:
- id: github
match:
path: github
action: agent
name: GitHub
transform:
module: github-transform.js
Async verifyAuth
verifyAuth can be async if needed:
export async function verifyAuth(ctx) {
// async validation (e.g., checking against external service)
return await validateSignature(ctx.headers, ctx.rawBody);
}
Security
- Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy.
- Use a dedicated hook token; do not reuse gateway auth tokens.
- Avoid including sensitive raw payloads in webhook logs.
- Hook payloads are treated as untrusted and wrapped with safety boundaries by default.
If you must disable this for a specific hook, set
allowUnsafeExternalContent: truein that hook's mapping (dangerous).