serverless-ircd 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/.github/workflows/ci.yml +7 -3
  2. package/.gitmodules +1 -1
  3. package/CHANGELOG.md +518 -29
  4. package/README.md +221 -161
  5. package/apps/aws-stack/package.json +1 -1
  6. package/apps/aws-stack/src/aws-stack.ts +186 -18
  7. package/apps/aws-stack/tests/stack.test.ts +400 -56
  8. package/apps/cf-tcp-container/package.json +1 -1
  9. package/apps/cf-tcp-container/src/container-server.ts +21 -1
  10. package/apps/cf-tcp-container/tests/config-loader.test.ts +43 -0
  11. package/apps/cf-tcp-container/tests/container-server.test.ts +249 -1
  12. package/apps/cf-tcp-container/tests/persistence.test.ts +9 -0
  13. package/apps/cf-tcp-container/tests/tls-e2e.test.ts +24 -5
  14. package/apps/cf-worker/package.json +1 -1
  15. package/apps/cf-worker/src/worker.ts +4 -4
  16. package/apps/cf-worker/tests/fixtures/web-dist/webclient/index.html +18 -0
  17. package/apps/cf-worker/tests/smoke.test.ts +5 -5
  18. package/apps/cf-worker/wrangler.test.toml +6 -6
  19. package/apps/cf-worker/wrangler.toml +7 -5
  20. package/apps/local-cli/package.json +1 -1
  21. package/apps/local-cli/src/config-loader.ts +8 -0
  22. package/apps/local-cli/src/main.ts +16 -0
  23. package/apps/local-cli/src/server.ts +95 -31
  24. package/apps/local-cli/tests/config-loader.test.ts +14 -0
  25. package/apps/local-cli/tests/config-resolution.test.ts +65 -0
  26. package/apps/local-cli/tests/motd-file-non-error.test.ts +29 -0
  27. package/apps/local-cli/tests/rehash.test.ts +147 -0
  28. package/apps/local-cli/tests/server-helpers.test.ts +63 -0
  29. package/apps/local-cli/tests/tcp.test.ts +89 -0
  30. package/apps/local-cli/tests/ws-subprotocol.test.ts +92 -0
  31. package/apps/web/landing/index.html +237 -16
  32. package/apps/web/package.json +3 -2
  33. package/apps/web/scripts/build.mjs +46 -16
  34. package/apps/web/src/build-env.ts +1 -1
  35. package/apps/web/src/config-schema.ts +6 -6
  36. package/apps/web/src/render-docs.ts +292 -0
  37. package/apps/web/tests/build-smoke.test.ts +43 -14
  38. package/apps/web/tests/config-schema.test.ts +1 -1
  39. package/apps/web/tests/landing-content.test.ts +103 -0
  40. package/apps/web/tests/render-docs.test.ts +198 -0
  41. package/docs/AWS-Adapter-Architecture.md +3 -2
  42. package/docs/AWS-Deployment.md +21 -8
  43. package/docs/Cloudflare-Deployment-Guide.md +22 -6
  44. package/docs/PlanExtensions.md +113 -3
  45. package/docs/Release-Process.md +23 -13
  46. package/docs/Services.md +578 -0
  47. package/docs/WebClientGuide.md +32 -31
  48. package/package.json +2 -2
  49. package/packages/aws-adapter/package.json +1 -1
  50. package/packages/aws-adapter/src/aws-runtime.ts +20 -1
  51. package/packages/aws-adapter/src/cdk-table-defs.ts +6 -0
  52. package/packages/aws-adapter/src/config-loader.ts +11 -0
  53. package/packages/aws-adapter/src/connection-counter.ts +89 -0
  54. package/packages/aws-adapter/src/dynamo-services-store.ts +649 -0
  55. package/packages/aws-adapter/src/handlers/connect.ts +55 -51
  56. package/packages/aws-adapter/src/handlers/default.ts +36 -4
  57. package/packages/aws-adapter/src/handlers/index.ts +15 -0
  58. package/packages/aws-adapter/src/handlers/nlb-stream.ts +25 -2
  59. package/packages/aws-adapter/src/handlers/sweeper.ts +5 -1
  60. package/packages/aws-adapter/src/index.ts +4 -0
  61. package/packages/aws-adapter/src/stats.ts +6 -1
  62. package/packages/aws-adapter/src/tables.ts +34 -4
  63. package/packages/aws-adapter/tests/aws-harness.ts +3 -0
  64. package/packages/aws-adapter/tests/aws-runtime.test.ts +23 -1
  65. package/packages/aws-adapter/tests/config-loader.test.ts +8 -0
  66. package/packages/aws-adapter/tests/connect.test.ts +158 -32
  67. package/packages/aws-adapter/tests/connection-counter.test.ts +144 -0
  68. package/packages/aws-adapter/tests/dynamo-services-store-dynamo.test.ts +183 -0
  69. package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +568 -0
  70. package/packages/aws-adapter/tests/global-setup.ts +28 -1
  71. package/packages/aws-adapter/tests/gone-exception.test.ts +21 -2
  72. package/packages/aws-adapter/tests/handlers.test.ts +105 -3
  73. package/packages/aws-adapter/tests/nlb-stream.test.ts +29 -1
  74. package/packages/aws-adapter/tests/sweeper.test.ts +20 -0
  75. package/packages/aws-adapter/tests/tables.test.ts +6 -1
  76. package/packages/cf-adapter/package.json +1 -1
  77. package/packages/cf-adapter/src/config-loader.ts +11 -0
  78. package/packages/cf-adapter/src/connection-do.ts +130 -8
  79. package/packages/cf-adapter/src/d1-services-store.ts +703 -0
  80. package/packages/cf-adapter/src/env.ts +8 -0
  81. package/packages/cf-adapter/src/index.ts +5 -0
  82. package/packages/cf-adapter/tests/config-loader.test.ts +19 -0
  83. package/packages/cf-adapter/tests/connection-do-nickserv-d1.test.ts +128 -0
  84. package/packages/cf-adapter/tests/connection-do-pure.test.ts +130 -0
  85. package/packages/cf-adapter/tests/connection-do.test.ts +150 -2
  86. package/packages/cf-adapter/tests/d1-services-store.test.ts +582 -0
  87. package/packages/cf-adapter/tests/serialize.test.ts +1 -0
  88. package/packages/in-memory-runtime/package.json +1 -1
  89. package/packages/irc-core/package.json +1 -1
  90. package/packages/irc-core/scripts/generate-build-info.mjs +26 -5
  91. package/packages/irc-core/src/commands/account-auth.ts +200 -0
  92. package/packages/irc-core/src/commands/chanserv.ts +1166 -0
  93. package/packages/irc-core/src/commands/hostserv.ts +522 -0
  94. package/packages/irc-core/src/commands/index.ts +13 -0
  95. package/packages/irc-core/src/commands/join.ts +171 -9
  96. package/packages/irc-core/src/commands/markread.ts +202 -0
  97. package/packages/irc-core/src/commands/memoserv.ts +319 -0
  98. package/packages/irc-core/src/commands/mode.ts +96 -4
  99. package/packages/irc-core/src/commands/nickserv.ts +402 -0
  100. package/packages/irc-core/src/commands/oper.ts +18 -1
  101. package/packages/irc-core/src/commands/operserv.ts +346 -0
  102. package/packages/irc-core/src/commands/pre-away.ts +3 -1
  103. package/packages/irc-core/src/commands/privmsg.ts +42 -0
  104. package/packages/irc-core/src/commands/read-marker.ts +8 -8
  105. package/packages/irc-core/src/commands/registration.ts +78 -12
  106. package/packages/irc-core/src/commands/sasl.ts +18 -49
  107. package/packages/irc-core/src/commands/service-aliases.ts +52 -0
  108. package/packages/irc-core/src/commands/tagmsg.ts +41 -6
  109. package/packages/irc-core/src/commands/topic.ts +60 -10
  110. package/packages/irc-core/src/config.ts +36 -5
  111. package/packages/irc-core/src/effects.ts +56 -1
  112. package/packages/irc-core/src/ports.ts +1653 -84
  113. package/packages/irc-core/src/protocol/numerics.ts +8 -0
  114. package/packages/irc-core/src/state/channel.ts +38 -1
  115. package/packages/irc-core/src/state/connection.ts +25 -1
  116. package/packages/irc-core/src/types.ts +48 -12
  117. package/packages/irc-core/tests/commands/chanserv.test.ts +2335 -0
  118. package/packages/irc-core/tests/commands/chathistory.test.ts +6 -0
  119. package/packages/irc-core/tests/commands/hostserv.test.ts +1006 -0
  120. package/packages/irc-core/tests/commands/join.test.ts +572 -1
  121. package/packages/irc-core/tests/commands/markread.test.ts +361 -0
  122. package/packages/irc-core/tests/commands/memoserv.test.ts +654 -0
  123. package/packages/irc-core/tests/commands/mode.test.ts +381 -2
  124. package/packages/irc-core/tests/commands/nickserv.test.ts +990 -0
  125. package/packages/irc-core/tests/commands/oper.test.ts +13 -0
  126. package/packages/irc-core/tests/commands/operserv.test.ts +656 -0
  127. package/packages/irc-core/tests/commands/privmsg.test.ts +147 -0
  128. package/packages/irc-core/tests/commands/read-marker.test.ts +28 -28
  129. package/packages/irc-core/tests/commands/registration.test.ts +1015 -20
  130. package/packages/irc-core/tests/commands/sasl.test.ts +229 -12
  131. package/packages/irc-core/tests/commands/server-info.test.ts +9 -5
  132. package/packages/irc-core/tests/commands/service-aliases.test.ts +52 -0
  133. package/packages/irc-core/tests/commands/tagmsg.test.ts +73 -33
  134. package/packages/irc-core/tests/commands/topic.test.ts +94 -2
  135. package/packages/irc-core/tests/commands/unified-account.test.ts +416 -0
  136. package/packages/irc-core/tests/config.test.ts +49 -5
  137. package/packages/irc-core/tests/effects.test.ts +19 -0
  138. package/packages/irc-core/tests/message-store.test.ts +63 -0
  139. package/packages/irc-core/tests/persistent-services-store.test.ts +582 -0
  140. package/packages/irc-core/tests/services-store.test.ts +1289 -0
  141. package/packages/irc-core/tests/state/channel.test.ts +3 -0
  142. package/packages/irc-server/package.json +1 -1
  143. package/packages/irc-server/src/actor.ts +121 -16
  144. package/packages/irc-server/src/dispatch.ts +94 -7
  145. package/packages/irc-server/src/routing.ts +19 -0
  146. package/packages/irc-server/tests/actor.test.ts +988 -15
  147. package/packages/irc-server/tests/dispatch.test.ts +270 -2
  148. package/packages/irc-server/tests/routing.test.ts +6 -0
  149. package/packages/irc-test-support/package.json +1 -1
  150. package/packages/irc-test-support/src/in-memory-harness.ts +37 -8
  151. package/packages/irc-test-support/src/index.ts +1 -0
  152. package/packages/irc-test-support/src/scenarios.ts +21 -6
  153. package/packages/irc-test-support/tests/in-memory-harness.test.ts +51 -0
  154. package/packages/irc-test-support/vitest.config.ts +6 -1
  155. package/tools/ci-hardening/package.json +1 -1
  156. package/tools/load-test/package.json +1 -1
  157. package/tools/load-test/src/client.ts +13 -13
  158. package/tools/load-test/tests/client.test.ts +258 -2
  159. package/tools/load-test/tests/config.test.ts +39 -0
  160. package/tools/load-test/tests/harness.test.ts +21 -0
  161. package/tools/load-test/tests/metrics.test.ts +7 -0
  162. package/tools/tcp-ws-forwarder/package.json +1 -1
  163. package/tools/tcp-ws-forwarder/tests/close-error.test.ts +40 -0
  164. package/tools/tcp-ws-forwarder/tests/defensive-branches.test.ts +78 -0
  165. package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +53 -2
  166. package/tools/tcp-ws-forwarder/tests/logger.test.ts +31 -1
  167. package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +0 -18
  168. package/packages/irc-core/tests/read-marker-store.test.ts +0 -108
package/CHANGELOG.md CHANGED
@@ -7,24 +7,513 @@ and this project adheres to [Semantic Versioning](https://semver.org/) with
7
7
  the 0.x convention (any release may change anything).
8
8
 
9
9
  For the release process itself — versioning policy, pre-release checklist,
10
- cutting a tag, rolling back — see [`docs/release.md`](docs/release.md).
10
+ cutting a tag, rolling back — see [`docs/Release-Process.md`](docs/Release-Process.md).
11
11
  Cross-reference `progress.md` / `tickets.md` for per-ticket detail.
12
12
 
13
+ ## [0.9.0] - 2026-08-10
14
+
15
+ The main change in this release is **rounding out the integrated IRC
16
+ services surface** that landed in v0.8.0: ChanServ gains the prefix and
17
+ roster mutation verbs operators expect (`OP` / `DEOP` / `VOICE` /
18
+ `DEVOICE` / `KICK` / `BAN` / `UNBAN`), HostServ **auto-applies an
19
+ assigned vhost on identify** (no per-session `HostServ ON`), HostServ
20
+ HELP hides oper-only commands from non-opers, NickServ grows an `ID`
21
+ alias for `IDENTIFY`, and **NickServ `INFO` no longer leaks the
22
+ registrant's email** to non-owners. Alongside it: service **shortcut
23
+ verbs** (`/NICKSERV`, `/NS`, `/CS`, `/HS`, `/MS`, `/OS`, …) that the
24
+ daemon rewrites to the equivalent `PRIVMSG <Service>`, `echo-message`
25
+ reflection of `PRIVMSG` to service nicks, and the long-missing **JOIN
26
+ `332`/`333` topic numerics** on reconnect/rejoin (regardless of how the
27
+ topic came to be set). A real PASS-login bug is fixed: a client that
28
+ completes registration before sending `PASS <nick>:<password>` is now
29
+ logged in instead of receiving a silent no-op. The AWS adapter gets
30
+ another round of consistency hardening — every `Connections` `GetCommand`
31
+ now uses `ConsistentRead: true`. The web client renders the `docs/` wiki
32
+ to standalone HTML at `/docs/` and gains a client-specific "how to
33
+ connect" section on the landing page.
34
+
35
+ ### Added — Services surface expansion (`@serverless-ircd/irc-core`)
36
+
37
+ - **ChanServ `OP` / `DEOP` / `VOICE` / `DEVOICE` / `KICK` / `BAN` /
38
+ `UNBAN`**: founder-or-`AUTOOP`-level callers can grant/remove prefix
39
+ modes on demand, kick through ChanServ, and manage channel bans. Each
40
+ verb emits the corresponding `:ChanServ!ChanServ@services MODE` /
41
+ `KICK` broadcast and an `ApplyChannelDelta` against the channel
42
+ authority (the same mechanism MLOCK reasserts use), so the roster
43
+ and ban list update atomically with the broadcast. A target not on
44
+ the channel produces a NOTICE and no state change. Privilege gating
45
+ reuses the existing access-list check — no new store methods.
46
+ - **HostServ auto-applies an assigned vhost on identify.** SASL
47
+ PLAIN/EXTERNAL, NickServ `IDENTIFY`, and PASS-auth now consult
48
+ `services.getVhost(account)` on a successful login: when an assigned
49
+ vhost exists, `applyVhost` runs immediately — `state.host` is set,
50
+ `state.vhostActive` is stamped, and any currently-joined
51
+ `chghost`-capable peers see the `CHGHOST` broadcast. At SASL/PASS-auth
52
+ time (pre-JOIN) there are no peers so no broadcast, but the vhost is
53
+ visible in the subsequent `001` welcome. Users no longer need to run
54
+ `HostServ ON` every session.
55
+ - **NickServ `ID` alias for `IDENTIFY`.** `PRIVMSG NickServ :ID <pw>`
56
+ and `ID <nick> <pw>` behave identically to `IDENTIFY` — the alias
57
+ is not advertised in HELP (mirrors Atheme's hidden shorthand).
58
+ - **Service shortcut verbs** (`@serverless-ircd/irc-server`):
59
+ `/NICKSERV`, `/NS`, `/CS`, `/HS`, `/MS`, `/OS` (and their long forms)
60
+ are rewritten at the actor to the equivalent
61
+ `PRIVMSG <ServiceNick> :<args>`. Clients with a dedicated
62
+ slash-command UX (WeeChat, HexChat, IRCCloud) now work without a
63
+ custom alias file. The aliases are case-insensitive and accept both
64
+ colon-trailing and space-joined arg shapes.
65
+ - **`echo-message` reflection for service PRIVMSG**: a client that
66
+ has negotiated `echo-message` now sees its own
67
+ `PRIVMSG NickServ :IDENTIFY hunter2` reflected back before the
68
+ NickServ NOTICE reply, matching the echo behaviour for every other
69
+ PRIVMSG target. The echo is emitted before the reply so labeled-
70
+ response batches wrap both correctly.
71
+
72
+ ### Added — Web client (`apps/web`)
73
+
74
+ - **`docs/` wiki rendered to `/docs/`.** The Gitea-wiki markdown under
75
+ the `docs/` submodule is now rendered to standalone HTML at
76
+ `apps/web/dist/docs/<slug>.html` and served by the existing
77
+ `[assets]` binding at `/docs/<slug>.html` (with `Home.md` →
78
+ `/docs/index.html`). Intra-docs Markdown links are rewritten to
79
+ `.html`, a shared stylesheet is emitted, and an `ADR-Index` page
80
+ links every ADR. Requires `git submodule update --init docs` before
81
+ build; the build fails loudly with the recovery command if the
82
+ submodule is missing or empty.
83
+ - **Landing-page "how to connect" section.** The static landing page
84
+ gains a CSS-only tabbed picker with copy-paste connect snippets for
85
+ the common stock IRC clients (WeeChat, HexChat, irssi, IRCCloud),
86
+ replacing the previous per-client static guides. The picker covers
87
+ both the WebSocket path and the `irc+tls :6697` path.
88
+
89
+ ### Changed — PASS-based account login (`@serverless-ircd/irc-core`)
90
+
91
+ - **A late `PASS <nick>:<password>` now logs the connection in.**
92
+ Previously, a client that completed registration (`NICK` + `USER`
93
+ resolving before `PASS` arrived, or any `PASS` sent after `CAP END`
94
+ resolved) was silently rejected — the `passReducer` short-circuited
95
+ at `state.registration === 'registered'` and emitted `462
96
+ ERR_ALREADYREGISTRED` for any post-registration `PASS`. The shared
97
+ verify + `applyAccountSuccess` pipeline now runs on a
98
+ post-registration `PASS <nick>:<password>`: on success it emits
99
+ `900 RPL_LOGGEDIN` + `903 RPL_SASLSUCCESS`, stamps `+r`, and runs
100
+ the read-marker / away / memo replay in-band, exactly like the
101
+ at-registration path. A bare `PASS <value>` (no colon) is a silent
102
+ no-op once registration has completed — a server password is
103
+ meaningless post-registration, and emitting `462` for it was
104
+ counterproductive. The pre-registration duplicate-PASS `462` (two
105
+ `PASS`es before `NICK`+`USER`) is preserved unchanged.
106
+
107
+ ### Fixed — Channel correctness (`@serverless-ircd/irc-core`)
108
+
109
+ - **JOIN now emits `332 RPL_TOPIC` / `333 RPL_TOPICWHOISTIME` from any
110
+ source of the topic.** The previous JOIN reducer keyed topic
111
+ emission solely on the ChanServ KEEPTOPIC restore path (an empty
112
+ channel this join), so reconnecting clients after a deploy saw the
113
+ NAMES list but no topic — even when the topic survived the deploy
114
+ in storage. The emission now keys off `state.topic`, so a topic
115
+ restored from a KEEPTOPIC snapshot, a deploy snapshot with
116
+ KEEPTOPIC off, or an earlier joiner all produce the correct
117
+ numerics. The KEEPTOPIC restore path still maintains `state.topic`
118
+ for an empty channel; only the emission trigger changed.
119
+ - **No auto-op for the first joiner of a registered channel off the
120
+ access list.** A registered channel that had been emptied (so its
121
+ roster snapshot was fresh) used to auto-op the first joiner
122
+ unconditionally as a "founder fallback", which let any client claim
123
+ `+o` by joining an empty registered channel. The fallback now only
124
+ fires when the joiner is identified as the founder (or is on the
125
+ access list); an unidentified first joiner joins without prefix
126
+ modes.
127
+
128
+ ### Fixed — AWS adapter (`@serverless-ircd/aws-adapter`)
129
+
130
+ - **Strongly-consistent `Connections` reads across every handler.**
131
+ `$disconnect`'s `cleanupConnection` and the NLB streaming handler's
132
+ per-chunk row read were eventually consistent, so under DynamoDB
133
+ replication lag they could miss the row that was just written (or
134
+ updated) — dropping the first frame of a raw `:6697` connection or
135
+ skipping nick release + membership fanout on `$disconnect`. Every
136
+ `GetCommand` against the `Connections` table in `aws-adapter/src/**`
137
+ now sets `ConsistentRead: true`, matching the `$default` fix from
138
+ v0.8.0. A unit test asserts each call site carries the flag.
139
+ - **Bounded `docker info` probe in the test global-setup.** On hosts
140
+ with Docker Desktop installed but not running, the macOS `docker`
141
+ CLI hangs indefinitely waiting for the daemon to spin up — and
142
+ `testcontainers` inherits that hang, stalling the whole `pnpm test`
143
+ run. The aws-adapter `globalSetup` now pre-probes `docker info`
144
+ with a 5-second exec timeout; if Docker is not reachable, it logs
145
+ and falls through to the Java ZIP fallback (or skip) instead of
146
+ hanging. No behaviour change when Docker is up.
147
+
148
+ ### Fixed — HostServ help leak (`@serverless-ircd/irc-core`)
149
+
150
+ - **HostServ HELP hides oper-only commands from non-opers.** The
151
+ help/unknown NOTICE previously listed all seven HostServ commands
152
+ (`ON, OFF, REQUEST, SET, APPROVE, REJECT, LIST`) to every caller,
153
+ leaking the existence and names of oper commands. The notice
154
+ helpers now branch on `state.userModes.oper`: a non-oper sees only
155
+ `ON, OFF, REQUEST`; an oper sees the full list. The per-handler
156
+ oper gates are unchanged, so a non-oper who guesses an oper
157
+ command name still gets the existing permission-denied NOTICE.
158
+
159
+ ### Fixed — NickServ INFO privacy (`@serverless-ircd/irc-core`)
160
+
161
+ - **NickServ `INFO` no longer leaks the registrant's email to
162
+ non-owners.** `handleInfo` previously returned the `Email:` line
163
+ unconditionally to any caller, identified or not — a remote,
164
+ unidentified user could enumerate registered nicks and harvest
165
+ email addresses. The `Email:` line is now gated behind
166
+ `state.account === rec.account` (the owner) or
167
+ `state.userModes.oper === true` (an oper). The owner path still
168
+ shows the full record; `INFO` with no target defaults to the
169
+ caller's own nick (owner path). The `Nick:` / `Account:` lines
170
+ remain visible to all callers (consistent with how a WHOIS against
171
+ a registered nick surfaces the account name).
172
+
173
+ ### Changed — Local CLI (`apps/local-cli`)
174
+
175
+ - **`operCreds` plumbed through the config schema.** The local CLI's
176
+ `ServerConfig` now carries `operCreds` end-to-end (was wired ad-hoc
177
+ through constructor options); OPER auth against configured creds
178
+ works the same way it does in the cloud adapters. The transport
179
+ seams (WS / TCP) are also extracted into named helpers for
180
+ readability.
181
+
182
+ ### Changed — CI / tooling
183
+
184
+ - CI workflows now explicitly checkout every submodule (`docs/`,
185
+ `apps/web/upstream/`) so the render-docs and Kiwi build steps run
186
+ deterministically on every runner, not just maintainer laptops.
187
+
188
+ ### ⚠️ Migration required
189
+
190
+ - **No persisted-DO-state schema change.** `PERSISTED_STATE_VERSION`
191
+ stays `1`. The ChanServ roster commands, HostServ auto-vhost, and
192
+ the JOIN topic-numeric fix are runtime behaviour, not persisted DO
193
+ fields. **Code rollback across this release is safe.**
194
+ - **No D1 / DynamoDB schema change.** The existing services tables
195
+ (`nickserv_accounts`, `chanserv_channels`, …) carry the new
196
+ behaviour unchanged. The `cert_subjects` column flagged for the
197
+ in-progress services credential consolidation is **not yet
198
+ present** — operators do not need to run any migration command for
199
+ this release.
200
+ - **Behaviour change: late `PASS` no longer emits `462`.** A
201
+ post-registration `PASS <value>` used to reply
202
+ `462 ERR_ALREADYREGISTRED`; it is now silent. Clients that keyed
203
+ off the `462` to detect "you already sent PASS" will no longer see
204
+ it. No client is known to do this (the numeric is informational),
205
+ but scripted regression tests against this behaviour must be
206
+ updated (the in-tree `irc-test-support` scenario was updated).
207
+ - **Behaviour change: late `PASS <nick>:<password>` now logs in.**
208
+ Clients that intentionally sent the colon form post-registration
209
+ expecting it to be ignored will now be logged in. This is the
210
+ intended fix; flag it in operator runbooks if the deployment was
211
+ relying on the silent-reject for some access-control flow.
212
+
213
+ ### Known limitations
214
+
215
+ - **The `AccountStore` / `ServicesStore` credential drift is still
216
+ present.** A nick registered via NickServ `REGISTER` after the
217
+ worker booted still cannot authenticate via SASL PLAIN/EXTERNAL or
218
+ `PASS <nick>:<password>` against the *fresh* password — those paths
219
+ consult the SASL `AccountStore` (`accounts` table), which
220
+ `registerNick` does not write back to. The PASS-login fix above
221
+ makes the at-registration and post-registration paths symmetric,
222
+ but the underlying cross-store drift remains tracked for a
223
+ follow-up release that consolidates the credential into a single
224
+ store. Operators who need a NickServ-registered nick to work with
225
+ SASL/PASS immediately should re-seed via
226
+ `tools/seed-cf-accounts.ts` (D1) / `tools/seed-aws-accounts.ts`
227
+ (DynamoDB) with the same password.
228
+ - Same transport limits as v0.8.0: the **CF TCP path requires
229
+ Cloudflare Spectrum (Enterprise tier)** plus a stateful Container
230
+ origin; the **AWS TCP path** uses NLB + Lambda streaming and is
231
+ subject to Lambda idle-timeout / stream-duration limits. The
232
+ WebSocket path remains the zero-extra-deps default.
233
+ - **mTLS (for SASL `EXTERNAL`) requires a custom domain + uploaded
234
+ CA/trust-store** on both platforms and is not enabled by default.
235
+ - **Client compatibility sweep** (WeeChat / HexChat / IRCCloud /
236
+ TheLounge / matrix-IRC bridge) remains pending.
237
+ - **Coverage hardening is partial.** `irc-core`, `irc-server`, and
238
+ `in-memory-runtime` sit at 100%; `aws-adapter` and `aws-stack`
239
+ clear the 90% gate. The remaining packages (`cf-adapter`,
240
+ `local-cli`, `load-test`, `cf-tcp-container`, `tcp-ws-forwarder`,
241
+ `irc-test-support`, `web`) are **above the gate but below 100%** —
242
+ follow-ups drive each to full coverage.
243
+
244
+ ### Security
245
+
246
+ - **NickServ `INFO` email disclosure** (Fixed above): a remote,
247
+ unidentified client could harvest email addresses from every
248
+ registered nick. The disclosure is now gated behind owner-or-oper.
249
+ - **First-joiner op-takeover on empty registered channels** (Fixed
250
+ above): an unidentified client could claim `+o` by being the first
251
+ to join an empty registered channel. The auto-op fallback now
252
+ requires founder/access-list identification.
253
+ - No other auth path changed. The scrypt-hashed credential store,
254
+ server-password gate, and CSWSH defense are unchanged from v0.8.0.
255
+
256
+ ---
257
+
258
+ ## [0.8.0] - 2026-08-08
259
+
260
+ The main change in this release is **integrated IRC services**: NickServ,
261
+ ChanServ, HostServ, OperServ, and MemoServ ship as part of the daemon
262
+ itself — there is **no separate services process and no S2S link**. The
263
+ service nicks are reserved and routed to dedicated pure reducers backed
264
+ by a new `ServicesStore` port, with **persistent D1 (Cloudflare) and
265
+ DynamoDB (AWS) backends** so registrations survive redeploys. Alongside
266
+ it: the SASL `AccountStore` and NickServ accounts are **unified** under
267
+ one scrypt-hashed credential store, `PASS <nick>:<password>` identifies
268
+ at registration, the `draft/read-marker` `MARKREAD` verb lands, and a
269
+ round of **AWS adapter hardening** (O(1) admission counter, strongly-
270
+ consistent `$default` reads, WebSocket subprotocol echo fix).
271
+
272
+ ### Added — Integrated IRC services (`@serverless-ircd/irc-core`, `@serverless-ircd/irc-server`)
273
+
274
+ The ten services tickets add the full
275
+ services surface. Service names are reserved nicks recognized at
276
+ `PRIVMSG` time and routed to dedicated pure reducers when a
277
+ `ServicesStore` is bound; when no store is bound every services command
278
+ replies `501` and the rest of the daemon is unaffected. See
279
+ `docs/Services.md` for the operator reference.
280
+
281
+ - **`ServicesStore` port + in-memory reference impl**: a synchronous,
282
+ injectable store holding NickServ / ChanServ / HostServ / MemoServ /
283
+ OperServ / read-marker state. Reducers stay pure — all side effects
284
+ flow through the store port. The `ReadMarkerStore` from v0.6.0 is
285
+ **folded into** `ServicesStore` so read-markers, `draft/pre-away`,
286
+ and account state share one persistence path.
287
+ - **NickServ** (`REGISTER` / `IDENTIFY` / `DROP` / `INFO` / `SET`):
288
+ registers the current nick, identifies to an account (setting the
289
+ read-only user mode **`+r`**, fanning `ACCOUNT` to `account-notify`
290
+ peers, then delivering unread MemoServ memos), and supports
291
+ `SET ENFORCE` — a registered nick held by a non-identified
292
+ connection is reclaimed on the `NICK` path (enforcement policy
293
+ mirrors Atheme/Anope). `IDENTIFY` falls back to SASL account
294
+ credentials when no NickServ row exists.
295
+ - **ChanServ** (`REGISTER` / `DROP` / `SET` / `INFO` / `ACCESS` /
296
+ `LEVELS`): channel registration with a founder, the services-only
297
+ channel modes **`+r`** (registered), **`+R`** (block unidentified
298
+ join/message), and **`+M`** (moderated-identified), `ACCESS`/`LEVELS`
299
+ (SOP/AOP/VOP/HOP access lists), and auto-op on `JOIN` per access
300
+ level. The three services modes are settable only via ChanServ, not
301
+ via `MODE` (`472 ERR_UNKNOWNMODE` / `501`).
302
+ - **HostServ** (`ON` / `OFF` / `REQUEST` + oper `SET` / `APPROVE` /
303
+ `REJECT` / `LIST`): per-account vhost with `CHGHOST` fanout to shared
304
+ channels. `REQUEST` enters an **oper approval queue**; opers `APPROVE`
305
+ to activate or `REJECT` to drop it.
306
+ - **OperServ** (`AKILL` / `JUPE` / `UNJUPE` / `RAW`): oper-gated
307
+ network management. `AKILL` records an `*@host` mask, `JUPE` reserves
308
+ a juped nick (rejected at `NICK` with `432 ERR_ERRONEUSNICKNAME`),
309
+ `RAW` is the escape hatch. Wired end-to-end: `PRIVMSG OperServ :…`
310
+ routes to the OperServ reducer.
311
+ - **MemoServ** (`SEND` / `LIST` / `READ` / `DEL`): per-account memos
312
+ with queue delivery — unread memos are replayed at identify (NickServ
313
+ `IDENTIFY` / SASL login / PASS-based login), the same path that
314
+ replays `draft/read-marker` and `draft/pre-away`.
315
+
316
+ ### Added — Persistent services backends (`@serverless-ircd/cf-adapter`, `@serverless-ircd/aws-adapter`)
317
+
318
+ Both adapters ship write-behind persistence for the `ServicesStore` so
319
+ registrations, channel access lists, vhosts, memos, and read-markers
320
+ survive a redeploy/reconnect.
321
+
322
+ - **Cloudflare (D1)**: `D1ServicesStore` over the shared `ACCOUNTS_DB`
323
+ binding (the same D1 database used for SASL accounts since v0.4.0).
324
+ The ten services tables **auto-provision on cold start** (`CREATE
325
+ TABLE IF NOT EXISTS` inside `loadD1ServicesStore`), so a fresh deploy
326
+ self-provisions and an existing deploy re-runs the batch as a cheap
327
+ no-op. `ConnectionDO` load/flush hooks wrap each frame and teardown.
328
+ - **AWS (DynamoDB)**: `DynamoServicesStore` plus a new `Services`
329
+ table definition and handler wiring so NLB-stream / `$default`
330
+ invocations hydrate + flush services alongside the existing account
331
+ store. Enabled when the `SERVICES_TABLE` env var / table name is
332
+ supplied.
333
+ - **`PersistentServicesStore` base** (`irc-core`): the shared
334
+ write-behind base — mutates the inherited cache synchronously and
335
+ enqueues matching writes for `flush()`, so reducers stay free of IO.
336
+
337
+ ### Added — Unified accounts & registration login (`@serverless-ircd/irc-core`)
338
+
339
+ - **Unified SASL + NickServ account store**: the SASL `AccountStore`
340
+ and NickServ `ServicesStore` now share one **scrypt-hashed**
341
+ credential shape. A NickServ registration *is* a SASL account —
342
+ SASL `PLAIN`'s services fallback verifies against a row created by
343
+ NickServ `REGISTER` without a second registration, and vice versa.
344
+ The plaintext password on `ServicesNickRow` is replaced with a
345
+ `HashedAccountCredential` (scrypt + random salt). `ingestAccountCredential`
346
+ adopts a pre-hashed SASL credential as a registered nick (idempotent;
347
+ existing records win).
348
+ - **PASS-based account login**: a client may identify to its NickServ
349
+ account at registration by sending `PASS <nick>:<password>` (the same
350
+ literal the `SASL_ACCOUNTS` seed tooling uses). On success the
351
+ connection is logged in — emitting `900`/`903`, stamping `+r`, and
352
+ running the same read-marker / away / memo replay as SASL. A bare
353
+ `PASS <value>` (no colon) is always treated as a server-password
354
+ candidate. The shared login reduction (`applyAccountSuccess`) is
355
+ extracted and reused from SASL.
356
+ - **Server-password gate with SASL short-circuit**: an optional gate
357
+ enforced in the registration reducer at `001` — when
358
+ `ServerConfig.serverPassword` is set, every connection must supply a
359
+ matching `PASS` (or authenticate via SASL / PASS-based account login)
360
+ before `RPL_WELCOME`, else `464 ERR_PASSWDMISMATCH` + disconnect.
361
+ Wired through all three adapters (`SERVER_PASSWORD` Worker secret /
362
+ Lambda env var / `--server-password` CLI flag).
363
+
364
+ ### Added — `draft/read-marker` `MARKREAD` verb (`@serverless-ircd/irc-core`)
365
+
366
+ - The timestamp-based `MARKREAD` command from the read-marker draft
367
+ lands alongside the existing `+draft/read-marker=<msgid>` tag path.
368
+ `MARKREAD <target>` (GET) replies with the stored marker's timestamp;
369
+ `MARKREAD <target> <timestamp>` (SET) advances the marker, with
370
+ timestamp↔msgid translation against the message store. Failure modes
371
+ use `standard-replies` `FAIL MARKREAD …`. The mark fanout is
372
+ **scoped to the originator's account** (spec compliance), not
373
+ broadcast to every cap-enabled peer.
374
+
375
+ ### Added — Cloudflare adapter (`@serverless-ircd/cf-adapter`)
376
+
377
+ - **Client source IP capture at WebSocket upgrade**: reads
378
+ `CF-Connecting-IP` (with `X-Real-IP` fallback) from the upgrade
379
+ request and seeds `ConnectionState.host`, so WHOIS / hostmasks carry
380
+ a real `@host` segment instead of an `undefined` fallback.
381
+ - **Ghost-member pruning via alarm**: when a WebSocket `close` event is
382
+ lost (client drop, edge hiccup), the DO's alarm prunes the ghost
383
+ member from its channels on the next sweep, preventing stale roster
384
+ entries.
385
+
386
+ ### Changed — AWS adapter (`@serverless-ircd/aws-adapter`, `@serverless-ircd/aws-stack`)
387
+
388
+ - **O(1) admission counter**: `$connect`'s full-table `Scan COUNT` is
389
+ replaced with an atomic increment/decrement on a single meta row,
390
+ dropping admission cost from O(connections) to O(1). Reservations
391
+ roll back on rejection or a failed `Put`; the sweeper and stats
392
+ exclude the meta row.
393
+ - **Strongly-consistent `$default` reads + observability**: `$default`
394
+ now reads the `Connections` row strongly consistent (closes a
395
+ read-after-write `410` window), warns on a missing row, logs
396
+ `postToConnection` failures (previously silently swallowed), and
397
+ emits a summary log per invocation.
398
+ - **`ServerName` / `NetworkName` as CFN parameters**: server identity
399
+ is now a CloudFormation parameter (was env-var-only), and the
400
+ sweeper Lambda's `loadServerConfigFromLambdaEnv` is wired with the
401
+ server-identity env vars it requires. `SweeperScheduleExpression`,
402
+ `PingCheckerScheduleExpression`, and `ApiGatewayLoggingLevel` are
403
+ also exposed as parameters.
404
+ - **WebSocket subprotocol echo fixed**: API Gateway WebSocket
405
+ `IntegrationResponse` uses `route.response.header.*` (not
406
+ `method.response.header.*`, which is REST API only), so the
407
+ `Sec-WebSocket-Protocol` header was not being echoed back to clients.
408
+ AWS now correctly negotiates the IRCv3 subprotocol. Subprotocol
409
+ advertisement is forced text-only (`text.ircv3.net`) to match the
410
+ Lambda response-stream path.
411
+ - **APIGW logging role corrected** and Biome lint/format applied.
412
+
413
+ ### Changed
414
+
415
+ - **`MODE +o` echo on oper-up**: the connection that grants oper now
416
+ receives a `MODE` echo, matching the cross-connection oper-up path.
417
+ - **NICK change broadcast fix**: nick changes now broadcast to every
418
+ shared-channel peer (a code path that dropped peers under certain
419
+ roster shapes is corrected).
420
+ - **Server version from `package.json`**: the default `serverVersion`
421
+ advertised in `002`/`003` is derived from the workspace
422
+ `package.json` at build time (was a hardcoded placeholder when the
423
+ config omitted it).
424
+ - **Web client SPA path `/app/` → `/webclient/`**: the vendored Kiwi
425
+ IRC SPA is now served at `/webclient/` (the `[assets]` publicPath
426
+ and config are repointed). Operators with custom Kiwi configs or
427
+ bookmarks must update to `/webclient/`.
428
+ - **Source / issue links migrated to Gitea**; landing-page copy
429
+ tightened and the upstream-install guard hardened.
430
+
431
+ ### Changed — CI / tooling
432
+
433
+ - `actions/setup-java` bumped to v5; pnpm pinned to 11.20.0. CI jobs
434
+ are bounded by explicit timeouts and the unreachable pnpm cache step
435
+ is dropped.
436
+
437
+ ### ⚠️ Migration required
438
+
439
+ - **Cloudflare: services are enabled when `ACCOUNTS_DB` is bound.** The
440
+ D1 binding is the same one introduced in v0.4.0 for SASL accounts;
441
+ the ten services tables **self-provision on cold start** (no manual
442
+ `wrangler d1 execute` needed). If `ACCOUNTS_DB` is already bound,
443
+ services are now live after deploy — no action. If it is unset,
444
+ services commands reply `501` and the daemon is otherwise unchanged.
445
+ - **AWS: new `Services` DynamoDB table + `SERVICES_TABLE`.** To enable
446
+ services, supply a services table name (`SERVICES_TABLE` env var /
447
+ CDK `servicesTableName` prop); the table is created by the CDK stack.
448
+ Without it, services are unbound.
449
+ - **NickServ / SASL credentials are now scrypt-hashed at rest.** A
450
+ NickServ `REGISTER` creates the same hashed credential shape SASL
451
+ uses; the `password` column on the NickServ row is replaced by
452
+ `algorithm` / `salt` / `hash`. Existing seeded SASL accounts are
453
+ adopted as registered nicks idempotently. Operators re-seeding via
454
+ `tools/seed-*-accounts.ts` see no change (the tools already hashed).
455
+ - **Web client SPA moved to `/webclient/`.** Update any bookmarks,
456
+ custom Kiwi configs, or `WEB_ORIGINS` entries pointing at `/app/`.
457
+ - **No persisted-DO-state schema change.** `PERSISTED_STATE_VERSION`
458
+ stays `1`. The new read-only user mode `r` (registered) and the
459
+ services channel modes `r`/`R`/`M` are runtime state, not persisted
460
+ DO fields; the `ReadMarkerStore` fold moves read-markers into the
461
+ services backend (D1/DynamoDB), out of DO storage. **Code rollback
462
+ across this release is safe.**
463
+ - No toolchain change. Node ≥ 24 / pnpm 11 (from v0.4.0) still apply.
464
+
465
+ ### Known limitations
466
+
467
+ - **Services are not binary-compatible with Atheme or Anope.** There is
468
+ no S2S protocol, so an existing Atheme/Anope services process cannot
469
+ connect, and existing services databases cannot be imported.
470
+ Operators migrating from a services-aware network must re-register
471
+ nicks and channels. See `docs/Services.md` §12.
472
+ - **The web client remains partial.** The Playwright headless-browser
473
+ e2e against the deployed staging frontend is still pending; the SPA
474
+ is exercised via the WS smoke (`scripts/smoke.mjs`) and the `apps/web`
475
+ unit / build-smoke suite. The web client remains Cloudflare-only (the
476
+ `[assets]` binding is a Worker feature).
477
+ - Same transport limits as v0.7.0: the **CF TCP path requires
478
+ Cloudflare Spectrum (Enterprise tier)** plus a stateful Container
479
+ origin; the **AWS TCP path** uses NLB + Lambda streaming and is
480
+ subject to Lambda idle-timeout / stream-duration limits. The
481
+ WebSocket path remains the zero-extra-deps default.
482
+ - **mTLS (for SASL `EXTERNAL`) requires a custom domain + uploaded
483
+ CA/trust-store** on both platforms and is not enabled by default.
484
+ - **Client compatibility sweep** (WeeChat / HexChat / IRCCloud /
485
+ TheLounge / matrix-IRC bridge) remains pending.
486
+ - **Coverage hardening is partial.** `aws-adapter` and `aws-stack`
487
+ clear the 90% gate; the remaining packages (`cf-adapter`,
488
+ `local-cli`, `load-test`, `cf-tcp-container`, `tcp-ws-forwarder`,
489
+ `irc-test-support`, `web`) are **above the gate but below 100%** —
490
+ follow-ups drive each to 100%.
491
+
492
+ ### Security
493
+
494
+ - **NickServ / SASL credentials are now uniformly scrypt-hashed.** The
495
+ unified account store means a NickServ-registered nick and a SASL
496
+ account share one hashed credential — never plaintext, in memory or
497
+ at rest. The server-password gate and CSWSH defense are unchanged
498
+ from v0.7.0; no other auth path changed.
499
+
500
+ ---
501
+
13
502
  ## [0.7.0] - 2026-08-04
14
503
 
15
- The headline is the **web client (Phase 14)**: a vendored **Kiwi IRC**
504
+ The main change in this release is the **web client**: a vendored **Kiwi IRC**
16
505
  SPA served at `/app/` and a static project landing page served at `/`
17
506
  directly by the Cloudflare Worker, with **Cross-Site WebSocket
18
507
  Hijacking (CSWSH) defense** wired into the WS upgrade path. The browser
19
- opens a native `wss://` straight to the existing IRC-over-WebSocket
20
- endpoint — no proxy, gateway, or transport adaptation in the hot path.
508
+ opens a native `wss://` directly to the IRC-over-WebSocket
509
+ endpoint — no proxy, gateway, or transport adaptation in the request path.
21
510
  Alongside it: a round of **test hardening** (mutation-killing tests for
22
511
  `irc-core`'s parser / batch / numerics, and CF-harness / scenario fixes
23
512
  for slow transports).
24
513
 
25
514
  ### Added — Web client (`@serverless-ircd/web`, `@serverless-ircd/cf-worker`)
26
515
 
27
- Six Phase 14 tickets light up the project's browser surface. The Worker
516
+ Six tickets add the project's browser surface. The Worker
28
517
  becomes the single origin for the landing page, the SPA, **and** the
29
518
  WebSocket. Same-origin by default — which is what makes the CSWSH
30
519
  defense almost zero-config.
@@ -67,7 +556,7 @@ defense almost zero-config.
67
556
  with a `200 OK` (the WS endpoint and the landing page share path
68
557
  `/`; only the Worker can tell them apart).
69
558
  - **Static landing page** (`apps/web/landing/index.html`): framework-
70
- free HTML + inline CSS, no JavaScript. The project's front door
559
+ free HTML + inline CSS, no JavaScript. The project's entry page
71
560
  explains what ServerlessIRCd is, links to `/app/` (the IRC client),
72
561
  the docs, and the source repo. The build copies it verbatim to
73
562
  `dist/index.html`. Renders without JS; responsive; the build-smoke
@@ -182,7 +671,7 @@ contributor can deploy a staging SPA following only this doc.
182
671
 
183
672
  ### Known limitations
184
673
 
185
- - **Phase 14 is partial.** The Playwright headless-browser e2e
674
+ - **The web client is partial.** The Playwright headless-browser e2e
186
675
  against the deployed staging frontend remains pending
187
676
  — the SPA is exercised via the existing WS smoke
188
677
  (`scripts/smoke.mjs`) and the new `apps/web` unit / build-smoke
@@ -206,7 +695,7 @@ contributor can deploy a staging SPA following only this doc.
206
695
  (`cf-adapter`, `local-cli`, `load-test`, `cf-tcp-container`,
207
696
  `tcp-ws-forwarder`, `irc-test-support`, and now `web`) are **above
208
697
  the gate but below 100%** — follow-ups drive each to 100%.
209
- - The IRC services Phase 13 work (NickServ / ChanServ / HostServ /
698
+ - The IRC services work (NickServ / ChanServ / HostServ /
210
699
  OperServ / MemoServ) is pending; `draft/read-marker` and
211
700
  `draft/pre-away` ship with in-memory reference stores, so marker /
212
701
  away persistence does not survive an adapter restart until a
@@ -226,20 +715,20 @@ contributor can deploy a staging SPA following only this doc.
226
715
 
227
716
  ## [0.6.0] - 2026-08-03
228
717
 
229
- The headline is the **IRCv3 extension sweep (Phase 12)**: ten negotiated
718
+ The main change in this release is the **IRCv3 extension sweep**: ten negotiated
230
719
  caps and the user-mode-`S` TLS indicator land in one release, taking the
231
720
  negotiated capability surface from "modern baseline" to "draft-leading-
232
721
  edge". Alongside it: the **required-`serverName` config gate** (the
233
- last open v0.5.0 follow-up), a new **`tools/load-test`** client farm
234
- (Phase 7's 10k-connection harness), and the **coverage / CI unblocks**
722
+ last open v0.5.0 follow-up), a new **`tools/load-test`** client pool
723
+ (the 10k-connection harness), and the **coverage / CI unblocks**
235
724
  that lift `aws-adapter` over the 90% gate and switch `cf-adapter` /
236
725
  `cf-worker` to istanbul (clearing the v0.5.0 workerd-incompatibility
237
726
  caveat). Deploy workflows are now **manual-only** (`workflow_dispatch`).
238
727
 
239
728
  ### Added — IRCv3 extensions (`@serverless-ircd/irc-core`, `@serverless-ircd/irc-server`)
240
729
 
241
- The ten Phase 12 cap tickets light up the remaining negotiated-cap
242
- surface, and the two non-cap Phase 12 tickets (ISUPPORT tokens and
730
+ The ten cap tickets add the remaining negotiated-cap
731
+ surface, and the two non-cap tickets (ISUPPORT tokens and
243
732
  user mode `S`) round out the wire-visible advertisement. Each cap
244
733
  ships a pure reducer (where applicable), an actor route case,
245
734
  advertisement in `SUPPORTED_CAPABILITIES` (or conditional
@@ -349,10 +838,10 @@ with a readable, field-listing error.
349
838
 
350
839
  ### Added — Load-test harness (`@serverless-ircd/load-test`)
351
840
 
352
- PLAN §7 / Phase 7's 10k-connection harness. A synthetic WebSocket IRC
353
- client farm: opens N connections, registers (NICK/USER), joins a
841
+ The PLAN §7 10k-connection harness. A synthetic WebSocket IRC
842
+ client pool: opens N connections, registers (NICK/USER), joins a
354
843
  channel, and optionally chats. Captures **p50 / p95 / p99 latency per
355
- phase** (connect / register / join / message) plus the **drop rate**,
844
+ stage** (connect / register / join / message) plus the **drop rate**,
356
845
  and prints a markdown report to stdout (or `--report <path>`).
357
846
 
358
847
  ```bash
@@ -467,11 +956,11 @@ resolves it.
467
956
  `irc-test-support`) are **above the gate but below 100%** —
468
957
  follow-ups drive each to 100%.
469
958
  - The IRC services (NickServ / ChanServ / HostServ / OperServ /
470
- MemoServ) Phase 13 work is pending; `draft/read-marker` and
959
+ MemoServ) work is pending; `draft/read-marker` and
471
960
  `draft/pre-away` ship with in-memory reference stores, so marker /
472
961
  away persistence does not survive an adapter restart until a
473
962
  services backend lands.
474
- - The web client Phase 14 work (vendor Kiwi IRC, serve SPA from the
963
+ - The web client work (vendor Kiwi IRC, serve SPA from the
475
964
  Worker, WS Origin allowlist) is pending.
476
965
 
477
966
  ### Security
@@ -486,8 +975,8 @@ resolves it.
486
975
 
487
976
  ## [0.5.0] - 2026-08-02
488
977
 
489
- The headline is **protocol completion**: the deferred IRC verbs land
490
- (Phase 11), and the S2S / obsolete RFC 2812 verbs are **formally
978
+ The main change in this release is **protocol completion**: the deferred IRC verbs land,
979
+ and the S2S / obsolete RFC 2812 verbs are **formally
491
980
  dropped** rather than left "deferred". With this release every standard
492
981
  IRC verb a modern client expects is implemented — the only remaining
493
982
  `421 ERR_UNKNOWNCOMMAND` paths are the deliberately-unimplemented S2S
@@ -497,10 +986,10 @@ client compatibility matrix) and the required-`serverName` config gate.
497
986
 
498
987
  ### Added — Deferred IRC verbs (`@serverless-ircd/irc-core`, `@serverless-ircd/irc-server`)
499
988
 
500
- Six Phase 11 tickets light up the remaining command surface. Each ships
989
+ Six tickets add the remaining command surface. Each ships
501
990
  a pure reducer, an actor route case (no more `421`), new numerics in
502
991
  `protocol/numerics.ts`, and unit + actor coverage. (The remaining two
503
- Phase 11 tickets are drops — see below.)
992
+ tickets are drops — see below.)
504
993
 
505
994
  - **`KILL`** (oper-gated force-disconnect): `KILL <nick>
506
995
  <comment>` resolves the target via the nick registry + a live
@@ -554,7 +1043,7 @@ Phase 11 tickets are drops — see below.)
554
1043
  channel-scoped). The originator is always excluded (skip-self,
555
1044
  matching NOTICE/PRIVMSG semantics). Empty message → `461`. The `+w`
556
1045
  user mode was already accepted by `MODE` and advertised in `004
557
- RPL_MYINFO`; this lights up the consumer.
1046
+ RPL_MYINFO`; this enables the consumer.
558
1047
  - **`SETNAME`** (realname change): `SETNAME :<realname>`
559
1048
  updates `state.realname` so a subsequent `WHOIS` reflects the new
560
1049
  value in `311 RPL_WHOISUSER`. No broadcast is emitted in v1 (there
@@ -564,7 +1053,7 @@ Phase 11 tickets are drops — see below.)
564
1053
 
565
1054
  ### Changed — S2S & obsolete verbs formally dropped (`@serverless-ircd/irc-core`)
566
1055
 
567
- The remaining two Phase 11 tickets convert the "deferred" framing into
1056
+ The remaining two tickets convert the "deferred" framing into
568
1057
  an explicit **drop**. There is no behaviour change on the wire — the verbs
569
1058
  already returned `421 ERR_UNKNOWNCOMMAND` — but the framing, comments,
570
1059
  and reserved numerics are cleaned up so the codebase no longer carries
@@ -661,13 +1150,13 @@ dead scaffolding.
661
1150
 
662
1151
  ## [0.4.0] - 2026-07-31
663
1152
 
664
- The headline is **dual transport**: both adapters now speak a real
1153
+ The main change in this release is **dual transport**: both adapters now speak a real
665
1154
  `irc+tls :6697` (TLS-over-TCP) surface for stock IRC clients in
666
1155
  production — not just the local CLI — alongside the WebSocket path. This
667
1156
  lands **SASL `EXTERNAL` via mTLS**, the **remaining standard IRC verbs**
668
1157
  (WHOWAS, VERSION, TIME, ADMIN, INFO, USERHOST, ISON), a **Cloudflare
669
1158
  D1-backed SASL account store**, config-driven server identity, and a
670
- **Node 24 / pnpm 11** toolchain. Phase 7 (TLS hardening & raw TCP) is
1159
+ **Node 24 / pnpm 11** toolchain. TLS hardening & raw TCP is
671
1160
  complete.
672
1161
 
673
1162
  ### Added — Dual transport: WebSocket + `irc+tls :6697`
@@ -737,7 +1226,7 @@ pipeline is unchanged.
737
1226
 
738
1227
  - The `002` / `003` / `004` registration replies now reflect
739
1228
  `ServerConfig.serverVersion` / `createdAt` — the hardcoded
740
- `'0.1.0'` / `'Phase 1'` placeholders are gone. A build step
1229
+ `'0.1.0'` version / placeholder strings are gone. A build step
741
1230
  (`scripts/generate-build-info.mjs`) bakes the package version and a
742
1231
  build timestamp into `RPL_CREATED` (`003`) when the config omits them.
743
1232
 
@@ -1005,7 +1494,7 @@ AWS are now both deployable.
1005
1494
  ### Added — Shared test harness (`@serverless-ircd/irc-test-support`)
1006
1495
 
1007
1496
  - New workspace package owning the parametrized IRC scenario suite
1008
- (`runIrcScenarios`, 31 Phase 2 scenarios) and the
1497
+ (`runIrcScenarios`, 31 scenarios) and the
1009
1498
  `IrcHarnessFactory` / `IrcHarness` / `ClientHarness` seam each
1010
1499
  adapter implements. Ships the reference `inMemoryHarnessFactory`.
1011
1500
  cf-adapter and aws-adapter integration tests both consume it, so the
@@ -1036,7 +1525,7 @@ AWS are now both deployable.
1036
1525
  end-to-end rather than advertised-only.
1037
1526
  - The local CLI now binds a **second RFC-style TCP listener** by default
1038
1527
  (`--tcp-port`, default `<port> + 1`) alongside the WebSocket listener,
1039
- so stock IRC clients can dial in directly without the forwarder.
1528
+ so stock IRC clients can connect directly without the forwarder.
1040
1529
  Pass `--no-tcp` for WebSocket-only. `--motd-file` loads the MOTD from
1041
1530
  disk.
1042
1531