Scripting API Reference
RelayCore embeds a Deno/V8 runtime for intercepting and modifying traffic via globalThis hook functions. The scripting engine supports both JavaScript and TypeScript.
Lifecycle Hooks
Scripts register hooks by defining named functions on globalThis. RelayCore 1.0 provides 11 hooks covering HTTP, WebSocket, and connection lifecycles:
| Hook | Trigger | Signature |
|---|---|---|
onRequestHeaders | After request headers received | (ctx, flow) => flow | undefined |
onRequest | Request body ready, before forward | (ctx, flow, body) => action |
onResponseHeaders | After response headers received | (ctx, flow) => flow | undefined |
onResponse | Response body ready | (ctx, flow, body) => action |
onWebSocketMessage | WebSocket frame received | (ctx, flow, message) => message | undefined |
onWebSocketStart | WebSocket handshake complete | (ctx, flow) => void |
onWebSocketEnd | WebSocket normal close | (ctx, flow, code, reason) => void |
onWebSocketError | WebSocket protocol/IO error | (ctx, flow, error) => void |
onConnect | Client TCP connection established | (ctx, conn) => {drop:true} | undefined |
onDisconnect | Connection closed | (ctx, conn, stats) => void |
onError | Any hook throws an error | (ctx, flow, error, stage) => void |
Flow Object
The flow parameter contains the complete state of the current flow:
// Flow structure (script-side view)
{
id: string, // UUID
start_time: string, // ISO 8601
tags: string[],
network: {
client_ip: string,
server_ip: string,
protocol: "TCP" | "UDP",
tls: boolean,
sni: string | null,
},
// Rule bridge (RelayCore exclusive)
matched_rules: string[], // Rule IDs that matched
rule_variables: Record, // Variables set by SetVariable action
// Application-layer protocol data
layer: {
type: "Http" | "WebSocket",
data: {
// When type = "Http":
request: {
method: string,
url: string, // Full URL
version: string, // "HTTP/1.1" | "HTTP/2.0"
headers: [string, string][],
body: {
encoding: string,
size: number,
content: string, // base64 encoded
} | null,
cookies: [string, string][],
},
response: {
status: number,
status_text: string,
version: string,
headers: [string, string][],
body: { encoding, size, content } | null,
cookies: [string, string][],
} | null,
}
} Built-in Utilities relay.*
The global relay object provides commonly needed utility functions:
relay.uuid()
const id = relay.uuid(); // "550e8400-e29b-41d4-a716-446655440000" relay.hash(algorithm, data)
const sha = relay.hash("sha256", "hello");
// Supported: sha1, sha256, sha512, md5
// Returns hex string relay.base64.encode / relay.base64.decode
const b64 = relay.base64.encode("hello world"); // "aGVsbG8gd29ybGQ="
const raw = relay.base64.decode(b64); // Uint8Array relay.json.parseSafe / relay.json.stringifyPretty
const obj = relay.json.parseSafe('{"a":1}'); // {a:1} or undefined
const txt = relay.json.stringifyPretty(obj); // 2-space indent relay.env(name)
Reads whitelisted environment variables. Requires --script-env-allow CLI flag:
// CLI: relay-core-cli run --script-env-allow=BACKEND_HOST
const host = relay.env("BACKEND_HOST"); // "api.example.com"
const miss = relay.env("SECRET"); // undefined (not whitelisted) relay.fetch(url, options?)
Make controlled outbound HTTP sub-requests. Disabled by default; requires --enable-script-fetch.
const resp = await relay.fetch("http://auth.internal/verify", {
method: "POST",
headers: { "Authorization": flow.layer.data.request.headers.find(h => h[0]==="authorization")?.[1] },
body: JSON.stringify({ flow_id: flow.id }),
timeout_ms: 2000,
});
// resp: { status, headers, body }
if (resp.status !== 200) {
throw new Error("Auth failed");
} | Control | Description |
|---|---|
| Disabled by default | Must pass --enable-script-fetch |
| Allowlist | --script-fetch-allow=host1,host2 |
| Anti-recursion | Fetching own proxy port is rejected |
| Concurrency cap | --script-fetch-max-concurrency=8 |
| Timeout cap | Hard limit 30s |
| Audit | Every fetch enters the Audit Trail |
relay.readBody(flow)
const body = await relay.readBody(flow);
// For streaming bodies, returns accumulated content Global State sharedState
globalThis.sharedState is a key-value store shared across all hooks within the same engine instance:
// Store in onRequest
globalThis.onRequest = (ctx, flow) => {
sharedState.set(flow.id, { start: Date.now() });
return flow;
};
// Read in onResponse
globalThis.onResponse = (ctx, flow) => {
const meta = sharedState.get(flow.id);
if (meta) {
console.log(`Flow ${flow.id} took ${Date.now() - meta.start}ms`);
sharedState.delete(flow.id);
}
}; Graded Logging console.*
Five log levels map to Rust tracing:
console.log("Request received"); // tracing::info! — visible at runtime
console.info("Status OK"); // tracing::info!
console.warn("Rate limit hit"); // tracing::warn! — needs attention
console.error("Auth failed"); // tracing::error! — always visible
console.debug("Var value: " + x); // tracing::debug! — only with RUST_LOG=debug Connection Hooks
// Connection filtering — return {drop:true} to reject
globalThis.onConnect = (ctx, conn) => {
// conn: { id, client_ip, client_port }
if (conn.client_ip.startsWith("192.168.1.")) {
return { drop: true };
}
};
// Connection statistics
globalThis.onDisconnect = (ctx, conn, stats) => {
// stats: { duration_ms, flows_count }
console.log(`Connection ${conn.id}: ${stats.flows_count} flows`);
}; WebSocket Lifecycle
globalThis.onWebSocketStart = (ctx, flow) => {
console.log(`WS opened: ${flow.layer.data.request.url}`);
};
globalThis.onWebSocketEnd = (ctx, flow, code, reason) => {
console.log(`WS closed: ${code} ${reason}`);
};
globalThis.onWebSocketError = (ctx, flow, error) => {
console.error(`WS error: ${error}`);
}; Rule Bridge
RelayCore exclusive — scripts can observe rule engine execution results:
globalThis.onResponse = (ctx, flow) => {
// Read matched rules
if (flow.matched_rules.includes("rule-block-tracking")) {
flow.layer.data.response.headers.push(["X-Blocked-By", "RelayCore"]);
}
// Read variables set by rule engine SetVariable action
const count = flow.rule_variables["rate_limit_count"];
if (count && parseInt(count) > 100) {
console.warn(`Rate limit exceeded: ${count}`);
}
}; Script Management & Deployment
CLI Commands
# Load a script via HTTP API
curl -X POST http://127.0.0.1:8082/api/v1/script \
-H "Content-Type: application/json" \
-d '{"script": "globalThis.onRequest = (ctx, flow) => { console.log(flow.layer.data.request.url); }"}'
# MCP tool (via AI Agent)
mcp: relay-core set_script --script script.js
# Bundle mode: use npm packages
relay-core scripts init my-project # Scaffold
cd my-project && npm install jwt-decode # Install deps
relay-core scripts build # esbuild bundle
relay-core scripts dev # build + hot reload + watch Bundle Mode & npm Ecosystem
RelayCore's V8 isolate does not include an npm resolver. Bundle scripts with dependencies via esbuild:
// src/main.ts — import npm packages as usual
import jwtDecode from "jwt-decode";
import { XMLParser } from "fast-xml-parser";
globalThis.onRequest = (ctx, flow) => {
const auth = flow.layer.data.request.headers.find(h => h[0] === "authorization");
if (auth) {
const payload = jwtDecode(auth[1]);
console.log("JWT subject:", payload.sub);
}
};
// relay-core scripts build → dist/bundle.js (single IIFE file)
// Load bundle.js directly Script environment
# Allow env vars for relay.env() at startup
relay-core-cli run --script-env-allow API_TOKEN,DEBUG Observability
Script execution metrics are automatically reported to Prometheus:
| Metric | Description |
|---|---|
script_hook_duration_us{hook} | Per-hook execution duration (histogram) |
script_hook_invocations_total{hook} | Cumulative hook invocations |
script_hook_errors_total{hook} | Cumulative hook errors |
script_fetch_total{target,status} | fetch call count |
script_env_access_total{key} | env access count (values not recorded) |