serverless-ircd 0.5.0 → 0.7.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 (394) hide show
  1. package/.github/workflows/ci.yml +40 -3
  2. package/.github/workflows/deploy-aws.yml +1 -2
  3. package/.github/workflows/deploy-cf-tcp.yml +1 -5
  4. package/.github/workflows/deploy-cf.yml +1 -2
  5. package/.gitmodules +3 -0
  6. package/CHANGELOG.md +474 -0
  7. package/README.md +219 -22
  8. package/apps/aws-stack/README.md +33 -0
  9. package/apps/aws-stack/package.json +1 -1
  10. package/apps/aws-stack/src/aws-stack.ts +74 -9
  11. package/apps/aws-stack/tests/coverage-command.test.ts +155 -0
  12. package/apps/aws-stack/tests/stack.test.ts +2 -2
  13. package/apps/aws-stack/tests/synth-no-bundle.test.ts +129 -0
  14. package/apps/aws-stack/tests/vitest.setup.ts +18 -0
  15. package/apps/aws-stack/vitest.config.ts +3 -0
  16. package/apps/cf-tcp-container/package.json +1 -1
  17. package/apps/cf-tcp-container/src/config-loader.ts +6 -7
  18. package/apps/cf-tcp-container/src/container-server.ts +3 -0
  19. package/apps/cf-tcp-container/tests/config-loader.test.ts +10 -3
  20. package/apps/cf-tcp-container/wrangler.toml +1 -1
  21. package/apps/cf-worker/package.json +2 -1
  22. package/apps/cf-worker/src/origin-allowlist.ts +99 -0
  23. package/apps/cf-worker/src/worker.ts +89 -9
  24. package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +18 -0
  25. package/apps/cf-worker/tests/fixtures/web-dist/index.html +18 -0
  26. package/apps/cf-worker/tests/origin-allowlist.test.ts +101 -0
  27. package/apps/cf-worker/tests/smoke.test.ts +28 -2
  28. package/apps/cf-worker/tests/ws-origin.test.ts +89 -0
  29. package/apps/cf-worker/vitest.config.ts +1 -1
  30. package/apps/cf-worker/wrangler.test.toml +23 -0
  31. package/apps/cf-worker/wrangler.toml +59 -3
  32. package/apps/local-cli/package.json +1 -1
  33. package/apps/local-cli/src/config-loader.ts +6 -6
  34. package/apps/local-cli/src/main.ts +20 -0
  35. package/apps/local-cli/src/server.ts +9 -7
  36. package/apps/local-cli/tests/e2e.test.ts +112 -28
  37. package/apps/local-cli/tests/tcp.test.ts +71 -19
  38. package/apps/local-cli/tests/ws-subprotocol.test.ts +12 -7
  39. package/apps/web/landing/index.html +217 -0
  40. package/apps/web/package.json +28 -0
  41. package/apps/web/scripts/build.mjs +125 -0
  42. package/apps/web/src/build-env.ts +43 -0
  43. package/apps/web/src/config-schema.ts +138 -0
  44. package/apps/web/static/config.json +28 -0
  45. package/apps/web/static/config.prod.json +28 -0
  46. package/apps/web/static/config.staging.json +28 -0
  47. package/apps/web/tests/build-env.test.ts +63 -0
  48. package/apps/web/tests/build-smoke.test.ts +103 -0
  49. package/apps/web/tests/config-schema.test.ts +432 -0
  50. package/apps/web/tests/workspace.test.ts +12 -0
  51. package/apps/web/tsconfig.json +10 -0
  52. package/apps/web/upstream/.browserslistrc +3 -0
  53. package/apps/web/upstream/.editorconfig +9 -0
  54. package/apps/web/upstream/.eslintignore +3 -0
  55. package/apps/web/upstream/.eslintrc.js +90 -0
  56. package/apps/web/upstream/.github/FUNDING.yml +12 -0
  57. package/apps/web/upstream/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  58. package/apps/web/upstream/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  59. package/apps/web/upstream/.github/workflows/push.yml +26 -0
  60. package/apps/web/upstream/.github/workflows/release.yml +33 -0
  61. package/apps/web/upstream/.github/workflows/release_manual.yml +55 -0
  62. package/apps/web/upstream/.prettierrc.js +9 -0
  63. package/apps/web/upstream/.stylelintignore +5 -0
  64. package/apps/web/upstream/.stylelintrc.js +21 -0
  65. package/apps/web/upstream/LICENSE +201 -0
  66. package/apps/web/upstream/README.md +76 -0
  67. package/apps/web/upstream/babel.config.js +25 -0
  68. package/apps/web/upstream/crowdin.yml +3 -0
  69. package/apps/web/upstream/index.html +35 -0
  70. package/apps/web/upstream/jest.config.js +7 -0
  71. package/apps/web/upstream/jsdoc.json +22 -0
  72. package/apps/web/upstream/package.json +80 -0
  73. package/apps/web/upstream/scripts/pre-commit +43 -0
  74. package/apps/web/upstream/src/components/App.vue +516 -0
  75. package/apps/web/upstream/src/components/AppSettings.vue +587 -0
  76. package/apps/web/upstream/src/components/AutoComplete.vue +284 -0
  77. package/apps/web/upstream/src/components/Avatar.vue +105 -0
  78. package/apps/web/upstream/src/components/AwayStatusIndicator.vue +77 -0
  79. package/apps/web/upstream/src/components/BufferKey.vue +121 -0
  80. package/apps/web/upstream/src/components/BufferSettings.vue +97 -0
  81. package/apps/web/upstream/src/components/Captcha.vue +85 -0
  82. package/apps/web/upstream/src/components/ChannelBanlist.vue +96 -0
  83. package/apps/web/upstream/src/components/ChannelInfo.vue +159 -0
  84. package/apps/web/upstream/src/components/ChannelInvitelist.vue +276 -0
  85. package/apps/web/upstream/src/components/ChannelList.vue +345 -0
  86. package/apps/web/upstream/src/components/Container.vue +401 -0
  87. package/apps/web/upstream/src/components/ContainerHeader.vue +522 -0
  88. package/apps/web/upstream/src/components/ControlInput.vue +1016 -0
  89. package/apps/web/upstream/src/components/LoadingAnimation.vue +104 -0
  90. package/apps/web/upstream/src/components/MediaViewer.vue +199 -0
  91. package/apps/web/upstream/src/components/MessageInfo.vue +192 -0
  92. package/apps/web/upstream/src/components/MessageList.vue +1028 -0
  93. package/apps/web/upstream/src/components/MessageListAvatar.vue +20 -0
  94. package/apps/web/upstream/src/components/MessageListMessageCompact.vue +361 -0
  95. package/apps/web/upstream/src/components/MessageListMessageInline.vue +257 -0
  96. package/apps/web/upstream/src/components/MessageListMessageModern.vue +430 -0
  97. package/apps/web/upstream/src/components/NetworkSettings.vue +528 -0
  98. package/apps/web/upstream/src/components/Nicklist.vue +309 -0
  99. package/apps/web/upstream/src/components/NicklistUser.vue +181 -0
  100. package/apps/web/upstream/src/components/NotConnected.vue +271 -0
  101. package/apps/web/upstream/src/components/SelfUser.vue +283 -0
  102. package/apps/web/upstream/src/components/ServerSelector.vue +162 -0
  103. package/apps/web/upstream/src/components/ServerView.vue +126 -0
  104. package/apps/web/upstream/src/components/SettingsAdvanced.vue +232 -0
  105. package/apps/web/upstream/src/components/SettingsAliases.vue +121 -0
  106. package/apps/web/upstream/src/components/Sidebar.vue +429 -0
  107. package/apps/web/upstream/src/components/SidebarAboutBuffer.vue +288 -0
  108. package/apps/web/upstream/src/components/SidebarState.vue +145 -0
  109. package/apps/web/upstream/src/components/StartupError.vue +35 -0
  110. package/apps/web/upstream/src/components/StateBrowser.vue +420 -0
  111. package/apps/web/upstream/src/components/StateBrowserBuffer.vue +124 -0
  112. package/apps/web/upstream/src/components/StateBrowserNetwork.vue +823 -0
  113. package/apps/web/upstream/src/components/StateBrowserUsermenu.vue +162 -0
  114. package/apps/web/upstream/src/components/TypingStatusIndicator.vue +34 -0
  115. package/apps/web/upstream/src/components/TypingUsersList.vue +56 -0
  116. package/apps/web/upstream/src/components/UrlEmbed.vue +149 -0
  117. package/apps/web/upstream/src/components/UserBox.vue +728 -0
  118. package/apps/web/upstream/src/components/inputtools/Emoji.vue +67 -0
  119. package/apps/web/upstream/src/components/inputtools/TextStyle.vue +142 -0
  120. package/apps/web/upstream/src/components/startups/CommonLayout.vue +208 -0
  121. package/apps/web/upstream/src/components/startups/CustomServer.vue +495 -0
  122. package/apps/web/upstream/src/components/startups/KiwiBnc.vue +361 -0
  123. package/apps/web/upstream/src/components/startups/Personal.vue +317 -0
  124. package/apps/web/upstream/src/components/startups/Welcome.vue +544 -0
  125. package/apps/web/upstream/src/components/startups/ZncLogin.vue +229 -0
  126. package/apps/web/upstream/src/components/utils/InputConfirm.vue +68 -0
  127. package/apps/web/upstream/src/components/utils/InputPrompt.vue +118 -0
  128. package/apps/web/upstream/src/components/utils/InputText.vue +172 -0
  129. package/apps/web/upstream/src/components/utils/IrcInput.vue +579 -0
  130. package/apps/web/upstream/src/components/utils/PluginWrapper.vue +26 -0
  131. package/apps/web/upstream/src/components/utils/TabbedView.vue +149 -0
  132. package/apps/web/upstream/src/components/utils/TransitionExpand.vue +97 -0
  133. package/apps/web/upstream/src/helpers/Colours.js +128 -0
  134. package/apps/web/upstream/src/helpers/IrcdDiffs.js +26 -0
  135. package/apps/web/upstream/src/helpers/Md5.js +193 -0
  136. package/apps/web/upstream/src/helpers/Misc.js +401 -0
  137. package/apps/web/upstream/src/helpers/TextFormatting.js +237 -0
  138. package/apps/web/upstream/src/libs/AliasRewriter.js +157 -0
  139. package/apps/web/upstream/src/libs/AudioManager.js +60 -0
  140. package/apps/web/upstream/src/libs/BouncerMiddleware.js +247 -0
  141. package/apps/web/upstream/src/libs/BouncerProvider.js +551 -0
  142. package/apps/web/upstream/src/libs/ChathistoryMiddleware.js +153 -0
  143. package/apps/web/upstream/src/libs/ConfigLoader.js +94 -0
  144. package/apps/web/upstream/src/libs/EmojiProvider.js +46 -0
  145. package/apps/web/upstream/src/libs/GlobalApi.js +346 -0
  146. package/apps/web/upstream/src/libs/IPC.js +51 -0
  147. package/apps/web/upstream/src/libs/InputHandler.js +921 -0
  148. package/apps/web/upstream/src/libs/IrcClient.js +1506 -0
  149. package/apps/web/upstream/src/libs/Logger.js +71 -0
  150. package/apps/web/upstream/src/libs/Message.js +169 -0
  151. package/apps/web/upstream/src/libs/MessageFormatter.js +364 -0
  152. package/apps/web/upstream/src/libs/MessageParser.js +249 -0
  153. package/apps/web/upstream/src/libs/Notifications.js +78 -0
  154. package/apps/web/upstream/src/libs/ServerConnection.js +270 -0
  155. package/apps/web/upstream/src/libs/ServerSession.js +102 -0
  156. package/apps/web/upstream/src/libs/SoundBleep.js +20 -0
  157. package/apps/web/upstream/src/libs/StatePersistence.js +90 -0
  158. package/apps/web/upstream/src/libs/ThemeManager.js +128 -0
  159. package/apps/web/upstream/src/libs/TypingMiddleware.js +105 -0
  160. package/apps/web/upstream/src/libs/WindowTitle.js +63 -0
  161. package/apps/web/upstream/src/libs/batchedAdd.js +74 -0
  162. package/apps/web/upstream/src/libs/bufferTools.js +177 -0
  163. package/apps/web/upstream/src/libs/polyfill/Element.closest.js +18 -0
  164. package/apps/web/upstream/src/libs/renderers/Html.js +113 -0
  165. package/apps/web/upstream/src/libs/settingTools.js +31 -0
  166. package/apps/web/upstream/src/libs/state/BufferState.js +703 -0
  167. package/apps/web/upstream/src/libs/state/NetworkState.js +158 -0
  168. package/apps/web/upstream/src/libs/state/UserState.js +89 -0
  169. package/apps/web/upstream/src/libs/state/common.js +26 -0
  170. package/apps/web/upstream/src/libs/state.js +961 -0
  171. package/apps/web/upstream/src/libs/storage/Local.js +51 -0
  172. package/apps/web/upstream/src/main.js +566 -0
  173. package/apps/web/upstream/src/res/autocompleteCommands.js +31 -0
  174. package/apps/web/upstream/src/res/configTemplates.js +373 -0
  175. package/apps/web/upstream/src/res/globalStyle.css +277 -0
  176. package/apps/web/upstream/src/res/kiwiLoadingLogo.png +0 -0
  177. package/apps/web/upstream/src/res/locales/app.af-ZA.po +1145 -0
  178. package/apps/web/upstream/src/res/locales/app.ar-SA.po +1145 -0
  179. package/apps/web/upstream/src/res/locales/app.bg-BG.po +1145 -0
  180. package/apps/web/upstream/src/res/locales/app.bs-BA.po +1145 -0
  181. package/apps/web/upstream/src/res/locales/app.ca-ES.po +1145 -0
  182. package/apps/web/upstream/src/res/locales/app.cs-CZ.po +1145 -0
  183. package/apps/web/upstream/src/res/locales/app.da-DK.po +1145 -0
  184. package/apps/web/upstream/src/res/locales/app.de-DE.po +1145 -0
  185. package/apps/web/upstream/src/res/locales/app.dev.po +1246 -0
  186. package/apps/web/upstream/src/res/locales/app.el-GR.po +1145 -0
  187. package/apps/web/upstream/src/res/locales/app.en-US.po +1145 -0
  188. package/apps/web/upstream/src/res/locales/app.es-419.po +1145 -0
  189. package/apps/web/upstream/src/res/locales/app.es-AR.po +1145 -0
  190. package/apps/web/upstream/src/res/locales/app.es-EM.po +536 -0
  191. package/apps/web/upstream/src/res/locales/app.es-ES.po +1145 -0
  192. package/apps/web/upstream/src/res/locales/app.es-US.po +1145 -0
  193. package/apps/web/upstream/src/res/locales/app.eu-ES.po +1145 -0
  194. package/apps/web/upstream/src/res/locales/app.fi-FI.po +1145 -0
  195. package/apps/web/upstream/src/res/locales/app.fr-FR.po +1145 -0
  196. package/apps/web/upstream/src/res/locales/app.gl-ES.po +1145 -0
  197. package/apps/web/upstream/src/res/locales/app.he-IL.po +1145 -0
  198. package/apps/web/upstream/src/res/locales/app.hi-IN.po +1145 -0
  199. package/apps/web/upstream/src/res/locales/app.hu-HU.po +1145 -0
  200. package/apps/web/upstream/src/res/locales/app.id-ID.po +1145 -0
  201. package/apps/web/upstream/src/res/locales/app.it-IT.po +1145 -0
  202. package/apps/web/upstream/src/res/locales/app.ja-JP.po +1145 -0
  203. package/apps/web/upstream/src/res/locales/app.ko-KR.po +1145 -0
  204. package/apps/web/upstream/src/res/locales/app.nl-NL.po +1145 -0
  205. package/apps/web/upstream/src/res/locales/app.no-NO.po +1145 -0
  206. package/apps/web/upstream/src/res/locales/app.pl-PL.po +1145 -0
  207. package/apps/web/upstream/src/res/locales/app.pt-BR.po +1145 -0
  208. package/apps/web/upstream/src/res/locales/app.pt-PT.po +1145 -0
  209. package/apps/web/upstream/src/res/locales/app.ro-RO.po +1145 -0
  210. package/apps/web/upstream/src/res/locales/app.ru-RU.po +1145 -0
  211. package/apps/web/upstream/src/res/locales/app.sl-SI.po +1145 -0
  212. package/apps/web/upstream/src/res/locales/app.sq-AL.po +1145 -0
  213. package/apps/web/upstream/src/res/locales/app.sr-SP.po +1145 -0
  214. package/apps/web/upstream/src/res/locales/app.sv-SE.po +1145 -0
  215. package/apps/web/upstream/src/res/locales/app.tr-TR.po +1145 -0
  216. package/apps/web/upstream/src/res/locales/app.uk-UA.po +1145 -0
  217. package/apps/web/upstream/src/res/locales/app.vi-VN.po +1145 -0
  218. package/apps/web/upstream/src/res/locales/app.zh-CN.po +1145 -0
  219. package/apps/web/upstream/src/res/locales/app.zh-TW.po +1145 -0
  220. package/apps/web/upstream/src/res/localesList.json +27 -0
  221. package/apps/web/upstream/src/res/logo.png +0 -0
  222. package/apps/web/upstream/src/thirdparty/about.html +43 -0
  223. package/apps/web/upstream/src/thirdparty/index.js +5 -0
  224. package/apps/web/upstream/src/thirdparty/kiwiirccom.vue +185 -0
  225. package/apps/web/upstream/static/config.json +28 -0
  226. package/apps/web/upstream/static/emoticons/smile.png +0 -0
  227. package/apps/web/upstream/static/favicon.png +0 -0
  228. package/apps/web/upstream/static/highlight.mp3 +0 -0
  229. package/apps/web/upstream/static/highlight.ogg +0 -0
  230. package/apps/web/upstream/static/locales/.gitignore +2 -0
  231. package/apps/web/upstream/static/plugins/customise.html.example +10 -0
  232. package/apps/web/upstream/static/themes/coffee/theme.css +145 -0
  233. package/apps/web/upstream/static/themes/common/base.css +1064 -0
  234. package/apps/web/upstream/static/themes/dark/theme.css +196 -0
  235. package/apps/web/upstream/static/themes/default/theme.css +92 -0
  236. package/apps/web/upstream/static/themes/elite/theme.css +248 -0
  237. package/apps/web/upstream/static/themes/grayfox/theme.css +143 -0
  238. package/apps/web/upstream/static/themes/nightswatch/theme.css +367 -0
  239. package/apps/web/upstream/static/themes/osprey/theme.css +115 -0
  240. package/apps/web/upstream/static/themes/radioactive/theme.css +1211 -0
  241. package/apps/web/upstream/static/themes/sky/theme.css +107 -0
  242. package/apps/web/upstream/tests/unit/BatchAdd.spec.js +174 -0
  243. package/apps/web/upstream/tests/unit/MessageParser.spec.js +125 -0
  244. package/apps/web/upstream/tests/unit/Misc.spec.js +24 -0
  245. package/apps/web/upstream/tests/unit/NetworkState.spec.js +58 -0
  246. package/apps/web/upstream/tests/unit/StartupError.spec.js +20 -0
  247. package/apps/web/upstream/vue.config.js +150 -0
  248. package/apps/web/upstream/yarn.lock +9767 -0
  249. package/apps/web/vitest.config.ts +13 -0
  250. package/biome.json +2 -0
  251. package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
  252. package/docs/ADR-002-location-of-authority.md +82 -0
  253. package/docs/ADR-003-durable-object-sharding.md +110 -0
  254. package/docs/ADR-004-dynamodb-schema.md +96 -0
  255. package/docs/ADR-005-wss-only-transport-v1.md +83 -0
  256. package/docs/ADR-006-sasl-mechanism-scope.md +96 -0
  257. package/docs/ADR-007-deterministic-ports.md +82 -0
  258. package/docs/ADR-008-monorepo-tooling.md +60 -0
  259. package/docs/ADR-009-dual-transport-wss-and-irc-tls.md +138 -0
  260. package/docs/ADR-Index.md +15 -0
  261. package/docs/AWS-Adapter-Architecture.md +496 -0
  262. package/docs/AWS-Deployment.md +1275 -0
  263. package/docs/AWS-TCP-Deployment.md +410 -0
  264. package/docs/Cloudflare-Deployment-Guide.md +719 -0
  265. package/docs/Cloudflare-TCP-Deployment.md +510 -0
  266. package/docs/Home.md +16 -0
  267. package/docs/Observability.md +87 -0
  268. package/docs/PlanExtensions.md +812 -0
  269. package/docs/PlanIRCv3Websocket.md +503 -0
  270. package/docs/PlanWebClient.md +451 -0
  271. package/docs/Release-Process.md +443 -0
  272. package/docs/WebClientGuide.md +535 -0
  273. package/package.json +2 -2
  274. package/packages/aws-adapter/package.json +1 -1
  275. package/packages/aws-adapter/src/handlers/default.ts +3 -0
  276. package/packages/aws-adapter/src/handlers/nlb-stream.ts +9 -0
  277. package/packages/aws-adapter/src/serialize.ts +29 -1
  278. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +49 -8
  279. package/packages/aws-adapter/tests/account-store.test.ts +4 -7
  280. package/packages/aws-adapter/tests/aws-harness.ts +5 -2
  281. package/packages/aws-adapter/tests/aws-integration.test.ts +8 -4
  282. package/packages/aws-adapter/tests/aws-runtime.test.ts +79 -0
  283. package/packages/aws-adapter/tests/config-loader.test.ts +11 -2
  284. package/packages/aws-adapter/tests/connect.test.ts +9 -9
  285. package/packages/aws-adapter/tests/disconnect-fanout.test.ts +3 -4
  286. package/packages/aws-adapter/tests/global-setup.ts +55 -30
  287. package/packages/aws-adapter/tests/gone-exception.test.ts +4 -9
  288. package/packages/aws-adapter/tests/handlers.test.ts +90 -4
  289. package/packages/aws-adapter/tests/message-store.test.ts +6 -5
  290. package/packages/aws-adapter/tests/nlb-stream.test.ts +60 -8
  291. package/packages/cf-adapter/package.json +2 -1
  292. package/packages/cf-adapter/src/connection-do.ts +3 -0
  293. package/packages/cf-adapter/src/serialize.ts +25 -4
  294. package/packages/cf-adapter/tests/cf-harness.ts +12 -1
  295. package/packages/cf-adapter/tests/cf-runtime.test.ts +269 -0
  296. package/packages/cf-adapter/tests/channel-do.test.ts +117 -0
  297. package/packages/cf-adapter/tests/config-loader.test.ts +49 -0
  298. package/packages/cf-adapter/tests/connection-do-pure.test.ts +91 -0
  299. package/packages/cf-adapter/tests/d1-account-store.test.ts +48 -0
  300. package/packages/cf-adapter/tests/registry-do.test.ts +104 -0
  301. package/packages/cf-adapter/tests/serialize.test.ts +308 -0
  302. package/packages/cf-adapter/vitest.config.ts +1 -1
  303. package/packages/in-memory-runtime/package.json +1 -1
  304. package/packages/irc-core/package.json +1 -1
  305. package/packages/irc-core/reports/mutation/mutation.html +342 -0
  306. package/packages/irc-core/src/caps/capabilities.ts +37 -3
  307. package/packages/irc-core/src/caps/index.ts +1 -0
  308. package/packages/irc-core/src/caps/sts.ts +84 -0
  309. package/packages/irc-core/src/commands/account-notify.ts +53 -0
  310. package/packages/irc-core/src/commands/away.ts +9 -3
  311. package/packages/irc-core/src/commands/cap.ts +23 -5
  312. package/packages/irc-core/src/commands/chathistory.ts +40 -11
  313. package/packages/irc-core/src/commands/index.ts +34 -0
  314. package/packages/irc-core/src/commands/isupport.ts +53 -0
  315. package/packages/irc-core/src/commands/mode.ts +8 -1
  316. package/packages/irc-core/src/commands/monitor.ts +327 -0
  317. package/packages/irc-core/src/commands/multiline.ts +256 -0
  318. package/packages/irc-core/src/commands/pre-away.ts +112 -0
  319. package/packages/irc-core/src/commands/privmsg.ts +19 -12
  320. package/packages/irc-core/src/commands/read-marker.ts +108 -0
  321. package/packages/irc-core/src/commands/registration.ts +8 -0
  322. package/packages/irc-core/src/commands/sasl.ts +24 -1
  323. package/packages/irc-core/src/commands/tagmsg.ts +71 -1
  324. package/packages/irc-core/src/commands/whois.ts +5 -0
  325. package/packages/irc-core/src/config.ts +65 -10
  326. package/packages/irc-core/src/effects.ts +14 -0
  327. package/packages/irc-core/src/ports.ts +389 -0
  328. package/packages/irc-core/src/protocol/index.ts +14 -0
  329. package/packages/irc-core/src/protocol/labeled-response.ts +133 -0
  330. package/packages/irc-core/src/protocol/numerics.ts +15 -0
  331. package/packages/irc-core/src/protocol/outbound.ts +16 -1
  332. package/packages/irc-core/src/protocol/standard-replies.ts +277 -0
  333. package/packages/irc-core/src/state/connection.ts +32 -1
  334. package/packages/irc-core/src/types.ts +112 -0
  335. package/packages/irc-core/stryker.commands.conf.json +1 -2
  336. package/packages/irc-core/tests/away-store.test.ts +73 -0
  337. package/packages/irc-core/tests/batch.test.ts +15 -0
  338. package/packages/irc-core/tests/caps/capabilities.test.ts +119 -2
  339. package/packages/irc-core/tests/caps/sts.test.ts +123 -0
  340. package/packages/irc-core/tests/commands/account-notify.test.ts +171 -0
  341. package/packages/irc-core/tests/commands/away.test.ts +85 -2
  342. package/packages/irc-core/tests/commands/cap.test.ts +165 -0
  343. package/packages/irc-core/tests/commands/chathistory.test.ts +148 -1
  344. package/packages/irc-core/tests/commands/echo-message.test.ts +9 -3
  345. package/packages/irc-core/tests/commands/isupport.test.ts +80 -0
  346. package/packages/irc-core/tests/commands/mode.test.ts +72 -0
  347. package/packages/irc-core/tests/commands/monitor.test.ts +411 -0
  348. package/packages/irc-core/tests/commands/multiline.test.ts +428 -0
  349. package/packages/irc-core/tests/commands/pre-away.test.ts +241 -0
  350. package/packages/irc-core/tests/commands/privmsg.test.ts +154 -3
  351. package/packages/irc-core/tests/commands/read-marker.test.ts +211 -0
  352. package/packages/irc-core/tests/commands/registration.test.ts +64 -0
  353. package/packages/irc-core/tests/commands/sasl.test.ts +116 -2
  354. package/packages/irc-core/tests/commands/tagmsg.test.ts +357 -0
  355. package/packages/irc-core/tests/commands/whois.test.ts +34 -0
  356. package/packages/irc-core/tests/config.test.ts +103 -13
  357. package/packages/irc-core/tests/labeled-response.test.ts +181 -0
  358. package/packages/irc-core/tests/message-tags.test.ts +114 -0
  359. package/packages/irc-core/tests/monitor-store.test.ts +200 -0
  360. package/packages/irc-core/tests/numerics.test.ts +13 -0
  361. package/packages/irc-core/tests/parser.test.ts +109 -0
  362. package/packages/irc-core/tests/read-marker-store.test.ts +108 -0
  363. package/packages/irc-core/tests/standard-replies.test.ts +412 -0
  364. package/packages/irc-core/tests/types.test.ts +27 -0
  365. package/packages/irc-server/package.json +1 -1
  366. package/packages/irc-server/src/actor.ts +450 -3
  367. package/packages/irc-server/src/dispatch.ts +108 -16
  368. package/packages/irc-server/tests/actor.test.ts +1432 -39
  369. package/packages/irc-server/tests/dispatch.test.ts +263 -2
  370. package/packages/irc-test-support/package.json +1 -1
  371. package/packages/irc-test-support/src/index.ts +6 -0
  372. package/packages/irc-test-support/src/scenarios.ts +24 -2
  373. package/packages/irc-test-support/src/test-config.ts +54 -0
  374. package/packages/irc-test-support/tests/test-config.test.ts +51 -0
  375. package/tools/ci-hardening/package.json +1 -1
  376. package/tools/ci-hardening/tests/ddb-local.test.ts +82 -0
  377. package/tools/load-test/package.json +33 -0
  378. package/tools/load-test/src/client.ts +351 -0
  379. package/tools/load-test/src/config.ts +313 -0
  380. package/tools/load-test/src/harness.ts +116 -0
  381. package/tools/load-test/src/main.ts +120 -0
  382. package/tools/load-test/src/metrics.ts +168 -0
  383. package/tools/load-test/src/report.ts +106 -0
  384. package/tools/load-test/tests/client.test.ts +212 -0
  385. package/tools/load-test/tests/config.test.ts +152 -0
  386. package/tools/load-test/tests/framing.test.ts +37 -0
  387. package/tools/load-test/tests/harness.test.ts +165 -0
  388. package/tools/load-test/tests/metrics.test.ts +174 -0
  389. package/tools/load-test/tests/report.test.ts +161 -0
  390. package/tools/load-test/tests/smoke.test.ts +67 -0
  391. package/tools/load-test/tsconfig.build.json +12 -0
  392. package/tools/load-test/tsconfig.test.json +10 -0
  393. package/tools/load-test/vitest.config.ts +29 -0
  394. package/tools/tcp-ws-forwarder/package.json +1 -1
@@ -0,0 +1,812 @@
1
+ # ServerlessIRCd — Extensions Plan (post-v1)
2
+
3
+ Plan for the IRC features that v1 deliberately left out: the remaining
4
+ **IRCv3 client-server extensions** and **IRC services** (NickServ /
5
+ ChanServ / OperServ / HostServ / MemoServ). Each section is scoped,
6
+ mapped onto the existing hexagonal architecture, and proposed as
7
+ tickets for `tickets.md` / `progress.md` once ratified.
8
+
9
+ > **Status:** planning. Nothing here has landed. Ticket numbers below
10
+ > start at **TICKET-110** (next free id per `todo.md`). When this plan
11
+ > is ratified the tickets get appended to `tickets.md` / `progress.md`
12
+ > under two new phase headings (**Phase 12 — IRCv3 extensions**,
13
+ > **Phase 13 — IRC services**) and the corresponding sections here are
14
+ > superseded.
15
+
16
+ ---
17
+
18
+ ## 1. Goals & non-goals
19
+
20
+ **Goals**
21
+ - Close the gap between the v1 cap set and what mainstream clients
22
+ (IRCCloud, TheLounge, Textual, gamja) actually negotiate today.
23
+ - Add the IRCv3 features that compose with work already shipped:
24
+ `account-tag` is meaningless without `account-notify`; `message-tags`
25
+ without `msgid` / `labeled-response` leaves clients correlating by
26
+ content.
27
+ - Provide a services story (NickServ/ChanServ) that fits the
28
+ pure-reducer + location-of-authority architecture instead of bolting
29
+ on a pseudo-server.
30
+ - Stay serverless-native: every new persistence need is behind a port
31
+ (`MessageStore`, `AccountStore`, `ServicesStore`) so the same core
32
+ runs on Cloudflare and AWS unchanged.
33
+
34
+ **Non-goals (this plan)**
35
+ - **Server-to-server linking** (TS6, IRCv3 S2S, SALR). Remains a
36
+ non-goal, same as v1 (`PLAN.md` §1). A `ServerLink` seam is the
37
+ documented design-for-later; no S2S work is proposed here.
38
+ - **Atheme / Anope compatibility.** Services are integrated into the
39
+ daemon, not implemented as a separate pseudo-server speaking a link
40
+ protocol. See §6 architecture decision.
41
+ - **Full services suite parity.** BotServ and StatServ are explicitly
42
+ out of scope; OperServ ships a minimal AKILL/JUPE set only.
43
+ - **Plaintext :6667 support.** Still TLS-only (ADR-009).
44
+
45
+ ---
46
+
47
+ ## 2. Current cap set (baseline)
48
+
49
+ `packages/irc-core/src/caps/capabilities.ts:28` advertises:
50
+
51
+ ```
52
+ account-tag, away-notify, batch, chghost, draft/chathistory,
53
+ echo-message, extended-join, invite-notify, message-tags,
54
+ multi-prefix, safelist, sasl=PLAIN[,EXTERNAL], server-time
55
+ ```
56
+
57
+ What follows fills in the missing pieces, grouped by spec maturity.
58
+
59
+ ---
60
+
61
+ ## 3. Missing IRCv3 extensions
62
+
63
+ ### 3.1 Stable spec extensions (high-value, low-risk)
64
+
65
+ | Cap | Spec | What it adds | Effort | Depends on |
66
+ |-----|------|--------------|--------|------------|
67
+ | `account-notify` | [ircv3.net/specs/extensions/account-notify-3.2](https://ircv3.net/specs/extensions/account-notify-3.2.html) | Push `ACCOUNT` messages to peers in shared channels on login/logout (complements `account-tag`, `extended-join`) | S (2 pts) | SASL AccountStore wiring (TICKET-071 ✅) |
68
+ | `msgid` | [ircv3.net/specs/extensions/message-ids](https://ircv3.net/specs/extensions/message-ids.html) | Stable per-message id as a `msgid` tag on every PRIVMSG/NOTICE to cap-enabled peers (live fanout), sharing the single `IdFactory.nonce()` value also recorded for chathistory replay; needed for `draft/read-marker`, replies, dedup | S (2 pts) | `message-tags` ✅, `IdFactory` ✅ |
69
+ | `standard-replies` | [ircv3.net/specs/extensions/standard-replies-3.0](https://ircv3.net/specs/extensions/standard-replies-3.0.html) | Replace a class of bare numerics with `FAIL`/`WARN`/`NOTE` for cap-enabled clients; better client UX | M (3 pts) | none |
70
+ | `monitor` | [ircv3.net/specs/extensions/monitor-3.2](https://ircv3.net/specs/extensions/monitor-3.2.html) | `MONITOR` command, modern replacement for `ISON`; per-connection watchlist + `730`/`731`/`732`/`733`/`734` numerics | M (5 pts) | Registry lookups ✅ |
71
+ | `labeled-response` | [ircv3.net/specs/extensions/labeled-response-3.2](https://ircv3.net/specs/extensions/labeled-response-3.2.html) | Client tags a request with `label`; server wraps the reply in a `BATCH` carrying that label. Composes with `batch` ✅ | L (8 pts) | `batch` ✅, `message-tags` ✅ |
72
+ | `sts` | [ircv3.net/specs/extensions/sts](https://ircv3.net/specs/extensions/sts.html) | Strict Transport Security: advertise a TLS+port policy to upgrade plaintext/WebSocket clients. Relevant once raw TCP `:6697` is exposed (it is, per Phase 7 ✅) | S (2 pts) | dual transport ✅ |
73
+
74
+ **Notes:**
75
+ - `account-notify` is the cheapest win and unblocks the
76
+ `account-tag`/`extended-join`/`account-notify` triple that modern
77
+ clients render fully.
78
+ - `msgid` is mostly there: the chathistory work (TICKET-063) already
79
+ produces stable msgids via `IdFactory.nonce()`. This ticket is the
80
+ cap advertisement + emitting the tag on every PRIVMSG/NOTICE/TAGMSG,
81
+ not just recorded ones.
82
+ - `labeled-response` is the largest item because the request→reply
83
+ correlation touches every reducer that emits a client-facing reply;
84
+ the `BATCH` wrapper already exists (TICKET-025), so the work is in
85
+ plumbing a per-request label through `ctx`.
86
+ - `sts` is config-driven only (no protocol state); the value is
87
+ advertising the right TLS port for the deployed adapter.
88
+
89
+ ### 3.2 Draft / spec-pending extensions
90
+
91
+ | Cap | Spec | What it adds | Effort | Depends on |
92
+ |-----|------|--------------|--------|------------|
93
+ | `draft/pre-away` | [ircv3 draft](https://github.com/ircv3/ircv3-specifications/pull/415) | Persist away status + message across reconnects; cross-device away sync. Noted out-of-scope in TICKET-063. | M (3 pts) | `AccountStore` ✅, services `UserStore` (§6) |
94
+ | `draft/read-marker` | [ircv3 draft](https://github.com/ircv3/ircv3-specifications/pull/416) | Per-`(user, channel)` last-read marker, propagated via `+draft/read-marker` tag. The chathistory plumbing (TICKET-063) already keeps the marker locally; this ticket persists it across connections and shares it. | M (5 pts) | chathistory ✅, services `UserStore` |
95
+ | `draft/multiline` | [ircv3 draft](https://ircv3.net/specs/extensions/multiline.html) | `BATCH +id draft/multiline :target` for multi-line messages up to a byte budget. Clients (TheLounge, gamja) render as joined blocks. | M (5 pts) | `batch` ✅, `message-tags` ✅ |
96
+ | `draft/typing` | [ircv3 client-behavior/typing](https://ircv3.net/specs/client-behavior/typing-notification.html) | `TAGMSG +draft/typing=active|paused|done` to channel members (excluding sender unless `echo-message`). Client UX only; no server state. | S (2 pts) | `message-tags` ✅, `TAGMSG` ✅ |
97
+
98
+ **Notes:**
99
+ - The `draft/` prefix caps stay namespaced until the spec stabilizes,
100
+ matching the existing `draft/chathistory` precedent.
101
+ - `draft/typing` is a near-trivial follow-on from TAGMSG (TICKET-076 ✅):
102
+ just whitelist the `+draft/typing=…` client tag and broadcast.
103
+
104
+ ### 3.3 Already-shipped caps blocked on adapter wiring
105
+
106
+ Worth calling out explicitly so they are not re-implemented by mistake:
107
+
108
+ | Cap | Status |
109
+ |-----|--------|
110
+ | `account-tag` | Reducer emits; only meaningful once `AccountStore` is bound per adapter (TICKET-071 ✅). |
111
+ | `extended-join` | Reducer emits the `account realname` form; same dependency as above. |
112
+ | `draft/chathistory` | Reducer + in-memory store shipped (TICKET-063 ✅); adapter `MessageStore` wiring landed in TICKET-070 ✅. Persistent CF (D1/DO) and AWS (DynamoDB) stores are the next step. |
113
+ | `msgid` | Standalone cap advertised and emitted on the live PRIVMSG/NOTICE fanout (TICKET-111 ✅). A single `IdFactory.nonce()` is shared between the live `@msgid=<id>` tag and the chathistory record so the two match for the same message. |
114
+
115
+ ---
116
+
117
+ ## 4. Missing ISUPPORT tokens
118
+
119
+ `packages/irc-core/src/commands/isupport.ts` should grow:
120
+
121
+ - `MONITOR=<n>` — maximum entries per connection (TICKET for `monitor`).
122
+ - `ACCOUNTEXTBAN` / `EXTBAN=~,acc` — extended ban by account (services
123
+ `+r` work).
124
+ - `STATUSMSG=@+` — prefixes that map to status-msg delivery (ships with
125
+ any `STATUSMSG`-aware PRIVMSG change).
126
+ - `TYPING` — draft advertisement when `draft/typing` lands.
127
+
128
+ ---
129
+
130
+ ## 5. Channel + user modes that arrive with services
131
+
132
+ The v1 mode set (`o v b i k l t n m s p` channel, `i o w s` user) gains:
133
+
134
+ | Mode | Type | Meaning | Source |
135
+ |------|------|---------|--------|
136
+ | `r` (channel) | A | Channel is registered with ChanServ | services |
137
+ | `R` (channel) | B | Block join/message from unregistered nicks | services |
138
+ | `M` (channel) | B | Mod speak requires registered nick (vs. `+m` = op) | services |
139
+ | `c` (channel) | B | Block ANSI color (requires message inspection) | optional |
140
+ | `r` (user) | — | User is identified to NickServ | services |
141
+ | `q` (user, recv-only) | — | Protected from oper kick (U-line / services) | services |
142
+ | `S` (user) | — | Connected via TLS (set by transport, read-only) | transport |
143
+
144
+ `S` is a near-freebie — the transport already knows whether TLS is in
145
+ use (`ConnectionActor` accepts a `transportMeta`); surfacing it as a
146
+ user mode costs one reducer branch.
147
+
148
+ ---
149
+
150
+ ## 6. IRC services
151
+
152
+ ### 6.1 Architecture decision: integrated services (not pseudo-servers)
153
+
154
+ Two designs exist in the wild:
155
+
156
+ **A. Pseudo-client services (Atheme / Anope model).** A separate
157
+ process connects as one or more virtual IRC clients named `NickServ`
158
+ etc.; users interact via `PRIVMSG NickServ :REGISTER hunter2 email`.
159
+ The link between ircd and services speaks a server-to-server protocol
160
+ (TS6 / P10 / IRCv3 S2S).
161
+
162
+ **B. Integrated services.** Service names are reserved nicks
163
+ recognized by the daemon at PRIVMSG time and routed to dedicated
164
+ reducers backed by a `ServicesStore` port. No S2S, no separate process,
165
+ no pseudo-client connection.
166
+
167
+ **Decision: B — integrated services.** Rationale:
168
+
169
+ - S2S is an explicit non-goal (`PLAN.md` §1). Model A requires it.
170
+ - The pure-reducer + location-of-authority architecture already has the
171
+ right seam: a new `ServicesStore` port (`packages/irc-core/src/ports.ts`,
172
+ mirroring `AccountStore` / `MessageStore`) absorbs the side effects,
173
+ and reducers stay pure.
174
+ - One storage layer is shared with the existing SASL `AccountStore`
175
+ (TICKET-071 ✅) — a NickServ registration *is* a SASL account, so the
176
+ services work composes with auth rather than duplicating it.
177
+ - The serverless substrates (Durable Objects, DynamoDB) handle
178
+ persistence natively; spinning up a separate long-running services
179
+ process would defeat the deployment model.
180
+
181
+ **Trade-off documented:** integrated services are not binary-compatible
182
+ with existing Atheme/Anope modules. Operators migrating from a
183
+ services-aware network should expect to re-register nicks and channels.
184
+ This is called out in `docs/` when the work ships.
185
+
186
+ ### 6.2 The `ServicesStore` port
187
+
188
+ ```ts
189
+ /** Persists services-owned state. Mirrors the AccountStore/MessageStore pattern. */
190
+ export interface ServicesStore {
191
+ // NickServ
192
+ registerNick(account: string, passwordHash: string, email?: string): Promise<void>;
193
+ verifyNick(account: string, password: string): Promise<boolean>;
194
+ dropNick(account: string): Promise<void>;
195
+ isRegisteredNick(account: string): Promise<boolean>;
196
+ setNickEnforce(account: string, mode: 'none' | 'kill' | 'ghost'): Promise<void>;
197
+
198
+ // ChanServ
199
+ registerChannel(chan: string, founder: string): Promise<void>;
200
+ dropChannel(chan: string): Promise<void>;
201
+ getChannelFounder(chan: string): Promise<string | null>;
202
+ setChannelAccess(chan: string, account: string, level: AccessLevel): Promise<void>;
203
+
204
+ // HostServ
205
+ setVhost(account: string, vhost: string): Promise<void>;
206
+ getVhost(account: string): Promise<string | null>;
207
+
208
+ // MemoServ
209
+ recordMemo(to: string, from: string, body: string): Promise<void>;
210
+ listMemos(account: string): Promise<Memo[]>;
211
+ markMemoRead(account: string, id: string): Promise<void>;
212
+ }
213
+ ```
214
+
215
+ Adapter implementations:
216
+
217
+ | Adapter | Backend |
218
+ |---------|---------|
219
+ | in-memory-runtime | `Map`s, used by local-cli + tests |
220
+ | cf-adapter | D1 (shared with the SASL store from TICKET-093 ✅) or a dedicated `ServicesDO` |
221
+ | aws-adapter | DynamoDB `Accounts` table extended (TICKET-072 ✅ already provisions it) + new `Channels`/`Memos` tables |
222
+
223
+ ### 6.3 Services scope
224
+
225
+ | Service | In scope | Out of scope (this pass) |
226
+ |---------|----------|--------------------------|
227
+ | **NickServ** | `REGISTER`, `IDENTIFY`, `LOGOUT`, `GHOST`, `DROP`, `INFO`, `SET ENFORCE`/`PASSWORD`/`EMAIL`, link to SASL account, `+r` user mode, nick enforcement | CertFP (covered by mTLS SASL EXTERNAL ✅), grouped nicks (alias list) |
228
+ | **ChanServ** | `REGISTER`, `IDENTIFY`, `DROP`, `SET FOUNDER`/`MLOCK`/`RESTRICTED`/`KEEPTOPIC`, `ACCESS`/`LEVELS` (founder/sop/aop/vop/hop), `+r`/`+R`/`+M` channel modes, KEEPTOPIC persistence | ACL inheritance, fantasy commands, `OP`/`DEOP` shorthand (defer) |
229
+ | **HostServ** | `ON`/`OFF`/`REQUEST`/`SET` (oper), integrate with the existing cloak machinery (`packages/irc-core` cloaking shipped in Phase 5), `+t` user mode (vhost in use) | per-channel vhosts |
230
+ | **OperServ** | `AKILL` (network-wide ban, persisted + applied on connect), `JUPE` (block a nick/server name locally), `RAW` (oper-only, useful for recovery) | STATS aggregation (TICKET-104 🔄 covers server stats), `IGNORE` list |
231
+ | **MemoServ** | `SEND`, `LIST`, `READ`, `DEL`, `FORWARD`, send-on-identify (queued memos delivered at login) | channel memos, group memos |
232
+ | **BotServ** | — | **Out of scope entirely.** Channel-joined bots are a runtime cost serverless deployments should not absorb by default. Operators can run their own bots as ordinary clients. |
233
+ | **StatServ** | — | **Out of scope.** `STATS` (TICKET-104) + observability dashboards (TICKET-045 ✅) cover it. |
234
+
235
+ ### 6.4 Nick enforcement policy
236
+
237
+ `NickServ SET ENFORCE <mode>` decides what happens when an
238
+ unidentified client uses a registered nick:
239
+
240
+ - `none` (default): warning notice only.
241
+ - `ghost`: kick the existing unidentified client off after a grace
242
+ period (default 30s); connection is force-disconnected via the
243
+ existing `disconnect(conn, reason)` runtime method.
244
+ - `kill`: same as ghost but immediately.
245
+
246
+ Implemented as a new `EnforceNick` effect emitted from the registration
247
+ reducer when the chosen nick matches a registered account the
248
+ connection is not identified as. The dispatch layer resolves it against
249
+ `runtime.disconnect(...)`.
250
+
251
+ ### 6.5 Services ↔ daemon hooks
252
+
253
+ Already-present seams the services reuse:
254
+
255
+ - `AccountStore.verify('PLAIN', ...)` (TICKET-026 ✅) → a successful
256
+ NickServ `IDENTIFY` and a SASL PLAIN login both reduce to "this
257
+ connection is now account X", setting the `+r` user mode and
258
+ emitting `ACCOUNT` to `account-notify`-capable peers.
259
+ - `MessageStore` (TICKET-063 ✅) → MemoServ's `SEND` is a record into
260
+ a per-account memo queue; delivery is a fanout at identify time.
261
+ - `clock` / `IdFactory` ports → memo ids, enforcement grace timers.
262
+
263
+ ---
264
+
265
+ ## 7. Proposed tickets
266
+
267
+ Ticket numbers are placeholders starting at **TICKET-110** (next free
268
+ id per `todo.md`). Final ids assigned at ratification. Each ticket
269
+ follows strict TDD (Red → Green → Refactor) per `CLAUDE.md` /
270
+ `AGENTS.md`.
271
+
272
+ ### Phase 12 — IRCv3 extensions
273
+
274
+ #### TICKET-110 — IRCv3 `account-notify` (push `ACCOUNT` on login/logout)
275
+ - **Phase:** 12
276
+ - **Points:** 2
277
+ - **Dependencies:** TICKET-071 (`AccountStore` wiring, ✅)
278
+ - **Description:** Advertise `account-notify`; emit `ACCOUNT <name|*>`
279
+ to peers in shared channels (excluding sender unless `echo-message`)
280
+ on SASL success, NickServ IDENTIFY (services), and `LOGOUT`. Reuses
281
+ the `extended-join`/`chghost` broadcast helper.
282
+ - **Acceptance Criteria:**
283
+ - `CAP LS` includes `account-notify`.
284
+ - A login by peer A visible to peer B (sharing a channel, cap
285
+ negotiated) produces `:A ACCOUNT alice`.
286
+ - A logout produces `:A ACCOUNT *`.
287
+ - No `ACCOUNT` messages to clients without the cap.
288
+ - 100% coverage on the new broadcast branch.
289
+ - **TDD Outline:** Red on "B sees `ACCOUNT` after A's SASL success";
290
+ green via the broadcast helper; then cap-gating + echo-message path.
291
+
292
+ #### TICKET-111 — IRCv3 `msgid` (emit `msgid` tag on every message)
293
+ - **Phase:** 12
294
+ - **Points:** 2
295
+ - **Dependencies:** TICKET-063 (chathistory msgid plumbing, ✅)
296
+ - **Description:** Advertise `msgid` as a standalone cap. Every
297
+ PRIVMSG/NOTICE/TAGMSG emitted to cap-enabled clients carries a
298
+ `+msgid=<id>` tag from `IdFactory.nonce()`. The chathistory reducer
299
+ already records the msgid; this ticket surfaces it on the live
300
+ fanout line, not just on replay.
301
+ - **Acceptance Criteria:**
302
+ - `CAP LS` includes `msgid`.
303
+ - Live PRIVMSG to a cap-enabled peer carries `@+msgid=<id>`.
304
+ - The msgid on the live line and on the chathistory replay line
305
+ match for the same recorded message.
306
+ - Clients without the cap see no `msgid` tag (existing behavior).
307
+ - 100% coverage on the tag-emission branch.
308
+ - **TDD Outline:** Red on "peer with `msgid` cap sees `@+msgid=…` on a
309
+ PRIVMSG"; green; then assert parity with the recorded msgid.
310
+
311
+ #### TICKET-112 — IRCv3 `standard-replies` (`FAIL`/`WARN`/`NOTE`)
312
+ - **Phase:** 12
313
+ - **Points:** 3
314
+ - **Dependencies:** —
315
+ - **Description:** Advertise `standard-replies`. For cap-enabled
316
+ clients, replace a curated subset of bare numeric error replies
317
+ (`461 ERR_NEEDMOREPARAMS`, `403 ERR_NOSUCHCHANNEL`, `432/433`
318
+ registration errors, SASL failures) with the structured
319
+ `FAIL COMMAND code context :description` form. The code-path
320
+ branches that *should not* change (numeric-only replies like MOTD
321
+ lines, NAMES) are left on the numeric form.
322
+ - **Acceptance Criteria:**
323
+ - `CAP LS` includes `standard-replies`.
324
+ - For each curated numeric, a cap-enabled client receives a
325
+ `FAIL`/`WARN`/`NOTE` line; a legacy client receives the unchanged
326
+ numeric.
327
+ - The mapping is data-driven (a `numericToStandardReply` table), not
328
+ scattered `if (hasCap)` checks.
329
+ - 100% coverage on the table + dispatch helper.
330
+ - **TDD Outline:** Red on `461`→`FAIL` for cap-enabled client; green
331
+ via the table; then each curated code; then the legacy fallback.
332
+
333
+ #### TICKET-113 — IRCv3 `MONITOR` (modern `ISON` replacement)
334
+ - **Phase:** 12
335
+ - **Points:** 5
336
+ - **Dependencies:** —
337
+ - **Description:** Advertise `MONITOR=<n>` (configurable max, default
338
+ 30). Implement the `MONITOR + nick{,nick}` / `-` / `C` / `L` / `S`
339
+ subcommands and the `730 RPL_MONONLINE` / `731 RPL_MONOFFLINE` /
340
+ `732 RPL_MONLIST` / `734 ERR_MONLISTFULL` numerics. Online/offline
341
+ transitions push `730`/`731` asynchronously via the Registry's nick
342
+ reserve/release hooks.
343
+ - **Acceptance Criteria:**
344
+ - `005` advertises `MONITOR=30`.
345
+ - `MONITOR +alice` while alice is offline → `731`.
346
+ - Alice connects → `730` pushed to the watcher.
347
+ - `MONITOR L` lists the watchlist; `MONITOR C` clears it.
348
+ - Over-limit → `734` with the rejected nick.
349
+ - 100% coverage on the reducer and the async push hook.
350
+ - **TDD Outline:** Red on the offline→`731` path; green; then the
351
+ online push; then list/clear/over-limit.
352
+
353
+ #### TICKET-114 — IRCv3 `labeled-response` (correlate request→reply via `BATCH`)
354
+ - **Phase:** 12
355
+ - **Points:** 8
356
+ - **Dependencies:** TICKET-025 (`batch`, ✅)
357
+ - **Description:** Advertise `labeled-response`. A client sends a
358
+ message with `+label=<id>`; the server wraps every reply caused by
359
+ that message in a `BATCH +id labeled-response … BATCH -id`. If the
360
+ request produces no reply, the server emits an empty batch. The
361
+ label is plumbed through `ctx` so every reducer's emitted effects
362
+ inherit it.
363
+ - **Acceptance Criteria:**
364
+ - `CAP LS` includes `labeled-response`.
365
+ - A `PRIVMSG` carrying `+label=foo` produces a `BATCH +foo
366
+ labeled-response` (echo + any numerics) `BATCH -foo` for
367
+ cap-enabled clients.
368
+ - A command that produces no reply (e.g. a no-op `PING` already
369
+ covered by `PONG` is *not* empty; a `JOIN` to a nonexistent channel
370
+ that 403s still wraps the 403) — spec edge cases enumerated.
371
+ - 100% coverage on the label plumbing + wrapping helper.
372
+ - **TDD Outline:** Red on "PRIVMSG with `+label=foo` wraps echo in
373
+ BATCH"; green; then the no-reply case; then the multi-reply case.
374
+
375
+ #### TICKET-115 — IRCv3 `sts` (Strict Transport Security policy)
376
+ - **Phase:** 12
377
+ - **Points:** 2
378
+ - **Dependencies:** dual transport (Phase 7 ✅)
379
+ - **Description:** Advertise `sts` with a deployment-configurable
380
+ policy (`duration`, `port` for the TLS listener, optional `preload`).
381
+ On a plaintext/WebSocket connection (when one exists), the server
382
+ emits the `STS` policy so compliant clients upgrade; on the TLS
383
+ connection the policy is still sent to lock in the duration.
384
+ - **Acceptance Criteria:**
385
+ - `CAP LS` includes `sts=duration=…,port=6697`.
386
+ - Values come from `ServerConfig`, not literals.
387
+ - A wss / irc+tls connection advertises the duration.
388
+ - 100% coverage on the policy builder.
389
+ - **TDD Outline:** Red on cap advertisement; green; then override via
390
+ config.
391
+
392
+ #### TICKET-116 — IRCv3 `draft/typing` (typing indicator broadcast)
393
+ - **Phase:** 12
394
+ - **Points:** 2
395
+ - **Dependencies:** TICKET-076 (`TAGMSG`, ✅)
396
+ - **Description:** Advertise `draft/typing`. A client sends
397
+ `TAGMSG <target> +draft/typing=active|paused|done`; the server
398
+ broadcasts to channel members (or routes to a user target),
399
+ excluding the sender unless `echo-message` is negotiated. No state
400
+ is kept server-side.
401
+ - **Acceptance Criteria:**
402
+ - `CAP LS` includes `draft/typing`.
403
+ - Channel typing TAGMSG fans out to cap-enabled members.
404
+ - The `+draft/typing=*` client tag is whitelisted for clients
405
+ without `message-tags` (per the spec's client-tag exception).
406
+ - 100% coverage on the whitelist + broadcast.
407
+ - **TDD Outline:** Red on channel typing fanout; green; then
408
+ echo-message path; then user-target path.
409
+
410
+ #### TICKET-117 — IRCv3 `draft/multiline` (multi-line `BATCH`)
411
+ - **Phase:** 12
412
+ - **Points:** 5
413
+ - **Dependencies:** TICKET-025 (`batch`, ✅)
414
+ - **Description:** Advertise `draft/multiline` with a configurable
415
+ byte budget (default 4096). A cap-enabled client sends
416
+ `BATCH +id draft/multiline :target` followed by N PRIVMSG/NOTICE
417
+ lines, then `BATCH -id`. The server concatenates per spec (with
418
+ `\n`), enforces the byte cap, and broadcasts as a single PRIVMSG
419
+ carrying a `+draft/multiline` tag plus the joined body (or a
420
+ batched replay to other multiline-capable peers — spec calls for
421
+ the joined form to legacy peers and the batch form to cap peers).
422
+ - **Acceptance Criteria:**
423
+ - `CAP LS` includes `draft/multiline=35615` (or configured max).
424
+ - Multi-line batch from a cap-enabled client is delivered to
425
+ cap-enabled peers as a `draft/multiline` BATCH.
426
+ - Legacy peers receive the joined single PRIVMSG.
427
+ - Over-budget batch is rejected with `FAIL`.
428
+ - 100% coverage on the reducer + byte-budget enforcement.
429
+ - **TDD Outline:** Red on a 2-line batch delivered as joined PRIVMSG
430
+ to a legacy peer; green; then the cap-peer batched delivery; then
431
+ budget enforcement.
432
+
433
+ #### TICKET-118 — IRCv3 `draft/read-marker` (persistent last-read)
434
+ - **Phase:** 12
435
+ - **Points:** 5
436
+ - **Dependencies:** TICKET-063 (chathistory, ✅), services `UserStore`
437
+ (TICKET-122)
438
+ - **Description:** Advertise `draft/read-marker`. The chathistory
439
+ work already keeps a per-`(connection, channel)` last-read marker
440
+ in `ConnectionState`. This ticket persists the marker per
441
+ `(account, channel)` in `ServicesStore`, propagates updates via
442
+ `+draft/read-marker=<msgid>` tags on relevant messages, and seeds a
443
+ fresh connection's marker from the persisted value at identify time.
444
+ - **Acceptance Criteria:**
445
+ - `CAP LS` includes `draft/read-marker`.
446
+ - Marking a message read updates the persisted marker.
447
+ - Reconnect restores the marker.
448
+ - `CHATHISTORY BEFORE` honours the marker in the absence of an
449
+ explicit pivot.
450
+ - 100% coverage on the persistence + tag emission.
451
+ - **TDD Outline:** Red on reconnect-restore; green via the store;
452
+ then tag emission; then CHATHISTORY integration.
453
+ - **Implementation notes (shipped):** Landed as a focused
454
+ `ReadMarkerStore` port (`get` / `set` / `forAccount`) plus an
455
+ `InMemoryReadMarkerStore` reference impl in `irc-core/ports.ts`, rather
456
+ than waiting on the full services `UserStore` (TICKET-122). The mark is
457
+ delivered as a `TAGMSG` carrying `+draft/read-marker=<msgid>` (msgid-based,
458
+ composing with the chathistory marker plumbing, diverging from the
459
+ work-in-progress ircv3 draft's timestamped `MARKREAD` verb). A mark TAGMSG
460
+ is persisted via `persistReadMarker`, fanned out to `message-tags` OR
461
+ `draft/read-marker` peers (mirroring the `draft/typing` precedent), and
462
+ `+draft/read-marker` is whitelisted in `filterClientTags` so draft-only
463
+ peers keep the tag. SASL identify seeds `lastReadMarkers` via
464
+ `seedReadMarkers`; `CHATHISTORY BEFORE` with no pivot falls back to the
465
+ connection's marker. TICKET-122 should fold this store into the unified
466
+ services `UserStore` when it lands.
467
+
468
+ #### TICKET-119 — IRCv3 `draft/pre-away` (persist away status)
469
+ - **Phase:** 12
470
+ - **Points:** 3
471
+ - **Dependencies:** TICKET-061 (`AWAY`, ✅), services `UserStore`
472
+ (TICKET-122)
473
+ - **Description:** Advertise `draft/pre-away`. Persist away reason in
474
+ `ServicesStore` keyed by account. On identify (or SASL login), the
475
+ server re-applies the away state and emits the `AWAY` line +
476
+ `306 RPL_NOWAWAY` as if the user had sent `AWAY :reason` themselves.
477
+ Noted out-of-scope in TICKET-063.
478
+ - **Acceptance Criteria:**
479
+ - `CAP LS` includes `draft/pre-away`.
480
+ - An identified user who previously went `AWAY :brb` and reconnects
481
+ is auto-marked away.
482
+ - `AWAY` (unset) clears the persisted reason.
483
+ - 100% coverage on the persistence + replay.
484
+ - **TDD Outline:** Red on reconnect auto-away; green; then the unset
485
+ path.
486
+ - **Implementation notes (shipped):** Landed as a focused `AwayStore`
487
+ port (`get` / `set` / `clear`) plus an `InMemoryAwayStore` reference
488
+ impl in `irc-core/ports.ts`, rather than waiting on the full services
489
+ `UserStore` (TICKET-122) — same shape as the TICKET-118 `ReadMarkerStore`
490
+ precedent. The `awayReducer` calls `persistAway` on set and
491
+ `clearPersistedAway` on unset (gated on a bound store AND an
492
+ identified connection, so unidentified sessions still track their own
493
+ in-memory `away` for the duration of the connection). SASL identify
494
+ calls `replayPersistedAway` to copy a stored reason onto the fresh
495
+ connection's `ConnectionState.away` and emits a single
496
+ `306 RPL_NOWAWAY` so the user sees they are still away. The
497
+ companion `away-notify` broadcast is intentionally NOT emitted at
498
+ identify time — the connection has not joined any channels yet, so it
499
+ would have no audience; peers learn the away state through the normal
500
+ `WHO` / `WHOIS` / `AWAY` queries once the user joins. TICKET-122
501
+ should fold this store into the unified services `UserStore` when it
502
+ lands.
503
+
504
+ #### TICKET-120 — ISUPPORT tokens for new caps + modes
505
+ - **Phase:** 12
506
+ - **Points:** 2
507
+ - **Dependencies:** the cap tickets above + services modes
508
+ (TICKET-122)
509
+ - **Description:** Add `MONITOR=<n>`, `STATUSMSG=@+`, `EXTBAN=~,q`,
510
+ `ACCOUNTEXTBAN=a`, draft tokens for `draft/typing`/`draft/multiline`
511
+ to `generateIsupport`. Tokens are config-derived where applicable
512
+ (max monitor count, extban prefix).
513
+ - **Acceptance Criteria:**
514
+ - Each token advertises the configured value.
515
+ - Tokens only appear when the relevant cap/mode is supported (e.g.
516
+ `ACCOUNTEXTBAN` only once services ship).
517
+ - 100% coverage on the new branches.
518
+ - **TDD Outline:** Red on each missing token; green by adding it.
519
+
520
+ #### TICKET-121 — User mode `S` (TLS connected)
521
+ - **Phase:** 12
522
+ - **Points:** 1
523
+ - **Dependencies:** dual transport (Phase 7 ✅)
524
+ - **Description:** When the connection is over TLS (wss or irc+tls),
525
+ set user mode `S` read-only on the connection at registration. The
526
+ mode is not settable via `MODE` (parse rejects `+S` with `501`).
527
+ Surface in WHOIS (`:server 276 nick target :is using a secure
528
+ connection`) for clients that advertise the `RPL_WHOISSECURE` cap.
529
+ - **Acceptance Criteria:**
530
+ - TLS connection → `+S` user mode visible in `WHO`/`WHOIS`.
531
+ - `MODE nick +S` rejected with `501 ERR_UMODEUNKNOWNFLAG`.
532
+ - 100% coverage on the mode-set + parse rejection.
533
+ - **TDD Outline:** Red on TLS registration setting `+S`; green; then
534
+ the parse rejection.
535
+
536
+ ### Phase 13 — IRC services
537
+
538
+ #### TICKET-122 — `ServicesStore` port + in-memory reference impl
539
+ - **Phase:** 13
540
+ - **Points:** 5
541
+ - **Dependencies:** TICKET-029 (`IrcRuntime` port, ✅),
542
+ TICKET-071 (`AccountStore`, ✅)
543
+ - **Description:** Define `ServicesStore` (§6.2) in
544
+ `packages/irc-core/src/ports.ts`. Ship `InMemoryServicesStore`
545
+ (in-memory-runtime) covering NickServ/ChanServ/HostServ/MemoServ
546
+ primitives. Adapter backends (D1, DynamoDB) ship in dedicated
547
+ follow-on tickets.
548
+ - **Acceptance Criteria:**
549
+ - Port interface compiles under strict mode.
550
+ - `InMemoryServicesStore` implements every method; 100% coverage.
551
+ - Reused by local-cli and unit tests.
552
+ - **TDD Outline:** Red on `registerNick`/`verifyNick` round-trip;
553
+ green; then each method.
554
+
555
+ #### TICKET-123 — NickServ: REGISTER / IDENTIFY / DROP / INFO + `+r`
556
+ - **Phase:** 13
557
+ - **Points:** 8
558
+ - **Dependencies:** TICKET-122
559
+ - **Description:** Wire `PRIVMSG NickServ :<command>` to a NickServ
560
+ reducer. `REGISTER password email` creates a SASL account (reuses
561
+ the existing `AccountStore` so SASL PLAIN works immediately after
562
+ registration). `IDENTIFY password` logs the current connection in
563
+ (sets `+r` user mode, emits `ACCOUNT` to `account-notify` peers).
564
+ `DROP` removes the registration. `INFO [nick]` queries it.
565
+ Successful IDENTIFY triggers MemoServ delivery of queued memos.
566
+ - **Acceptance Criteria:**
567
+ - `REGISTER hunter2 alice@example.com` followed by SASL PLAIN with
568
+ `alice:hunter2` succeeds.
569
+ - `IDENTIFY hunter2` from an unidentified connection sets `+r` and
570
+ emits `ACCOUNT`.
571
+ - `DROP alice` requires re-identify.
572
+ - 100% coverage on the reducer + state transitions.
573
+ - **TDD Outline:** Red on REGISTER→SASL round-trip; green; then
574
+ IDENTIFY; then DROP; then INFO.
575
+
576
+ #### TICKET-124 — NickServ: nick enforcement (`SET ENFORCE`)
577
+ - **Phase:** 13
578
+ - **Points:** 5
579
+ - **Dependencies:** TICKET-123
580
+ - **Description:** Implement `NickServ SET ENFORCE none|ghost|kill`
581
+ plus the `EnforceNick` effect. When a client registers (NICK) with
582
+ a nick that is registered to an account they are not identified as,
583
+ the policy fires after the configured grace period. `ghost` force-
584
+ disconnects; `kill` force-disconnects immediately. Default policy
585
+ per registered account is `none`.
586
+ - **Acceptance Criteria:**
587
+ - `SET ENFORCE ghost` + unidentified use of the nick → grace
588
+ period → `disconnect("Nick enforced by NickServ")`.
589
+ - `SET ENFORCE kill` → immediate disconnect.
590
+ - `SET ENFORCE none` → warning notice only.
591
+ - 100% coverage on the effect + dispatch integration.
592
+ - **TDD Outline:** Red on grace-period disconnect; green; then each
593
+ policy.
594
+
595
+ #### TICKET-125 — ChanServ: REGISTER / DROP / SET + `+r`/`+R`/`+M`
596
+ - **Phase:** 13
597
+ - **Points:** 8
598
+ - **Dependencies:** TICKET-123, TICKET-011 (`MODE`, ✅)
599
+ - **Description:** Wire `PRIVMSG ChanServ :<command>`. `REGISTER`
600
+ records the channel founder; sets channel mode `+r`. `SET
601
+ FOUNDER/MLOCK/RESTRICTED/KEEPTOPIC` updates the persisted channel
602
+ record. New channel modes: `+r` (registered, settable only by
603
+ ChanServ), `+R` (block join/message from unidentified), `+M` (mod
604
+ speak requires identification). `KEEPTOPIC` persists the topic and
605
+ restores it on the first join after the channel becomes empty.
606
+ - **Acceptance Criteria:**
607
+ - `REGISTER #chan` by an identified founder sets `+r`.
608
+ - `+R` blocks join from unidentified (`519 ERR_ALLMUSTREGISTER`,
609
+ server-specific numeric) and PRIVMSG (`404`).
610
+ - `KEEPTOPIC` round-trips a topic across empty-recreate.
611
+ - `MLOCK` reapplies the locked modes if an op tries to unset them.
612
+ - 100% coverage on the reducer + mode interactions.
613
+ - **TDD Outline:** Red on REGISTER→`+r`; green; then `+R`/`+M`;
614
+ then KEEPTOPIC; then MLOCK.
615
+
616
+ #### TICKET-126 — ChanServ: ACCESS / LEVELS (SOP/AOP/VOP/HOP)
617
+ - **Phase:** 13
618
+ - **Points:** 5
619
+ - **Dependencies:** TICKET-125
620
+ - **Description:** `ChanServ ACCESS #chan ADD nick level` and the
621
+ shorthand `SOP`/`AOP`/`VOP`/`HOP` variants map an account to a
622
+ privilege level. On JOIN, ChanServ applies the matching channel
623
+ prefix (`@`, `+`) automatically. `LEVELS` lets the founder redefine
624
+ what numeric level each command requires.
625
+ - **Acceptance Criteria:**
626
+ - `AOP alice ADD` → alice auto-opped on JOIN.
627
+ - `VOP bob ADD` → bob auto-voiced.
628
+ - `ACCESS #chan DEL alice` removes.
629
+ - `LEVELS SET AUTOOP 5` redefines the threshold.
630
+ - 100% coverage on the access reducer + JOIN hook.
631
+ - **TDD Outline:** Red on AOP→auto-op; green; then DEL; then LEVELS.
632
+
633
+ #### TICKET-127 — HostServ: ON / OFF / REQUEST / SET (oper)
634
+ - **Phase:** 13
635
+ - **Points:** 3
636
+ - **Dependencies:** TICKET-122, cloaking (Phase 5 ✅)
637
+ - **Description:** Wire `PRIVMSG HostServ :<command>`. `REQUEST
638
+ vhost` queues a request for oper approval (or auto-approves if
639
+ configured). `SET account vhost` (oper-only) sets it directly.
640
+ `ON`/`OFF` toggles the cloak at runtime; the cloak machinery
641
+ already exists, this ticket wires it to per-account state and emits
642
+ `CHGHOST` (TICKET-022 ✅) on toggle.
643
+ - **Acceptance Criteria:**
644
+ - `SET alice example.net` + `ON` → `CHGHOST` emitted to peers;
645
+ WHOIS shows the vhost.
646
+ - `OFF` restores the real (cloaked) host.
647
+ - 100% coverage on the reducer + CHGHOST emission.
648
+ - **TDD Outline:** Red on `ON`→`CHGHOST`; green; then `OFF`; then
649
+ `REQUEST`/`SET` approval path.
650
+
651
+ #### TICKET-128 — OperServ: AKILL / JUPE / RAW
652
+ - **Phase:** 13
653
+ - **Points:** 5
654
+ - **Dependencies:** TICKET-075 (`OPER`, ✅), TICKET-122
655
+ - **Description:** Wire `PRIVMSG OperServ :<command>` (oper-only;
656
+ rejected with a services notice otherwise). `AKILL ADD mask reason`
657
+ records a network-wide ban persisted in `ServicesStore`; the
658
+ `$connect`/`$default` admission path checks it on every new
659
+ connection and on every PRIVMSG, force-disconnecting on match.
660
+ `JUPE nick` blocks a nick/server name locally (NICK collision
661
+ reducer returns `432` for the jupe'd nick). `RAW <line>` is oper-
662
+ only and emits a raw IRC line (useful for recovery; logged).
663
+ - **Acceptance Criteria:**
664
+ - `AKILL ADD *!*@bad.example.net flooding` → next matching
665
+ connection is disconnected at `$connect`.
666
+ - `JUPE moo` → `NICK moo` returns `432 ERR_ERRONEUSNICKNAME`.
667
+ - Non-oper → service notice rejection, no state change.
668
+ - 100% coverage on each reducer + the admission hook.
669
+ - **TDD Outline:** Red on AKILL disconnect; green; then JUPE; then
670
+ RAW; then non-oper rejection.
671
+
672
+ #### TICKET-129 — MemoServ: SEND / LIST / READ / DEL + queue delivery
673
+ - **Phase:** 13
674
+ - **Points:** 5
675
+ - **Dependencies:** TICKET-123, TICKET-122 (`MessageStore` pattern,
676
+ ✅)
677
+ - **Description:** Wire `PRIVMSG MemoServ :<command>`. `SEND nick
678
+ body` records a memo against the recipient's account. On successful
679
+ NickServ IDENTIFY or SASL login, queued memos are delivered (via
680
+ the existing MemoServ pseudo-client notice format). `LIST`/`READ`/
681
+ `DEL`/`FORWARD` round out the surface.
682
+ - **Acceptance Criteria:**
683
+ - `SEND alice Hi` while alice is offline → alice receives the memo
684
+ on next IDENTIFY.
685
+ - `LIST` enumerates unread.
686
+ - `READ n` delivers memo #n; `DEL n` removes.
687
+ - 100% coverage on the reducer + delivery hook.
688
+ - **TDD Outline:** Red on offline-send→identify-delivery; green; then
689
+ list/read/del.
690
+
691
+ #### TICKET-130 — Adapter backends for `ServicesStore`
692
+ - **Phase:** 13
693
+ - **Points:** 8
694
+ - **Dependencies:** TICKET-122, all of TICKET-123..129 (reducers
695
+ landed against the in-memory store first)
696
+ - **Description:** Ship persistent `ServicesStore` implementations:
697
+ - CF: D1 (share the database from TICKET-093 ✅), new tables
698
+ `nickserv_accounts`, `chanserv_channels`, `chanserv_access`,
699
+ `hostserv_vhosts`, `memoserv_memos`.
700
+ - AWS: DynamoDB, extending the existing `Accounts` table
701
+ (TICKET-072 ✅) and adding `Channels`, `ChannelAccess`, `Vhosts`,
702
+ `Memos`.
703
+ - Both adapters wire the store into `ConnectionActorOptions.services`.
704
+ - **Acceptance Criteria:**
705
+ - Each adapter: end-to-end NickServ register→identify→SASL PLAIN.
706
+ - Channel registration survives a deploy (state persisted).
707
+ - Memos delivered on reconnect after identify.
708
+ - 100% coverage on the new store implementations.
709
+ - **TDD Outline:** Red per adapter on REGISTER→survive-restart; green
710
+ via the persistent store.
711
+
712
+ #### TICKET-131 — Docs: services reference + admin tooling
713
+ - **Phase:** 13
714
+ - **Points:** 2
715
+ - **Dependencies:** TICKET-130
716
+ - **Description:** `docs/Services.md` covering: integrated-services
717
+ architecture decision (§6.1), per-service command reference, admin
718
+ tooling (`tools/seed-nickserv-accounts.ts` mirroring the existing
719
+ `seed-{aws,cf}-accounts.ts` scripts), migration notes for operators
720
+ coming from Atheme/Anope.
721
+ - **Acceptance Criteria:**
722
+ - A new contributor can register a NickServ account and a ChanServ
723
+ channel end-to-end following only the doc.
724
+ - The migration caveat (no Atheme/Anope compatibility) is called
725
+ out up front.
726
+ - **TDD Outline:** N/A (documentation). Reviewed by walking the
727
+ steps from a clean checkout.
728
+
729
+ ---
730
+
731
+ ## 8. Sequencing
732
+
733
+ Recommended landing order, respecting dependencies:
734
+
735
+ 1. **Cheap IRCv3 wins first.** TICKET-110 (`account-notify`),
736
+ TICKET-111 (`msgid`), TICKET-115 (`sts`), TICKET-121 (`+S` mode),
737
+ TICKET-116 (`draft/typing`). All small, all compose with shipped
738
+ work, all unblock better client UX.
739
+ 2. **`MONITOR` + `standard-replies`.** TICKET-113, TICKET-112.
740
+ 3. **`labeled-response`.** TICKET-114 — largest IRCv3 item, do last
741
+ among the stable caps; touches every reducer that emits a reply.
742
+ 4. **Services foundation.** TICKET-122 (port + in-memory store).
743
+ 5. **NickServ core.** TICKET-123, TICKET-124 — needed before ChanServ
744
+ because every other service keys off the identified account.
745
+ 6. **ChanServ.** TICKET-125, TICKET-126.
746
+ 7. **HostServ / OperServ / MemoServ.** TICKET-127, TICKET-128,
747
+ TICKET-129 (parallelizable).
748
+ 8. **Persistent adapter backends.** TICKET-130 (unblocks any
749
+ production use of services).
750
+ 9. **Draft caps that depend on services.** TICKET-118 (`draft/read-
751
+ marker`), TICKET-119 (`draft/pre-away`).
752
+ 10. **`draft/multiline`.** TICKET-117 — independent, can land
753
+ anywhere after the IRCv3 cheap wins.
754
+ 11. **Docs + ISUPPORT catch-up.** TICKET-131, TICKET-120.
755
+
756
+ ---
757
+
758
+ ## 9. Risks
759
+
760
+ | Risk | Mitigation |
761
+ |------|------------|
762
+ | `labeled-response` plumbing touches every reducer | Land `BATCH` wrapping first (already shipped); add label propagation as a `ctx` field and a single dispatch hook. Refactor incrementally per reducer. |
763
+ | Integrated services drift from user expectations (Atheme parity) | Document the decision up front (TICKET-131); keep command names/behaviour aligned where it's free. |
764
+ | Nick enforcement races (NICK + IDENTIFY interleaved) | All mutations through the single `ServicesStore` authority per account; the `EnforceNick` effect is emitted after `reserveNick` succeeds, so the nick is owned before enforcement fires. |
765
+ | Services-backed caps (`draft/read-marker`, `draft/pre-away`) coupled to services launch | Track as Phase 12 tickets with explicit Phase 13 dependencies; ship IRCv3 cheap wins independently. |
766
+ | D1/DynamoDB cost from per-message `msgid` recording | `msgid` is emitted on the live line without persisting; only `MessageStore` records (chathistory) hit storage. Same cost profile as today. |
767
+ | `draft/multiline` byte budget abuse | Hard cap enforced in the reducer (`FAIL` over-budget); per-connection flood control (TICKET-065 ✅) bounds the burst. |
768
+
769
+ ---
770
+
771
+ ## 10. Definition of done (per phase)
772
+
773
+ **Phase 12 — IRCv3 extensions** is done when:
774
+
775
+ - [ ] Every stable IRCv3 cap in §3.1 is advertised and exercised by
776
+ the parametrized contract suite on the in-memory runtime.
777
+ - [ ] The draft caps in §3.2 are advertised under `draft/` namespace
778
+ and exercised; they can be flipped off via config without code
779
+ changes.
780
+ - [ ] `005 RPL_ISUPPORT` advertises every new token.
781
+ - [ ] Client compat sweep (TICKET-050 ⬜) passes against ≥3 clients
782
+ with the new caps negotiated.
783
+
784
+ **Phase 13 — IRC services** is done when:
785
+
786
+ - [ ] NickServ/ChanServ/HostServ/OperServ/MemoServ reducers ship at
787
+ 100% coverage against `InMemoryServicesStore`.
788
+ - [ ] Both adapters ship persistent `ServicesStore` backends; the
789
+ parametrized contract suite is green on both.
790
+ - [ ] End-to-end: register → identify → SASL PLAIN → ChanServ
791
+ REGISTER → KEEPTOPIC round-trip survives a deploy restart.
792
+ - [ ] `docs/Services.md` is complete and reviewer-validated.
793
+
794
+ ---
795
+
796
+ ## 11. Open questions
797
+
798
+ - **Account name vs. nick.** Should NickServ register an *account*
799
+ (multiple nicks group to it, Atheme-style) or register a single
800
+ nick? Default proposal: account-per-registration with nick grouping
801
+ as a follow-up, matching SASL's existing account model.
802
+ - **Memo size + retention.** Default cap (256 bytes? 2 KiB?) and TTL
803
+ (30 days?). Default proposal: 2 KiB per memo, 30-day TTL, both
804
+ config-tunable.
805
+ - **`+R`/`+M` numerics.** Server-specific `519 ERR_ALLMUSTREGISTER`
806
+ vs. reusing `473 ERR_INVITEONLYCHAN` semantics. Default proposal:
807
+ introduce the spec-recognized numerics where they exist, document
808
+ server-specific ones in `isupport`.
809
+ - **AKILL scope.** Network-wide (persisted) vs. per-listener (edge
810
+ only). Default proposal: persisted at the `ServicesStore`, enforced
811
+ in the admission path of every adapter — true network-wide on a
812
+ single-server deployment.