Skip to content

Modern API 1.0: full IMAP4rev1/rev2 client with per-requirement compliance measurement (M0–M6) - #18

Open
LoveAndCoding wants to merge 138 commits into
modern-apifrom
claude/modern-api-implementation-x3r57j
Open

Modern API 1.0: full IMAP4rev1/rev2 client with per-requirement compliance measurement (M0–M6)#18
LoveAndCoding wants to merge 138 commits into
modern-apifrom
claude/modern-api-implementation-x3r57j

Conversation

@LoveAndCoding

Copy link
Copy Markdown
Owner

What this is

The complete modern-API implementation, M0 through M6, ending at version 1.0.0: a spec-compliant, fully typed IMAP client with a promise/async-iterable API, measured throughout by the repo's per-RFC-requirement compliance suite. Every milestone ratcheted the same matrix per-row (status + failure kind), with regressions blocked at every merge; every milestone's snapshot and close notes live in docs/compliance-history/.

The M0–M6 arc

  • M0 — violations burn-down (notes): the pre-existing suite measured 118 violation row-profiles against the old code. M0 fixed the connection/security families (TLS identity per RFC 9525, STARTTLS sequencing, greeting policy, parser case-insensitivity and tolerance, bigint number64) with no new public surface: 118 → 3 violations.
  • M1 — client shell and auth (notes): ImapClient, typed config, the SASL framework (PLAIN/OAUTHBEARER/XOAUTH2/LOGIN-fallback), ENABLE, capability epochs, the typed error hierarchy, and removal of the legacy Session class. 486 pass.
  • M2 — mailbox management (notes): list/lsub (LIST-EXTENDED, LIST-STATUS, SPECIAL-USE), select/examine → MailboxSession, create/rename/delete/subscribe, STATUS, NAMESPACE, APPEND, the mUTF-7 codec. 602 pass.
  • M3 — message operations (notes): literal streaming, the FETCH engine (async-iterable, part streaming), the typed SEARCH criteria compiler + ESEARCH, STORE verbs, COPY/MOVE + UIDPLUS, EXPUNGE, MULTIAPPEND/CATENATE, SequenceSet, and the seq facet mirroring every UID-grain verb. 780 pass.
  • M4 — live mail and synchronization (notes): IDLE + the updates() iterator, CONDSTORE/QRESYNC, NOTIFY, SORT/THREAD (+ SORT=DISPLAY, ESORT), SEARCHRES, WITHIN, FUZZY. 882 pass.
  • M5 — extension families (notes): SCRAM-SHA-1/256 (with a terminal-failure MITM protection) + ANONYMOUS, the QUOTA/ACL/METADATA/URLAUTH facets, COMPRESS=DEFLATE, UNAUTHENTICATE, UIDONLY, CONVERT, REPLACE, LANGUAGE/COMPARATOR, referrals, UTF8=ACCEPT/ONLY, X-GM-EXT-1, the CONTEXT updating machinery, and RFC 9394 PARTIAL. Its phase review caught and fixed a critical queue deadlock (compress()/unauthenticate() racing an in-flight command). 1138 pass.
  • M6 — close-out and 1.0 (notes): SHOULD/MAY sweep (zero unexplained rows; 11 satisfied-by-mechanism ledger entries), all five tracked follow-ups resolved with zero deferrals (including closing the pre-TLS/pre-codec complete-line injection window at all three topology switch points), typedoc (1029 warnings → 0), docs/MIGRATION.md, README rewrite, CHANGELOG, version 1.0.0, and a three-lens final review (zero criticals).

Compliance: before → after

Measure Baseline (pre-M0) Final (1.0.0, M6 snapshot)
Violations 118 row-profiles (73 requirements) 6 — every one carries a written adjudication
Passing requirement-profiles — (332 at M0 close) 1138
Unimplemented 819 (at M0 close) 2 — both adjudicated (deliberate scope boundary)
Untestable (cataloged rationale each) 427 451
MUST/MUST NOT (testable, both profiles) 927/929 raw (99.8%); 100% excluding the 2 adjudicated rows
SHOULD 78/84 raw (92.9%); 100% excluding the 6 adjudicated rows
MAY 133/133 (100%)
Suite problems [], nine consecutive byte-identical full runs
Unit tests 232 (1 failing) 1936 (all passing, 133 files)

Adjudications (read this before reading "violations: 6" as defects)

All non-passing rows — the 6 violations and 2 unimplemented — are documented, deliberate decisions in docs/compliance-adjudications.md (~25 entries):

  • RFC9051-7.1-1 (SHOULD, rev2): pre-confidentiality ALERT content is surfaced with an untrusted marking rather than suppressed — trading one SHOULD for two MUSTs.
  • RFC9051-A-1 (MUST, rev2): the client does not auto-ENABLE IMAP4rev2 — a deliberate profile-stability decision (rev2 behavior differences are opt-in).
  • RFC7162-3.1.3-5/-6 (SHOULD, both profiles): no automatic CONDSTORE re-probe/retry — callers own retry policy.
  • RFC9051-2.3.2-1/-2 (rev2): no automatic $Junk/$NotJunk conflict STOREs and no refusal of $Forwarded removal — keyword policy belongs to callers; the library exposes the raw signal.
  • RFC 4422 security-layer block + SCRAM channel-binding block (22 row-profiles, untestable): vacuous-by-design — no shipped mechanism negotiates a SASL security layer (TLS is the security layer), and SCRAM-*-PLUS is a documented non-goal with reactivation conditions written into the catalog.

Breaking changes vs 0.9

The public API is new — 0.9 was a TypeScript port of node-imap. Session is gone, callbacks are gone, event-DSL fetch/search are gone, boolean tls config is gone. docs/MIGRATION.md maps every node-imap idiom to its 1.0 equivalent (all destination samples are compile-checked in CI via npm run typecheck), including the one data-model change (getBoxes() tree → flat list()).

Deferred, with rationale

  • Auto-reconnect, TRYCREATE auto-create, MIME body decoding, connection pooling, client-side seq↔uid mapping: design non-goals (spec §13) — the primitives to build them are exposed.
  • SCRAM-*-PLUS channel binding (tls-exporter per RFC 9266): legitimate post-1.0 feature; catalog rows carry reactivation conditions.
  • Post-1.0 hardening notes (M6.8 review): a second-layer discard for the compressed-UNAUTHENTICATE boundary guard; root-exporting four updates()-signature types.
  • npm run test:e2e (real-server suite) was not run in this environment (no credentials); all other gates ran on every merge.

Verification

npm test (1936), npm run typecheck (incl. compiled doc samples), npm run typecheck:compliance, npx eslint . (0 errors), npm run docs (0 warnings), npm run build + npm pack --dry-run (tarball: 344 files / 2.2 MB), and npm run test:compliance — nine consecutive byte-identical runs across all 1597 requirement-profile rows, problems: [].


Per the session's standing decision this PR is opened for human review and is not merged by the automation.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW


Generated by Claude Code

claude added 30 commits July 12, 2026 07:30
- Fix newline.transform test asserting undefined where modern Node's
  stream end() callback delivers null
- Remove useless regex/string escapes (verified char-for-char regex
  equivalence by codepoint sweep for every touched character class)
- Targeted eslint-disable comments for intentional control-char regexes
  and the typed-emitter declaration-merging pattern
- Scope case-block declarations, drop dead assignments, drop unused
  eslint-disable directives

No behavior changes. npm test 232/232, typecheck clean, lint 0 errors,
compliance suite pass/fail totals byte-identical to pre-change baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Add src/connection/tls.ts as the single TLS policy module: every TLS
socket (implicit connect and STARTTLS upgrade) is created via openTls(),
which always sets servername from the configured host, keeps Node's
default checkServerIdentity and rejectUnauthorized:true, and rejects
caller tlsOptions that try to override identity-critical keys
(RangeError). Handshake/identity failures now destroy the socket and
reject with a typed TLSSocketError (reason: identity-mismatch/handshake/
policy) instead of resolving false or hanging.

Also fixes the starttls() entry guard that checked this.connected before
connect() ever sets it, which made the upgrade path unreachable.

Compliance: 20 requirement-rows flip violation→pass (RFC9525-6.6-1,
RFC7817-3-1/-3-7/A-1, RFC2595-2.4-1/-2.4-8, RFC8314-3.2-1/-5.3-1,
RFC3501-11.1-3/-4/-7, RFC9051-11.1-6); violations 118→98, zero
regressions. RFC3501-11.1-8 stays annotated: it additionally requires
the M0.2 STARTTLS choreography.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…4, I-1, I-2)

- CommandQueue.hold()/release(): while held no command anywhere in the
  queue may write bytes; contexts consult the live held flag at dispatch
  time so a context promoted to active mid-hold still withholds writes.
  Used to guarantee nothing is written between the STARTTLS tagged OK
  and TLS handshake completion.
- CapabilityRegistry precursor on Connection: invalidated on socket
  close and on STARTTLS handshake success; repopulated by a post-TLS
  CAPABILITY round trip before connect() resolves. Pre-TLS capability
  data is only read locally to decide whether to attempt STARTTLS and
  never exposed as current.
- Session.start() reuses the registry when valid instead of issuing a
  redundant second CAPABILITY round trip.
- Fix strict-STARTTLS connect() always throwing: the post-upgrade check
  read this.isSecure, which is gated on this.connected and is never set
  at that point; check the local upgrade outcome instead.
- RFC3501-11.1-8 script: a compliant client aborts the handshake on a
  wrong-host cert, so the server-side TLS wrap can never complete;
  ended the script at the STARTTLS reply + destroy (documented in-test).

Compliance: 24 rows flip violation→pass (RFC3501-6.2.1-1/-2/-3,
RFC9051-6.2.1-1/-2/-3, RFC9051-6.1.1-1, RFC2595-2.5-1/-3.1-1/-2/-3/-9-3,
RFC9051-11.1-7/-11.2-1, RFC8314-5.1-7/-5.2-4, RFC3501-11.1-8);
violations 98→74, zero regressions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…arisons (spec §11.1, I-5)

- src/lexer/case-insensitive.ts: ciEquals/ciIncludes/ciCanonicalize/
  ciCanonicalFrom; placed in the lexer layer because lexer rules need it
  too and parser already depends on lexer
- matchesFormat atom comparison now ci (covers ~20 dispatch sites);
  status dispatch (OK/NO/BAD/PREAUTH/BYE) stores canonical uppercase;
  capability map keys canonicalized; FlagList stores/looks up canonical
  keys while Flag.name keeps original casing for display; LIST/LSUB,
  SEARCH/ESEARCH+MODSEQ, resp-code kinds, untagged response type all
  canonicalized; NIL matching in the lexer now ci
- Fixes a real X-GM-THRID/X-GM-MSGID misclassification for lowercase
  input and an exact-case QUOTA dispatch failure

Compliance: RFC3501-9-2, RFC9051-9-2, RFC9208-7-1 flip violation→pass,
plus RFC9585-5-1 (lowercase inprogress resp-code) as a side effect of
resp-code kind canonicalization. Violations -6, zero regressions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…pec §10.5, §10.6, I-7, I-8)

- awaitGreeting() extracted and used on every connect path: OK proceeds,
  BYE rejects with the server text, timeout rejects; PREAUTH marks the
  connection authenticated (exposed to Session) and, when the config
  demands STARTTLS on a cleartext socket, closes immediately and rejects
  with a policy error (§10.5 — PREAUTH forecloses STARTTLS-before-auth)
- ALERT (I-7): always logged at warn with the alert text; on a
  non-confidential transport the log detail carries
  { code: "ALERT", trusted: false } and the response is not emitted as
  a serverStatus event; on a confidential transport it is logged and
  emitted normally
- Connection config gains an optional logger (driver.connectLow paths
  now produce observable logs)

Adjudications (docs/compliance-adjudications.md, new):
- RFC9051-7.1-1 (SHOULD-ignore unprotected ALERT): deliberate deviation
  mandated by spec I-7 — display-with-untrusted-marking; incompatible
  with RFC3501-7.1-1 (rev1 MUST-present) and RFC9051-7.1-2 (MUST-mark).
  Annotated as a permanent expected violation.
- RFC9051-7.1.4-2 test was mis-scripted (security:"none" cannot satisfy
  the catalog's 'clients configured to require mandatory TLS' condition);
  corrected to security:"starttls", now passes against the §10.5 policy.
- RFC9051-11.3-1/-3 (pre-auth LIST / non-selected EXISTS suppression):
  blocked-on-surface until M1's router splits Layer-1 events from
  client-level state effects — 10+ currently-passing acceptance rows
  assert the same events ARE surfaced on the same observation channel.
  Deferred to M1, annotations left in place.

Compliance: 7 rows flip violation→pass (RFC3501-7.1-1, RFC3501-7.1.4-1,
RFC9051-7.1-2/-3, RFC9051-7.1.4-1/-2, RFC9051-11.3-2); RFC9051-7.1-1
becomes a deliberate violation; net 273 pass / 62 violation, problems 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…pec §11.2, I-6)

- UntaggedResponse: per-matcher try/catch + UnknownContent fallback —
  unknown or malformed-but-framed untagged responses become tolerated
  data; the Transform stream never dies on valid IMAP framing
- VanishedResponse: real parse for VANISHED (EARLIER) and bare VANISHED
- AtomTextCode: bare (unparenthesized) resp-code arguments preserved in
  wire order (REFERRAL, UNDEFINED-FILTER, NOUPDATE, MAXCONVERT*,
  BADCOMPARATOR)
- ESearchReturnData: ordered multi-entry map — repeated ESEARCH return
  items (ADDTO ADDTO) no longer clobber
- MailboxListing: RFC 5258 extended data (OLDNAME) captured raw instead
  of throwing past the mailbox name
- ThreadMessage.children: fix self-recursive getter (RangeError)
- BODYSTRUCTURE: fix parseDisposition token-count check; extension data
  accepts bare NILs per §7.4.2
- SortResponse: tolerate trailing (MODSEQ n); lexer accepts literal8
  ~{n} with NUL-safe payloads

Compliance: 48 rows flip violation→pass across RFC 7162/5255/5256/5259/
5267/5465/5466/4467/5524/2193/2221. Zero regressions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
The tolerance batch switched AtomTextCode to bare-list splitting, which
preserved REFERRAL/UNDEFINED-FILTER/NOUPDATE-style bare arguments but
broke the parenthesized tuple forms (INPROGRESS, MAILBOXID, BADEVENT):
paren tokens leaked into contents and nested grouping stopped working,
regressing 8 requirement-rows that the batch's totals-only verification
masked. Pick the split mode by the leading token: '(' means the tuple
form with the original paren-stripping delimiters; anything else is a
bare argument list.

Merged result: 329 pass / 6 violations (remaining: RFC2971-3.3-2 (M0.7),
RFC9051-D-1 (M0.6), RFC9051-11.3-1/-3 (deferred to M1 router),
RFC9051-7.1-1 (adjudicated deviation)); zero regressions vs every prior
snapshot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
IdCommand validates at the command boundary and throws RangeError on
input exceeding RFC 2971 §3.3's MUST NOT limits (>30 pairs, field >30
octets, value >1024 octets). Session sanitizes consumer-supplied ID
config instead of rejecting it — over-long field names are dropped
(a truncated field name is a different field), over-long values are
truncated at a UTF-8 code-point boundary to 1024 octets — so the ID
exchange still happens with a compliant command.

Compliance: RFC2971-3.3-2 flips violation→pass on both profiles
(verified via targeted spec run; full-suite snapshot lands with the
milestone close).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…3, I-10)

The lexer already promotes values above 2^32 to BigIntToken, but the
RFC822.SIZE and QUOTA matchers only accepted number tokens, so any
63-bit size failed the msg-att parse and the response was dropped.
Widen size/usage/limit to number | bigint with the same alternation
pattern MODSEQ already uses. Values within 32-bit range still surface
as plain number; UID/UIDVALIDITY/UIDNEXT keep their strict 2^32-1
bound. Flips RFC9051-D-1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
- SORT/SEARCH (MODSEQ n) with zero preceding ids parsed via a negative
  slice index and threw/mis-parsed on grammatically valid input; read
  the MODSEQ value positionally and guard the id-list slice (empty list
  + modseq value now round-trip)
- Capability classification (standard/extension/unknown, AUTH= scheme
  collection) now canonicalizes before comparing; display casing kept
- NilRule gained an atom-continuation boundary check so atoms beginning
  with a case-insensitive 'nil' prefix (NILVANA, Nilsson) tokenize as
  atoms, not NIL + fragment
- IdCommand: document the deliberate synchronous RangeError contract;
  truncateToOctets distinguishes a mid-code-point cut from a value that
  legitimately ends in U+FFFD by re-encoding; quota.ts comment cites
  RFC 9051 Appendix D / RFC 9208 correctly

Findings 008/009/010/013/014d/014e from the M0 review; regression
tests added for each. Targeted compliance runs byte-identical to
pre-fix baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHssyTzswqsW
- Reset preauthed at the start of every connect() attempt and in the
  new teardownFailedConnect() helper used by every failure path — a
  rejected cleartext-PREAUTH attempt could previously leave the flag
  set, silently skipping STARTTLS on a retried connect() (a
  server-triggerable plaintext downgrade that also misreported
  authenticated state)
- Attach a transient socket 'error' handler the moment a socket exists
  in every connect phase (raw, implicit TLS, post-upgrade), raced
  against every hazardous await; previously most of connect() had no
  error listener, so an ordinary connection failure could crash the
  process or hang forever. Queue stop() also resets the held flag so a
  socket death mid-STARTTLS-hold cannot park commands forever
- Fire greeting detection from an internal pre-suppression signal so a
  legal '* OK [ALERT] ...' greeting no longer hangs connect() until
  timeout (consumer-facing pre-confidentiality ALERT suppression
  unchanged)
- Opportunistic STARTTLS: capability-absent now continues on plaintext
  per spec §3.3 instead of tearing down; strict mode still fails
- Swap the upgraded socket in before releasing the queue hold so a
  command parked during the hold cannot write to the pre-upgrade socket
- Discard buffered partial-line pipeline state at the STARTTLS tagged
  OK (plaintext-residue defense). Residual: a complete injected line in
  the same TCP segment as the OK is parsed before the upgrade code can
  intervene; closing that requires pipeline-level changes, tracked for
  the M1 router work
- Session.start() failure path resets authed/capabilityList/serverInfo
  and awaits disconnect()

Findings 001-004, 006, 007 (partial, documented), 011, 014a-c from the
M0 review; every fix carries a regression test verified to fail with
the fix reverted. 355/355 unit+integration tests green; targeted
compliance runs byte-identical to baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…8 discrimination

Security fix (review finding 012, confirmed empirically): Node's
built-in checkServerIdentity falls back to the Subject CN when a
certificate carries no subjectAltName, so a SAN-less CN-matching
certificate was accepted — RFC 9525 §6.6 forbids CN as an identity
source. openTls now wraps checkServerIdentity with a SAN-presence
precondition (rejects as identity-mismatch) before delegating to the
built-in matcher. New cn-only cert fixture + empirical unit test
witness the behavior; spec §10.1 amended to describe the wrapper
mechanism instead of the incorrect 'relies on Node' claim.

Harness fidelity (review finding 005): ScriptedServer.doStartTls now
settles on a client-side handshake abort ('close' without 'secure' on
either socket) instead of hanging, with an expectAbort option; the
RFC3501-11.1-8 test again performs a REAL wrong-host-cert handshake the
client must abort — a client skipping identity verification would
complete the handshake and fail the script, restoring the row's
discriminating power. Stale 'session notes' citation removed.

Full suite after all review fixes: 356 unit/integration green;
compliance 332 pass / 3 violations (RFC9051-7.1-1 adjudicated,
RFC9051-11.3-1/-3 deferred to M1 router) / 0 problems.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Baseline 118 violation rows → 3 (all itemized: one adjudicated SHOULD
deviation, two blocked-on-surface rows scheduled for M1's router).
Exit criteria met; phase-boundary review completed with all confirmed
findings fixed (including one empirically confirmed security gap in
Node's default identity check that the review had flagged as
speculative).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
- ClientStateMachine: §3.1 transition table encoded as data, single
  writer, synchronous onTransition (stateChange-before-settle contract),
  assertIn for command legal-state checks; any→logout and
  any→disconnected as rules; disconnected→disconnected a tolerated no-op
- CapabilityRegistry: ingests parsed CapabilityList or string iterables,
  canonical storage, AUTH= mechanism parsing, live stable-identity
  CapabilityView, epoch bumps on every set AND invalidate, onChange hook
  for capabilitiesChanged

60 unit tests covering every legal edge, illegal-edge rejection,
ordering, epochs, ci semantics, both ingestion forms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
ImapError base with standard cause options; ConnectionError (phase,
bye), TlsError (reason, certificate), ProtocolError (bytes, context),
CommandError (command/tag/status/code/text) with ServerNoError and
ServerBadError enforcing their status at type and runtime level,
AuthError (mechanismsTried, code), CapabilityError (capability, rfc),
StateError (state, required). Legacy error classes untouched — they
become wrapped internal causes as each subsystem is ported.

src/protocol/response-codes.ts holds the minimal TypedResponseCode
fallback shape; the full §5.5 discriminated registry lands with the
router.

Note: cause is declared with 'declare readonly' — under ES2022 class
fields a plain field initializer would wipe the value Error's
constructor sets from options.cause.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
… (spec §7.2)

All value emission goes through validated primitives (no raw escape
hatch): atom (strict ATOM-CHAR, throws), astring three-way
atom/quoted/literal decision with byte-length literals, quoted escaping,
CR/LF and 8-bit content routed to literal form (atom alone throws —
bare atoms have no escape mechanism), literal8 ~{N}, LITERAL+/LITERAL−
non-sync selection with the 4096-octet LITERAL− ceiling, INBOX
canonicalization with an mUTF-7 codec hook stubbed until M2,
RFC 3501 date/date-time rendering in UTC, validated flag lists,
nested parenthesized groups with correct spacing, and per-call
atomicity (a throwing call rewinds all partial output).

Output is WireSegment[] — segment boundaries carry awaitContinuation
for the queue's literal gate (§6.2). 83 unit tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
….2 part 1)

Mechanism interface exchanges raw bytes only — the AUTHENTICATE command
(landing with the client shell) owns base64 framing, SASL-IR, empty
responses and * cancellation. Factory-based registry so per-attempt
mechanism state never leaks across attempts.

- PLAIN (RFC 4616): NUL-framed authzid/authcid/passwd, embedded-NUL
  rejection, exact RFC example vector tested
- OAUTHBEARER (RFC 7628): GS2 header with RFC 5801 saslname escaping
  (=3D before =2C, order matters), kvsep grammar verified against the
  RFC's worked example (an initially missing post-header kvsep was
  caught by the byte-exact test), JSON error-challenge → 0x01 dummy →
  AuthError with status/scope
- XOAUTH2: user/auth kvpair form, error-challenge → empty response →
  AuthError

All three requiresSecureTransport: true (§10.3 policy hook). 32 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…s, literal gate (spec §6, §7.1, §7.3, §8)

- Command<TResult>: verb/states/capability/queueMode with protected
  write/claims/accept/onContinuation/onError hooks; tag assigned by the
  queue at write time via a static driving surface so subclasses keep
  encapsulation; one-submission semantics
- ResponseCollector with untagged/first/tagged/codes and a conservative
  TypedResponseCode renderer; FETCH streaming bridge left as a
  documented seam for M3
- Router owns the tag map, ordered claimants and the single
  continuation-owner slot (double registration throws); the M0
  ALERT/hygiene handling re-homes here as the state-tracker lane;
  routing is additive (claim attribution never filters the public
  Connection events - the choice that keeps M0 behavior provably
  identical)
- executeCommand(): the only writer of command bytes besides
  CommandWriter; implements the §6.2 literal gate (withhold all other
  writes until + or tagged NO/BAD abort) composing independently with
  the STARTTLS hold()/release()
- Queue: pipeline/serial/isolated modes replace requiresOwnContext;
  cancellation rejects with ConnectionError(phase:steady) — the old
  string reject is gone; per-connection tag generator
- CAPABILITY/NOOP/ID/STARTTLS ported; ID keeps its RFC 2971 limits;
  commands/encoding.ts deleted (no importers remain)

615/615 unit+integration tests; full compliance per-row byte-identical
to the M0 snapshot (332/3/819/427, problems []), verified twice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…gout/close (spec §2, §3.2, §3.3)

- ImapClientConfig validation (synchronous TypeError/RangeError, deep
  copy, §2 default resolution) in src/client/config.ts
- ImapClient over the M0-hardened Connection: connect() ritual steps
  1-4 (+ID in any state per RFC 2971; auth is a NotImplementedError
  seam filled by the next task), state machine wiring with
  stateChange-before-settle, client-owned CapabilityRegistry fed by
  bridging Connection's public events, error mapping into the §4
  hierarchy, alert/unhandled/close/error events, run() enforcing
  command states (StateError) and capabilities (CapabilityError) with
  zero bytes written, LOGOUT command + drain choreography (BYE normal
  in the logout window), idempotent logout, close() teardown
- Fixes two dormant pre-existing parser bugs the shell exposed: the
  resp-text-code dispatcher matched 'CAPABILITIES' instead of the wire
  keyword CAPABILITY, and CapabilityTextCode parsed its unparenthesized
  payload with the parenthesized default, yielding an empty set

671/671 unit+integration; compliance per-row identical to the M0
snapshot (332/3/819/427, problems []). Session stays until the M1.9
driver rewire so the suite is green at every commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…3, §10.3)

- AuthenticateCommand (isolated): owns base64 framing both directions,
  SASL-IR initial response inline iff advertised ('=' for empty, RFC
  4959), deferred initial response on first continuation otherwise,
  '*' cancellation when a mechanism throws mid-exchange, tagged NO/BAD
  mapped to AuthError with the typed resp-code and mechanismsTried
- LoginCommand (serial): astring credentials through CommandWriter
  (8-bit passwords take the literal path through the M1.4 gate)
- Selection (§9.3): cleartext credential gate first (TlsError policy,
  zero bytes, §10.3), preference order from config or defaults,
  unadvertised/unregistered/transport-filtered candidates recorded
  with exclusion reasons, AUTHENTICATIONFAILED never falls through,
  negotiation failures do, LOGIN fallback gated on LOGINDISABLED,
  post-auth capability refresh honoring a tagged-OK [CAPABILITY] code
  via registry epochs
- ImapClient.authenticate() + connect() step 5 wired
- Continuation-path fixes surfaced by first real use: ContinueResponse
  no longer guess-decodes base64 (uppercase-only regex made it
  nondeterministic; raw text now, AUTHENTICATE decodes), executeCommand
  catches hook rejections as '*' aborts instead of hanging, accept()
  may return a promise so mechanism.finish() can reject after tagged OK
  (SCRAM server-signature seam)

692/692 unit+integration; compliance per-row identical to the M0
snapshot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…4, RFC 5161)

EnabledResponse parser structure (canonical upper-case, empty list
valid); EnableCommand (serial — state-changing; authenticated state
only; RangeError on empty request; tolerant empty accept);
ImapClient.enableExtensions() filtering to advertised capabilities
(zero survivors → zero bytes), accumulative client.enabled;
connect() step 6 auto-ENABLE with the M1 understood-enable set
(UTF8=ACCEPT; QRESYNC/CONDSTORE/UIDONLY join in their milestones,
IMAP4rev2 excluded per §3.4).

713/713 unit+integration; compliance per-row identical to the M0
snapshot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…t surface

- Compliance driver connect() drives ImapClient (config mapping,
  allowInsecureAuth:true as a documented harness affordance — the
  §10.3 policy itself is exercised by the RFC 8314 rows); login/
  authenticate/logout/noop/enable stubs wired to client calls with
  zero protocol logic; connectLow stays Layer 1
- src/session.ts deleted; src/index.ts exports per spec §1.1
  (ImapClient, client types, §4 error hierarchy, Connection, parser
  output classes; no lexer, no Parser stream); package.json exports
  map for '.', './commands', './sasl'
- sessionPrelude sends a bare greeting so its scripted CAPABILITY
  round trip is always the one consumed; login/auth exchanges can fold
  [CAPABILITY] codes into the tagged OK for the §3.3 step-5 refresh
- enableExtensions treats UTF8=ONLY as advertising UTF8=ACCEPT
  (RFC 6855 §3: the required reaction is ENABLE UTF8=ACCEPT)

Unit/integration 708/708 green. Compliance sweep and remaining script
updates land in part 2 (this commit alone leaves known script stalls
in rev2 greeting-code scenarios — tracked, in progress).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
62 expectFailure annotations removed across 27 spec files for tests
that now genuinely pass with login/authenticate/logout/noop/enable
wired (NOOP, LOGIN/LOGINDISABLED, AUTHENTICATE families, SASL-IR,
OAUTH, ENABLE, IDLE prohibition rows, credential-policy rows, and
capability-gated prohibition witnesses). Adjacent 'not yet
implemented' comments corrected only where the verb is now real.
problems: [] reproducible across 4 consecutive full runs; totals
440 pass / 32 violation / 682 unimplemented / 427 untestable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Per-milestone plan doc per the repo convention: mailbox-name codec
first (mUTF-7/UTF-8 in src/protocol/, writer stub becomes delegator),
SELECT/EXAMINE + MailboxSession skeleton, CREATE/DELETE/RENAME/
SUBSCRIBE/UNSUBSCRIBE, unified LIST with RFC 5258 combination rules,
LSUB, STATUS, NAMESPACE, APPEND single, RFC 3691 catalog extraction
(parallel suite growth), UNSELECT/CLOSE, milestone close. Notes which
verbs need freshly authored compliance specs (DELETE/SUBSCRIBE/
UNSUBSCRIBE have zero coverage today), the client.ts merge-order
bottleneck, and the PERMANENTFLAGS null-vs-assume-all reading.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
- Greeting-code stalls: scripts that greeted with a [CAPABILITY] code
  then expected a CAPABILITY round trip the client rightly skips
  (§3.3) now either greet bare (when the test needs specific
  capabilities) or witness greeting-code consumption via
  driver.hasCapability (when greeting handling IS the requirement)
- Missing-connect tests (LOGIN-credentials rows) connect first;
  SELECT/RENAME-dependent rows restored to clean unimplemented
  classification
- RFC4422-3.4/3.5 abort test rewritten to a genuine two-challenge
  scenario: PLAIN answers the first (respond leg), a surplus second
  challenge forces step() to throw → real '*' abort (abort leg)
- RFC9051-A-1 adjudicated (docs/compliance-adjudications.md): spec
  §3.4 permanently excludes IMAP4rev2 from auto-ENABLE; test asserts
  the positive requirement and is annotated as a deliberate violation
- RFC9051-11.3-1/-3 (M0-deferred) now genuinely PASS: retargeted to
  the ImapClient surface; the unhandled diagnostics channel is I-6
  tolerance, not a failure to ignore; capabilitiesChanged count
  asserted stable at exactly 1

467 pass / 2 violations (both adjudicated) / problems []; 708/708
unit+integration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…ncel scripts

EXTERNAL (RFC 4422 App A): client-first single pair, authzid initial
response ('=' zero-length wire form via the existing SASL-IR path),
opt-in only. CRAM-MD5 (RFC 2195): server-first (no IR argument),
keyed-MD5 digest verified against the RFC's worked vector,
requiresSecureTransport false (the password never crosses the wire).
Cancel tests rescripted to a scenario a §9.3-compliant client can
produce: PLAIN + surplus second challenge → step() throws → bare '*'
→ tagged BAD [AUTHENTICATIONFAILED]. Ripple fixes in the EXTERNAL/
CRAM-MD5/SASL-IR spec files (missing [CAPABILITY] fold-ins, a latent
double-continuation script bug, driver identity constants).

725/725 unit+integration. Compliance: 486 pass / 2 adjudicated
violations / problems [], twice, zero deltas between runs. M1 exit
families: both cores §6.1-6.2, RFC4616/4959/7628/XOAUTH2 at 100%;
RFC4422 at 70% with all 6 remaining rows security-layer-only
(no roadmap mechanism negotiates one); RFC5161 SELECT-gated row → M2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
486 pass / 2 adjudicated violations / problems []. All M1-attainable
exit families at 100%; RFC4422 security-layer rows itemized as
vacuous-by-design (M6 adjudication candidates); RFC5161's remaining
row is SELECT-gated (M2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…e test covers §1.1 subpaths

Four requirements extracted with verbatim-quote verification and
per-entry profile adjudication (rev2 semantics scored via the existing
RFC9051-6.4.2-1 row per the RFC 5161 precedent; capability gate is
rev1-only per the RFC 4959-3-3 precedent). Registry coverage flips
UNSELECT out-of-scope→cataloged (tallies 74/8); new
specs/ext/unselect-3691.test.ts with two real-signal passes today and
unimplemented annotations awaiting the M2.13 verb.

The import-hygiene meta test now allows exactly the spec §1.1 public
entry points (., ./commands, ./sasl) instead of src/index only — the
driver's src/sasl import is the public subpath surface, not a deep
import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
claude added 7 commits July 16, 2026 06:52
typedoc 0.28.20 pinned exact; npm run docs generates docs/api/
(gitignored — on-demand, rationale in typedoc.jsonc). Entry points are
exactly the package exports map (., ./commands, ./sasl) with
entryPointStrategy resolve, so parser/lexer/router internals stay
undocumented as a deliberate 1.0 surface statement. ~990 missing-docs
warnings fixed with real TSDoc summaries across ~83 src files; 3 broken
links de-linked; 29 internal shape types listed intentionallyNotExported
with per-symbol comments; Connection.router/.capabilityRegistry tagged
@internal; one type-only restructuring (IdCommandValues mapped-type →
equivalent explicit object literal so RFC 2971 fields carry per-key
docs — byte-identical resulting type). The M2.2 permanentFlags
null-vs-implied and M5.15 UIDONLY polarity-inversion judgment calls
verified readable in generated HTML. Flagged for a future exports pass:
MailboxUpdate/MailboxUpdatesOptions/MailboxSessionDriver/LiveUpdatesMode
appear in updates()'s signature but aren't re-exported from the root.

npm run docs: 0 warnings. Tests 1936 pass, typecheck 0 (incl. docs
samples), eslint 0 errors, compliance per-row identical (1138/6/2/451,
problems []).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Full-document rewrite keeping M3.11's already-correct examples:
what-it-is → install → quickstart → feature tour (client/session
verbs, search, fetch/streaming, flags, copy/move, append, seq facet,
events, updates()/idle(), the four facets, compression, UNAUTHENTICATE,
UIDONLY, TLS/auth config, errors, connection escape hatch) → compliance
story (matrix cited by command/link only — M6.7 fills no numbers here)
→ MIGRATION.md pointer → API docs pointer. Deleted the ~750-line stale
node-imap Legacy API section (superseded by docs/MIGRATION.md) and the
false Roadmap claiming IDLE/CONDSTORE/NOTIFY/UIDONLY/COMPRESS were
unimplemented. Project-identity block kept (badge, Node >=22, MIT,
node-imap attribution). All 18 samples compile-gated via
test/docs/readme-samples.ts (gate proven live by deliberate-error
injection). 1194 → 645 lines.

Tests 1936 pass, typecheck 0, eslint 0 errors, compliance per-row
identical (1138/6/2/451, problems []).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
package.json → 1.0.0 with the new identity description; CHANGELOG.md
(Keep-a-Changelog: the M0–M6 arc, the compliance-measurement origin,
and an honest breaking-changes-vs-0.9 section pointing at
docs/MIGRATION.md). Build verified: exports map matches dist/, npm pack
includes README/MIGRATION/dist and excludes test/. Final measurement:
1936 unit tests; THREE more consecutive compliance runs byte-identical
across all 1597 rows (nine consecutive total incl. M6.2's six runs) at
1138 pass / 6 adjudicated violations / 2 adjudicated-unimplemented /
451 untestable / problems []. Snapshot + NOTES.md (exit-target table,
task ledger, adjudication tally, post-1.0 carry-forwards) at
docs/compliance-history/M6/. test:e2e excluded per standing directive
(no real-server credentials in this environment).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Three lenses over the whole M0–M6 effort. Invariants+scope: all 13 spec
§12 invariants verified single-chokepoint-enforced with pinning tests;
every §13 non-goal confirmed absent. Security: TLS option enforcement,
dual credential chokepoints, constant-time SCRAM verification with
terminal no-fallback, log hygiene, injection-proof CommandWriter,
bounded literals — clean. Docs+ledger: ledger complete both directions,
README quickstart RUNS verbatim against a scripted server, typedoc
matches the three public entry points, all links resolve.

Fixed pre-PR: (1) package.json files allowlist — tarball was shipping
708 files / 24.9 MB unpacked incl. internal planning docs and typedoc
HTML; now 344 files / 2.2 MB (dist + README + CHANGELOG + LICENSE +
MIGRATION.md + compliance-adjudications.md). (2) M6.1 I-9 ledger entry
wording corrected (gate-before-dispatch, not uniformly
before-construction). (3) README quickstart UID-grain note added,
mirrored in the compiled samples. Itemized as post-1.0 hardening notes
in M6 NOTES: zlib boundary-synchronicity assumption in the
compressed-UNAUTHENTICATE injection guard; RFC 2047 header-field
decoding boundary.

Tests 1936 pass, typecheck 0, eslint 0 errors, compliance per-row
identical (1138/6/2/451, problems []).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW

@LoveAndCoding LoveAndCoding left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Report — "Modern API 1.0" (M0–M6, v1.0.0)

Recommendation: HOLD FOR CHANGES

This is impressively thorough, well-documented work, and the compliance-measurement discipline is real. But the review surfaced three Critical and six High findings that are merge-blocking, two of which were reproduced as live defects during validation:

  • Two remote, one-line process crashes: a malformed tagged or untagged server response throws a raw exception that no listener catches, taking down the whole Node process — reproduced end-to-end (exit 1) against the real parser pipeline. For a network client that parses untrusted server bytes, this is a denial-of-service in the library's own stated "never crash" (I-6) invariant.
  • A SCRAM mutual-authentication fail-open: the "terminal-failure MITM protection" the PR headlines can be bypassed by a server/MITM that never presents a verifiable server-signature; the client accepts the session as mutually authenticated.
  • Plus a silent connection wedge, an unbounded hang, an auth-breaking reused-instance bug, and a logout() that silently no-ops after reconnect (leaving a live, authenticated connection the caller believes is torn down).

None of these are subjective. Each has a concrete file/line and, for the reproduced ones, a trigger. They should be fixed (and regression-tested) before merge. The large Medium tier is mostly hardening and wire-correctness edge cases that can be follow-ups, but several (injection surfaces, capability-epoch staleness) deserve a look given this is a 1.0 security-sensitive library.

Validated finding counts: 3 Critical · 6 High · 26 Medium · 12 Low. (3 candidate findings dropped as invalid after validation; 2 reclassified as not-relevant dead code.)


Critical

1. SCRAM server-signature can go unverified (MITM fail-open)

ScramMechanism.finish() (src/sasl/scram.ts:351-360) takes zero parameters while its interface declares finish(data, ctx) (src/sasl/mechanism.ts:105); TS bivariance hides the mismatch. AuthenticateCommand.accept() always calls finish(null, ctx) (src/commands/authenticate.ts:161-163). Combined with handleServerFinal's "case 1" (scram.ts:41-48) resolving normally when no server-final message was ever presented, a server/MITM that never delivers a verifiable v= through the one channel this code inspects is accepted as mutually authenticated. Directly undercuts the PR's headline MITM protection claim.
Fix: Have finish(data, ctx) parse and verify v= via timingSafeEqual whenever a server-final wasn't already consumed; make a tagged-OK "success" with no verifiable signature fail closed. Also update authenticate.ts to pass the real data.

2. Malformed tagged response crashes the whole process

TaggedResponse (src/parser/structure/tagged.ts:30) calls StatusResponse.match(tokens,2) with no try/catch, unlike the untagged path. APPENDUID/COPYUID (RFC 4315) ride the tagged OK, so their parse-throw sites are primarily reached unprotected. Reproduced a real process exit(1) feeding A1 OK [UIDVALIDITY 0] / malformed [APPENDUID …] through the real pipeline.
Fix: Wrap TaggedResponse's StatusResponse.match() in try/catch with an UnknownContent fallback; also fix #4 below.

3. Trivial malformed untagged line crashes the process

UntaggedResponse's constructor reads tokens[1] (src/parser/structure/untagged.ts:85) and contentTokens[0] (:97) without null-guards, before its own try/catch loop. Reproduced TypeError-to-process-crash from the one-line inputs "*\r\n" and "* \r\n", in the exact file meant to be the tolerance backstop.
Fix: Guard both accesses (!secondToken || !secondToken.isType(...), and check contentTypeToken) → ParsingError/UnknownContent instead of TypeError.


High

4. Parser pipeline has no 'error' listener (crash amplifier)

connection.ts resetProcessingPipeline() (src/connection/connection.ts:782-830) attaches untagged/tagged/continue/unknown listeners to this.parser but never 'error'; no process.on("uncaughtException") exists anywhere in src. .pipe() does not forward destination errors. This is the linchpin turning #2/#3 (and any future parser error) into full process crashes.
Fix: Attach an 'error' listener to the parser (and lexer/pipeline) that converts a stream error into a controlled connection-level failure.

5. Compression codec errors silently wedge the connection

compress() routes zlib inflate/deflate errors to onSocketError (src/connection/connection.ts:1209-1211), which only emits connectionError — no socket destroy, queue stop, or state reset. A codec error fires on the transform, not the socket, so no teardown runs. connected stays true, reads are dead, writeBytes keeps feeding a destroyed deflate — a permanent silent wedge from one malformed compressed frame.
Fix: Route wrapCompression's onError through a real teardown (destroy socket + stop queue), distinguishing codec from socket errors.

6. No timeout on post-greeting negotiations → indefinite hang

execute-command.ts has no timeout mechanism at all. The post-greeting isolated-command awaits — STARTTLS (connection.ts:997/1032/1104), COMPRESS (:1172), UNAUTHENTICATE (:1272) — and runCommand() generally are unbounded; only TCP-connect/greeting/TLS-handshake are timed. A server that accepts the connection then goes silent (or a graceful FIN during the STARTTLS window) hangs connect()/the caller forever and wedges the held queue — contradicting the file's own "never hangs" claim.
Fix: Bound these negotiations with a command timeout that tears down + rejects on expiry; extend teardown detection to graceful close throughout the connect() ritual.

7. Command submitted after CommandQueue.stop() never settles

AsyncQueueContext.add() (src/connection/queue.ts:157-169) only dispatches if (this.running) with no rejecting else-branch. After stop(), a subsequent add() parks the command, never settled; because the queue is reused across reconnects, a later start() can dispatch that stale command against a fresh connection. unauthenticate() also lacks the !this.socket guard that starttls()/compress() have.
Fix: Reject synchronously in add() when stopped/torn down; add the missing unauthenticate() not-connected guard.

8. Reused SASL mechanism instances retain stale per-attempt state

mechanism.ts's contract (62-72) requires implementations usable as a literal mechanisms object to reset ALL per-attempt state in start(). Violated across the board: ScramMechanism.start() never resets verificationFailure; CramMd5Mechanism/OAuthBearerMechanism/XOAuth2Mechanism never reset stepCalled/errorPayload. On a reused instance, one transient failure permanently poisons the mechanism, and any finish() throw is force-classified terminal:true, blocking all fallback — turning a benign reconnect into unrecoverable auth failure.
Fix: Reset all per-attempt fields at the top of each start(); correct the false "ScramMechanism follows this rule" contract claim in mechanism.ts:71.

9. logout() silently no-ops after reconnect

logout() (src/client/client.ts:612-621) caches _logoutPromise but never clears it (unlike _unauthenticatePromise). Reconnect on the same instance is explicitly supported, so connect→logout→connect→logout returns the stale resolved promise from cycle 1 on the second logout — no LogoutCommand, no disconnect(), no state transition. The caller believes the session was torn down while the connection remains fully live and authenticated.
Fix: Clear _logoutPromise in the disconnected bridge; add a logout→connect→logout regression test.


Medium (26 findings — highlights)

  • Implicit-TLS connect() re-entrancy gap — two un-awaited connect()s on imaps:// can leak/cross-wire a TLS socket.
  • Stale transient-error listener across STARTTLS swap, never detached from the pre-upgrade socket.
  • COMPRESS/UNAUTHENTICATE boundary-injection defense has an unproven split-write smuggling risk across the async inflate boundary.
  • Post-negotiation topology surgery (STARTTLS/COMPRESS/UNAUTHENTICATE) not guarded before commandQueue.release() — a sync throw permanently wedges the queue.
  • disconnect() is async but resolves before cleanup actually completes.
  • Connection-local capabilityRegistry never invalidated on COMPRESS/UNAUTHENTICATE for Layer-1-only callers.
  • Unbounded PBKDF2 iteration count in SCRAM blocks the whole event loop (server-controlled i=).
  • OAUTHBEARER/XOAUTH2 don't reject \x01 in user/host/token fields — kvpair-separator injection risk if any field carries untrusted input.
  • OAUTHBEARER/XOAUTH2 structured error diagnostics are dead code against a spec-compliant server (only fire on tagged OK; real failures end in tagged NO).
  • authenticate()/logout() race leaks a raw internal IllegalStateTransitionError instead of the public error hierarchy.
  • Public connection escape hatch bypasses every client-side safety gate, including the cleartext-credential policy — client.connection.runCommand(new LoginCommand(...)) sends a password in cleartext even with allowInsecureAuth: false.
  • STARTTLS doesn't invalidate the client-level capability registry — violates the PR's own "capability epochs" invariant; a greeting [CAPABILITY] can fire capabilitiesChanged with pre-TLS (forgeable) data before STARTTLS runs.
  • Unbounded UID expansion from VANISHED (EARLIER) 1:4294967295 — a one-line DoS via expandUidSet's unbounded loop.
  • LOGIN fallback can't be excluded via an explicit mechanisms list — a SASL-only intent can still send the real password via LOGIN.
  • CommandWriter's documented atomic-rollback guarantee is broken (chunks reassignment vs. length-truncation restore) — reproduced data loss, though latent (no shipped command currently triggers it).
  • listMailbox() regresses a previously-fixed &-escaping bug that mailbox() already has — reproduced: listMailbox("Sent&Received") emits a raw, unescaped &, corrupting LIST/LSUB patterns on ordinary input.
  • Empty SEARCH or/fuzzy operand breaks OR/FUZZY wire arity — reproduced: {or:[{},{subject:"x"}], from:"y"} silently compiles to OR SUBJECT x FROM y, changing AND semantics to OR.
  • updates() iterator listener leak on a specific driver-acquisition-reject path in MailboxSession.
  • NUL byte and DEL (0x7F) serialized without proper escaping in several CommandWriter paths.
  • date()/dateTime() don't bound-check the year, producing malformed wire dates for out-of-range Date values.
  • KEYWORD/UNKEYWORD SEARCH criteria emitted via astring instead of atom, silently quoting invalid keywords instead of throwing.
  • Binary APPEND (literal8/RFC 3516) sent without checking the server advertised BINARY, unlike every other optional extension in that file.
  • zlib.createInflateRaw() has no output-size cap — a decompression-bomb DoS vector from a malicious/compromised server.

Low (12 findings)

Mostly hardening, documentation-accuracy, and spec-hygiene items — including a contained (non-crashing) TypeError in ENVELOPE parsing, dead/unreachable defensive code (assertIn(), src/connection/utils.ts), unsanitized server text embedded in thrown error messages, an ALERT-trust-marking bypass via a stray space in the resp-code bracket, and several RFC-citation/documentation-accuracy nits.


Notably invalidated during independent verification (flagged for transparency, not for action)

  • A suspected mUTF-7 decoder crash — the utf7 package never throws on malformed input; not a real issue.
  • A suspected SequenceSet bug where "5:*,10:*" coalescing to "5:*" would drop messages — disproven; the coalescing is provably RFC 3501 §9-correct for any actual mailbox max.
  • A suspected HIGHESTMODSEQ=0 rejection bug — disproven; conflated two distinct response-code grammars (the resp-code position correctly requires ≥1; the STATUS data-item position, which allows 0, is handled correctly elsewhere).

Scope notes

Deep-reviewed: all of src/ across 16 file groups using state/correctness/error-handling/security/spec-compliance/api-compatibility/fidelity/maintainability lenses as fit each group, including both mega-files (client/client.ts, client/mailbox.ts) read in full. Not independently re-audited: the generated docs/compliance-history/ snapshots, and the ~4MB test corpus's assertion strength/coverage (the "1936 passing tests" claim was not re-verified). RFC text could not be fetched directly in this environment (network-restricted); spec claims were checked against this repo's own verbatim-quoting compliance catalog plus established ABNF.


Automated review orchestration — findings independently re-verified against the running code where feasible, including live reproductions for the process-crash, SCRAM, and several writer/search findings.


Generated by Claude Code

claude added 4 commits July 17, 2026 06:36
Addresses seven reproduced review findings (all verified real, each
revert-verified with a new test):
- listMailbox() now applies the same &→&- mUTF-7 escaping mailbox()
  has (wildcards */% still pass through) — a literal & no longer
  corrupts LIST/LSUB patterns.
- Empty SEARCH or/fuzzy/not operand now throws RangeError instead of
  silently vanishing and flipping (A OR B) AND C into A OR (B AND C).
- CommandWriter snapshot/restore now captures the chunks array
  reference and truncates it (was reassigned on a synchronizing
  literal, silently discarding rolled-back bytes).
- NUL rejected from plain literals (allowed only under binary/literal8),
  DEL (0x7F) excluded from bare ATOM-CHAR, raw() rejects the whole
  C0+DEL range (was CR/LF only).
- date()/dateTime() reject years outside [0,9999] (date-year is 4DIGIT;
  padStart didn't truncate a 5-digit or negative year).
- KEYWORD/UNKEYWORD emit via atom not astring (flag-keyword = atom;
  astring silently quoted invalid keywords).
- Binary APPEND (literal8) gated on BINARY/IMAP4rev2 capability
  (CapabilityError, zero bytes), like every sibling extension.
- expandUidSet caps at 1M entries (VANISHED (EARLIER) 1:4294967295 took
  10.6s / massive memory before this — a one-line DoS).

Tests 1977 pass (+41), typecheck 0, eslint 0 errors, compliance per-row
identical (1138/6/2/451, problems []).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…crash the process

Addresses the two Critical process-crash findings + the contained
ENVELOPE TypeError (all verified real, revert-verified with tests):
- UntaggedResponse guards its secondToken/contentTypeToken access —
  the one-line inputs "*\r\n" and "* \r\n" now yield
  ParsingError / the UnknownContent tolerance backstop (I-6) instead of
  a raw TypeError that killed the parser Transform (and the process).
- TaggedResponse wraps StatusResponse.match in try/catch (the untagged
  path already had this): a malformed resp-code riding a tagged OK
  (e.g. [UIDVALIDITY 0], a bad [APPENDUID]) no longer throws uncaught;
  tag+status are preserved so the waiting command still settles, and a
  following tagged response still parses.
- Envelope.match validates the 10-field arity before destructuring
  (was a raw TypeError from new AddressList(undefined); already
  contained by the FETCH backstop, now a typed ParsingError).

The connection-level parser 'error' listener (review High #4, the crash
amplifier) is handled in the connection-lifecycle change.

Tests +new parser structure cases, typecheck 0, eslint 0 errors,
compliance per-row identical (1138/6/2/451, problems []).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Addresses six review findings (all verified real, revert-verified):
- CRITICAL: SCRAM server-signature is now actually verified. finish()
  fails closed unless a matching v= was consumed — a server/MITM that
  jumps from client-final straight to a tagged OK with no verifiable
  server signature is REJECTED (was silently accepted; the old code's
  RFC5802-5.1-14 justification only excuses omitting server-final on
  FAILURE, a tagged-NO path that never reaches finish()). Preserves the
  existing terminal-AuthError-on-forged-signature behavior.
- HIGH: all four reusable mechanisms (SCRAM/CRAM-MD5/OAUTHBEARER/
  XOAUTH2) reset their per-attempt state at the top of start() — a
  reused instance no longer stays poisoned by one transient failure
  (which also force-classified every later finish() throw terminal,
  blocking fallback). mechanism.ts's false 'ScramMechanism follows
  this' claim corrected.
- PBKDF2 iteration count capped at 1M (server-controlled i=99999999 ran
  a 28s event-loop-blocking PBKDF2 before this).
- OAUTHBEARER/XOAUTH2 reject \x01 / CR / LF in user/host/token fields
  (kvpair-separator injection).
- Explicit non-empty mechanisms list without LOGIN now excludes the
  LOGIN password fallback (opt back in by listing "LOGIN"); the
  mechanisms:[] 'skip SASL' idiom and LOGINDISABLED are preserved.
- OAUTHBEARER/XOAUTH2 failure diagnostics wired to the real tagged-NO
  path via a describeFailure() hook (were dead code on tagged OK only).

4 SCRAM compliance scripts updated to present a genuine v= (they used a
bare-OK shortcut the Critical fix now correctly rejects) — no matcher
weakened; RFC5802/7677 MUST rows stay 100%. Tests 2019 pass, typecheck
0, eslint 0 errors, compliance per-row identical (1138/6/2/451,
problems []).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Addresses the remaining High findings + connection Mediums/Lows (all
verified real except one disproven; each revert-verified with tests):
- HIGH #4: parser/lexer/pipeline 'error' events now route through
  onPipelineError → controlled connection teardown (was uncaught →
  process crash; the amplifier behind the two parser-crash Criticals).
- HIGH #5: compression codec errors now force a real teardown via the
  socket close cascade (was a silent permanent wedge — connected stayed
  true, reads dead, writes fed a destroyed deflate).
- HIGH #6: STARTTLS/COMPRESS/UNAUTHENTICATE negotiations + CAPABILITY
  round trips bounded by a command timeout (ImapClientTimeouts.command
  is now actually wired through; was dead config) — a silent server no
  longer hangs connect() forever.
- HIGH #7: CommandQueue gained a stopped flag; add-after-stop rejects
  synchronously instead of parking forever (and can't dispatch a stale
  command against a reconnected socket); unauthenticate() got the
  missing !this.socket guard.
- HIGH #9: _logoutPromise cleared in the disconnected bridge — logout()
  after reconnect actually sends LOGOUT and tears down (was a stale
  resolved-promise no-op leaving a live authenticated connection).
- Capability epochs (I-2): connection-local registry invalidated on
  COMPRESS/UNAUTHENTICATE; new secureUpgrade event invalidates the
  client registry the instant STARTTLS succeeds.
- Also: implicit-TLS connect() re-entrancy guard; stale STARTTLS-swap
  error listener detached; boundary-injection split-write hardening
  (postBoundaryDiscard) + topology-surgery-throw guards that always
  release the queue; disconnect() awaits real socket close;
  authenticate()/logout() race maps to a typed StateError;
  decompression-bomb output cap (256 MiB); updates() iterator
  listener-leak on the CapabilityError join-reject path; replace.ts
  binary now gated on BINARY/IMAP4rev2; ALERT stray-space trust-marking
  bypass tightened; two throw-site server-text sanitizations.

Escape-hatch cleartext-credential bypass: doc-strengthened, not
functionally guarded (a real fix needs a CommandWriter-level
security-context plumb-through — architecturally significant, flagged
for follow-up). assertIn() dead-code finding DISPROVEN (it is live and
tested in state.ts). Negotiation FIN-detection: timeout bound judged
sufficient; instant-FIN reaction flagged as a larger follow-up.

Tests 2053 pass (+34 files), typecheck 0, eslint 0 errors, compliance
per-row identical (1138/6/2/451, problems []). The 22 raw compliance
vitest failures are pre-existing (8 adjudicated rows + 14 driver-env/
import-hygiene carry-forwards) — verified byte-identical with these
changes reverted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW

Copy link
Copy Markdown
Owner Author

Review addressed — all Critical & High findings fixed, plus the Medium/Low tier

Thank you for the exceptionally thorough review — the reproductions especially made these fast to confirm and fix. Every finding was independently verified against the code before touching it (I did not treat any as automatic), each real fix landed with a dedicated unit test and an undo→red→redo→green revert-verification, and every push kept the full gate suite green (npm test, npm run typecheck, eslint 0 errors, and the compliance matrix byte-identical at 1138 pass / 6 adjudicated violations / 2 adjudicated-unimplemented / 451 untestable / problems: []). Unit tests grew 1936 → 2053.

Four commits, grouped by concern:

  • 212e3a3 command-writer + search wire-correctness
  • 9bd8dd6 parser crash-safety
  • 6eaeafb SASL/auth correctness
  • 147ce7e connection & client lifecycle hardening

Critical

# Finding Resolution
1 SCRAM mutual-auth fail-open Fixed. finish(data, ctx) now verifies v= (timingSafeEqual) and fails closed unless a matching server signature was consumed — a server/MITM that jumps to a tagged OK with no verifiable v= is rejected. The old code's RFC5802-5.1-14 justification only excuses omitting server-final on failure (a tagged-NO path that never reaches finish()). Terminal-AuthError-on-forged-signature preserved.
2 Malformed tagged response crashes process Fixed. TaggedResponse wraps StatusResponse.match() in try/catch; a bad resp-code riding a tagged OK ([UIDVALIDITY 0], malformed [APPENDUID]) preserves tag+status so the command still settles, and a following response still parses.
3 Malformed untagged line crashes process Fixed. UntaggedResponse guards its secondToken/contentTypeToken access; "*\r\n" / "* \r\n" now yield ParsingError / the UnknownContent tolerance backstop (I-6) instead of a raw TypeError.

High

# Finding Resolution
4 Parser pipeline has no 'error' listener Fixed. onPipelineError wired to parser/lexer/pipeline 'error' events → controlled connection teardown. Reverting reproduced the uncaught process-level exception.
5 Compression codec errors silently wedge Fixed. Codec onError now forces a real teardown via the socket close cascade (destroy + queue stop + reset).
6 No timeout on post-greeting negotiations Fixed. ImapClientTimeouts.command is now actually wired through (it was dead config) and bounds STARTTLS/COMPRESS/UNAUTHENTICATE + the CAPABILITY round trips; a silent server no longer hangs connect(). (Instant graceful-FIN reaction during the ritual I judged covered by the timeout bound rather than a larger socket-lifecycle rewire — flagged as a follow-up rather than pretending it's a full FIN-detector.)
7 Command after stop() never settles Fixed. CommandQueue gained a stopped flag; add-after-stop rejects synchronously (and can't dispatch a stale command against a reconnected socket). unauthenticate() got the missing !this.socket guard.
8 Reused SASL instances retain stale state Fixed. All four reusable mechanisms reset per-attempt state at the top of start(); the false "ScramMechanism follows this" claim corrected.
9 logout() no-ops after reconnect Fixed. _logoutPromise cleared in the disconnected bridge; connect→logout→connect→logout regression test added.

Medium — all addressed

Fixed: implicit-TLS connect() re-entrancy guard · stale STARTTLS-swap error listener detached · boundary-injection split-write hardening (postBoundaryDiscard, cleared via a single chokepoint so a mid-swap throw can't strand it) · topology-surgery-throw guards that always release the queue · disconnect() now awaits real socket close · capability-epoch invalidation on COMPRESS/UNAUTHENTICATE (connection-local) and on STARTTLS success (client-level, via a new secureUpgrade event) — closing the I-2 gap · unbounded PBKDF2 iteration cap (1M) · OAUTHBEARER/XOAUTH2 reject \x01/CR/LF in fields · OAUTHBEARER/XOAUTH2 diagnostics wired to the real tagged-NO path · authenticate()/logout() race maps to a typed StateError · STARTTLS capability-registry invalidation (same secureUpgrade fix) · VANISHED (EARLIER) 1:4294967295 DoS capped (was 10.6s / massive memory) · explicit non-empty mechanisms list without LOGIN now excludes the LOGIN password fallback · CommandWriter atomic rollback fixed (captures the array reference, truncates) · listMailbox() &&- escaping restored · empty SEARCH or/fuzzy operand now throws instead of silently changing AND→OR · updates() iterator listener leak on the driver-acquisition-reject path · NUL/DEL escaping in the writer · date()/dateTime() year bounds · KEYWORD/UNKEYWORD emit as atom not astring · binary APPEND (and REPLACE) gated on BINARY/IMAP4rev2 · zlib.createInflateRaw() output-size cap (256 MiB).

One judgment call — the public connection escape-hatch cleartext bypass: I did not add a functional guard. connection.runCommand()/LoginCommand have no path to transport-security state without a CommandWriter-level security-context plumb-through, which is architecturally significant, and Connection is documented as deliberately policy-agnostic (the Layer-1 escape hatch). I strengthened the doc comments on both to spell out the gap and rationale, and I'm flagging the plumb-through as a deliberate follow-up for your call rather than making a sweeping change unbidden. Happy to implement it if you'd prefer it in this PR.

Low — addressed

Fixed: contained ENVELOPE TypeError (now typed ParsingError) · ALERT trust-marking stray-space bypass ([ ALERT]) tightened · two throw-site server-text sanitizations (BYE-greeting rejection, LOGIN AuthError) escaping control bytes. Disproven: the assertIn() "dead code" finding — it lives in src/client/state.ts (not connection/utils.ts), and is actively used and tested. RFC-citation nits swept where spotted.

Notes on your three invalidated candidates

Agreed on all three (mUTF-7 decoder, SequenceSet 5:*,10:* coalescing, HIGHESTMODSEQ=0) — my re-check reached the same conclusions.

Deferred, explicitly (not silently)

  • Escape-hatch credential plumb-through (above).
  • Instant graceful-FIN reaction during connect() (timeout bound covers the hang; a live FIN-detector is a larger change).

Compliance stayed byte-identical throughout, so the fixes tighten client-side refusal/robustness on malformed-or-adversarial inputs the suite's fixtures don't exercise in those exact shapes, without moving any scored row. Ready for another look whenever you are — and let me know on the escape-hatch item.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Automated multi-agent merge-readiness review — PR #18 (Modern API 1.0)

Recommendation: HOLD for changes (address the Critical + the untrusted-server-data robustness items), then merge-with-follow-ups

This is a large, unusually well-engineered and heavily self-documented 1.0. The compliance adjudications and migration/README docs were checked against the actual code and hold up (see G26 below) — the "6 violations" are genuinely deliberate, documented decisions, not defects. However, the review surfaced one Critical and several High-severity robustness defects, concentrated in two areas: (1) the response parser's handling of malformed/adversarial server data, and (2) connection/client teardown races. These are exactly the areas a spec-compliant client must get right because the server is an untrusted peer. None are in the adjudication ledger, so they read as genuine gaps rather than accepted trade-offs.

Confidence: Moderate. Because of a tooling limitation in this environment, the sub-agent review-validator second-pass could not run; findings below are single-reviewer deep-pass results, self-verified by direct code tracing (several token-by-token) but not independently validated. Several carry explicit reachability caveats, called out inline. The Critical in particular should be reproduced with a test before acting, but the trace is concrete.

Coverage (transparency)

27 file-groups were dispatched across ~154 src/ files + docs + a test-quality sample. 17 groups returned complete reviews (client.ts ×2, mailbox.ts part 1, client fetch/idle/state, command infra search/fetch-store/auth-verb/acl-quota, parser core/fetch/mailbox, connection.ts ×2, connection queue, protocol, sasl, docs). 1 group returned a partial (G12). 9 groups did not return findings in time (forced-async pipeline stalls): mailbox.ts parts 2-3, client config/facets, commands base/writer/collector, select/status/list, misc mailbox commands, parser misc (incl. text.code.ts), lexer + top-level, and the test-quality sample. Note that several gap files (e.g. writer.ts, collector.ts, base.ts, mailbox.ts) were read as dependencies by adjacent groups, so they received partial indirect coverage. The 3047-line mailbox.ts and the lexer were reviewed only in part — a re-run is advisable for those before merge.


Findings (most-severe first; single-reviewer, validator pass unavailable)

CRITICAL

C1. Nested-multipart BODYSTRUCTURE mis-parsed → uncaught TypeError on common real-world mailsrc/parser/structure/fetch/body.structure.ts:326-355 (child loop at 332-335). MessageBodyMultipartStructure builds every child via new MessageBodyStructure(...) without the multipart-vs-single-part check that already exists a few lines up (the MESSAGE/RFC822 branch, lines 250-267). For a multipart/* whose child is itself multipart/* (i.e. virtually every HTML+plaintext-with-attachment email, and all signed/encrypted mail), the split yields 2 blocks instead of 7 and destructuring throws a raw TypeError rather than parsing. This would break FETCH BODYSTRUCTURE/BODY for a large fraction of real messages. Fix: apply the existing multipart-detection pattern recursively in the children loop; add a nested-multipart regression test. (Traced token-by-token; reachability depends on real server responses but nested multipart is ubiquitous.)

HIGH

H1. Teardown racing an in-flight connect() hangs / leaks statesrc/client/client.ts:677-693 (close()/waitForDisconnect()) + src/connection/connection.ts:340-367,784-823,848-877. Permanent socket handlers (which stop the queue, reset the router, clear connectInProgress, fire teardown) are wired only at the end of a successful connect(). A logout()/close()/disconnect() overlapping a still-negotiating connect (greeting/STARTTLS window) can leave connectInProgress stuck true forever (instance permanently unusable) and/or leak _logoutPromise, and disconnect()'s bare socket.destroy(undefined) fires neither handler set. Two groups independently found this from the client and connection sides. Fix: guarantee "disconnected"/teardown from every path including teardownFailedConnect() and mid-connect disconnect().

H2. postBoundaryDiscard not cleared by teardown paths → silent black-hole on reconnectsrc/connection/connection.ts:340-367 (teardownFailedConnect), 784-823 (onSocketClose). These reset secure/preauthed/compression but not postBoundaryDiscard; if a socket error fires mid-boundary-negotiation, a reused instance's next connect() can have its greeting (and everything) silently dropped until greeting-timeout, with no error. Fix: reset the flag in both teardown chokepoints.

H3. Opportunistic STARTTLS may abort the whole connection on server declinesrc/connection/connection.ts:720-734. The unconditional teardown-on-false after starttls() runs for both STARTTLS and STARTTLS_OPTIONAL; if starttls() ever returns false for a mere decline (vs. a genuine failure) under the optional policy, opportunistic-TLS callers lose the connection against any server not offering STARTTLS. Uncertainty: depends on starttls()'s return contract; flagged High pending confirmation (the G21 pass did not conclusively resolve it).

H4. unhandled event silently drops tagged/continuation anomaliessrc/client/client.ts:1610-1614. The bridge narrows a 4-way union with instanceof UntaggedResponse || UnknownResponse, swallowing unmatched-tag TaggedResponse and unclaimed ContinueResponse — the exact protocol-desync signals the event exists to surface. No error/log fires. Fix: widen to the full union or route the two cases to the error/log channel.

H5. Empty-array SEARCH criteria bypass the empty-operand guard → wire-invalid commandsrc/commands/search-criteria.ts:394-433 (estimateKeyCount uses Math.max(v.length,1)), src/commands/search.ts:604-608. {keyword: []}, {header: []}, {and: []}, or nested fuzzy/or/not with an empty array compile to a bare SEARCH with zero search-keys (illegal per RFC grammar) or a malformed sub-expression — the very class the recent empty-{} fix targeted, but empty arrays slip through. Reachable via the typed public API. Fix: count empty arrays as 0; add a top-level zero-token check; regression tests.

H6. body-fld-octets/body-fld-lines reject bigint (63-bit sizes)src/parser/structure/fetch/body.structure.ts:233-238,270-282. Only TokenTypes.number accepted; RFC822Size/BinarySize siblings correctly accept number|bigint per RFC 9051 App. D-1. A >2^32-octet body part throws ParsingError on FETCH BODYSTRUCTURE — and this contradicts the I-10 ledger claim that bigint promotion is handled uniformly. Fix: accept bigint; verify D-1's production list.

H7. Empty LIST mailbox name incorrectly rejected → breaks delimiter discoverysrc/parser/structure/mailbox/listing.ts:186-190. Throws on an empty name, but LIST "" ""* LIST (\Noselect) "/" "" (empty name) is the standard, spec-documented hierarchy-delimiter-discovery response. Not adjudicated. Fix: allow well-formed empty astring; add a regression test.

H8. Empty list block → raw TypeError that errors the whole parser streamsrc/parser/utility.ts:17-88 (splitSpaceSeparatedList) + src/parser/parser.ts:79-113. A trivially malformed ( )-shaped list produces an empty block; getSpaceSeparatedStringList then dereferences undefined, throwing TypeError (not the usual typed ParsingError), which _transform delivers to done(error) — putting the Transform stream into a permanently errored state and killing parsing for the rest of the connection from one malformed line. Fix: skip/guard empty blocks and raise ParsingError.

H9. BODY[section] and BINARY[section] collide on map key → silent data losssrc/client/fetch.ts:659-717. buildFetchedMessage() keys one Map by section string only, so a single FETCH requesting both BODY[1] and BINARY[1] has the BODY result silently clobbered by BINARY. Fix: composite (section, binary) key. (Reachability depends on whether commands/fetch.ts's wire compiler — a stalled group — permits both in one request; confirm.)

H10. part.buffer() hangs forever if the stream is destroyed mid-drainsrc/client/fetch.ts:35-69 (drainReadableAsync). Listens for readable/end/error but not close; destroy() emits only close, so an abandoned fetch() iterator (a break out of for await while a not-yet-awaited buffer() is draining) leaves that promise unsettled forever. Fix: add a close handler that settles (reject as known-incomplete).

MEDIUM

  • M1. FETCH section-spec / binarySize strings bypass grammar validation → structural-char injection into BODY[...]/BINARY[...]src/commands/fetch.ts:128-150,249-262 (via writer.raw()).
  • M2. STORE MODIFIED UID-set expansion has no DoS ceiling (duplicates but doesn't reuse collector.ts's MAX_EXPANDED_UIDS cap) — src/commands/store.ts:213-230.
  • M3. Unbounded ESEARCH range expansion is a DoS vector from server data — src/commands/search.ts:482-516. (M2/M3 + collector share one root pattern: server-supplied UID/seq ranges materialized without a bound.)
  • M4. No recursion-depth limit on nested BODYSTRUCTURE/extension data → stack-overflow DoS — src/parser/structure/fetch/body.structure.ts:161-191,250-267,332-335.
  • M5. Unbounded recursion parsing nested ESEARCH data — src/parser/structure/mailbox/search.ts:338-357. (M4/M5 share the "no nesting-depth cap on untrusted server data" root; a single project-wide depth guard would address both.)
  • M6. MessageBodyStructure lacks the min-field-count validation Envelope has → raw TypeError on truncated data; parseParamList unguarded against undefinedsrc/parser/structure/fetch/body.structure.ts:208-246.
  • M7. Unbalanced nested parens silently corrupt list parsing (silent data loss, negative openParenCount) — src/parser/utility.ts:90-118.
  • M8. A single parse exception is fatal to the entire parser stream (amplifies H8/M6/M7) — decide + document recoverable-vs-fatal — src/parser/parser.ts:79-113.
  • M9. stream()-after-buffer() before the buffer settles hands the same live stream to two consumers → silent corruption — src/client/fetch.ts (stream() at 529-541 vs buffer() at 506-521).
  • M10. COMPRESS/UNAUTHENTICATE topology swap can corrupt the DEFLATE stream when a TCP segment straddles the boundary (discarded bytes may be real data, unlike the STARTTLS case) — src/connection/connection.ts:1427-1532,1574-1648.
  • M11. disconnect() is a silent no-op during the implicit-TLS connect() window (checks !this.socket but not connectInProgress) — src/connection/connection.ts:848-877.
  • M12. CommandQueue.hold()/release() cannot nest (plain boolean) though flushPending()'s doc claims nested support — src/connection/queue.ts:452-467. Confirm no nested-hold caller in connection.ts, or use a depth counter.
  • M13. SCRAM missing describeFailure() → loses server e= diagnostics on a tagged-NO rejection (its OAUTH siblings got this exact fix) — src/sasl/scram.ts:373-431. (Diagnostic only; accept/reject decision is unaffected and fail-closed.)
  • M14. Server-text CRLF/control sanitization (present in login.ts) not applied to authenticate.ts:210-227 and starttls.ts:52-74 (the latter literally builds message += "\r\n" + text) → log-injection from an untrusted/pre-auth peer.
  • M15. CompressCommand omits the capability field its sibling commands declare → Layer-2 client.run(new CompressCommand()) is ungated — src/commands/compress.ts.
  • M16. EnableCommand likewise has no capability field → ImapClient.run()'s capability enforcement (client.ts:1539) is silently skipped for ENABLE via the escape hatch — src/commands/enable.ts. (M15/M16 + M17 are one theme: capability/state gating gaps on the Layer-2 escape hatch.)
  • M17. ImapClient.compress() performs no client-state precondition check (unlike every other verb) and conflates "no connection" with "server declined" — src/client/client.ts:2029-2046.
  • M18. unauthenticate()'s post-await race handling doesn't cover a concurrent logout() (state left mislabeled) — src/client/client.ts:2183-2249.
  • M19. Stale/self-contradictory comment about parseTokens' null return (references removed push() code); underlying "bare null on the unknown event" question still open — src/parser/parser.ts:116-123.

LOW (grouped; titles + refs)

Parser/robustness: partial-decode state not cleared on non-partial decode failure (parser/encoding.ts:8-130); INTERNALDATE/SAVEDATE silently produce Invalid Date (fetch/internaldate.ts, fetch/extension.ts); oversized ESEARCH COUNT/MIN/MAX silently dropped (mailbox/search.ts:308-329); unanchored ESEARCH key-format regex (mailbox/search.ts:32-34).
Connection: tautological !this.isSecure STARTTLS check (connection.ts:693-698); connected local reused for two meanings (connection.ts:592,700); onSocketError param implicitly any (connection.ts:781-783); invalid tls config silently coerced to DEFAULT (connection.ts:450-462); deflate write path ignores backpressure (compress.ts:106-109); legacy untyped deepEqual helper (utils.ts:54-169); isIsolated conflates two queueModes (queue.ts:365).
Commands: MultiAppendCommand duplicates writeAppendMessageBody (append.ts:698-722); STORE emits FLAGS () for empty array — confirm intent (store.ts:162); mechanism.step() exception discarded on abort (authenticate.ts:115-142); IdCommand silently drops non-standard field names (id.ts:145-196); GmailLabelsStoreCommand no labels validation (gmail-labels.ts:77-83); GetMetadataCommand echoes raw (non-canonicalized) mailbox, inconsistent with siblings (metadata/get-metadata.ts:143,180); SEARCH RETURN error message omits RELEVANCY (search.ts:298).
SASL: public nonce-override footgun on public factory surface (scram.ts:105-140); NFKC applied to username but not authzid (scram.ts:242,246).
Client/mailbox: enableExtensions() skips state validation on empty result (client.ts:789-808); duplicate resync-flush comment block (mailbox.ts:718-735); MailboxUpdate flags variant hand-duplicates fields (mailbox.ts:216-220); duplicated modseq/flags construction (client.ts:1828-1881); stale M1.6-era class doc comment (client.ts:99-107, reported by two groups).
Docs: garbled duplicated doc fragment (protocol/response-codes.ts:1-43); RFC9051-A-1 classification asymmetry vs. RFC4422 rows (uncertainty); CHANGELOG "121 sources" figure not independently recounted.


Cross-cutting themes for the maintainer

  1. Untrusted-server-data hardening is the dominant risk. C1, H6, H7, H8, M2-M8 are all "a malformed/hostile server response crashes, hangs, over-allocates, or silently corrupts." A shared max-nesting-depth guard, a shared bounded UID-set expansion (reuse collector.ts's cap everywhere), and a decision on parser-stream fatal-vs-recoverable (M8) would close most of them at once.
  2. Connection/client teardown races (H1, H2, H3, M10, M11, M17, M18) — the file is already heavily hardened from a prior review round, but a few teardown/topology-swap paths remain.
  3. Layer-2 escape-hatch gating gaps (M14-M17) — the capability/state gate that ImapClient.run() provides is bypassed by a few commands that don't declare capability/state.

Totals

1 Critical · 11 High · 19 Medium · ~28 Low. No finding contradicts the documented adjudications; the adjudication ledger, MIGRATION.md, and README were verified accurate against code.


Generated by an automated multi-agent code review (orchestrator + per-group review-runner deep passes). The review-validator second-pass and 9 of 27 groups did not complete in this environment; findings are single-reviewer, self-verified by direct code tracing, and should be confirmed before acting — start with a reproduction test for C1.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Addendum to the automated review — two more groups completed (lexer + test-quality)

Two review groups returned after the main report above. Both add material findings, including a new High and a confirmed production bug tests route around. The overall recommendation is unchanged (HOLD for changes), and these reinforce the two dominant themes (untrusted-server-data robustness; under-tested complex parsers).

Added HIGH

H12. Lexer swallows unrecoverable tokenization errors as "incomplete" → unbounded memory / DoSsrc/lexer/lexer.ts:185-227 + src/lexer/rules/string.ts:20-25. _transform's catch unconditionally treats every tokenize() throw as "probably mid-literal, wait for more bytes." That's correct for the literal-length case but wrong for StringRule's "Unable to find end of string" (an unterminated quoted string — which per RFC can't cross a CRLF, so on an already-complete line it's terminal, not partial). The buffer is never reset and has no size cap, so a malformed/hostile server response makes the lexer accumulate all subsequent lines indefinitely (memory DoS), and a later stray " can retroactively "close" the bogus string and silently misparse large amounts of valid traffic. Fix: distinguish "needs more bytes" from "syntactically terminal" and propagate the latter via done(error).

H13. MessageHeader.mergeIn is broken and untested (confirmed production bug)src/parser/structure/fetch/header.ts:132 (and MessageBody.mergeIn, src/parser/structure/fetch/body.ts:115). withHeader.fields.forEach(([key, val]) => ...) destructures Map.forEach's callback wrong (it receives (value, key), not [key, value]), so the real field name is discarded and the value is mis-destructured. This is the code path for a header split across multiple FETCH responses. A test (test/unit/parser/structure/fetch/body.section.test.ts:140-152) explicitly documents the bug and tests around it; no test exercises mergeIn in either direction. Fix: iterate .entries()/for...of; add direct unit tests.

Added MEDIUM

  • M20. IMAPLogMessage public discriminated union isn't discriminating — "warn" is in both arms, so level === "warn" can't narrow to detail/errorsrc/types.ts:10-27 (re-exported public type).
  • M21. Lexer.tokenize() reassigns a shrinking substring per token → potential O(n²)/whole-buffer retention on very large single-line responses (e.g. 100k-UID SEARCH) — src/lexer/lexer.ts:283-318. Benchmark on target V8.
  • M22. IMAPError (legacy, Layer-1) vs ImapError (public §4) naming collision with no distinguishing comment; and Connection's connectionError type (IMAPError/ConnectionErrors) isn't exported from index.ts, so the sanctioned Layer-1 escape hatch's error channel isn't type-consumable — src/errors.ts, src/index.ts:228-230.
  • M23. Inconsistent literal +-marker ({n+}) tolerance between framing layers: newline.transform.ts:47 and lexer.ts:25 tolerate it but rules/string.ts:36 doesn't → below-threshold {n+} desyncs the two layers on adversarial input — confirm reachability with a test.
  • M24. No dedicated BODYSTRUCTURE unit test — only 3 incidental cases in tolerance.test.ts:312-370; test/unit/parser/structure/fetch/body.structure.test.ts is absent. This is the same parser as Critical C1 — the nested-multipart crash lives in an under-tested surface. Add systematic single/multipart/nested/malformed coverage.
  • M25. Compliance-suite expectFailure: "unimplemented" annotations and "unimplemented today" comments are stale vs. driver.ts (fetch/search/uidFetch are now wired) — the aggregator flags a passing-but-stale-hint case, but the annotation bookkeeping that keeps the compliance score honest can drift. Audit test/compliance/specs/** against driver.ts.

Added LOW

  • LiteralBodyStream.fed is write-only dead state (src/literal-body-stream.ts:69,121).
  • Dead *-boundary branch in AtomRule* already excluded by the atom char-class (src/lexer/rules/atom.ts:26,40-41).

Positive signal from the test-quality sample (G27)

Where tests exist, quality is high: SASL/SCRAM tests use real RFC worked-example vectors and fail-closed assertions; the wire-writer and sequence-set suites are byte-exact; STARTTLS tests use real sockets. Importantly, the compliance harness is not self-certifying — RFC text is extracted into a catalog independently of the implementation, unannotated failures default to "violation" (not "unimplemented"), and acknowledged gaps still count against the denominator. That is an honest scoring model. Caveat: only 9 of 133+ unit files and 2 of ~35 compliance specs were sampled.

Revised totals and coverage

1 Critical · 13 High · 25 Medium · ~30 Low. Groups: 19 of 27 returned complete reviews, 1 partial (ENABLE capability-gate finding, folded into M16); 7 did not complete in this environment — mailbox.ts parts 2-3, client config/facets, commands base/writer/collector, select/status/list, misc mailbox commands, and parser-misc (incl. the 15KB text.code.ts response-code parser). Those files got partial indirect coverage via adjacent groups but warrant a focused re-run before merge.


Automated multi-agent review addendum. Findings are single-reviewer deep-pass results (validator second-pass unavailable in this environment); confirm before acting. Start with reproduction tests for C1 (nested-multipart BODYSTRUCTURE) and H13 (mergeIn), which are the two most concrete confirmed defects.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Addendum 2 — parser-misc group completed (resp-text-code / capability / THREAD)

One more previously-incomplete group returned (src/parser/structure/ misc: text.code.ts, capability.ts, untagged.ts/tagged.ts, quota.ts, thread.ts, etc.). Recommendation unchanged (HOLD for changes).

Added MEDIUM

  • M26. COMPRESS=DEFLATE (and the other KIND=VALUE entries in standardCapabilityNames) is misclassified as an unknown capability — CapabilityList.add() branches on includes("=") before consulting isStandardCapability(), so those entries are dead code and COMPRESS's kind isn't in kindValueStandardCapabilityNames, yielding isUnknown = true for a widely-deployed extension. Consumers filtering/logging on isUnknown misreport it — src/parser/structure/capability.ts:365-390,186-231,49-61.
  • M27. MODIFIED resp-text-code reuses the UID-only UIDSet, which rejects the legal "*" wildcard when MODIFIED rides a plain (non-UID) STORE/EXPUNGE (message-sequence context) — e.g. [MODIFIED 2:*] throws — src/parser/structure/text.code.ts:148-159, src/parser/structure/uid.ts:37-98. (Medium-confidence; verify RFC 7162 §3.8 ABNF.)
  • M28. Untagged * OK/NO/BAD/BYE [code ...] lines whose resp-text-code throws lose all structure (fall through to UnknownContent) — asymmetric with the graceful "bare status word" fallback this PR added for the tagged path. A single malformed code (e.g. the UIDVALIDITY 0 server quirk, or M27) silently destroys an otherwise-informative SELECT/EXAMINE untagged OK — src/parser/structure/untagged.ts:110-151 vs src/parser/structure/tagged.ts:30-56.

Added LOW

  • Several .match() methods (quota.ts, sort.ts, thread.ts, vanished.ts, urlauth.ts) slice past an unverified SP token and access tokens unguarded → raw TypeError instead of ParsingError on truncated input (currently masked by the untagged blanket try/catch).
  • Unbounded recursion depth parsing nested THREAD responses (src/parser/structure/thread.ts:11-41) — same untrusted-server recursion theme as C1/M4/M5.

Coverage update

Now 20 of 27 groups returned complete reviews + 1 partial. Still incomplete (forced-async pipeline stalls): 6 groupsmailbox.ts parts 2-3 (the bulk of the 3047-line MailboxSession), client config/facets, commands base/writer/collector infra, select/status/list, and misc mailbox commands. writer.ts/collector.ts/base.ts and mailbox.ts received partial indirect coverage from adjacent groups, but a focused re-run of these six is the main outstanding gap.

Running totals: 1 Critical · 13 High · 28 Medium · ~32 Low.


Automated multi-agent review, final addendum. The recurring theme across the parser findings (C1, H8, H12, M4-M8, M23, M27, M28, and the THREAD/QUOTA items) is untrusted-server-data robustness — a shared nesting-depth cap, bounded expansion, and a uniform "degrade the code, keep the response" tolerance policy on the untagged path would resolve most of them together.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Addendum 3 — validation pass complete; four previously-incomplete groups now returned + validated

Four of the six groups flagged incomplete in the main report finished after it was posted, each with its review-validator pass run (the step missing from the earlier comments). Recommendation unchanged: HOLD for changes, then merge-with-follow-ups. Validation earned its keep this round — it rejected one High candidate (see below), so the findings here are higher-confidence than the main report's single-pass results.

Groups now complete + validated: G4 (mailbox.ts message-op dispatch, lines 1016–2030), G7 (config/auth/capabilities/facets), G8 (command infra: base.ts/writer.ts/collector.ts — all 7 lenses, findings validated by direct re-inspection), G15 (expunge/copy/move/create).


Added HIGH

H14. seq.replace() skips the NOTIFY expunge-safety guard → stale MSN can replace/delete the wrong messagesrc/client/mailbox.ts:1610-1651 (runReplace). Every sibling seq-grain static (runStore, runGmailLabelsStore, runConvert, runCopyOrMove, runFetch) calls assertSequenceGrainSafeUnderNotify(); runReplace never does. Under an active SELECTED MessageExpunge NOTIFY registration, seq.replace(<seq>, …) can be dispatched with a sequence number that resolves to a different message by the time the server parses it — and because REPLACE atomically appends-and-removes (RFC 8508 §3.2), a stale MSN silently replaces/deletes the wrong message. This is exactly the class of hazard docs/compliance-adjudications.md's RFC5465-5.3-2 entry says the library refuses everywhere — but REPLACE was missed, and the omission is not itself adjudicated. Validator: Valid. Fix: add the hasActiveNotifySelectedMessageExpunge() refusal for kind === "seq", mirroring the siblings; add coverage.

Added MEDIUM

  • M29. UID EXPUNGE capability gate ignores IMAP4rev2's base-protocol fold-insrc/client/mailbox.ts:1809-1817 gates on !hasCapability("UIDPLUS") alone; ExpungeCommand.capability is a bare "UIDPLUS" (src/commands/expunge.ts:85-87). Sibling rev2-absorbed verbs OR-gate correctly (idle() :1102, unselect() :1327, MOVE :1866-1867). The repo's own catalog (test/compliance/catalog/ext/rfc4315.ts:62) confirms RFC 9051 §6.4.9 absorbs UID EXPUNGE into rev2-base. A pure-IMAP4rev2 server not separately advertising UIDPLUS gets every expunge(uids)/seq.expunge() rejected with CapabilityError. Validator: Valid (fails closed, so Medium not High). Fix: OR in IMAP4rev2; widen ExpungeCommand.capability to the array form.
  • M30. [CAPABILITY …] resp-code reported as "CAPABILITIES", not the wire keywordsrc/commands/collector.ts:111,291-295. name is derived from CapabilityTextCode.kind (a plural display label, explicitly documented in text.code.ts as distinct from the singular wire keyword), so any consumer of ResponseCollector.codes() / ServerNoError.code matching code.name === "CAPABILITY" silently fails. Found by two independent lenses; confirmed by direct re-inspection. Fix: emit the literal "CAPABILITY" in that branch.
  • M31. ResponseCollector.settle() lacks the terminal-state guard abort() hassrc/commands/collector.ts:406-410 vs 436-443. settle() has no if (this.settledFlag) return;, though its own doc comment claims the "same single-terminal-state discipline." A settle() landing after an abort() (teardown racing the final tagged response) overwrites taggedResp while leaving abortErr set, so tagged() and live() disagree about whether the command succeeded. Confirmed by direct re-inspection. Fix: add the first-wins guard so the invariant is self-enforced, not caller-dependent.
  • M32. expandUidSet() throws a bare RangeError, violating the "every public promise rejects with ImapError" invariantsrc/commands/collector.ts:55-79, reached unguarded from CopyCommand.accept() (copy.ts:90), MoveCommand.accept() (move.ts:111,123), and defaultOnError (base.ts:211). A hostile/non-conformant server sending a >1M-wide COPYUID/MODIFIED range surfaces a RangeError to the caller instead of an ImapError subtype (worst case, an uncaught synchronous throw on the live untagged path). Validator: Valid (Low-Medium; the actionable fix lives in shared infra — wrap toTypedResponseCode()).
  • M33. Two resp-code citations attribute arguments to RFCs that don't define themsrc/commands/collector.ts:191-211. APPENDLIMIT is treated as an RFC 7889 resp-code, but 7889 defers over-limit APPEND to TOOBIG (RFC 4469) and defines no such resp-code; BADCOMPARATOR parses a trailing charset arg that RFC 5255 §4.9's ABNF (resp-text-code =/ "BADCOMPARATOR", argument-less) doesn't have. Both corroborated by the repo's own compliance catalog. Fix: drop/rename the speculative branches or correct the citations.
  • M34. ExpungeCommand is omitted from the src/commands/index.ts barrel export — all 9 sibling commands are exported; this one is reachable externally only via the documented client.run(new ExpungeCommand(...)) escape hatch (README:187-190), which the omission silently breaks. Validator: Valid.
  • M35. No authzid config field → EXTERNAL/ANONYMOUS SASL can't carry non-empty trace infosrc/client/config.ts:30-41,186-190. validateAuth() also throws unless pass/accessToken is set, with no carve-out for mechanisms that need neither, so the config-embedded path rejects them outright. A 1.0 that fully implements both mechanisms exposes no first-class way to use their defining feature. Validator: Valid. Fix: add authzid?: string, thread it into the SaslContext, relax the pass/token requirement for no-secret mechanisms.
  • M36. idle()'s IdleController isn't torn down on session closesrc/client/mailbox.ts:1100-1119. Unlike updates() (which subscribes to "closed"), idle() never reacts to close/reselect; after a reselect mid-idle it can silently idle on the newly-selected mailbox. Validator: Valid (Medium — reachable without caller misuse).

Added LOW (grouped)

fetchOne()'s doc comment contradicts fetchOneOf()'s actual (protect-not-destroy) behavior (mailbox.ts:1039-1049); astring()'s "never throws on content grounds" is false for NUL (writer.ts:553-559); writer atom-validation over-excludes "[" (legal ATOM-CHAR) and mis-cites it to RFC atom-specials (writer.ts:82-104); SEQUENCE_SET_RE is a character-class check the docs oversell as grammar validation (writer.ts:305,728-737); stale "future milestone / not-yet-wired" comments in base.ts (states/capability are enforced) and "(stubbed)" mailbox-codec comment; CreateCommand's empty-USE () refusal misquotes RFC 6154 §6 (the ABNF list is optional) — also duplicated in the test title test/unit/client/mailbox-verbs.test.ts:83; config.ts allowInsecureAuth coerces instead of validating; defaultCandidates() silently omits CRAM-MD5; plus assorted naming/dup/declare-comment consistency items across the four groups.


Validation rejected one candidate (working as intended)

REJECTED — deferred-dispatch (chainFamily) "wrong-mailbox" race (originally drafted High). The validator traced it and found it timing-impossible: SelectCommand.queueMode is "serial", so a reselect enqueued behind an in-flight same-family command cannot even write bytes until that command drains; and performSelectOrExamine() holds state at "authenticated" for the whole SELECT round-trip, so the deferred continuation (firing microtasks after the prior command settles) hits ImapClient.run()'s state gate and throws StateError long before SELECT completes. No data-integrity hazard; at most a harmless defensive _closed re-check. Not included as a defect.


Coverage after this pass

24 of 27 groups complete (23 with a validator pass; G8 validated by direct re-inspection). Still without a dedicated findings pass: 2 groupsselect/status/list and the mailbox.ts tail (lines 2031–3047, the SeqFacet delegator + updates()/fetch drivers). Both received full context-gathering, and the mailbox tail's fetch-driver surface was covered indirectly by the fetch/idle/state group (H9/H10/M9). Candidly: further sub-agent fan-out was halted by the account's monthly spend limit (one runner terminated on it mid-pass) — those two groups are the remaining gap, best closed by a focused re-run once budget allows.

Final running totals

1 Critical · 14 High · ~36 Medium · ~45 Low, across 24 reviewed groups. No finding contradicts the documented adjudications; the adjudication ledger, MIGRATION.md, and README were verified accurate against code. Start remediation with C1 (nested-multipart BODYSTRUCTURE), H13 (mergeIn), and H14 (seq.replace NOTIFY guard) — the three most concrete confirmed defects.


Automated multi-agent review — validation pass. Findings validated by review-validator sub-agents (or, for G8, direct re-inspection); one High candidate was rejected on validation. Two groups remain uncovered due to a spend-limit halt, disclosed above rather than papered over.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Addendum 4 — coverage complete: final two groups reviewed + validated (27/27)

The two groups still open after Addendum 3 — select/status/list and the mailbox.ts tail (2031–3047) — are now reviewed. Because the account spend limit blocked further agent fan-out, I ran these as direct single-reviewer deep passes with inline validation (each candidate checked against the repo's own compliance catalog before standing — the same catalog the suite extracts RFC text into). That validation step rejected one candidate and confirmed one, below. All 27 groups are now covered. Recommendation unchanged: HOLD for changes, then merge-with-follow-ups.

Added MEDIUM

M37. RETURN (STATUS …) gate ignores IMAP4rev2's LIST-STATUS fold-insrc/commands/list.ts:452. The outer gate is statusItems.length > 0 && !caps.has("LIST-STATUS"), with no || caps.has("IMAP4rev2"). The repo's own catalog is explicit: test/compliance/catalog/ext/rfc5819.ts:41"REV2-CORE ADJUDICATION … RFC 9051 folded LIST-STATUS into rev2 core." So a bare-IMAP4rev2 server that doesn't separately advertise LIST-STATUS has every list({ returnStatus: […] }) call rejected with CapabilityError. This is internally inconsistent with the codebase's own established handling of the same rev2-fold-in pattern: the per-item STATUS gate already ORs in rev2 (status.ts:80, SIZE → STATUS=SIZE || IMAP4rev2), NamespaceCommand uses capability = ["NAMESPACE","IMAP4rev2"] (namespace.ts:44), and MOVE/UNSELECT/IDLE all OR-gate — only the LIST-STATUS outer gate was missed. Same class as M29 (UID EXPUNGE). Fails closed, so Medium. Fix: !caps.has("LIST-STATUS") && !caps.has("IMAP4rev2").

Added LOW

L. Attribute-algebra step order lets a contradictory listing keep both child-state attributessrc/commands/list.ts:140-151. The \HasChildren+\HasNoChildren contradiction-collapse (lines 141-144) runs before the \NoInferiors ⇒ \HasNoChildren inference (145-147). A server sending the (self-contradictory) pair \NoInferiors \HasChildren therefore ends with both \HasChildren and \HasNoChildren in the normalized set, since the collapse already ran before \HasNoChildren was added. Only triggers on server-contradictory input and the result is arguably acceptable ("report what the server said"), but the algebra's stated intent is to prevent exactly that pair. Fix: re-run (or reorder) the contradiction check after the implication step.

Validation rejected one candidate (working as intended)

REJECTED — "SPECIAL-USE gate missing IMAP4rev2 fold-in." I suspected list.ts:470's !caps.has("SPECIAL-USE") (no rev2 fallback) was the same bug as M37. The catalog disproves it: test/compliance/catalog/ext/rfc6154.ts:41"REV2-CORE ADJUDICATION … RFC 9051 (IMAP4rev2) did NOT absorb RFC 6154's special-use surface into core … RFC 6154 remains a standalone extension under rev2." Gating SPECIAL-USE on its own capability with no rev2 fallback is therefore correct. Not a finding.

Verified clean (no findings)

  • mailbox.ts tail (2031–3047): the refcounted live-update driver's construction-race guards (ST2/ST3 — placeholder-write + released flag) correctly prevent orphaning the IdleController under every interleaving traced; createUpdatesIterator's cleanup paths are symmetric (the 147ce7e fix closed the one real listener leak; return()/throw()/onClosed all unsubscribe idempotently and queued updates still drain post-close); driveFetch/fetchOneOf/drainAbandoned correctly use raw .next()+protectLiveParts() (never .return()) so a returned message's live parts survive while the command still settles. SeqFacet is a pure delegator.
  • select.ts: CONDSTORE gated on plain advertisement, QRESYNC on the _enabled-aware probe, the combined-refusal is the adjudicated SF3 decision, and the qresync-param grammar guards (uidvalidity nz-number, seqMatch-requires-knownUids, pre-write validation) are all correct.
  • status.ts: the per-item gate table correctly encodes every rev2 fold-in (SIZE, DELETED) and correctly excludes MAILBOXID (OBJECTID is not rev2-core, per catalog); name attribution is by-mailbox, never most-recent-wins.
  • lsub.ts / namespace.ts: LSUB correctly ungated (rev1 base grammar), RLSUB/RLIST/NAMESPACE gates correct.

Final coverage & totals

27 of 27 groups reviewed. 23 groups carried a review-validator sub-agent pass; the remaining 4 (G8 command-infra + these last two) were validated by direct re-inspection against source and the compliance catalog after the spend limit halted agent fan-out. Across the full review, validation rejected 3 candidate findings (the chainFamily race, the SPECIAL-USE gate, and — in earlier group passes — assorted mis-scoped items) and corrected numerous priorities, which is the point of running it.

Final totals: 1 Critical · 14 High · ~37 Medium · ~47 Low. No finding contradicts the documented adjudications; the adjudication ledger, MIGRATION.md, and README were verified accurate against code. Remediation priority remains C1 (nested-multipart BODYSTRUCTURE), H13 (mergeIn), H14 (seq.replace NOTIFY guard).


Automated multi-agent review — final coverage-closing pass. The last two groups were reviewed inline (single-reviewer + catalog-validated) because the account spend limit blocked further sub-agent runs; that constraint is stated rather than hidden.


Generated by Claude Code

claude added 8 commits July 18, 2026 06:09
…ening

Second-review parser/fetch findings (all verified; each revert-verified
with tests):
- CRITICAL C1: nested-multipart BODYSTRUCTURE no longer throws an
  uncaught TypeError. The multipart-children loop built every child as a
  single-part structure; and the 'correct' MESSAGE/RFC822 detection the
  review cited was ITSELF broken (misparsed every embedded single-part
  body into a bogus multipart). Fixed with one shared
  parseBodyStructureFromParts() using the real discriminant (is the
  first field a parenthesized list) at all three call sites.
- H6: body-fld-octets/body-fld-lines accept number|bigint (RFC 9051
  App. D-1), matching RFC822SIZE — a >2^32 body part no longer throws.
- H13: MessageHeader.mergeIn iterates fields.entries() correctly
  (Map.forEach callback is (value,key), not [key,value]) — the
  split-header merge path was silently discarding field names. The test
  that documented-and-tested-around the bug now asserts correct behavior.
- M4: BODY_STRUCTURE_MAX_DEPTH=100 recursion cap (stack-overflow DoS).
- M6: min-field-count guard on single-part BODYSTRUCTURE (typed
  ParsingError, not raw TypeError, on truncated data).
- M24: new 18-test body.structure.test.ts (single/multipart/nested/
  malformed/bigint/truncated/depth/embedded).
- Lows: INTERNALDATE/SAVEDATE reject Invalid Date; address.ts truncated-
  tuple guard. envelope.ts finding disproven (already guarded).

Tests 2087 pass, typecheck 0, eslint 0 errors, compliance per-row
identical (1138/6/2/451, problems []).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Second-review lexer/capability/types findings (all verified; each
revert-verified with tests; M21 benchmarked as a non-issue):
- H12: the lexer no longer buffers forever on a terminal tokenization
  error. New UnterminatedStringError (a quoted string can't cross CRLF,
  so on a complete line it's terminal, not mid-literal) is propagated
  via done(error); a 10 MiB buffer cap backstops any other runaway. A
  hostile unterminated quote no longer accumulates all later bytes or
  gets retroactively 'closed' by a stray quote.
- M20: IMAPLogMessage's discriminated union now discriminates —
  'warn' removed from the error arm (verified every warn call site
  carries only detail), so level==="error" narrows correctly. Compile-
  only type test added under test/docs.
- M23: {n+} literal marker now tolerated consistently in StringRule +
  token stripping (was tolerated by the newline/lexer framing layers
  but not the string rule → below-threshold desync).
- M26: COMPRESS=DEFLATE (and other KIND=VALUE registered capabilities)
  no longer misreport isUnknown=true.
- Lows: encoding.ts clears stale partial-decode state on the hard-
  failure path; dead AtomRule '*' branch removed.
- M21: tokenize() benchmarked linear (V8 SlicedString), NOT O(n²) —
  no rewrite; a perf regression guard added.

Tests 2111 pass, typecheck 0 (incl. doc-samples), eslint 0 errors,
compliance per-row identical (1138/6/2/451, problems []).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…e stream

Second-review parser-core findings (all verified; each revert-verified;
four Low candidates disproven as already-guarded):
- M8 (linchpin): a per-line ParsingError now degrades to an 'unknown'
  event and parsing continues; only a genuinely non-ParsingError
  exception stays fatal (Parser holds no cross-line state). This makes
  H7/H8/M5/M7 non-catastrophic.
- H7: empty LIST mailbox name accepted — LIST "" "" delimiter-
  discovery (* LIST (\Noselect) "/" "") parses instead of falling
  to UnknownContent.
- H8: '( )'-shaped empty list block raises ParsingError, not a raw
  TypeError that permanently errored the Transform.
- M5 / THREAD: shared MAX_NESTED_LIST_DEPTH cap on nested ESEARCH and
  THREAD recursion (measured multi-second DoS at deep nesting).
- M7: unbalanced parens (over- or under-closed) raise ParsingError
  instead of silently corrupting grouping.
- M27: [MODIFIED 2:*] accepts the '*' wildcard (RFC 7162 §3.8 —
  MODIFIED carries a sequence-set, not a uid-set) via an opt-in
  UIDSet.allowWildcard; UID-only usages untouched.
- M28: untagged * OK/NO/BAD/BYE with a throwing resp-code preserves the
  status word + text (mirrors the tagged-path tolerance) instead of
  losing all structure.
- M19: stale parseTokens comment corrected (no bug).
- Lows: quota.ts truncated-triplet guard; ESEARCH oversized
  COUNT/MIN/MAX preserved as bigint not dropped; anchored ESEARCH key
  regex. sort/thread-match/vanished/urlauth findings disproven
  (already guard truncated input).

Tests 2150 pass, typecheck 0, eslint 0 errors, compliance per-row
identical (1138/6/2/451, problems []).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…uthzid

Second-review search/SASL/config findings (all verified; each revert-
verified; two candidates verified not-bugs, one Low deferred):
- H5: empty-array SEARCH criteria ({keyword:[]}, {header:[]}, {and:[]},
  nested empty or/fuzzy/not) now throw instead of compiling to a
  wire-invalid bare SEARCH / short-operand OR — estimateKeyCount counts
  an empty array as 0 (was Math.max(len,1)), with a top-level zero-token
  check and empty-operand throws in not/and.
- M2: STORE MODIFIED expansion routed through collector's bounded
  expandUidSet (deleted the duplicate that omitted the 1M cap) — a
  [MODIFIED 1:4294967295] response no longer attempts a billion-entry
  allocation.
- M3: ESEARCH ALL/PARTIAL range expansion bounded (delegates to the
  shared expandUidSet where possible; local cap mirrors it otherwise).
- M13: ScramMechanism.describeFailure() surfaces the server e=
  diagnostic on the tagged-NO path (mirrors the OAUTH siblings;
  fail-closed verdict untouched).
- M35: authzid config support closed end-to-end — ImapAuthConfig gains
  authzid?, validateAuth() relaxes the pass/token requirement only for
  an explicit EXTERNAL/ANONYMOUS-only mechanisms list, and
  performAuthSelection threads authzid into the SaslContext (which
  EXTERNAL/ANONYMOUS already consume).
- Lows: NFKC now applied to authzid too (was username-only); config
  allowInsecureAuth validates (was silent coercion); SEARCH RETURN
  error hint lists RELEVANCY. Verified-not-bugs: empty FLAGS () is legal
  grammar; defaultCandidates() omitting CRAM-MD5 is spec §9.3. Deferred:
  the public SCRAM nonce-override footgun (test infra imports it via the
  public path — flagged for a separate follow-up).

Tests 2198 pass, typecheck 0, eslint 0 errors, compliance per-row
identical (1138/6/2/451, problems []).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…ctor + rev2 gates

Second-review message-op findings (all verified; each revert-verified):
- H9: BODY[section] and BINARY[section] no longer collide on the parts
  Map key (composite section+binary key; part() gains a {binary}
  disambiguator, BODY-first for back-compat).
- H10: part.buffer() rejects (ImapError) when the stream is destroyed
  mid-drain (added the missing 'close' listener) instead of hanging.
- H14: seq.replace() now applies assertSequenceGrainSafeUnderNotify
  like every sibling — a stale MSN under an active MessageExpunge
  NOTIFY can't silently replace/delete the wrong message (reuses the
  existing RFC5465-5.3-2 refusal class).
- M1: FETCH section-spec / binarySize validated against the RFC grammar
  before emission (blocks ]/space injection into BODY[...]/BINARY[...]).
- M9: stream()-after-buffer() while a drain is in flight now throws
  (single-consumer discipline).
- M29: UID EXPUNGE OR-gates IMAP4rev2 (mailbox gate + ExpungeCommand
  capability array) — pure-rev2 servers no longer get expunge(uids)
  rejected.
- M30: [CAPABILITY] resp-code reports the wire keyword "CAPABILITY"
  (was the plural display label "CAPABILITIES").
- M31: ResponseCollector.settle() gains abort()'s first-wins terminal
  guard (settle-after-abort no longer disagrees with itself).
- M32: expandUidSet on the resp-code path (COPYUID/APPENDUID/MODIFIED)
  rethrows ProtocolError (ImapError), not a bare RangeError.
- M33: dropped the fabricated APPENDLIMIT typed branch (RFC 7889
  defines no such resp-code; falls through to the tolerant fallback);
  BADCOMPARATOR comment corrected (kept — the suite pins a real server
  emitting a trailing charset).
- M34: ExpungeCommand + FetchCommand added to the commands barrel
  (fixed a latent SequenceSetLike double-export collision).
- M36: idle() tears down its IdleController on session close/reselect
  (once('closed')), mirroring updates().
- M37: RETURN (STATUS) OR-gates IMAP4rev2 (LIST-STATUS rev2 fold-in);
  SPECIAL-USE correctly left non-folded.
- Lows: list attribute-algebra order (contradiction after implication);
  corrected stale base.ts/fetchOne() doc comments; factored a duplicated
  uid/modseq spread helper.

Flagged for follow-up (out of R2-E1 territory): comparator/convert/
gmail-labels/language/replace/store also missing from the barrel;
CreateCommand's RFC 6154 §6 USE() misquote.

Tests 2233 pass, typecheck 0, eslint 0 errors, compliance per-row
identical (1138/6/2/451, problems []).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
Second-review connection/client findings (all verified; each revert-
verified; H2 disproven, M16 investigated-and-correctly-unchanged):
- H1: teardownFailedConnect() now emits 'disconnected' and a mid-connect
  disconnect() drives the same abort mechanism a socket error uses, so
  close()/logout() racing an in-flight connect() no longer hangs past
  timeout; port validation moved before connectInProgress is set.
- H3: an opportunistic (STARTTLS_OPTIONAL) server decline — a tagged
  NO/BAD mapped to TlsError — now continues plaintext instead of
  tearing down the connection; mandatory STARTTLS still hard-fails.
- H4: the client 'unhandled' bridge passes the full response union, so
  unmatched-tag TaggedResponse / unclaimed ContinueResponse (protocol-
  desync signals) surface instead of being silently dropped.
- M10: COMPRESS activation preserves residual bytes captured at the
  tagged-OK instant by pausing line-splitting (beginOpaqueCapture) and
  seeding them into the fresh inflate, instead of discarding its own
  opening bytes and desyncing decompression. (UNAUTHENTICATE side is
  correctly discard-only — its boundary sits downstream of any live
  codec; the 'preserve' variant there was reverted as a no-op.)
- M11: disconnect() works during the implicit-TLS handshake window
  (openTls exposes the socket synchronously via onSocket).
- M14: server-text sanitization (shared sanitizeForErrorMessage) applied
  to authenticate.ts + starttls.ts (the latter literally built
  message += CRLF + text) — log-injection from an untrusted/pre-auth
  peer is escaped.
- M15: CompressCommand declares capability COMPRESS=DEFLATE (Layer-2
  escape-hatch gate). M17: ImapClient.compress() rejects StateError from
  disconnected/connecting/logout (no longer conflates no-connection with
  server-declined). M18: unauthenticate() checks state first and rejects
  a typed StateError on a concurrent logout() race instead of
  mislabeling the session close + falsely resolving.
- M12: flushPending() nesting doc corrected (non-nesting is structural).
- Lows: tautological isSecure conjunct removed; onSocketError typed;
  dead deepEqual helper removed. H2 disproven (teardown already resets
  postBoundaryDiscard). M16 (ENABLE capability gate) left as-is —
  OR-with-rev2 broke real-world-server fixtures; documented in-file.

newline.transform.ts gained beginOpaqueCapture/endOpaqueCapture/
injectBytes for M10 (last touched at M3.2; no concurrent edits).

Tests 2252 pass, typecheck 0, eslint 0 errors, compliance per-row
identical (1138/6/2/451, problems []).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW
…misquote

Follow-ups the R2-E1 agent flagged as out of its territory:
- M34 (rest): comparator/convert/gmail-labels/language/replace/store
  were also missing from the src/commands/index.ts barrel (the public
  ./commands subpath), so a caller couldn't hand-build them for
  client.run(). Added all six exports (no export collisions) and
  extended the barrel test to pin them.
- Low: CreateCommand's empty-specialUse RangeError message misquoted
  RFC 6154 §6 as requiring >=1 use-attr; the catalog's own §6 ABNF
  brackets the list as optional ("USE" SP "(" [use-attr *(SP
  use-attr)] ")"), so an empty (USE ()) is legal. Reframed the message
  + comment + test title as the honest rationale (it's a client-side
  guardrail against a no-op, not a grammar requirement); behavior
  unchanged.

Tests 2258 pass, typecheck 0, eslint 0 errors, compliance per-row
identical (1138/6/2/451, problems []).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jvj25KC7SRUFHsyTzswqsW

Copy link
Copy Markdown
Owner Author

Second review addressed — the Critical, all 14 High, and the Medium/Low tier

Thank you for the second, much wider pass (and for the candid coverage/validation disclosures — they made triage faster). Everything actionable is fixed. Same discipline as before: every finding verified against the code before any change, each real fix landed with a unit test and an undo→red→redo→green revert-verification, and the compliance matrix stayed byte-identical throughout (1138 pass / 6 adjudicated violations / 2 adjudicated-unimplemented / 451 untestable / problems: []). Unit tests 2053 → 2258; typecheck, eslint (0 errors), and compliance green at every merge.

Seven commits, file-disjoint clusters:

  • 06c414c parser fetch / BODYSTRUCTURE · a788113 lexer / capability / types · 46bdb43 parser-core resilience · 846e3dd search / SASL / config · 229038b message-op lifecycle · f6d15af connection / client teardown · dde8139 barrel + RFC 6154 cleanup

Critical

  • C1 nested-multipart BODYSTRUCTURE crash — fixed. And while fixing it I found the MESSAGE/RFC822 detection the finding cited as the "correct pattern" was itself broken (its [1].length===1 check is true for both single- and multi-part shapes, silently misparsing every embedded single-part body). One shared parseBodyStructureFromParts() with the real discriminant (is the first field a parenthesized list) now serves all three call sites. New 18-test suite.

High — all 14 fixed

H1 connect()-teardown race (teardownFailedConnect now emits disconnected; mid-connect disconnect() drives the same abort path) · H3 opportunistic-STARTTLS decline no longer tears down the connection · H4 unhandled event passes the full response union · H5 empty-array SEARCH criteria now throw · H6 body-fld-octets/lines accept bigint · H7 empty LIST name (LIST "" "" delimiter discovery) accepted · H8/M8 a malformed line degrades to an unknown event instead of permanently erroring the parser stream (the linchpin — makes the whole "malformed line" family non-catastrophic) · H9 BODY[section]/BINARY[section] composite key · H10 buffer() rejects on destroy instead of hanging · H12 lexer no longer buffers forever on an unterminated quoted string (terminal UnterminatedStringError + a 10 MiB backstop) · H13 mergeIn iterates .entries() correctly (the confirmed production bug the test routed around — that test now asserts correct behavior) · H14 seq.replace() applies the NOTIFY expunge-safety guard like every sibling.

Medium — addressed

M1 section-spec injection validated · M2/M3 STORE-MODIFIED & ESEARCH range expansion now share the bounded-expansion cap · M4/M5 recursion-depth caps (BODYSTRUCTURE, ESEARCH, THREAD) · M6 truncated-BODYSTRUCTURE typed guard · M7 unbalanced parens → ParsingError · M9 stream()-after-buffer() single-consumer · M10 COMPRESS boundary now preserves straddling bytes into the fresh inflate (rather than discarding its own opening bytes) · M11 disconnect() works during the implicit-TLS window · M13 SCRAM describeFailure() · M14 server-text sanitization in authenticate/starttls · M15 CompressCommand capability field · M17 compress() state precondition · M18 unauthenticate()/logout() race → typed StateError · M20 IMAPLogMessage union now discriminates · M23 {n+} layer consistency · M26 COMPRESS=DEFLATE no longer misclassified unknown · M27 [MODIFIED 2:*] accepts the wildcard · M28 untagged resp-code tolerance (mirrors the tagged path) · M29 UID EXPUNGE OR-gates IMAP4rev2 · M30 [CAPABILITY] reports the wire keyword · M31 collector settle() terminal guard · M32 expandUidSet → ImapError on the resp-code path · M33 dropped the fabricated APPENDLIMIT branch / corrected BADCOMPARATOR comment · M34 all missing commands added to the ./commands barrel · M35 authzid config support (end-to-end) · M36 idle() tears down on session close/reselect · M37 RETURN (STATUS) OR-gates IMAP4rev2.

Low — addressed

NFKC on authzid, allowInsecureAuth validation, encoding partial-decode leak, dead deepEqual/AtomRule * removed, INTERNALDATE/SAVEDATE reject Invalid Date, address-tuple guard, list attribute-algebra order, the RFC 6154 §6 empty-(USE ()) message corrected (your catalog's own ABNF brackets the list optional — reframed as an honest client-side guardrail), stale doc comments swept, and more.

Verified not a bug / disproven (matching your validator's spirit)

  • H2 postBoundaryDiscard — already reset by both teardown chokepoints.
  • M21 tokenize O(n²) — benchmarked linear (V8 SlicedString makes the tail-slice O(1)); added a perf regression guard instead of a rewrite.
  • M16 ENABLE capability gate — an OR-with-rev2 gate breaks this repo's own fixtures (servers advertising bare IMAP4rev1 that genuinely support ENABLE, as real servers do); left as-is with the trade-off documented in-file.
  • Four parser .match() truncation Lows (quota-adjacent) — already guard truncated input.
  • Empty FLAGS () (legal flag-list grammar); CRAM-MD5 omission from auto-select (spec §9.3, still usable via explicit mechanisms).

Deferred, explicitly

  • Public SCRAM nonce-override footgun (M-tier Low): the compliance harness imports it via the public ./sasl path for deterministic RFC vectors, so a rename/reroute is a self-contained follow-up rather than a bundled change.
  • Still open from round one: the escape-hatch cleartext-credential plumb-through and instant graceful-FIN detection.

M10's fix required touching newline.transform.ts (the byte-buffering state is private there); no other cluster touched that file. Ready for another look whenever you are.


Generated by Claude Code

Cleaning up with prettier

**Test plan:**
Run tests
Removing all the compliance history files as those aren't needed to retain in the repo.

**Test plan:**
Make sure the files are gone
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants