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
@@ -21,29 +21,63 @@ export interface Capability {
21
21
  value?: string;
22
22
  }
23
23
 
24
+ /** Wire name of the SASL capability. */
25
+ export const SASL_CAP_NAME = 'sasl';
26
+
27
+ /** Wire name of the IRCv3 `draft/multiline` capability. */
28
+ export const MULTILINE_CAP_NAME = 'draft/multiline';
29
+
30
+ /** Wire name of the IRCv3 `draft/read-marker` capability. */
31
+ export const READ_MARKER_CAP_NAME = 'draft/read-marker';
32
+
33
+ /** Wire name of the IRCv3 `draft/pre-away` capability. */
34
+ export const PRE_AWAY_CAP_NAME = 'draft/pre-away';
35
+
36
+ /**
37
+ * Default byte budget advertised in `draft/multiline=<n>` and enforced on
38
+ * every finalized multi-line batch. Mirrors the IRCv3 spec's canonical
39
+ * recommendation; deployments override via `ServerConfig.multilineMaxBytes`.
40
+ */
41
+ export const DEFAULT_MULTILINE_MAX_BYTES = 4096;
42
+
43
+ /**
44
+ * Builds the `draft/multiline` cap *value* (the substring after `=`) for the
45
+ * advertised byte budget. The {@link CapReducer} and the multi-line reducer
46
+ * share this helper so the wire token has one source of truth.
47
+ */
48
+ export function multilineCapValue(maxBytes: number): string {
49
+ return String(maxBytes);
50
+ }
51
+
24
52
  /**
25
53
  * Every cap advertised by this server in PLAN §3, in canonical (alpha) order
26
54
  * so wire output is deterministic across runs.
27
55
  */
28
56
  export const SUPPORTED_CAPABILITIES: ReadonlyArray<Capability> = Object.freeze([
57
+ { name: 'account-notify' },
29
58
  { name: 'account-tag' },
30
59
  { name: 'away-notify' },
31
60
  { name: 'batch' },
32
61
  { name: 'chghost' },
33
62
  { name: 'draft/chathistory' },
63
+ { name: 'draft/multiline', value: multilineCapValue(DEFAULT_MULTILINE_MAX_BYTES) },
64
+ { name: 'draft/pre-away' },
65
+ { name: 'draft/read-marker' },
66
+ { name: 'draft/typing' },
34
67
  { name: 'echo-message' },
35
68
  { name: 'extended-join' },
36
69
  { name: 'invite-notify' },
70
+ { name: 'labeled-response' },
37
71
  { name: 'message-tags' },
72
+ { name: 'msgid' },
73
+ { name: 'monitor' },
38
74
  { name: 'multi-prefix' },
39
75
  { name: 'safelist' },
40
76
  { name: 'sasl', value: 'PLAIN' },
41
77
  { name: 'server-time' },
78
+ { name: 'standard-replies' },
42
79
  ]);
43
80
 
44
- /** Wire name of the SASL capability. */
45
- export const SASL_CAP_NAME = 'sasl';
46
-
47
81
  /**
48
82
  * Returns the `sasl` cap value to advertise given whether an mTLS identity
49
83
  * provider is bound for the connection.
@@ -1 +1,2 @@
1
1
  export * from './capabilities.js';
2
+ export * from './sts.js';
@@ -0,0 +1,84 @@
1
+ /**
2
+ * IRCv3 `sts` (Strict Transport Security) policy.
3
+ *
4
+ * Spec: https://ircv3.net/specs/extensions/sts
5
+ *
6
+ * The server advertises an `sts` capability whose value is a
7
+ * comma-separated `key=value` policy. Compliant clients inspect the
8
+ * policy and:
9
+ *
10
+ * - On a plaintext connection: reconnect over TLS to the advertised
11
+ * `port` and pin it for `duration` seconds. `preload` (optional)
12
+ * signals the client may hard-code the policy into a preload list.
13
+ * - On a TLS connection: only the `duration` is locked in — `port`
14
+ * is meaningless once the client is already on the secure listener,
15
+ * and `preload` is an upgrade-time directive.
16
+ *
17
+ * Config-driven only: there is no per-connection protocol state. The
18
+ * deployment supplies the policy through {@link StsConfig} on
19
+ * {@link ServerConfig}; when omitted entirely the cap is not advertised.
20
+ */
21
+
22
+ /**
23
+ * IRC-over-TLS port per RFC 7193 / IRCv3 STS spec examples. Used as the
24
+ * fallback `port` when a deployment turns on STS without pinning a port.
25
+ */
26
+ export const DEFAULT_STS_PORT = 6697;
27
+
28
+ /**
29
+ * Default pin duration (30 days, in seconds). Mirrors the IRCv3 spec's
30
+ * canonical example and what mainstream IRC networks publish. Deployments
31
+ * override via {@link StsConfig.duration}.
32
+ */
33
+ export const DEFAULT_STS_DURATION_SECONDS = 30 * 24 * 60 * 60;
34
+
35
+ /** Wire name of the STS capability. */
36
+ export const STS_CAP_NAME = 'sts';
37
+
38
+ /**
39
+ * Raw STS policy as supplied by {@link ServerConfig}. Every field is
40
+ * optional; {@link stsCapValue} applies the documented defaults when a
41
+ * field is omitted. `undefined` (the whole object) disables STS.
42
+ *
43
+ * Optional fields are widened to `T | undefined` to mirror Zod's
44
+ * `.optional()` output under `exactOptionalPropertyTypes: true` so the
45
+ * parsed config is structurally assignable here.
46
+ */
47
+ export interface StsPolicy {
48
+ /** Pin duration in seconds. Defaults to 30 days. */
49
+ readonly duration?: number;
50
+ /** TLS listener port advertised on plaintext connections. Defaults to 6697. */
51
+ readonly port?: number;
52
+ /** Optional preload directive (plaintext advertisements only). */
53
+ readonly preload?: boolean | undefined;
54
+ }
55
+
56
+ /**
57
+ * Builds the IRCv3 `sts` capability *value* (the substring after `sts=`)
58
+ * for the given policy and transport.
59
+ *
60
+ * Returns `undefined` when STS is disabled (`policy` undefined), so the cap
61
+ * table consumer can omit the token entirely from `CAP LS`.
62
+ *
63
+ * On a plaintext transport the full policy is advertised:
64
+ * `duration=<seconds>,port=<tls-port>[,preload]`
65
+ *
66
+ * On a TLS transport only `duration=<seconds>` is sent — the port is
67
+ * meaningless (the client is already on the secure listener) and
68
+ * `preload` is an upgrade-time directive that does not apply.
69
+ *
70
+ * Missing `duration`/`port` fall back to {@link DEFAULT_STS_DURATION_SECONDS}
71
+ * and {@link DEFAULT_STS_PORT} so a deployment can enable STS with a bare
72
+ * `{}` policy and still publish a sensible value.
73
+ */
74
+ export function stsCapValue(policy: StsPolicy | undefined, secure: boolean): string | undefined {
75
+ if (policy === undefined) return undefined;
76
+ const duration = policy.duration ?? DEFAULT_STS_DURATION_SECONDS;
77
+ if (secure) {
78
+ return `duration=${duration}`;
79
+ }
80
+ const port = policy.port ?? DEFAULT_STS_PORT;
81
+ const parts = [`duration=${duration}`, `port=${port}`];
82
+ if (policy.preload === true) parts.push('preload');
83
+ return parts.join(',');
84
+ }
@@ -0,0 +1,53 @@
1
+ import { Effect } from '../effects.js';
2
+ import type { Effect as EffectType, RawLine } from '../effects.js';
3
+ import type { ChanName } from '../state/channel.js';
4
+ import { hostmaskOf } from '../state/connection.js';
5
+ import type { ConnectionState } from '../state/connection.js';
6
+
7
+ export interface AccountNotifyInput {
8
+ conn: ConnectionState;
9
+ /**
10
+ * New account name on login. Pass `undefined` to signal a logout, which
11
+ * serializes the account field as `*` per IRCv3 `account-notify`.
12
+ */
13
+ account?: string | undefined;
14
+ }
15
+
16
+ /**
17
+ * Builds the cap-split Broadcast effects that announce an `ACCOUNT` login
18
+ * (or logout) to peers in every shared channel.
19
+ *
20
+ * Mirrors the `emitChghost` broadcast helper: each joined channel produces
21
+ * one {@link Effect.broadcast} carrying empty `lines` (so non-cap peers
22
+ * receive nothing) and the `ACCOUNT` line in `capLines` for peers that
23
+ * negotiated `account-notify`. The sender is excluded via `except` unless
24
+ * they negotiated `echo-message`, in which case a single echo
25
+ * {@link Effect.send} is appended (one echo regardless of how many shared
26
+ * channels the user occupies).
27
+ *
28
+ * Returns `[]` before registration completes or when the connection holds
29
+ * no joined channels — there is no audience to notify.
30
+ */
31
+ export function emitAccountNotify(input: AccountNotifyInput): EffectType[] {
32
+ const { conn, account } = input;
33
+
34
+ if (conn.registration !== 'registered') return [];
35
+ if (conn.joinedChannels.size === 0) return [];
36
+
37
+ const source = hostmaskOf(conn) ?? conn.nick ?? '?';
38
+ const accountField = account ?? '*';
39
+ const line: RawLine = { text: `:${source} ACCOUNT ${accountField}` };
40
+
41
+ const effects: EffectType[] = [];
42
+ for (const chan of conn.joinedChannels) {
43
+ effects.push(Effect.broadcast(chan as ChanName, [], conn.id, 'account-notify', [line]));
44
+ }
45
+
46
+ // IRCv3 echo-message: the sender observes its own ACCOUNT change exactly
47
+ // once, regardless of how many channels it shares with peers.
48
+ if (conn.caps.has('echo-message')) {
49
+ effects.push(Effect.send(conn.id, [line]));
50
+ }
51
+
52
+ return effects;
53
+ }
@@ -27,6 +27,7 @@ import type { ChanName } from '../state/channel.js';
27
27
  import { hostmaskOf } from '../state/connection.js';
28
28
  import type { ConnectionState } from '../state/connection.js';
29
29
  import type { Reducer } from '../types.js';
30
+ import { clearPersistedAway, persistAway } from './pre-away.js';
30
31
 
31
32
  /**
32
33
  * Handles `AWAY [:<reason>]`.
@@ -45,7 +46,10 @@ export const awayReducer: Reducer<ConnectionState> = (state, msg, ctx) => {
45
46
  const nick = state.nick ?? '*';
46
47
 
47
48
  if (isSettingAway) {
48
- state.away = reason;
49
+ // IRCv3 draft/pre-away: persist the reason keyed by account when an
50
+ // AwayStore is bound and the connection is identified. The in-memory
51
+ // away is also set so the current session tracks the new reason.
52
+ persistAway(ctx, reason as string);
49
53
  effects.push(
50
54
  Effect.send(ctx.connId, [
51
55
  {
@@ -54,8 +58,10 @@ export const awayReducer: Reducer<ConnectionState> = (state, msg, ctx) => {
54
58
  ]),
55
59
  );
56
60
  } else {
57
- // biome-ignore lint/performance/noDelete: exactOptionalPropertyTypes forbids `= undefined`.
58
- delete state.away;
61
+ // IRCv3 draft/pre-away: clear the persisted reason on unset so a
62
+ // reconnect does not re-mark the user away. The in-memory away is
63
+ // also cleared so the session's view stays consistent.
64
+ clearPersistedAway(ctx);
59
65
  effects.push(
60
66
  Effect.send(ctx.connId, [
61
67
  {
@@ -33,13 +33,17 @@
33
33
  */
34
34
 
35
35
  import {
36
+ DEFAULT_MULTILINE_MAX_BYTES,
37
+ MULTILINE_CAP_NAME,
36
38
  SASL_CAP_NAME,
37
39
  SUPPORTED_CAPABILITIES,
38
40
  capToLsString,
39
41
  isSupportedCap,
42
+ multilineCapValue,
40
43
  saslCapValue,
41
44
  splitCapsForWire,
42
45
  } from '../caps/capabilities.js';
46
+ import { STS_CAP_NAME, stsCapValue } from '../caps/sts.js';
43
47
  import { Effect } from '../effects.js';
44
48
  import type { RawLine } from '../effects.js';
45
49
  import { Numerics } from '../protocol/numerics.js';
@@ -91,11 +95,21 @@ function handleLs(state: ConnectionState, ctx: Ctx): ReducerResult<ConnectionSta
91
95
 
92
96
  const target = state.nick ?? '*';
93
97
  const hasMtls = ctx.mtlsIdentity !== undefined;
94
- const tokens = SUPPORTED_CAPABILITIES.map((cap) =>
95
- cap.name === SASL_CAP_NAME
96
- ? capToLsString({ ...cap, value: saslCapValue(hasMtls) })
97
- : capToLsString(cap),
98
- );
98
+ const multilineMax = ctx.serverConfig.multilineMaxBytes ?? DEFAULT_MULTILINE_MAX_BYTES;
99
+ const tokens = SUPPORTED_CAPABILITIES.map((cap) => {
100
+ if (cap.name === SASL_CAP_NAME) {
101
+ return capToLsString({ ...cap, value: saslCapValue(hasMtls) });
102
+ }
103
+ if (cap.name === MULTILINE_CAP_NAME) {
104
+ return capToLsString({ ...cap, value: multilineCapValue(multilineMax) });
105
+ }
106
+ return capToLsString(cap);
107
+ });
108
+ // STS is conditionally advertised: appended only when the deployment
109
+ // supplies a policy, and the value is per-transport (plaintext sees the
110
+ // full `duration,port[,preload]`; TLS sees only `duration`).
111
+ const stsValue = stsCapValue(ctx.serverConfig.sts, ctx.connection.secure);
112
+ if (stsValue !== undefined) tokens.push(`${STS_CAP_NAME}=${stsValue}`);
99
113
  const maxLen = maxCapsContentLen(ctx, target);
100
114
  const chunks = splitCapsForWire(tokens, maxLen);
101
115
 
@@ -164,8 +178,12 @@ function handleReq(
164
178
  const requested = rawCaps.split(' ').filter((c) => c.length > 0);
165
179
  const target = state.nick ?? '*';
166
180
 
181
+ // STS is conditionally supported: ACKed only when the deployment supplies
182
+ // a policy. Other caps consult the static table.
183
+ const stsConfigured = ctx.serverConfig.sts !== undefined;
167
184
  const allSupported = requested.every((tok) => {
168
185
  const name = tok.startsWith('-') ? tok.slice(1) : tok;
186
+ if (name === STS_CAP_NAME) return stsConfigured;
169
187
  return isSupportedCap(name);
170
188
  });
171
189
 
@@ -141,7 +141,17 @@ function handleChannelSub(
141
141
  // LATEST / BEFORE / AFTER / AROUND
142
142
  const pivot = msg.params[2];
143
143
  const limitRaw = msg.params[3];
144
- if (sub !== 'LATEST' && pivot === undefined) {
144
+
145
+ // IRCv3 draft/read-marker integration: BEFORE without an explicit pivot
146
+ // falls back to the connection's per-channel last-read marker so a client
147
+ // can resume "everything before my last read position" without re-supplying
148
+ // the msgid. AFTER/AROUND still require an explicit pivot.
149
+ const markerPivot =
150
+ sub === 'BEFORE' && pivot === undefined
151
+ ? ctx.connection.lastReadMarkers?.get(chanLower)
152
+ : undefined;
153
+
154
+ if (sub !== 'LATEST' && pivot === undefined && markerPivot === undefined) {
145
155
  return { effects: [Effect.send(ctx.connId, [needMoreParamsLine(ctx)])] };
146
156
  }
147
157
  const limit = parseLimit(limitRaw);
@@ -163,12 +173,13 @@ function handleChannelSub(
163
173
  return { effects: buildChathistoryBatch(ctx, channel.name, result) };
164
174
  }
165
175
 
166
- // BEFORE / AFTER / AROUND — pivot is required. The `sub !== 'LATEST' &&
167
- // pivot === undefined` guard above already rejected the missing-pivot
168
- // case and LATEST returned early just above; assert non-null for the
169
- // type-checker without an unreachable branch.
170
- const pivotMsgid = pivot as string;
171
- if (!storeHas(ctx, chanLower, pivotMsgid)) {
176
+ // BEFORE / AFTER / AROUND — pivot is required. Resolve from the explicit
177
+ // msgid when supplied, otherwise from the marker fallback computed above.
178
+ // The guard near the top already guaranteed at least one is defined.
179
+ const pivotMsgid = (pivot ?? markerPivot) as string;
180
+ // An explicit pivot must reference a retained message; a marker fallback
181
+ // may point at an evicted message ( empty result, not INVALID_PARAMS).
182
+ if (pivot !== undefined && !storeHas(ctx, chanLower, pivotMsgid)) {
172
183
  return { effects: [Effect.send(ctx.connId, [invalidParamsLine(ctx)])] };
173
184
  }
174
185
  const result = storeQuery(ctx, {
@@ -228,16 +239,34 @@ export function buildChathistoryBatch(
228
239
  /**
229
240
  * Reconstructs the wire line for one stored message, decorated with the
230
241
  * original server-time and msgid tags. PRIVMSG/NOTICE carry the trailing
231
- * text; TAGMSG has no trailing parameter.
242
+ * text; TAGMSG has no trailing parameter. Optional `clientTags` recorded
243
+ * with the message (e.g. `+draft/multiline`) are appended to the tag
244
+ * section so chathistory clients can recognise the message kind.
232
245
  */
233
246
  export function formatReplayLine(m: StoredMessage, displayChan: string): RawLine {
234
247
  const stamp = formatServerTime(m.time);
235
248
  const prefix = storedHostmask(m);
236
- const tag = `@time=${stamp};msgid=${m.msgid}`;
249
+ const tags = buildReplayTags(stamp, m.msgid, m.clientTags);
237
250
  if (m.command === 'TAGMSG') {
238
- return { text: `${tag} :${prefix} TAGMSG ${displayChan}` };
251
+ return { text: `${tags} :${prefix} TAGMSG ${displayChan}` };
252
+ }
253
+ return { text: `${tags} :${prefix} ${m.command} ${displayChan} :${m.text}` };
254
+ }
255
+
256
+ /**
257
+ * Builds the `@time=…;msgid=…[;+client-tag…]` tag section for a replay line.
258
+ * Client tags (when present) are appended after the server tags, matching the
259
+ * IRCv3 message-tags wire order.
260
+ */
261
+ function buildReplayTags(
262
+ stamp: string,
263
+ msgid: string,
264
+ clientTags: readonly string[] | undefined,
265
+ ): string {
266
+ if (clientTags === undefined || clientTags.length === 0) {
267
+ return `@time=${stamp};msgid=${msgid}`;
239
268
  }
240
- return { text: `${tag} :${prefix} ${m.command} ${displayChan} :${m.text}` };
269
+ return `@time=${stamp};msgid=${msgid};${clientTags.join(';')}`;
241
270
  }
242
271
 
243
272
  /** Builds the `nick!user@host` source for a stored message. */
@@ -46,6 +46,8 @@ export { generateIsupport } from './isupport.js';
46
46
  export { capReducer } from './cap.js';
47
47
  export { emitChghost } from './chghost.js';
48
48
  export type { ChgHostInput } from './chghost.js';
49
+ export { emitAccountNotify } from './account-notify.js';
50
+ export type { AccountNotifyInput } from './account-notify.js';
49
51
  export { awayReducer } from './away.js';
50
52
  export { setnameReducer } from './setname.js';
51
53
  export { authenticateReducer } from './sasl.js';
@@ -56,6 +58,18 @@ export {
56
58
  CHATHISTORY_CAP,
57
59
  } from './chathistory.js';
58
60
  export { tagmsgChannelReducer, tagmsgUserReducer } from './tagmsg.js';
61
+ export {
62
+ DEFAULT_MULTILINE_MAX_BYTES,
63
+ MULTILINE_CAP,
64
+ type FinalizeMultilineResult,
65
+ type MultilineEntry,
66
+ type MultilineFanout,
67
+ buildMultilineFanout,
68
+ finalizeMultilineBatch,
69
+ joinMultiline,
70
+ multilineByteLength,
71
+ recordMultilineMessage,
72
+ } from './multiline.js';
59
73
  export {
60
74
  versionReducer,
61
75
  timeReducer,
@@ -64,3 +78,23 @@ export {
64
78
  } from './server-info.js';
65
79
  export { userhostReducer, formatUserhostReply } from './userhost.js';
66
80
  export { isonReducer } from './ison.js';
81
+ export {
82
+ monitorReducer,
83
+ MONITOR_CAP,
84
+ DEFAULT_MONITOR_LIMIT,
85
+ } from './monitor.js';
86
+ export {
87
+ READ_MARKER_CAP,
88
+ READ_MARKER_TAG,
89
+ isReadMarkerTagmsg,
90
+ persistReadMarker,
91
+ readMarkerMsgid,
92
+ seedReadMarkers,
93
+ } from './read-marker.js';
94
+ export {
95
+ PRE_AWAY_CAP,
96
+ clearPersistedAway,
97
+ nowAwayLine,
98
+ persistAway,
99
+ replayPersistedAway,
100
+ } from './pre-away.js';
@@ -15,8 +15,10 @@
15
15
  * per-deployment (PLAN §10 capacity planning).
16
16
  */
17
17
 
18
+ import { DEFAULT_MULTILINE_MAX_BYTES } from '../caps/capabilities.js';
18
19
  import type { RawLine } from '../effects.js';
19
20
  import type { ServerConfig } from '../types.js';
21
+ import { DEFAULT_MONITOR_LIMIT } from './monitor.js';
20
22
 
21
23
  /** Max bytes per IRC wire line, including trailing `\r\n`. */
22
24
  const LINE_MAX_BYTES = 510;
@@ -36,6 +38,32 @@ const CHANMODES = {
36
38
  /** Channel-user prefix sigils, in PREFIX order (highest rank first). */
37
39
  const PREFIX = '(ov)@+';
38
40
 
41
+ /**
42
+ * STATUSMSG delivery prefixes — the channel-user sigils a sender may prefix
43
+ * a target with (`@#channel` / `+#channel`) so only that rank and above
44
+ * receive the message. Mirrors the PREFIX sigil tail.
45
+ */
46
+ const STATUSMSG_PREFIXES = '@+';
47
+
48
+ /**
49
+ * Default extended-ban prefix when {@link ServerConfig.extbanPrefix} is
50
+ * unset. `~` is the de-facto IRC standard (matches nearly every daemon).
51
+ */
52
+ const DEFAULT_EXTBAN_PREFIX = '~';
53
+
54
+ /**
55
+ * Extended-ban types this server understands independent of services.
56
+ * `q` = quiet ban (prevents matching targets from speaking).
57
+ */
58
+ const EXTBAN_TYPES = 'q';
59
+
60
+ /**
61
+ * Account-name extended-ban type, advertised as a dedicated `ACCOUNTEXTBAN=a`
62
+ * token only when services are enabled, since account bans require registered
63
+ * nicks to be meaningful.
64
+ */
65
+ const ACCOUNTEXTBAN_TYPE = 'a';
66
+
39
67
  /** Channel name sigils this server recognises. */
40
68
  const CHANTYPES = '#';
41
69
 
@@ -56,6 +84,9 @@ const TRAILER = 'are supported by this server';
56
84
  * can diff between servers. Tokens are space-separated, never empty.
57
85
  */
58
86
  function buildTokens(cfg: ServerConfig): string[] {
87
+ const monitorLimit = cfg.monitorLimit ?? DEFAULT_MONITOR_LIMIT;
88
+ const extbanPrefix = cfg.extbanPrefix ?? DEFAULT_EXTBAN_PREFIX;
89
+ const multilineMax = cfg.multilineMaxBytes ?? DEFAULT_MULTILINE_MAX_BYTES;
59
90
  return [
60
91
  `CHANTYPES=${CHANTYPES}`,
61
92
  `CHANMODES=${CHANMODES.list},${CHANMODES.paramSet},${CHANMODES.paramAlways},${CHANMODES.boolean}`,
@@ -73,6 +104,28 @@ function buildTokens(cfg: ServerConfig): string[] {
73
104
  // per-connection opt-in). LIST is always non-destructive here so the
74
105
  // token is unconditional.
75
106
  'SAFELIST',
107
+ // IRCv3 MONITOR cap ceiling. The value matches the per-connection
108
+ // watchlist size enforced by the monitor reducer; deployments lower it
109
+ // via `serverConfig.monitorLimit`.
110
+ `MONITOR=${monitorLimit}`,
111
+ // STATUSMSG — prefixes a sender may target (`@#chan`/`+#chan`) for
112
+ // rank-scoped delivery. The sigil set mirrors PREFIX.
113
+ `STATUSMSG=${STATUSMSG_PREFIXES}`,
114
+ // EXTBAN — extended-ban prefix and the base type list (`q` = quiet).
115
+ // The prefix is config-derived. The account-name extban is advertised
116
+ // as a separate `ACCOUNTEXTBAN=a` token (below) when services ship.
117
+ `EXTBAN=${extbanPrefix},${EXTBAN_TYPES}`,
118
+ // draft/typing advertisement — the cap is always supported, so the
119
+ // token is unconditional. (Draft caps have no RFC ISUPPORT token; this
120
+ // is the local marker clients can key on.)
121
+ 'TYPING',
122
+ // draft/multiline byte budget — mirrors the `draft/multiline=<n>` cap
123
+ // value so the ceiling is visible pre-cap-negotiation.
124
+ `MULTILINE=${multilineMax}`,
125
+ // ACCOUNTEXTBAN — account-name extended ban. Meaningful only once
126
+ // services register nicks, so suppressed until a deployment wires
127
+ // services up via `serverConfig.servicesEnabled`.
128
+ ...(cfg.servicesEnabled ? [`ACCOUNTEXTBAN=${ACCOUNTEXTBAN_TYPE}`] : []),
76
129
  ];
77
130
  }
78
131
 
@@ -528,7 +528,7 @@ const USER_MODE_LETTERS: Readonly<Record<string, keyof ConnectionState['userMode
528
528
  };
529
529
 
530
530
  /** User-mode letters in canonical 221 output order. */
531
- const USER_MODE_LETTER_ORDER = ['i', 'w', 's', 'o'] as const;
531
+ const USER_MODE_LETTER_ORDER = ['i', 'w', 's', 'o', 'S'] as const;
532
532
 
533
533
  /**
534
534
  * Handles `MODE <nick> [<modestring>]`.
@@ -640,6 +640,13 @@ export const userModeReducer: Reducer<ConnectionState> = (state, msg, ctx) => {
640
640
  function buildUserModeString(state: ConnectionState): string {
641
641
  let modeStr = '+';
642
642
  for (const letter of USER_MODE_LETTER_ORDER) {
643
+ // `S` (TLS connected) is read-only: it is not in USER_MODE_LETTERS
644
+ // (the write-side map the parser consults) so `MODE nick +S` rejects
645
+ // with 501. Surface it on the read side directly from its field.
646
+ if (letter === 'S') {
647
+ if (state.userModes.tls) modeStr += 'S';
648
+ continue;
649
+ }
643
650
  const field = USER_MODE_LETTERS[letter];
644
651
  if (field !== undefined && state.userModes[field]) modeStr += letter;
645
652
  }