Exprify's plugin system allows you to extend TrueHuman's detection capabilities by registering your own async or synchronous signal collectors. Custom plugins integrate seamlessly into the analyze() pipeline and contribute detection codes and structured values to the final result object alongside the built-in detectors.
Every plugin must conform to the Plugin interface, which requires a unique name and an fn function that performs the detection work. The interface is fully typed and ships with TrueHuman's type exports.
interface Plugin {
name: string
fn: PluginFn
}
type PluginFn = (context?: PluginContext) => PluginResult | Promise<PluginResult>
interface PluginContext {
integritychecks: (string | number)[]
errors: number[]
}
interface PluginResult {
codes?: (string | number)[]
value?: unknown
duration?: number
}name
A unique string identifier for the plugin. This value is used as the key under which the plugin's value output is stored in result.components. It should be lowercase, descriptive, and free of spaces - for example, "geolocation", "keyboard-timing", or "battery-status".
fn
The detection function. It receives a PluginContext object containing the integrity codes and error codes accumulated so far by the built-in detectors, which you can use to make conditional decisions in your plugin logic. It must return either a PluginResult object or a Promise that resolves to one.
PluginContext.integritychecks
An array of all detection codes emitted by the built-in feather detectors before your plugin ran. Inspect this if your plugin's behavior should depend on what the built-in pass already found.
PluginContext.errors
An array of error codes produced by the built-in pass. Useful for detecting degraded environments before executing expensive custom checks.
PluginResult.codes
An optional array of detection codes your plugin wants to contribute. These are appended to the session's integrity codes array and factored into the final score. Omit or return an empty array if your plugin gathered data but did not detect any suspicious signals.
PluginResult.value
Any structured data your plugin collected. This can be any serializable value - an object, array, boolean, or string. It is stored verbatim under result.components[plugin.name] and is accessible to consumers of the result.
PluginResult.duration
An optional number representing how long the plugin's detection logic took, in milliseconds. This field is informational and does not affect scoring.
The following plugin checks whether geolocation is available, requests the user's position, and emits a detection code if permission was denied or the API is unavailable.
import { analyze, type Plugin, type PluginContext } from "truehuman"
const geolocationPlugin: Plugin = {
name: "geolocation",
fn: async (context?: PluginContext) => {
if (!navigator.geolocation) {
return { value: { supported: false }, codes: [] }
}
const pos = await new Promise<GeolocationPosition>((resolve, reject) =>
navigator.geolocation.getCurrentPosition(resolve, reject, {
timeout: 3000,
})
).catch(() => null)
if (!pos) {
return { value: { supported: true, denied: true }, codes: [99.1] }
}
return { value: { lat: pos.coords.latitude, lng: pos.coords.longitude }, codes: [] }
},
}
const result = await analyze({ plugins: [geolocationPlugin] })
console.log(result.components.geolocation)
// { lat: 22.5726, lng: 88.3639 }When the plugin runs and permission is denied, code 99.1 is appended to the integrity codes array. When geolocation succeeds, the coordinates are stored under result.components.geolocation and no detection codes are emitted.
Understanding the order in which plugins execute helps you write plugins that interact correctly with built-in detection state.
analyze()is called and begins the built-in feathers pass, running all internal detectors in sequence.- Once the built-in pass completes, each registered plugin's
fnis invoked sequentially, in the order the plugins were passed toanalyze(). - Each plugin receives a
PluginContextsnapshot containing the integrity codes and errors accumulated by the built-in pass and all previously executed plugins. - Any
codesreturned by the plugin are appended to the session's integrity codes array and contribute to the final risk score. - Any
valuereturned by the plugin is stored underresult.components[plugin.name]in the result object. - If a plugin's
fnthrows an unhandled error at any point, TrueHuman automatically appends code90.2to the integrity codes array and continues execution. Your plugin will not crash the overallanalyze()call.
Custom plugins must follow TrueHuman's numeric code namespace convention to avoid collisions with built-in detector codes.
- Reserve codes in the
92.xthrough99.xrange for your custom plugins. Codes outside this range are reserved for internal use and should not be emitted by plugins. - Any new code your plugin emits must have a corresponding risk value registered in
lookup/codes.ts. Without a registered entry, the code will be appended to the integrity array but contribute a risk of0to the final score. - Infrastructure failures such as network errors, API timeouts, or browser API unavailability should use codes with a risk value of
0. These indicate an inconclusive environment rather than detected bot behavior. - Genuine detection signals - behaviors or conditions associated with non-human actors - should use codes with a risk value greater than
0, proportional to the confidence level of the signal.
Keeping risk values accurate and proportional ensures that the aggregate score produced by analyze() remains meaningful when custom plugins are mixed with built-in detectors.