a plugin system for booger
i'm changing the code a lot in a backwards incompatible way
booger's core attempts to provide things that all relays want. Booger plugs are a way to define custom behavior like rate limits and special validation rules.
There are a handful of things a booger operator might want to plugin to:
- connections/disconnections, eg
- preventing too many connections from a single IP address
- only allowing whitelisted ips
- subscription opens/closes, eg
- preventing too many subscriptions from a single IP address
- validating subscriptions with special rules
- subscription eose, eg
- collecting stats on event count and time to eose
- event acceptance, eg
- preventing duplicate messages within a certain time frame
- preventing certain types of content
- preventing blacklisted pubkeys
- payments
- validating events with special rules
- adding support for NIPs that booger doesn't support (requires tbd enhancements)
- notice and error messages
You can plugin to these actions by adding one or more
Web Workers
to the ./plugs
directory
(this directory is configurable).
On startup, booger will recursively walk ./plugs
looking for .js
and .ts
files. It will load and then send workers a 'getactions'
string message.
Workers must respond with an array containing one or more of the following
action strings:
'connect'
'disconnect'
'sub'
'unsub'
'eose'
'event'
'notice'
'error'
When an action occurs, workers who have registered for that action will get a message from booger in the form:
{
msgId: Number, // msgId to include in response if responding
action: String, // e.g. 'connect'
conn: {
id: String // unique id to this connection
headers: Object // http headers as a json object
},
data: Object // depends on the action and are documented further down
}
For the following action messages, booger plugs must respond indicating whether or not booger should reject the action:
'connect'
'sub'
'event'
Responses from these actions must take the form:
{
msgId: Number, // the msgId of the action message we're responding to
accept: Boolean, // true to accept, false if booger should prevent
reason: String // reason for rejection if accept is false, undefined otherwise
// TODO: we'll probably add a replyRaw to send replies directly to clients
}
The following actions cannot be rejected, so booger plugs should not respond to them:
'disconnect'
'unsub'
'eose'
'notice'
'error'
Booger plugs will receive relevant action data in the data
field of action
messages. This data varies depending on the action.
'connect'
data
isundefined
'disconnect'
data
isundefined
'sub'
-
data: { subId: String, // sub id as received from the client filters: [Filter] // array of filters as received from the client }
-
'unsub'
data
isundefined
'eose'
-
data: { subId: String, // sub id as received from the client count: Integer // the number of events sent to the client before eose }
-
'event'
-
data: { event: Event, // event as received from the client }
-
'notice'
-
data: { notice: String, // the notice message sent to the client }
-
'error'
-
data: { error: Object, // the Error object as a json object }
-
self.onmessage = ({ data }) => {
if (data === 'getactions') {
self.postMessage(['event'])
return
}
const { msgId } = data
if (data.action === 'event' && data.data.event.kind === 6) {
self.postMessage({
msgId,
accept: false,
reason: 'blocked: kind 6 not allowed',
})
return
}
self.postMessage({ msgId, accept: true })
}
self.onmessage = ({ data }) => {
if (data === 'getactions') {
self.postMessage(['event', 'sub'])
return
}
const { msgId } = data
if (data.action === 'event' && data.data.event.kind === 6) {
self.postMessage({
msgId,
accept: false,
reason: 'blocked: kind 6 not allowed',
})
return
}
if (data.action === 'sub' && data.data.filters.length > 100) {
self.postMessage({
msgId,
accept: false,
reason: 'blocked: >100 filters not allowed',
})
return
}
self.postMessage({ msgId, accept: true })
}
const timers = new Map()
self.onmessage = ({ data }) => {
if (data === 'getactions') {
self.postMessage(['sub', 'unsub'])
return
}
if (data.action === 'sub') {
const { msgId } = data
timers.set(data.data.subId, Date.now())
self.postMessage({ msgId, accept: true })
return
}
if (data.action === 'unsub') {
console.info(
`sub ${data.data.subId} took: ${
Date.now() - timers.get(data.data.subId)
}ms`,
)
timers.delete(data.data.subId)
return
}
}
The following plugs are bundled into booger implicitly.
- validation
- stats stores connection and subscription statistics
- limits provides some basic rate limiting
When running booger you can prevent these plugs from being used by removing them
from ./booger.jsonc
or with the --plugs-builtin-use
flag.
You can configure the behavior of the builtin plugs you use in ./booger.jsonc
.
If you want to put .js
or .ts
files in /plugs
that aren't workers or that
you want ignored, you can specify them in a .plugsignore
file. The intent is
to support a .gitignore
like syntax.
- hoytech's strfry - heavily inspired booger plugs with their write policy
- alex gleason's strfry write policies - awesome set of strfry policy examples