Script API Reference
Hodor runs your scripts in a built-in JavaScript engine. Depending on the script type, Hodor calls one of the entry functions below at the right time — you only implement the logic you need. If you do not modify anything, delete the unused entry function to avoid unnecessary calls.
Available global objects and classes:
| Category | Names |
|---|---|
| Entry functions | onRequest, onResponse, onSessionCompleted, onSchedule, abort() |
Native $ modules | $codec, $hash, $hmac, $crypto, $jwt, $format, $fs, $env, $uuid, $log |
| Web standard APIs | fetch, HTTPClient, setTimeout / setInterval, btoa / atob / sleep / require / TextEncoder / crypto, console |
| HTTP classes | Request, Response, Body, Headers, URLSearchParams |
| Storage | SharedPreferences, sdk variables |
| Contexts | RewriteContext, CompletedContext, ScheduleContext |
Use console.log(...) anywhere in a script to inspect values — output is shown in the script's log view.
Entry Functions
Scripts are detected by scanning for async function declarations — the parameter count of each entry function is fixed (onRequest/onResponse take 3, onSessionCompleted takes 4, onSchedule takes 1). Arrow-function assignments (let onRequest = async (...) => {}) are not detected. Parameter names are free.
onRequest(context, url, request)
Designed for Rewrite Script and Quick Request pre-request scripts. This function is called before the HTTP request is sent to the server. Return the (possibly modified) request.
| Parameter | Type | Description |
|---|---|---|
context | RewriteContext | The context object. |
url | string | The URL of the HTTP request. |
request | Request | The HTTP request object. |
// modify request body
async function onRequest(context, url, request) {
// parse to Text
var text = request.body.parseText()
console.log(text)
request.body = "this is a replaced request text"
// parse to JSON
var json = request.body.parseJSON()
json["script_json"] = "JSONValue"
request.body = json
// parse to URLSearchParams
var urlencoded = request.body.parseURLSearchParams()
urlencoded.append("script", "parseURLSearchParams")
request.body = urlencoded
// Done
return request;
}
// redirect, modify path / queries / headers
async function onRequest(context, url, request) {
console.log(url);
// set a parameter, you can get it in onResponse
context.parameters["custom"] = "custom parameter on request"
// redirect
request.scheme = "http"
request.path = "/upload/json"
request.host = "192.168.3.30"
request.port = 6000
// change method
request.method = "POST"
// queries operation
request.queries.append("name", "value1") // add query
request.queries.set("name", "value2") // replace or add query
request.queries.get("name") // get query by name
request.queries.delete("name") // delete query by name
// headers operation
request.headers.delete("accept") // delete header by name
request.headers.append("hodor", "custom value") // add name-value pair
request.headers.forEach((value, name) => {
console.log(name + ":" + value)
});
// Done
return request;
}
onResponse(context, url, response)
Designed for Rewrite Script and Quick Request post-response scripts. This function is called before the HTTP response is sent to the client. Return the (possibly modified) response.
| Parameter | Type | Description |
|---|---|---|
context | RewriteContext | The context object. |
url | string | The URL of the HTTP response. |
response | Response | The HTTP response object. |
// response body
async function onResponse(context, url, response) {
// parse to Text
var text = response.body.parseText()
console.log(text)
response.body = "this is a replaced response text"
// parse to JSON
var json = response.body.parseJSON()
json["script_json"] = "JSONValue"
response.body = json
// parse to URLSearchParams
var urlencoded = response.body.parseURLSearchParams()
urlencoded.append("script", "parseURLSearchParams")
response.body = urlencoded
// Done
return response;
}
// change status code and headers
async function onResponse(context, url, response) {
// get parameters set in onRequest
console.log(context.parameters["custom"]) // Output "custom parameter on request"
response.statusCode = 200 // change status code
response.statusPhrase = "OK" // change status phrase
response.headers.delete("accept")
response.headers.append("hodor", "custom value")
response.headers.forEach((value, name) => {
console.log(name + ":" + value)
});
// Done
return response
}
onSessionCompleted(context, url, request, response)
Designed for HTTP Completed Script. Called asynchronously after the HTTP response is returned, without affecting data transmission between the app and the server. Useful for data collection and forwarding.
| Parameter | Type | Description |
|---|---|---|
context | CompletedContext | The context object. |
url | string | The URL of the HTTP request. |
request | Request | The HTTP request object. |
response | Response | The HTTP response object. |
// sample: pass session data to your own server
async function onSessionCompleted(context, url, request, response) {
try {
var reqText = request.body.parseText();
var rspText = response.body.parseText()
var params = {
"body": { // HTTPClient post body data
"url": url, // request url
"reqHeader": request.headers.encode(), // request headers
"reqText": reqText, // request body
"rspHeader": response.headers.encode(), // response headers
"rspText": rspText, // response body
},
"headers": { // HTTPClient headers
"from": "onSessionCompleted"
},
}
const rsp = await HTTPClient.post("http://192.168.3.30:8000/upload", params)
let text = rsp.body.parseText()
console.log(text)
} catch(error) {
console.log(error)
}
}
onSchedule(context)
Designed for Schedule Script. Called at the scheduled trigger time (5 or 6-part cron expressions are supported). Use abort() to stop the schedule loop.
| Parameter | Type | Description |
|---|---|---|
context | ScheduleContext | The context object. |
async function onSchedule(context) {
// set custom parameters for next runloop
context.parameters["custom"] = "custom parameters for next runloop";
// repeated count
console.log(`repeatedCount ${context.repeatedCount}`);
// if repeatedCount is 2, abort schedule
if (context.repeatedCount == 2) {
abort()
}
// you can also use HTTPClient to send requests
const rsp = await HTTPClient.get("https://www.apple.com")
let text = rsp.body.parseText()
console.log(text)
}
abort()
Inside channel scripts (onRequest / onResponse / onSchedule), call abort() to stop the script and abort the current execution immediately — it throws an AbortError that Hodor reports back to you.
// ✅ correct — abort() is injected into the script scope
async function onRequest(context, url, request) {
if (url.includes("/tracking")) abort();
return request;
}
Do not declare onRequest(context, url, request, aborter) with a 4th parameter. The parameter count of entry functions is fixed — a 4th parameter makes the entry undetectable.
Native Modules
All $-prefixed modules are injected when the runtime activates. They are implemented natively, return synchronously (unless noted) and treat string inputs as UTF-8.
$codec — Encode & Decode
Base64 / Hex / URL percent encoding for protocol conversion and data transfer.
| Method | Returns |
|---|---|
$codec.base64Encode(str) | Base64 string. |
$codec.base64Decode(str) | Decoded string; invalid input returns "" (does not throw). |
$codec.hexEncode(str) | Lowercase hex string. |
$codec.hexDecode(str) | Decoded string; invalid characters are skipped. |
$codec.urlEncode(str) | Percent-encoded string. |
$codec.urlDecode(str) | Decoded string; invalid sequences are returned as-is. |
$codec.base64Encode("Hello, World!"); // "SGVsbG8sIFdvcmxkIQ=="
$codec.base64Decode("SGVsbG8sIFdvcmxkIQ=="); // "Hello, World!"
$codec.hexEncode("ABC"); // "414243"
$codec.hexDecode("414243"); // "ABC"
$codec.urlEncode("hello world&foo=bar"); // "hello%20world%26foo%3Dbar"
$hash — Hashing
Digest computation for integrity checks, content addressing and anonymization. Returns lowercase hex.
| Method | Output length |
|---|---|
$hash.md5(str) | 32 hex chars |
$hash.sha1(str) | 40 hex chars |
$hash.sha256(str) | 64 hex chars |
$hash.sha384(str) | 96 hex chars |
$hash.sha512(str) | 128 hex chars |
$hash.md5("hello"); // "5d41402abc4b2a76b9719d911017c592"
$hash.sha256("hello"); // "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
// integrity check
const checksum = $hash.sha256(JSON.stringify({user: "admin"}));
$hmac — Message Authentication Code
Keyed hashing for API signing and tamper detection. Both (data, key) are UTF-8 encoded; returns lowercase hex.
| Method | Algorithm |
|---|---|
$hmac.md5(data, key) | HMAC-MD5 |
$hmac.sha1(data, key) | HMAC-SHA1 |
$hmac.sha256(data, key) | HMAC-SHA256 |
$hmac.sha384(data, key) | HMAC-SHA384 |
$hmac.sha512(data, key) | HMAC-SHA512 |
const signature = $hmac.sha256("important data", "my-secret-key");
const isValid = (signature === $hmac.sha256("important data", "my-secret-key")); // true
$crypto — Symmetric Encryption
AES-GCM authenticated encryption (recommended) and DES (legacy compatibility).
| Method | Returns |
|---|---|
$crypto.encryptAES(plaintext, password, options?) | Base64 ciphertext; failures return an "Error: <reason>" string. |
$crypto.decryptAES(ciphertext, password, options?) | Plaintext; invalid ciphertext returns "Error: Invalid ciphertext". |
$crypto.encryptDES(plaintext, key) | DES-ECB + PKCS7, key truncated to 8 bytes → Base64. |
$crypto.decryptDES(ciphertext, key) | Plaintext; non-Base64 input returns "". |
options: { keySize: 128 \| 192 \| 256 } (default 256). The AES ciphertext format is base64(nonce[12] + ciphertext + tag[16]) — pair it with decryptAES; when interoperating with other languages you must unpack this format yourself.
const encrypted = $crypto.encryptAES("sensitive data", "my-password");
$crypto.decryptAES(encrypted, "my-password"); // "sensitive data"
// 128-bit key
const e128 = $crypto.encryptAES("data", "pass", { keySize: 128 });
// errors are returned as strings, not thrown — check the prefix
const out = $crypto.decryptAES("not-base64!!", "p");
if (out.startsWith("Error:")) { /* handle error */ }
$jwt — JSON Web Token
JWT creation, parsing and signature verification (HMAC family) for authentication flow scripts.
| Method | Returns |
|---|---|
$jwt.sign(payload, secret, algorithm?) | JWT string; default HS256. |
$jwt.decode(token) | {header, payload} object (signature is not verified); returns null on malformed tokens. |
$jwt.verify(token, secret, algorithm?) | boolean — signature verification result. |
algorithm: "HS256" | "HS384" | "HS512".
// sign
const payload = JSON.stringify({ sub: "user123", exp: Math.floor(Date.now() / 1000) + 3600 });
const token = $jwt.sign(payload, "my-secret", "HS256");
// decode — returns an object directly, no JSON.parse needed
const decoded = $jwt.decode(token);
decoded.header.alg; // "HS256"
decoded.payload.sub; // "user123"
// verify
$jwt.verify(token, "my-secret", "HS256"); // true
$jwt.verify(token, "wrong-secret", "HS256"); // false
// expiry check
const exp = $jwt.decode(token).payload.exp;
const expired = exp * 1000 < Date.now();
$format — Data Formatting
JSON/XML pretty-printing and minification for logging and debugging. JSON keys are sorted alphabetically in the output.
| Method | Returns |
|---|---|
$format.json(str, indent?) | Pretty-printed JSON; indent clamped to 1–8 (default 2); invalid input returns "Error: Invalid JSON". |
$format.minify(str) | Minified JSON (sorted keys, no whitespace); invalid input returns "Error: Invalid JSON". |
$format.xml(str) | Simple indented XML. |
$format.json('{"b":1,"a":2}');
// {
// "a": 2,
// "b": 1
// }
$format.json('{"a":2}', 4); // 4-space indent
$format.minify('{ "a": 2 }'); // '{"a":2}'
$format.xml('<root><child>t</child></root>');
// <root>
// <child>t</child>
// </root>
$fs — File System
Lightweight file read/write inside scripts (config, cache, export). Paths support ~ expansion; write creates missing parent directories automatically.
| Method | Returns |
|---|---|
$fs.read(path) | UTF-8 content; failure returns "Error: Cannot read file at <path>". |
$fs.readBinary(path) | Bytes as Base64; failure returns "". |
$fs.write(path, content) | boolean — success. |
$fs.exists(path) | boolean. |
$fs.delete(path) | boolean; missing files return false. |
$fs.list(path) | string[] of directory entries; failure returns []. |
$fs.info(path) | JSON string {size, modified, isDirectory, path}; missing paths return undefined. |
const file = $env.tmpDir() + "/hodor-test.txt";
$fs.write(file, "Hello from Hodor!");
$fs.read(file); // "Hello from Hodor!"
$fs.exists(file); // true
$fs.list($env.tmpDir()); // ["hodor-test.txt", ...]
const info = JSON.parse($fs.info(file));
info.size; // byte count
info.isDirectory; // false
$fs.delete(file); // true
Failure shapes differ per method: read returns an "Error: ..." string, readBinary returns "", list returns [] and info returns undefined — handle each accordingly.
$env — Environment Variables
Process environment variables and system paths. Note: $env is the process environment (shared across executions, lives with the process). To persist user-facing variables use sdk.setEnvVar or SharedPreferences instead.
| Method | Returns |
|---|---|
$env.get(key) | Value; missing keys return "". |
$env.set(key, value) | boolean — setenv result. |
$env.all() | JSON string of all environment variables. |
$env.cwd() | Current working directory. |
$env.tmpDir() | Temporary directory. |
$env.homeDir() | User home directory. |
$env.set("APP_VERSION", "1.0.0");
$env.get("APP_VERSION"); // "1.0.0"
const all = JSON.parse($env.all());
$uuid — UUID
Generate and validate UUIDs (request IDs, idempotency keys).
| Method | Returns |
|---|---|
$uuid.v4() | Standard v4 UUID (uppercase, hyphenated), e.g. "550e8400-E29B-41D4-A716-446655440000". |
$uuid.compact() | Hyphen-free, lowercase form. |
$uuid.validate(str) | Whether the string is a valid UUID. |
const id = $uuid.v4();
$uuid.compact(); // "550e8400e29b41d4a716446655440000"
$uuid.validate($uuid.v4()); // true
$uuid.validate("not-uuid"); // false
validate only accepts the hyphenated format — validate($uuid.compact()) returns false. Re-insert hyphens before validating a compact string.
$log — Structured Logging
Timestamped, leveled log lines routed through the engine's console system (collected into the script's log view).
| Method | Returns |
|---|---|
$log.log(level, message, tag?) | Formatted log line (also printed). |
$log.debug / info / warn / error(message) | Convenience level methods, output only. |
Log line format: [yyyy-MM-dd'T'HH:mm:ss.SSSZ] [LEVEL] [tag] message (the [tag] segment is omitted when no tag is given).
$log.info("Server started on port 8080");
// [2025-07-24T01:11:45.548+0800] [INFO] Server started on port 8080
$log.log("info", "Request processed", "API");
// [2025-07-24T01:11:45.548+0800] [INFO] [API] Request processed
Web APIs
fetch
Send real HTTP requests from scripts — the main channel for Postman-style workflows.
fetch(url: string, options?: object) → Promise<response>
options:
| Field | Type | Default | Description |
|---|---|---|---|
method | string | "GET" | Case-insensitive; unknown values fall back to GET. |
timeoutInterval | number | 30 | Seconds. |
headers | [[k, v]] or [{k: v}] | [] | Both shapes are accepted. |
textBody | string | — | Request body text. |
encoding | string | — | "hex" means textBody is a hex string. |
The resolved value is a plain object: statusCode (number), headers (array of single-key objects, duplicate keys preserved), and textBody() — a function that returns {text, encoding} with automatic character-set detection (undecodable bodies fall back to {text: <hex>, encoding: "hex"}). The rejected value is an error message string.
// GET
const rsp = await fetch("https://httpbin.org/get");
if (rsp.statusCode === 200) {
const body = rsp.textBody(); // {text: "...", encoding: "utf-8"}
const json = JSON.parse(body.text);
}
// POST JSON
const rsp2 = await fetch("https://httpbin.org/post", {
method: "POST",
headers: [["Content-Type", "application/json"]],
textBody: JSON.stringify({a: 1}),
timeoutInterval: 10,
});
HTTPClient
The HTTPClient class sends HTTP requests from any script (implemented on top of fetch). All methods are async and return a response object with statusCode, headers and textBody().
| Method | Description |
|---|---|
HTTPClient.get(url, params?) | Send a GET request. |
HTTPClient.post(url, params?) | Send a POST request. |
HTTPClient.put(url, params?) | Send a PUT request. |
HTTPClient.update(url, params?) | Send an UPDATE request. |
HTTPClient.delete(url, params?) | Send a DELETE request. |
HTTPClient.request(url, method, params?) | Send a request with a custom method. |
params is an optional object with:
headers: request headers object, e.g.{"key1": "value1"}body: request body — a string, JSON object (serialized automatically) or URLSearchParamstimeoutInterval: timeout in seconds (default 60)
try {
var params = {
"headers": {"key1": "value1"},
"body": {"a": "a1", "b": "b1"},
};
const response = await HTTPClient.post("https://dummyjson.com/products/1", params);
console.log(response.textBody().text);
} catch(error) {
console.log(error);
}
Timers
setTimeout / setInterval / clearTimeout / clearInterval work as in browsers. Callbacks are dispatched on a serial engine queue, so timers fire reliably regardless of the host thread.
| Function | Returns |
|---|---|
setTimeout(callback, ms) | Timer id (string), fires once. |
setInterval(callback, ms) | Timer id, fires repeatedly. |
clearTimeout(id) / clearInterval(id) | Cancel (idempotent, unknown ids are ignored). |
await new Promise(resolve => setTimeout(resolve, 500)); // wait 0.5s
const timer = setInterval(() => console.log("tick"), 1000);
setTimeout(() => clearInterval(timer), 3500);
Global Functions
| Name | Signature | Description |
|---|---|---|
btoa(str) | (string) → string | Base64 encode — UTF-8 symmetric semantics (non-Latin1 strings are allowed, unlike browsers). |
atob(str) | (string) → string | Base64 decode; invalid input returns "". |
performance.now() | () → number | Milliseconds since the epoch (not page load). |
sleep(ms) | (number) → Promise | Resolve after ms; use await sleep(1000). |
require(id) | (string) → any | Load a script library by CommonJS semantics (module.exports); the key is @libs/<key>, resolved as script-library identifier → script-library name (unique) → built-in libs; throws when not found. |
TextEncoder | new TextEncoder().encode(str) | UTF-8 encode into a Uint8Array (encoding is always "utf-8"). |
crypto.getRandomValues(n) | (Array) → number[] | Fill with n random UInt32 values. |
crypto.randomUUID() | () → string | Random UUID (uppercase, hyphenated). |
btoa("Hello"); // "SGVsbG8="
atob("SGVsbG8="); // "Hello"
btoa("中文"); // works — UTF-8 bytes are encoded
performance.now(); // 1760000000000.123
await sleep(100);
const { encode } = require("@libs/base64"); // built-in lib
const { sign } = require("@libs/sign-lib"); // your script library
new TextEncoder().encode("hi"); // Uint8Array [104, 105]
Libraries are managed in the script module manager. The require key is always @libs/<key>: your script libraries resolve by identifier first (stable across renames), then by name (unique match), then the built-in libs @libs/base64 / @libs/hex. Libraries export via module.exports, may require other libraries, and a missing key throws (pre-send failures abort the request, post-response failures keep the original response). See Quick Request scripting, Require Script Libraries for the full guide.
console
console.log / info / warn / error prints values to the script's log view. Multiple arguments are serialized, so you can log objects and arrays directly:
console.log("headers", {Accept: "application/json"}, [1, 2, 3]);
$log offers structured, leveled output (see $log) — both channels are visible in Hodor's script log view.