# Hooks and mixins

## Hook declaration

A pre-event subscription: the host calls `call("hook", {event, payload})` before the action and the addon may continue, cancel or modify it.

| Field | Type | Required | Description |
|---|---|---|---|
| `event` | `String` | yes |  |
| `priority` | `i32` | no | Lower runs first; ties ordered by addon id. Accepts an integer or a Bukkit-style name: `lowest` (0), `low` (50), `normal` (100), `high` (150), `highest` (200), `monitor` (1000, observe only). |
| `ignore_cancelled` | `bool` | no | Also receive the event when an earlier listener cancelled it. |
| `fail_open` | `bool` | no | On error/timeout: `true` lets the action continue, `false` blocks it. |
| `timeout_ms` | `u64` | no | Hook budget in milliseconds (capped by the sandbox timeout). |

Source: `crates/pelty-wasm/src/runtime.rs` (`Hook`).

## Priorities

| Name | Value |
|---|---|
| `lowest` | 0 |
| `low` | 50 |
| `normal` | 100 |
| `high` | 150 |
| `highest` | 200 |
| `monitor` | 1000 |

Integers from -1000 to 1000 are also accepted. Lower values run first, and ties are ordered by addon id.

## Event names a hook may use

- Pre-events: `login.before`, `client.before_register`, `order.before_place`, `invoice.before_create`.
- Request lifecycle: `<phase>:<METHOD> <route template>`, e.g. `before:PUT /api/v1/client/profile`; `before:*` matches every request.
- Domain events with a phase: `after:invoice.paid`, `finished:order.created`.
- Custom events: `before:<addon-id>:<event>`.

## Phases

Phases in order: `before`, `during`, `after`, `finished`. Source: `crates/pelty-api/src/addon_events.rs`.

- `before`: synchronous, in priority order; listeners may `modify` the payload or `cancel` it (`uncancel` too when `ignore_cancelled`). `monitor` listeners only observe. Money and secret rules apply.
- `during`: delivered while the action runs (concurrently, observe only).
- `after`: the action succeeded; the result is in the payload.
- `finished`: always last, with `status` = `ok` | `failed` | `cancelled`.

## Mixins

A mixin: wraps an approved host operation (an API route template such as `POST /api/v1/client/orders`). `at`: `before` (rewrite the input), `after` (rewrite the output within its schema), `around` (both, with shared state) or `return` (replace the output, same schema).

| Field | Type | Required | Description |
|---|---|---|---|
| `target` | `String` | yes |  |
| `at` | `String` | yes |  |
| `priority` | `i32` | no |  |
| `timeout_ms` | `u64` | no |  |

Source: `crates/pelty-wasm/src/runtime.rs` (`Mixin`).

`at` values: `before`, `after`, `around`, `return`. Mixins target `<METHOD> <route template>` of a core route. An `after` or `return` mixin may only change values of keys the response already has. Admins can turn an addon's mixins off.

## Example

_extensions/examples/addon-guide/src/lib.rs · snippet hooks_

```rust
/// Pre-events and lifecycle phases listed in `capabilities.hooks` call op
/// `hook`. Return `proceed`, `modify(payload)` or `cancel(reason)`.
fn hook(input: &Value) -> Value {
    let event = input["event"].as_str().unwrap_or("");
    if input["phase"].as_str().unwrap_or("before") != "before" {
        return reply::proceed(); // during/after/finished only observe
    }
    let mut p = input["payload"].clone();
    bump(&format!("hook:{event}"));
    match event {
        // Cancel: refuse sign-in for one domain.
        "login.before" if p["email"].as_str().is_some_and(|e| e.ends_with("@denied.test")) => reply::cancel("domain not allowed"),
        // Modify: tidy up the display name before the account is created.
        "client.before_register" => {
            p["display_name"] = json!(p["display_name"].as_str().unwrap_or("").trim().to_string());
            reply::modify(p)
        }
        // Own custom event (declared as `before:integration-guide:greeted`,
        // delivered with `event` = the bare name and `phase` = "before").
        "integration-guide:greeted" if p["name"] == "nobody" => reply::cancel("no greeting for nobody"),
        "order.before_place" => order_hook(input, p),
        _ => reply::proceed(),
    }
}
```

_extensions/examples/addon-guide/src/lib.rs · snippet mixin_

```rust
/// Mixins wrap a core route (`capabilities.mixins`). An `after` mixin may
/// change values of keys the response already has; new keys are dropped.
fn mixin(input: &Value) -> Value {
    let mut resp = input["response"].clone();
    // Opt-in per request so this example never changes other callers.
    let wanted = input["request"]["query"].as_str().unwrap_or("").contains("guide=1");
    if input["at"] == "after" && wanted {
        if let Some(c) = resp["city"].as_str() {
            resp["city"] = json!(c.to_uppercase());
        }
        return json!({ "response": resp });
    }
    json!({})
}
```

