# {{projectName}} / {{appName}}

Voltro API scaffold (template: **api-moderation**) — pre-commit content
moderation with `@voltro/plugin-moderation`.

## Boot

```bash
pnpm install
pnpm --filter @{{projectName}}/{{appName}} dev
# → http://localhost:4000  (store: memory — zero infra)
```

## What this shows

- **Two actions, two outcomes** — `moderationPlugin` checks named input fields
  on the rpc interceptors, BEFORE the handler:
  - **block** (`posts.create`, fields `title`/`body`): banned content fails
    typed `ContentRejected` and the write **never commits**.
  - **flag** (`comments.create`, field `body`): the comment IS written, but
    flagged for review (the dashboard's **Moderation** panel surfaces the queue).
- **Pluggable provider** — `keywordProvider([...])` is a zero-dep deny-list;
  `aiProvider()` is an optional LLM classifier (fails open). Swap one for the
  other in `app.config.ts` — the rules don't change.
- **In-handler redaction** — when you need to rewrite rather than reject, the
  `moderate(text)` helper returns a verdict you can act on inside a handler
  (the interceptor can't rewrite input).

## Try it

```bash
# banned word in a post → blocked, nothing written:
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
  -d '{"tag":"posts.create","input":{"title":"hi","body":"buy cheap spam now"}}'
# → { ok:false, error:{ _tag:"ContentRejected", reason:"matched denied term(s): spam" } }

# clean post → written:
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
  -d '{"tag":"posts.create","input":{"title":"hi","body":"a normal post"}}'
# → { ok:true, result:{ id, … } }

# banned word in a comment → WRITTEN but flagged (flag rule doesn't block):
curl -s localhost:4000/_voltro/inspect/invoke -H 'content-type: application/json' \
  -d '{"tag":"comments.create","input":{"body":"this is spam"}}'
# → { ok:true, result:{ id, … } }   (and queued for review)
```
