Skip to content

Creating Plugins

Choose a bundled plugin when extending code that already uses AuthrimPlugin. Choose a dynamic Worker when using the Runner’s supported hook contracts and approved host access. The two interfaces are not interchangeable.

Use the repository’s Node.js 22+ / pnpm 9 toolchain and the matching ar-lib-plugin version. AuthrimPlugin requires id, version, capabilities, configSchema, meta, and synchronous register(registry, config). Optional initialize runs before registration. Register handlers through the matching registry method, then include the plugin in the calling Worker’s loader.

The existing built-ins under packages/ar-lib-plugin/src/builtin/ provide typed examples. A package import or metadata entry alone does not wire an implementation into a protocol flow. Do not use the former example dependency on a hypothetical ^1.0.0 release.

This example only validates and acknowledges an account-created event. It performs no external action and is useful for checking the installation contract. Add actual behavior, input validation, and idempotency handling before using it for business operations.

Create plugins/example-account-hook/authrim.plugin-worker-capabilities.json:

{
"schemaVersion": 1,
"pluginId": "example-account-hook",
"backend": "dynamic_worker",
"workerBundle": { "path": "bundle.json" },
"resourceScope": "tenant",
"visibility": "tenant",
"bindings": [],
"resources": [],
"capabilities": [{
"name": "hook.account.lifecycle",
"execution": "async",
"failurePolicy": "retry_async",
"timeoutMs": 5000,
"mutationScopes": [],
"asyncOutbox": {
"enabled": true,
"succeededRetentionDays": 7,
"deadLetterRetentionDays": 90
}
}],
"credentials": [],
"egressAllowedHosts": []
}

The Worker module can start with:

export default {
async fetch(request) {
const url = new URL(request.url);
if (request.method !== 'POST' || url.pathname !== '/internal/plugin-hook') {
return new Response(null, { status: 404 });
}
const event = await request.json();
if (
request.headers.get('X-Authrim-Plugin-Contract') !== 'hook-reference-v1' ||
event.capability !== 'hook.account.lifecycle' ||
event.eventType !== 'account.created' ||
event.eventVersion !== 1
) {
return new Response(null, { status: 400 });
}
return new Response(null, { status: 204 });
}
};

Create bundle.json beside the manifest. It must be a JSON object with schemaVersion: 1, the same pluginId, a tested compatibilityDate, compatibilityFlags array, mainModule: "index.js", and modules: { "index.js": "<the complete JavaScript source as a JSON string>" }. Serialize with JSON.stringify; do not paste unescaped multiline JavaScript into JSON. This is a Worker bundle, not an npm package name or URL.

The current loader accepts at most 64 modules and a 2 MiB bundle. Bundle dependencies into supported module content; do not depend on a runtime package installation step.

Declare exact outbound hosts and credential slots when calling an external service. Declare approved host interfaces or tenant plugin resources when needed. Raw Core/PII bindings and Cloudflare management tokens are rejected by manifest validation.

For asynchronous hooks, retry_async and an enabled outbox must agree. Synchronous hooks disable the outbox and use their supported synchronous policy. A policy string is not permission to bypass the caller’s fail-closed behavior.

Validate with the parser in packages/setup/src/core/external-capabilities.ts, then test against the Runner contract. Include malformed input, non-success responses, timeouts, retries, duplicate delivery, and attempts to exceed declared host/resource access. See the Runner’s contract tests for the feature you implement.

Continue with Deployment and Capabilities.