feat(leveva): combination extbans $& (AND) / $| (OR) (P11 slice 307)
Port of charybdis extensions/extb_combi.c: two extban operator types whose
data is a comma-separated list of child extbans ([~]<type>[:<data>], no leading
$), optionally paren-wrapped, with paren-aware comma splitting and backslash
escaping. $& matches iff all children match; $| iff any does. Children may
nest (depth cap 5, <=10 children/node, data <= BANLEN 195).
A faithful byte-walk of eb_combi in extban/combi.rs dispatching to the existing
eval_type handlers — the C global recursion_depth is threaded as a depth param
(leveva runs many connections). Preserves the two charybdis subtleties: a child
type is validated for existence always (even when short-circuited, via the new
is_known_type), but child data only up to the short-circuit point, so combiban
validity is subject-dependent.
extban_chars() -> "&acjmorsxz|" so 005 advertises EXTBAN=$,&acjmorsxz|.
Divergence: leveva's $m is usermode (not charybdis's hostmask), so host
matching inside a combiban uses $x:<nick!user@host>#*.
Tests: 11 combi units (AND/OR/negated/nested/paren+escaped-comma + inverses for
every malformed shape, short-circuit type validation, the caps), extban_combi_
proptest (6 props: totality, AND==all/OR==any model vs an independent fold,
single-child==bare, De Morgan), golden_extban_combi (JOIN gates biting only
after OPER + the 005 token). isupport/elemental_extban/mod.rs asserts updated;
9 EXTBAN-token snapshots regenerated. EXTBANS.md documents the operators.
refactor: extract leveva-matching leaf crate (+ proptest fuzzing)
Continue the leaf-crate carve-out (after leveva-casemap / leveva-string /
leveva-message / leveva-patricia / leveva-cidr / leveva-cloak) by moving the IRC
glob matcher + nick!user@host hostmasks out of the monolith into its own
workspace crate:
- leveva-matching: the C match.c cluster (matches / Pattern / collapse / HostMask
/ host_component_matches / allow_host_matches, 498 LoC). The survey-flagged
"natural next pick": once leveva-cidr split, its only intra-crate edges were
casemap::to_lower and cidr::cidr_match — both already crates — plus nom. Deps =
leveva-casemap + leveva-cidr + nom.
Body moved verbatim via git mv; five repoints only: `use crate::casemap;` ->
`use leveva_casemap as casemap;`, `crate::cidr::cidr_match` ->
`leveva_cidr::cidr_match`, the module-header intra-doc link (crate::casemap) ->
(leveva_casemap), and two doctests `use leveva::matching::…` ->
`use leveva_matching::…`. leveva re-exports it as leveva::matching
(`pub use leveva_matching as matching;`), so the flat
`pub use matching::{collapse, matches, HostMask, Pattern};` and all ~13 consumers
(xline / kline / resv / webirc / metadata / registration +
command/{oper,map,mask,hunt,dline,tkline}) keep their crate::matching::* paths.
No dependency cycle — the code edges all run into matching.
Fuzzing: stable toolchain only (no nightly / no cargo-fuzz), so proptest is the
runnable fuzz tier, matching the other leaves. leveva's matcher deliberately
diverges from match.c (RFC 1459 case-fold, not ASCII tolowertab; #=any-digit), so
there is NO byte-faithful differential oracle — the 13 proptest invariants ARE the
gate. Fed arbitrary (incl. non-ASCII) strings + glob masks: totality/no-panic over
every entry point, `*` is universal, `?`^k iff exact byte-length, case-insensitivity
under leveva_casemap::fold of the name, compile == matches, collapse is
matching-preserving + idempotent + non-growing, HostMask re-parses its own Display,
matches_target == matches_parts, allow_host_matches decomposes into user-glob &&
host-component, and a glob host component falls back to matches.
Gate: cargo build --workspace (0 warnings) · cargo clippy --workspace --tests
(clean) · cargo test -p leveva-matching (28 unit/collapse/escaping + 13 fuzz, + 2
doctests) green · leveva matching-consumer tests (xline/kline/resv/webirc, 181) green.
What else can move out: uid (only crate::ident, blocked on extracting ident — ident
itself blocked by a single crate::command reference worth auditing, the next unlock);
msgid pulls cap/clock/ident; clock is a small candidate leaf. Recorded in the plan +
progress log.
feat(leveva): IRCv3 draft/read-marker + the MARKREAD command (P11 slice 300)
The read-state companion to draft/chathistory (slice 274). MARKREAD
<target> [timestamp=<iso8601>] records or queries how far a client has
read a target; a set stores max(existing, new) (monotonic — never
regresses), echoes the effective value, and on a genuine advance fans
the MARKREAD to the user's other capable connections (cross-device
read-state sync). A bare MARKREAD <target> queries the stored marker.
Markers are owned by the services account (synced across connections,
survives a reconnect) when logged in, else by the connection UID
(per-connection, dropped on Session::release). The cap is statically
advertised and valueless; the command is processed regardless of the
issuer's own cap — the cap gates only the cross-connection fan, per spec.
leveva-native: the C 2.11 oracle predates IRCv3, so the gate is unit +
golden + proptest (no differential).
- readmarker.rs: pure parse/format seam (parse_set_timestamp /
format_value) + the monotonic ReadMarkers store (account|UID owner key)
- command/markread.rs: the handler (NEED_MORE_PARAMS / query / set /
INVALID_PARAMS / advance + fan)
- cap.rs: DRAFT_READ_MARKER const + SUPPORTED entry + read_marker
ClientCaps bit; CAP LS snapshots refreshed (a long SASL mechlist now
tips the 302 LS into a spec-legal multiline split)
- server.rs: read_markers field; session.rs: forget_connection release hook
- help/MARKREAD.md + COMMANDS allowlist + index.md Queries line
- tests: readmarker/markread units (inverse invariants), golden_markread
(single-conn round-trip + cross-device push vs uncapped sibling),
readmarker_proptest (total parse + round-trip; advance keeps-max,
order-independent + idempotent)
feat(leveva): D-lines (DLINE/UNDLINE) — IP-level operator bans (P11 slice 284)
charybdis-style D-lines, the IP/CIDR sibling of the temporary K-line family
(slices 281–283): `DLINE <duration> <ip/cidr> [:reason]` bans a raw IP or CIDR
block, `UNDLINE <ip/cidr>` lifts it. Built as a near-clone of the K-line plane
keyed on a single IP/CIDR `mask` (matched with the CIDR-aware
`matching::host_component_matches`) instead of `user@host`.
- `dline.rs`: `DlineStore` mirrors `KlineStore` (Vec+Mutex, NOCASE dedup,
`find_active`, same `database {}` SQLite write-through + boot reload as slice
282); `Dline::covers` = `host_component_matches`. Reuses the kline duration
grammar/clock/clamp.
- `command/dline.rs`: `dline`/`undline` mirror `tkline`/`untkline` (461→481 gate,
empty/`*` mask → Incorrect format, no success reply); `reap_matching_ip` ejects
local matching clients by `orighost` (real connect IP), skipping remote +
`kline-exempt`. New `OperPrivilege::Dline` (bit 0x400000).
- `session.rs`: registration gate checked before the K-line gate, matching the
pre-cloak connect IP → 465+ERROR+REJ snomask, never counted/claimed.
- `s2s/dline.rs`+`forward.rs`+`burst.rs`: `ENCAP * DLINE`/`UNDLINE` propagation
(server-prefixed, remaining-seconds, slice-281 KLINE shape) + burst
re-assertion.
- `stats.rs`: `STATS d` → `250 RPL_STATSDLINE` via `dline_report`.
- `server.rs`: `ctx.dlines` opens against the same `database {}` file (persists
across restart); boot test + inverse extended.
- help `DLINE.md`/`UNDLINE.md` + COMMANDS allowlist.
Tests (TDD, inverse invariants, mandated fuzzing): unit+proptest per module;
`tests/golden_dline.rs` (real binary, `dline.kdl` fixture) and
`tests/dline_proptest.rs` (6 properties incl. model-lockstep registration gate
over `Session::feed`). leveva-native, no oracle differential.
`cargo test -p leveva` green (2530 lib); clippy clean; workspace 0 warnings.
feat(leveva): charybdis alias{} services command aliases (P11 slice 303)
Port charybdis's alias{} mechanism (modules/m_alias.c + newconf.c alias_entry):
an `alias "<name>" { target "<t>" }` block makes <name> a command that rewrites
`<name> <text...>` into `PRIVMSG <target> :<text...>`, delivered through the message
plane. A real command always takes precedence (the hook is the existing 421 fallback).
Found by comparing against a charybdis checkout: the 'configurable IDENTIFY nicks'
originally scoped does not exist in charybdis (the nicks are hardcoded #defines); the
alias{} block is the real, faithful, more general mechanism.
- alias.rs pure seam: parse_target (split on first @ -> nick vs user@server),
combined_args / reconstruct; fuzzed by tests/alias_proptest.rs.
- new top-level alias{} config block (model/parse/from_kdl), held live + REHASH-able
through ConfStore (live_aliases, the service-string chain); main.rs boot seed.
- command/alias.rs: deliver_to_service_nick shared core (target must be a present +S
service else 440 ERR_SERVICESDOWN; empty text -> 412; else synth PRIVMSG +
message::message, so a remote service is reached by UID routing). new numeric 440.
- folds in the slice-302 IDENTIFY faithfulness fix the comparison surfaced: empty args
-> 412 (was 461), absent/non-+S agent -> 440 (was 401), via the shared core.
- user@server target parsed/fuzzed but not yet routed (-> 440, documented follow-on).
Tests: alias seam + command + config-parse units; identify units updated; extended
rehash_conf_proptest alias reload; golden_alias (S2S +S burst: route + 440 + 421
inverses); golden_identify rewritten (S2S +S NickServ/ChanServ).
cargo test -p leveva green / clippy clean / build --workspace 0 warnings.
feat(leveva): +j join throttle channel mode — P11 slice 234
Charybdis-style `+j n:t` (MODE_JOINTHROTTLE): a channel admits at most n
joins per t-second window; the next join bounces 480 ERR_THROTTLE and is
not added. The first parameter-bearing flag mode since +l/+k and the first
JOIN-rate gate; continues the channel-mode parity track (220 +C, 232 +c,
233 +S). leveva-native — gate is unit + golden + proptest.
- Pure core join_throttle.rs: parse_param(n:t) + format_param + the charybdis
fixed-window decide() (both fuzz seams); JoinThrottle counter store (reads no
clock, self-draining) housed inside Channels (zero ServerContext-literal churn).
- ChanMode::JoinThrottle ('j', bit 0x8000000, Param kind) threaded through both
mode parsers + the three ModeChange matches + 324/CHANMODES-type-C/004 render.
- JOIN gate in command::join::join_one: non-members, local path only (remote
NJOIN gated at its home server); SAJOIN bypasses; refusal -> 480 ErrThrottle.
- Tests: 12 core units + 2 channel + 1 mode units; jointhrottle_proptest (5,
incl. no-window-over-admits) + golden_jointhrottle; 8 005/004 snapshots
regenerated (CHANMODES ...,k,lj,... / 004 ...IRklj, token-only).
refactor: extract leveva-matching leaf crate (+ proptest fuzzing)
Continue the leaf-crate carve-out (after leveva-casemap / leveva-string /
leveva-message / leveva-patricia / leveva-cidr / leveva-cloak) by moving the IRC
glob matcher + nick!user@host hostmasks out of the monolith into its own
workspace crate:
- leveva-matching: the C match.c cluster (matches / Pattern / collapse / HostMask
/ host_component_matches / allow_host_matches, 498 LoC). The survey-flagged
"natural next pick": once leveva-cidr split, its only intra-crate edges were
casemap::to_lower and cidr::cidr_match — both already crates — plus nom. Deps =
leveva-casemap + leveva-cidr + nom.
Body moved verbatim via git mv; five repoints only: `use crate::casemap;` ->
`use leveva_casemap as casemap;`, `crate::cidr::cidr_match` ->
`leveva_cidr::cidr_match`, the module-header intra-doc link (crate::casemap) ->
(leveva_casemap), and two doctests `use leveva::matching::…` ->
`use leveva_matching::…`. leveva re-exports it as leveva::matching
(`pub use leveva_matching as matching;`), so the flat
`pub use matching::{collapse, matches, HostMask, Pattern};` and all ~13 consumers
(xline / kline / resv / webirc / metadata / registration +
command/{oper,map,mask,hunt,dline,tkline}) keep their crate::matching::* paths.
No dependency cycle — the code edges all run into matching.
Fuzzing: stable toolchain only (no nightly / no cargo-fuzz), so proptest is the
runnable fuzz tier, matching the other leaves. leveva's matcher deliberately
diverges from match.c (RFC 1459 case-fold, not ASCII tolowertab; #=any-digit), so
there is NO byte-faithful differential oracle — the 13 proptest invariants ARE the
gate. Fed arbitrary (incl. non-ASCII) strings + glob masks: totality/no-panic over
every entry point, `*` is universal, `?`^k iff exact byte-length, case-insensitivity
under leveva_casemap::fold of the name, compile == matches, collapse is
matching-preserving + idempotent + non-growing, HostMask re-parses its own Display,
matches_target == matches_parts, allow_host_matches decomposes into user-glob &&
host-component, and a glob host component falls back to matches.
Gate: cargo build --workspace (0 warnings) · cargo clippy --workspace --tests
(clean) · cargo test -p leveva-matching (28 unit/collapse/escaping + 13 fuzz, + 2
doctests) green · leveva matching-consumer tests (xline/kline/resv/webirc, 181) green.
What else can move out: uid (only crate::ident, blocked on extracting ident — ident
itself blocked by a single crate::command reference worth auditing, the next unlock);
msgid pulls cap/clock/ident; clock is a small candidate leaf. Recorded in the plan +
progress log.
feat(leveva): IRCv3 draft/read-marker + the MARKREAD command (P11 slice 300)
The read-state companion to draft/chathistory (slice 274). MARKREAD
<target> [timestamp=<iso8601>] records or queries how far a client has
read a target; a set stores max(existing, new) (monotonic — never
regresses), echoes the effective value, and on a genuine advance fans
the MARKREAD to the user's other capable connections (cross-device
read-state sync). A bare MARKREAD <target> queries the stored marker.
Markers are owned by the services account (synced across connections,
survives a reconnect) when logged in, else by the connection UID
(per-connection, dropped on Session::release). The cap is statically
advertised and valueless; the command is processed regardless of the
issuer's own cap — the cap gates only the cross-connection fan, per spec.
leveva-native: the C 2.11 oracle predates IRCv3, so the gate is unit +
golden + proptest (no differential).
- readmarker.rs: pure parse/format seam (parse_set_timestamp /
format_value) + the monotonic ReadMarkers store (account|UID owner key)
- command/markread.rs: the handler (NEED_MORE_PARAMS / query / set /
INVALID_PARAMS / advance + fan)
- cap.rs: DRAFT_READ_MARKER const + SUPPORTED entry + read_marker
ClientCaps bit; CAP LS snapshots refreshed (a long SASL mechlist now
tips the 302 LS into a spec-legal multiline split)
- server.rs: read_markers field; session.rs: forget_connection release hook
- help/MARKREAD.md + COMMANDS allowlist + index.md Queries line
- tests: readmarker/markread units (inverse invariants), golden_markread
(single-conn round-trip + cross-device push vs uncapped sibling),
readmarker_proptest (total parse + round-trip; advance keeps-max,
order-independent + idempotent)
feat(leveva): learn the SASL mechanism list from services (ENCAP * MECHLIST) (P11 slice 299)
A linked SASL services agent announces its mechanisms via
`:<svcSID> ENCAP * MECHLIST :<comma-list>`; leveva learns the list, advertises
it (client-facing `sasl=` cap value + `908 RPL_SASLMECHS`) instead of the
hardcoded `PLAIN,EXTERNAL`, and relays whichever advertised mechanism a client
picks — faithful charybdis transparent relay (special-casing only EXTERNAL for
the CertFP; an unadvertised mechanism → 908+904, never relayed).
- sasl.rs: pure fuzzed `parse_mechlist`; `Mechanism` reworked
`{Plain,External,Other(Box<str>)}` with `from_advertised`/`name`/`is_external`.
- s2s/links.rs: PeerLinks learned-mechlist store, cleared when the last sasl
agent unlinks.
- s2s/mechlist.rs (new): `apply_encap_mechlist`, dispatched in forward.rs's
interpret-and-onward-relay block (broadcast, like SU).
- cap.rs: `apply`/`ls`/`req` `sasl_available: bool` → `sasl_mechs: Option<&str>`.
- session.rs: AUTHENTICATE gate uses `from_advertised`; 908 lists the advertised
set; relay_start takes &Mechanism.
leveva-native (the C 2.11 oracle predates SASL). Tests cover the happy path and
every inverse invariant (unadvertised rejected, all-junk keeps the prior list,
unlink clears, default fallback) + 2 proptests.
feat(leveva): WEBIRC webchat-gateway client IP/host spoofing (P11 slice 301)
Port charybdis extensions/m_webirc.c: a trusted webchat gateway sends a
pre-registration WEBIRC <password> <gateway> <hostname> <ip> so the proxied
connection is recorded under the real client's host/IP, not the gateway's.
Every later admission gate (D/K/X-line, config-ban, per-class conn caps, +x
cloak) and WHOIS key off the spoofed self.host.
- new pure fuzz seam leveva::webirc {parse_request, spoof_host, authorize}
- webirc {} config block (host mask + password) -> StatsConf (boot-read)
- Session::handle_webirc dispatched phase-independently (CAP/AUTHENTICATE seam),
one-spoof-per-connection, +s audit snotice; registered -> 462, bad-pw /
untrusted host / malformed -> NOTICE, no spoof
- HELP WEBIRC topic + commented example/dist config block
Divergences (leveva-native, no oracle): single-host/no-DNS collapse of
charybdis host/sockhost; trust via webirc {} not auth{}; boot-read.
Tests: webirc/config/session units (inverses), golden_webirc end-to-end,
webirc_proptest (parse total + round-trip, spoof_host range, authorize
contract).