Moderation
Moderate user content before it commits — keyword denylist or AI provider, block or flag via rpc interceptor, plus an in-handler redact helper.
@voltro/plugin-moderation screens user-submitted content. A rule matches an rpc tag + fields; the configured provider classifies the text; the interceptor blocks (typed ContentRejected, the write never commits) or flags (annotates, lets it through). Because the interceptor can't rewrite input, redact is an in-handler helper.
Wiring
// app.config.ts
import { moderationPlugin, keywordProvider, aiProvider } from '@voltro/plugin-moderation'
export default {
type: 'api' as const,
name: 'api',
plugins: [
moderationPlugin({
provider: keywordProvider(['spam', 'scam', 'http://badsite']), // or aiProvider() — uses @voltro/ai
rules: [
{ match: 'posts.create', fields: ['title', 'body'], action: 'block' },
{ match: /^comments\./, fields: ['text'], action: 'flag' },
],
}),
],
}Providers
keywordProvider(terms)— pure denylist; zero deps, deterministic, instant. Best for a known set of banned terms / URLs.aiProvider(opts?)— lazy@voltro/aiclassification (@voltro/aiis an optional dep). Fails open: a provider error resolves toCLEANso an AI outage can't block all writes. Use for nuanced toxicity/abuse detection.- Custom — any function with the provider signature
(text) => Effect.Effect<ModerationVerdict>plugs into the sameproviderslot. The verdict is{ flagged, categories?, score?, reason? }.
Block vs flag vs redact
block— interceptor fails the call with typedContentRejected({ tag, categories, reason }, merged into the wire-error union). The write never commits.flag— lets the write through; surfaces the classification for downstream review (e.g. a moderation queue).- Redact — the interceptor can't mutate input, so there is no
redactaction. Instead, call the in-handlermoderate(text)helper: it classifies the text against the configured provider (returning aModerationVerdict— it does NOT return redacted text) and the handler decides what to store:
import { moderate } from '@voltro/plugin-moderation'
export default (input: { body: string }, ctx) => Effect.gen(function* () {
const verdict = yield* moderate(input.body) // classifies via the configured provider
const body = verdict.flagged ? '[redacted]' : input.body
return yield* Effect.tryPromise(() => ctx.store.insert('posts', { body }))
})Dashboard panel
Both dashboards ship a Moderation panel (api apps): block/flag counts + the review queue of flagged content (tag, categories, reason, subject), each pending item with Confirm (real violation) / Dismiss (false positive) actions. Resolving gates on the canModerateContent capability. Backed by /_voltro/inspect/plugins/moderation/{flagged,resolve}.
Permissions
rpc:intercept:mutation + rpc:intercept:action (moderation gates both writes and external-I/O actions) + inspect:read (dashboard panel).