Both ran scram verify_plain (~1s PBKDF2 at 1.2M iters) inline under the engine
lock, freezing the whole daemon per login. Add NetAction::DeferAuthenticate +
AuthThen continuation + ctx.defer_authenticate, mirroring DeferPassword: the
module/SASL path fetches the verifier cheaply and defers; the link layer runs
verify_plain on spawn_blocking, then Engine::complete_authenticate finishes the
login (IDENTIFY reuses the same ctx helpers, so its login/AJOIN/vhost/memo
side-effects are unchanged). Tests resolve the defer inline (cfg(test)). The
gRPC web-login path was already fixed; no login path stalls services now.
(GHOST/DROP/CERT/GROUP still verify inline but are rare account ops, not logins.)
Mode locks were char-only, so Anope FLOOD/JOINFLOOD locks were dropped on
migration. Carry a value per locked param-mode through the event log, MLOCK
parsing/display, drift enforcement, compaction, and the Anope importer, so a
migrated network keeps its flood protection verbatim.
REGISTER was rate-limited but RESEND and RESETPASS weren't — anyone could
repeatedly request confirmation/reset codes for any account with an address
on file, flooding the victim's inbox and burning the service's sender
reputation. A per-account 60s cooldown (code_issue_wait, mirroring the vhost
request throttle) now gates both paths; guessing was already infeasible
(2^40 code + 5-try limit), so this closes the abuse, not an auth hole.
SQUIT arrives once for a splitting server (by SID), but a hub takes its
whole subtree with it — users behind downstream servers were left as stale
state. The parser now distinguishes a sourced downstream SERVER (tracked
with its parent) from the unsourced uplink handshake, the engine keeps the
SID->parent tree, and a SQUIT forgets every user across the departed
server's subtree. Tree is rebuilt each link. Wire formats verified against
the InspIRCd 4 m_spanningtree source.
A netsplit is signalled once via SQUIT, not as a QUIT per user, so every
user behind the departed server lingered in services' state — stale
sessions, session-limit slots, and channel memberships. The parser now
surfaces ServerSplit and the engine forgets every uid carrying that
server's SID prefix, leaving other servers' users untouched.
KILL was never parsed, so a killed user lingered in services' state (stale
session, session-limit slot, channel membership) and a killed services bot
vanished for good. The parser now surfaces UserKilled; a killed real user is
forgotten like a QUIT (shared forget_user helper), while a killed bot is
dropped and reconciled so it is reintroduced and rejoins its channels.
The inbound parser ignored METADATA entirely, so on a services netburst
(after a restart/relink) the ircd's replayed 'accountname' metadata was
dropped — every logged-in user looked logged out to services until they
re-identified, losing ChanServ access. Parse METADATA <uid> accountname
into a new AccountLogin event that restores account_of (empty = logout);
our own echoes and other keys are filtered.
Founders can attach a contact email to a channel, shown in INFO and
cleared when set empty. Mirrors SET URL exactly; Local-scope channel
metadata. Completes the channel DESC/URL/EMAIL trio.
Founders can attach a homepage URL to a channel; it shows in INFO and
clears when set empty, mirroring SET DESC. Local-scope (channel metadata),
not gossiped, consistent with the entrymsg/description events.
Lets an account keep its last-seen/online line in INFO visible only to
itself and to opers. Default stays public (Anope's default). Accepts
STATUS (with USERMASK as an alias) per Anope's SET HIDE <field> grammar.
The account's last-active time was tracked but never surfaced. INFO now
reports it, reading 'now (online)' when the account has a live session.
Threads NetView into the INFO handler for the session lookup and carries
last_seen on AccountView.
Nick protection was unconditional. SET KILL OFF lets an account opt out —
an unidentified user keeping one of its nicks is no longer prompted or
renamed to a guest. Stored inverted (no_protect) so protection stays the
default. Accepts Anope's ON/QUICK/IMMED/OFF; grace is a fixed interval
here, so the finer variants simply enable protection like ON.
A user with SET AUTOOP OFF is never auto-opped on join, even where they
hold channel access — they op themselves via ChanServ UP. Stored inverted
(no_autoop) so the default stays auto-op. Join gates on both the channel
setting and the account preference; either can opt out.
SET AUTOOP {ON|OFF} controls whether access members are auto-opped/voiced
on join (on by default). Stored inverted as a settings flag so the
default stays on, and gated in the join handler.
SET NOTIFY {ON|OFF} controls whether you're told about new memos on login;
SET LIMIT <n>|NONE sets your mailbox cap (honoured on SEND, NONE = the
network default). New memo_notify/memo_limit account fields + one
MemoPrefsSet event.
IGNORE ADD/DEL/LIST manages a per-account memo-ignore list; a memo from
an ignored account is silently dropped, and the sender is still told it
was sent so the ignore isn't revealed. New memo_ignore account field +
events, and a check in SEND.
SVSPART <nick> <#channel> [reason] forces a user out of a channel
(admin-only), completing the SVS command family alongside SVSNICK and
SVSJOIN. New ForcePart action rendered as the ircd SVSPART command.
GETEMAIL (auspex) lists accounts whose email matches a glob. SET
RESTRICTED makes a channel kick anyone without access (or oper) as they
join, mirroring the SECUREOPS on-join enforcement.
FORBID ADD/DEL/LIST bans a NICK, CHAN, or EMAIL glob from being
registered — a network-wide policy that gossips like an AKILL and every
node enforces. Nick registration is blocked in pre_register_check (both
NickServ REGISTER and the account-registration relay), and channel
registration in ChanServ REGISTER. New Forbid store + events, mirroring
the akill subsystem.
SET <#channel> SUCCESSOR <account>|OFF names an account that inherits the
channel if the founder's account is dropped or expires, instead of the
channel being released. Adds a ChannelInfo.successor field + event, and
centralises the founder-gone channel release into a single
release_founded_channels store method used by DROP, expiry, and the
gossip account-gone path so all three transfer consistently.
UPDATE re-applies your auto-joins and vhost and re-checks for waiting
memos without re-identifying. LIST (auspex-gated) shows registered
accounts matching a glob, capped at 100. Adds an accounts_matching store
method.
CANCEL recalls the sender's most recent unread memo to a target (one the
recipient already read can't be recalled); CHECK reports whether the last
memo you sent someone has been read; INFO summarises your mailbox. Adds
memo_cancel/memo_check store methods and shares MAX_MEMOS from the module
root instead of duplicating it in send.rs.
Echo is the authority the ircd account module forwards to: parse
ENCAP <us> SWACCTREG <reqid> <origin> <kind> <account> <p2> :<p3> into an
account request, and answer the origin server with
ENCAP <origin> SWACCTRES <reqid> <kind> <account> <status> <code> :<message>.
Thread the origin through the reply so responses route back to the
requesting server. Replaces the placeholder ACCTREGISTER/ACCTREGRESULT
names that nothing on the wire actually spoke.
Config::load now checks the SID is 3 alphanumeric chars and that
server.name and uplink.host are set, failing with a clear message
instead of a cryptic link-time error. Give the Service and Protocol
traits and ServiceCtx proper rustdoc so cargo doc is useful to module
authors.
The store's map_err(|_| Internal) discarded the real IO error, so a
disk-full or permissions failure was invisible; log it once in the log
append path where it happens. Add echo_api::require_oper (parameterised
by the required privilege) and route the three copied per-service guards
through it, keeping each service's own wording and policy.
Each service now exposes its help catalog via Service::help_topics; the
engine collects them into a network-wide index on Network, reachable
through two new NetView methods. HelpServ uses it: HELP <service>
[command] (or a bare service name) serves any service's per-command
help, bare HELP lists the services, and its own ticket commands still
work. No cross-crate dependency: the catalog flows through the SDK's
NetView, keeping every module dependent only on echo-api.
ChanServ, BotServ, HostServ, MemoServ, OperServ, and StatServ now route
HELP through the shared renderer with per-command topic tables, so every
service supports HELP <command>. The renderer matches slash-grouped
names (OP/DEOP/VOICE/DEVOICE) so a paired command resolves as one topic.
Adds unit tests for the renderer.
New echo-api primitive: a HelpEntry table plus a shared help() renderer.
HELP lists a service's commands; HELP <command> prints that command's
detail. Every service routes its HELP arm through the one renderer, so
the shape is identical across the network. Wired into NickServ and the
six newer services so far.
Users open tickets with REQUEST (or HELPME); the queue is event-sourced
and, because a request is an event, the audit feed announces it to staff
and records it in the searchable log — so OperServ LOGSEARCH finds a ticket
by any word, the same trail as reports. A user can CANCEL their own open
ticket. Operators work the queue: LIST [ALL], VIEW, TAKE (claim), NEXT
(claim the oldest unassigned), CLOSE. Filing is rate-limited. Opt-in.
That completes the six third-party modules — all interconnected: they
share the account/channel/oper identity, the event-sourced store, and the
one audit/incident trail.
The engine samples live op state on a short cadence, scoring op-time per
identity (account, else host) on the network view — a net +1 per pass
while opped, -1 while idle, so trusted regulars rise and stale ops fade.
ChanFix (operator-only) reads those scores: SCORES <#chan> shows the
standings, CHANFIX <#chan> reops the present regulars scoring above a
share of the top score, once the channel is opless enough and has real
history. It defers to ChanServ — a registered channel is refused. Opt-in.
C++ note: the original persists scores to the services DB and runs its own
timers; here scoring rides the existing periodic loop and lives on the
ephemeral network view, so it's node-local and rebuilds itself.
User groups (!name) — a founder plus member accounts, each with group-
access flags (the shared flag primitive: F founder, f manage, i invite,
c chan-access, s set, m memo). REGISTER/DROP/INFO/LIST/ADD/DEL/FLAGS;
event-sourced and federated like accounts.
The flagship interconnection: a channel access entry may name a !group, so
every member inherits that channel access. Resolution is group-aware — a
new db.channel_caps() unions a user's direct access with any group they
belong to, and the engine's join handler auto-ops/voices through it. So
"ChanServ FLAGS #chan !team +o" auto-ops every member of !team, and
removing someone from the group revokes it. Opt-in.
A granular access system, replacing the op/voice-only levels with per-user
flag letters (f full, o auto-op, O op, h auto-halfop, v auto-voice, t
topic, i invite, a modify-access, s settings, g greet). One shared
primitive in the api — level_caps() resolves a stored level to capabilities
(recognising the legacy op/voice/founder presets so nothing breaks) and
apply_flags() applies +/- deltas — so join_mode/is_op/access_rank all
route through it and a flag-granted +o auto-ops exactly like a legacy op
entry.
ChanServ FLAGS <#chan> [account [+/-flags]] lists or edits access;
changing needs the founder or the 'a' flag. This is the foundation
GroupServ will reuse for group membership flags.
Any user can REPORT <nick|#channel> <reason>; filing is rate-limited per
reporter. Reports are an event-sourced, federated queue operators review
with LIST / VIEW / CLOSE / DEL. The interconnection: because a filed report
is an event, the engine's audit feed already announces it to the staff
channel and records it in the searchable action log — so OperServ
LOGSEARCH finds a report by any word in it, tying reports into the same
trail as bans, kicks, and everything else. Opt-in (add "reportserv").
By default fedserv owns accounts itself — nothing changes, no external
service required. Set [auth] external = true and an outside authority (the
website) owns identity instead: NickServ REGISTER / DROP / SET
PASSWORD|EMAIL / RESETPASS / CONFIRM / CERT / GROUP and the IRCv3
registration relay are all refused ("managed on the website"), so IRC
can't mint or change a second identity. Login is unchanged — fedserv still
authenticates locally against the accounts the authority pushes in via the
existing gRPC Accounts API, so lookups stay in-memory fast.
fedserv keeps owning all IRC-domain data (channels, access, vhosts, bans,
memos) keyed by the account name in both modes. One config bool, off by
default, so standalone deployments are unaffected.