HTTP
Xenocept exposes a low-level HTTP proxy for plugins that want to call external services:
POST /api/v1/deliver/http
This is not a “destination type” the user picks from a dropdown. It’s a primitive for plugin code — when a plugin wants to ship a session to a webhook, it formats the body itself and calls this endpoint.
Request / Response
The request body is application/json:
{
"url": "https://example.com/intake",
"method": "POST",
"headers": {
"Authorization": "Bearer...",
"Content-Type": "application/json"
},
"body": "..." // verbatim string body
}
Supported methods: GET, POST, PUT, PATCH, DELETE.
The response:
{
"status": 200,
"body": "..."
}
The proxy returns the upstream’s status code and response body to the plugin. The plugin decides what counts as success.
What the Proxy Does (and Doesn’t Do)
The proxy is intentionally thin:
- Sends one HTTP request
- Returns the response
- Does SSRF defense: rejects URLs that resolve to loopback, link-local, or unspecified ranges, or have a
localhost-looking hostname - Returns 502 on transport failure
What it does not do:
- No retries. One shot. If the plugin needs retries, it loops.
- No multipart construction. Whatever string the plugin passes as
bodyis sent verbatim. Plugins that need multipart build it themselves. - No
${VAR}env-var substitution in URL or headers. Plugins assemble the final strings before calling. - No HMAC signing of outgoing requests. If your downstream service requires a signature, the plugin computes it.
- No streaming for huge payloads. The whole response is loaded into memory.
Why a Proxy?
Two reasons:
- SSRF safety. The frontend is a webview; if a plugin’s JavaScript could
fetcharbitrary URLs from inside Xenocept, that would be an SSRF risk against any local services on the user’s machine. Forcing outbound HTTP through this proxy gives us one place to enforce a denylist. - Auth wraps consistently. The deliver routes go through the same dev-unsafe-or-token middleware as the rest of the dangerous endpoints.
Plugin Usage Sketch
A destination plugin’s deliver typically looks like:
'use strict';
export async function deliver(session, attachments, config) {
const body = JSON.stringify({
session_id: session.id,
text: session.comments.map((c) => c.text).join('\n'),
});
const r = await fetch('/api/v1/deliver/http', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: config.endpoint,
method: 'POST',
headers: { 'Authorization': `Bearer ${config.apiKey}` },
body
})
});
const result = await r.json;
if (result.status >= 400)
throw new Error(`destination returned ${result.status}: ${result.body}`);
}
See Plugin Delivery for the full plugin contract.