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
@@ -175,18 +175,18 @@ function buildChannelReducer(command: MessageCommand): Reducer<ChannelState> {
175
175
 
176
176
  // All checks passed: persist the message for chathistory playback when a
177
177
  // MessageStore is bound. The msgid/time are generated once here and travel
178
- // on the StoredMessage; the live broadcast line stays bare (per-recipient
179
- // server-time/msgid decoration is the dispatch layer's job). Recording is
180
- // gated on ctx.messages so deployments with chathistory disabled see no
181
- // change whatsoever.
178
+ // on the StoredMessage AND on the live fanout line (IRCv3 msgid). A single
179
+ // nonce is shared between the live tag and the record so the value a cap
180
+ // enabled peer sees live matches the value replayed by chathistory.
182
181
  //
183
182
  // By this point the sender's hostmask has already been computed and used
184
183
  // in the `+b` ban check above, so the connection's nick/user/host are all
185
184
  // defined for any registered PRIVMSG (registration sets all three); the
186
185
  // fallbacks mirror `senderHostmask` purely for the type-checker.
186
+ const msgid = ctx.ids.nonce();
187
187
  if (ctx.messages !== undefined) {
188
188
  ctx.messages.record({
189
- msgid: ctx.ids.nonce(),
189
+ msgid,
190
190
  time: ctx.clock.now(),
191
191
  chan: state.nameLower,
192
192
  command,
@@ -197,14 +197,21 @@ function buildChannelReducer(command: MessageCommand): Reducer<ChannelState> {
197
197
  });
198
198
  }
199
199
 
200
- // All checks passed: broadcast to the channel, excluding the sender.
201
- const line: RawLine = { text: `:${hostmask} ${command} ${state.name} :${text}` };
202
- effects.push(Effect.broadcast(state.name, [line], ctx.connId));
203
-
204
- // IRCv3 echo-message: echo the same line back to the sender, after
205
- // the broadcast effect so clients observe their own message last.
200
+ // All checks passed: broadcast to the channel, excluding the sender. The
201
+ // bare line goes to every member; the `@msgid=<id>` decorated line is sent
202
+ // only to members who negotiated the `msgid` cap (IRCv3 msgid). The
203
+ // dispatch layer's cap-split resolves per-recipient.
204
+ const body = `${command} ${state.name} :${text}`;
205
+ const line: RawLine = { text: `:${hostmask} ${body}` };
206
+ const msgidLine: RawLine = { text: `@msgid=${msgid} :${hostmask} ${body}` };
207
+ effects.push(Effect.broadcast(state.name, [line], ctx.connId, 'msgid', [msgidLine]));
208
+
209
+ // IRCv3 echo-message: echo the line back to the sender, after the
210
+ // broadcast effect so clients observe their own message last. A sender
211
+ // that also negotiated `msgid` sees the decorated echo.
206
212
  if (ctx.connection.caps.has('echo-message')) {
207
- effects.push(Effect.send(ctx.connId, [line]));
213
+ const echoLine = ctx.connection.caps.has('msgid') ? msgidLine : line;
214
+ effects.push(Effect.send(ctx.connId, [echoLine]));
208
215
  }
209
216
 
210
217
  return { state, effects };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Pure helpers for IRCv3 `draft/read-marker` (persistent last-read marker).
3
+ *
4
+ * Spec (work-in-progress): https://ircv3.net/specs/extensions/read-marker
5
+ *
6
+ * The serverless-ircd deployment diverges from the work-in-progress draft:
7
+ * instead of the draft's `MARKREAD` command + server-time timestamp, this
8
+ * implementation is **msgid-based** so it composes with the
9
+ * per-`(connection, channel)` last-read marker the chathistory command
10
+ * already maintains in {@link ConnectionState.lastReadMarkers}.
11
+ *
12
+ * Flow:
13
+ * - A cap-enabled client marks a message read by sending a
14
+ * `TAGMSG <target>` carrying `+draft/read-marker=<msgid>`. The
15
+ * {@link tagmsgChannelReducer} detects the tag, calls
16
+ * {@link persistReadMarker} to advance the per-`(account, channel)`
17
+ * record in the bound {@link ReadMarkerStore}, and fans the TAGMSG out
18
+ * (carrying the tag) so the user's other connections learn the new
19
+ * read position.
20
+ * - On SASL identify, the actor calls {@link seedReadMarkers} so a fresh
21
+ * connection's `lastReadMarkers` is restored from the stored values and
22
+ * a reconnect resumes at the user's last read position.
23
+ */
24
+
25
+ import { READ_MARKER_CAP_NAME } from '../caps/capabilities.js';
26
+ import type { ReadMarkerStore } from '../ports.js';
27
+ import type { ConnectionState } from '../state/connection.js';
28
+ import type { Ctx } from '../types.js';
29
+
30
+ /**
31
+ * Wire name of the IRCv3 `draft/read-marker` capability. Aliased from
32
+ * {@link READ_MARKER_CAP_NAME} (the canonical home in the cap data table)
33
+ * for ergonomic use inside the reducer.
34
+ */
35
+ export const READ_MARKER_CAP = READ_MARKER_CAP_NAME;
36
+
37
+ /**
38
+ * The client-tag key that carries a read-marker update. Presence of this key
39
+ * (with a non-empty value) on a `TAGMSG` identifies a read-marker update the
40
+ * server persists and propagates.
41
+ */
42
+ export const READ_MARKER_TAG = '+draft/read-marker';
43
+
44
+ /** Returns true iff `tags` carries the `+draft/read-marker` client tag. */
45
+ export function isReadMarkerTagmsg(tags: Readonly<Record<string, string>>): boolean {
46
+ return READ_MARKER_TAG in tags;
47
+ }
48
+
49
+ /**
50
+ * Returns the msgid carried by the `+draft/read-marker` tag, or `undefined`
51
+ * when the tag is absent or valueless. A valueless tag carries no msgid and
52
+ * therefore no marker to persist.
53
+ */
54
+ export function readMarkerMsgid(tags: Readonly<Record<string, string>>): string | undefined {
55
+ const value = tags[READ_MARKER_TAG];
56
+ if (value === undefined || value === '') return undefined;
57
+ return value;
58
+ }
59
+
60
+ /**
61
+ * Advances the per-`(account, channel)` last-read marker.
62
+ *
63
+ * Always advances the connection's in-memory {@link ConnectionState.lastReadMarkers}
64
+ * so the current session tracks the read position. When a {@link ReadMarkerStore}
65
+ * is bound AND the connection is identified (has an `account`), the marker is
66
+ * also persisted so it survives reconnects and is shared across the account's
67
+ * connections.
68
+ *
69
+ * `chanLower` is the lowercased channel name (the storage key); callers pass
70
+ * the authoritative `ChannelState.nameLower`.
71
+ */
72
+ export function persistReadMarker(ctx: Ctx, chanLower: string, msgid: string): void {
73
+ if (ctx.connection.lastReadMarkers === undefined) {
74
+ ctx.connection.lastReadMarkers = new Map<string, string>();
75
+ }
76
+ ctx.connection.lastReadMarkers.set(chanLower, msgid);
77
+
78
+ const store = ctx.readMarkers;
79
+ const account = ctx.connection.account;
80
+ if (store !== undefined && account !== undefined) {
81
+ store.set(account, chanLower, msgid);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Seeds a fresh connection's `lastReadMarkers` from the persisted values for
87
+ * `account`. Called at SASL identify so a reconnect restores the user's last
88
+ * read position across every channel they have a marker for.
89
+ *
90
+ * Existing in-memory markers are preserved: a session may have already
91
+ * advanced past the stored value, and the in-memory (newer) marker wins.
92
+ */
93
+ export function seedReadMarkers(
94
+ conn: ConnectionState,
95
+ store: ReadMarkerStore,
96
+ account: string,
97
+ ): void {
98
+ const entries = store.forAccount(account);
99
+ if (entries.length === 0) return;
100
+ if (conn.lastReadMarkers === undefined) {
101
+ conn.lastReadMarkers = new Map<string, string>();
102
+ }
103
+ for (const entry of entries) {
104
+ if (!conn.lastReadMarkers.has(entry.channel)) {
105
+ conn.lastReadMarkers.set(entry.channel, entry.msgid);
106
+ }
107
+ }
108
+ }
@@ -117,6 +117,14 @@ export function emitWelcomeIfReady(state: ConnectionState, ctx: Ctx): EffectType
117
117
  // so the `001` hostmask and every downstream message uses the cloak.
118
118
  applyCloakIfEnabled(state, ctx);
119
119
 
120
+ // Surface the transport's TLS fact as read-only user mode `S` so clients
121
+ // learn the connection is secure via `221` / `WHOIS`. The adapter sets
122
+ // `state.secure` from the platform's transport metadata at construction;
123
+ // the mode is not settable via `MODE` (the parser rejects `+S`/`-S`).
124
+ if (state.secure) {
125
+ state.userModes.tls = true;
126
+ }
127
+
120
128
  state.registration = 'registered';
121
129
  return [Effect.send(ctx.connId, buildWelcomeLines(state, ctx))];
122
130
  }
@@ -35,6 +35,9 @@ import { Numerics } from '../protocol/numerics.js';
35
35
  import { hostmaskOf } from '../state/connection.js';
36
36
  import type { ConnectionState } from '../state/connection.js';
37
37
  import type { Ctx, Reducer } from '../types.js';
38
+ import { emitAccountNotify } from './account-notify.js';
39
+ import { nowAwayLine, replayPersistedAway } from './pre-away.js';
40
+ import { seedReadMarkers } from './read-marker.js';
38
41
 
39
42
  /** Per-ircdocs.horse: a single `AUTHENTICATE` payload chunk is at most 400 bytes. */
40
43
  const SASL_CHUNK_BYTES = 400;
@@ -218,9 +221,29 @@ function applyResult(
218
221
 
219
222
  if (result.ok) {
220
223
  state.account = result.account;
224
+ // IRCv3 draft/read-marker: restore the user's per-channel last-read
225
+ // markers from the persisted store so a reconnect resumes at their
226
+ // last read position. No-op when no ReadMarkerStore is bound.
227
+ if (ctx.readMarkers !== undefined) {
228
+ seedReadMarkers(state, ctx.readMarkers, result.account);
229
+ }
230
+ // IRCv3 draft/pre-away: replay the user's persisted away reason onto
231
+ // the fresh connection so a reconnect restores the away state, and
232
+ // emit the 306 numeric so the user sees they are still away. No-op
233
+ // when no AwayStore is bound or no reason is stored.
234
+ const replayedAway =
235
+ ctx.away !== undefined ? replayPersistedAway(state, ctx.away, result.account) : undefined;
236
+ const awayEffects: EffectType[] =
237
+ replayedAway !== undefined
238
+ ? [Effect.send(ctx.connId, [nowAwayLine(state, ctx.serverName)])]
239
+ : [];
221
240
  return {
222
241
  state,
223
- effects: [Effect.send(ctx.connId, [loggedIn(ctx, result.account), saslSuccess(ctx)])],
242
+ effects: [
243
+ Effect.send(ctx.connId, [loggedIn(ctx, result.account), saslSuccess(ctx)]),
244
+ ...awayEffects,
245
+ ...emitAccountNotify({ conn: state, account: result.account }),
246
+ ],
224
247
  };
225
248
  }
226
249
  return { state, effects: [Effect.send(ctx.connId, [saslFail(ctx)])] };
@@ -27,10 +27,58 @@ import type { ChannelState } 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 { Ctx, Reducer } from '../types.js';
30
+ import {
31
+ READ_MARKER_CAP,
32
+ isReadMarkerTagmsg,
33
+ persistReadMarker,
34
+ readMarkerMsgid,
35
+ } from './read-marker.js';
30
36
 
31
37
  /** The cap name advertising message-tags support. */
32
38
  const MESSAGE_TAGS_CAP = 'message-tags';
33
39
 
40
+ /**
41
+ * The cap name advertising IRCv3 `draft/typing` support. Typing TAGMSGs
42
+ * (those carrying the `+draft/typing` client tag) fan out to members that
43
+ * announced EITHER `message-tags` (the baseline for every TAGMSG) OR
44
+ * `draft/typing` (the spec's client-tag exception for peers that did not
45
+ * negotiate `message-tags`).
46
+ */
47
+ const TYPING_CAP = 'draft/typing';
48
+
49
+ /**
50
+ * The client-tag key that identifies a typing TAGMSG. Presence of this key
51
+ * (regardless of value, including the valueless form) triggers the OR-caps
52
+ * broadcast above.
53
+ */
54
+ const TYPING_TAG = '+draft/typing';
55
+
56
+ /** Returns true iff `tags` carries the `+draft/typing` client tag. */
57
+ function isTypingTagmsg(tags: Readonly<Record<string, string>>): boolean {
58
+ return TYPING_TAG in tags;
59
+ }
60
+
61
+ /**
62
+ * Returns the OR-semantics cap list a TAGMSG fanout should use, or `undefined`
63
+ * when the plain single-cap (`message-tags`) broadcast path applies.
64
+ *
65
+ * A TAGMSG carrying `+draft/typing` widens to `message-tags` OR `draft/typing`;
66
+ * one carrying `+draft/read-marker` widens to `message-tags` OR
67
+ * `draft/read-marker`. Both spec exceptions let a peer that announced only the
68
+ * draft cap (not `message-tags`) still receive the notification. The baseline
69
+ * `message-tags` cap always leads the list so message-tags peers keep receiving
70
+ * every TAGMSG.
71
+ */
72
+ function widenAudienceCaps(tags: Readonly<Record<string, string>>): readonly string[] | undefined {
73
+ const typing = isTypingTagmsg(tags);
74
+ const readMarker = isReadMarkerTagmsg(tags);
75
+ if (!typing && !readMarker) return undefined;
76
+ const caps = [MESSAGE_TAGS_CAP];
77
+ if (typing) caps.push(TYPING_CAP);
78
+ if (readMarker) caps.push(READ_MARKER_CAP);
79
+ return caps;
80
+ }
81
+
34
82
  /**
35
83
  * Returns true iff `hostmask` matches the IRC ban mask `mask`. Identical to
36
84
  * the implementation in `privmsg.ts`; duplicated to keep the message path
@@ -149,10 +197,32 @@ export const tagmsgChannelReducer: Reducer<ChannelState> = (state, msg, ctx) =>
149
197
  });
150
198
  }
151
199
 
200
+ // IRCv3 draft/read-marker: a TAGMSG carrying `+draft/read-marker=<msgid>`
201
+ // marks `<msgid>` as the last read message for the sender's account in
202
+ // this channel. Persist the marker (advances the connection's in-memory
203
+ // marker too); a valueless tag carries no msgid and is dropped here. The
204
+ // TAGMSG still fans out below so the user's other connections learn it.
205
+ if (isReadMarkerTagmsg(msg.tags)) {
206
+ const msgid = readMarkerMsgid(msg.tags);
207
+ if (msgid !== undefined) {
208
+ persistReadMarker(ctx, state.nameLower, msgid);
209
+ }
210
+ }
211
+
152
212
  // Broadcast the tag-bearing line to message-tags-enabled members only.
213
+ // When the TAGMSG carries `+draft/typing` or `+draft/read-marker`, the
214
+ // audience widens to also include members that announced the matching
215
+ // draft cap (the spec's client-tag exception): such peers receive the
216
+ // notification even without `message-tags`. The caps are passed via
217
+ // `caps` so dispatch applies OR semantics per recipient.
153
218
  const tagPrefix = serializeTags(msg.tags);
154
219
  const line: RawLine = { text: `${tagPrefix}:${hostmask} TAGMSG ${state.name}` };
155
- effects.push(Effect.broadcast(state.name, [line], ctx.connId, MESSAGE_TAGS_CAP));
220
+ const widenCaps = widenAudienceCaps(msg.tags);
221
+ if (widenCaps !== undefined) {
222
+ effects.push(Effect.broadcast(state.name, [line], ctx.connId, undefined, undefined, widenCaps));
223
+ } else {
224
+ effects.push(Effect.broadcast(state.name, [line], ctx.connId, MESSAGE_TAGS_CAP));
225
+ }
156
226
 
157
227
  // IRCv3 echo-message: echo the same line back to the sender, after
158
228
  // the broadcast effect so clients observe their own message last.
@@ -134,6 +134,11 @@ export function whoisReducer(
134
134
  text: `:${ctx.serverName} ${Numerics.RPL_WHOISOPERATOR.toString().padStart(3, '0')} ${requesterNick} ${tNick} :is an IRC operator`,
135
135
  });
136
136
  }
137
+ if (target.userModes.tls) {
138
+ lines.push({
139
+ text: `:${ctx.serverName} ${Numerics.RPL_WHOISSECURE.toString().padStart(3, '0')} ${requesterNick} ${tNick} :is using a secure connection`,
140
+ });
141
+ }
137
142
  if (target.account !== undefined) {
138
143
  lines.push({
139
144
  text: `:${ctx.serverName} ${Numerics.RPL_WHOISACCOUNT.toString().padStart(3, '0')} ${requesterNick} ${tNick} ${target.account} :is logged in as`,
@@ -18,6 +18,7 @@
18
18
 
19
19
  import { z } from 'zod';
20
20
  import { BUILD_DATE } from './build-info.js';
21
+ import { DEFAULT_MULTILINE_MAX_BYTES } from './caps/capabilities.js';
21
22
 
22
23
  /**
23
24
  * Schema for a single IRC operator credential.
@@ -61,6 +62,33 @@ export const CloakingConfigSchema = z.object({
61
62
  cloakedSuffix: z.string().min(1).optional(),
62
63
  });
63
64
 
65
+ /**
66
+ * IRCv3 `sts` (Strict Transport Security) policy. When supplied, the
67
+ * server advertises an `sts` capability whose value tells compliant
68
+ * clients how long to pin TLS and which port to upgrade to on a
69
+ * plaintext connection. Omit entirely (the default) to disable STS.
70
+ *
71
+ * Spec: https://ircv3.net/specs/extensions/sts
72
+ */
73
+ export const StsConfigSchema = z.object({
74
+ /**
75
+ * Pin duration in seconds. Defaults to 30 days at the cap-builder layer
76
+ * when omitted; supply an explicit value in production.
77
+ */
78
+ duration: z.number().int().positive(),
79
+ /**
80
+ * TLS listener port advertised on plaintext connections. The canonical
81
+ * IRC-over-TLS port is 6697 (RFC 7193). Defaults to 6697 at the
82
+ * cap-builder layer when omitted.
83
+ */
84
+ port: z.number().int().positive(),
85
+ /**
86
+ * Optional preload directive. When true the policy may be hard-coded
87
+ * into a client preload list (only advertised on plaintext connections).
88
+ */
89
+ preload: z.boolean().optional(),
90
+ });
91
+
64
92
  /**
65
93
  * Per-IP connection-rate limit. Caps how many new connections a single
66
94
  * source IP may open within a sliding `windowMs` window before the
@@ -272,6 +300,43 @@ export const ServerConfigSchema = z.object({
272
300
  * lower per deployment. Only consulted when a MessageStore is bound.
273
301
  */
274
302
  chatHistoryPlaybackLimit: z.number().int().positive().default(50),
303
+
304
+ /**
305
+ * IRCv3 `sts` (Strict Transport Security) policy. Omit entirely (the
306
+ * default) to disable STS advertisement; supply `{ duration, port }`
307
+ * (plus optional `preload`) to advertise the cap and let compliant
308
+ * clients upgrade from plaintext to TLS and pin the secure listener.
309
+ */
310
+ sts: StsConfigSchema.optional(),
311
+
312
+ /**
313
+ * IRCv3 `draft/multiline` byte budget. Advertised as
314
+ * `draft/multiline=<n>` in `CAP LS` and enforced on every finalized
315
+ * multi-line batch. Defaults to the IRCv3-recommended 4096; raise or
316
+ * lower per deployment.
317
+ */
318
+ multilineMaxBytes: z.number().int().positive().default(DEFAULT_MULTILINE_MAX_BYTES),
319
+
320
+ /**
321
+ * Per-connection MONITOR watchlist ceiling, advertised as `MONITOR=<n>`
322
+ * in `005 RPL_ISUPPORT`. Omit to inherit the monitor reducer's built-in
323
+ * ceiling (DEFAULT_MONITOR_LIMIT, 30).
324
+ */
325
+ monitorLimit: z.number().int().positive().optional(),
326
+
327
+ /**
328
+ * Extended-ban prefix sigil for `EXTBAN=<prefix>,…` in
329
+ * `005 RPL_ISUPPORT`. Omit for the IRC-standard `~`.
330
+ */
331
+ extbanPrefix: z.string().min(1).optional(),
332
+
333
+ /**
334
+ * Enables IRC-services-derived ISUPPORT tokens (currently
335
+ * `ACCOUNTEXTBAN=a`). Set to true once a `ServicesStore` is bound; until
336
+ * then account-name extended bans are suppressed because unregistered
337
+ * nicks make them meaningless.
338
+ */
339
+ servicesEnabled: z.boolean().optional(),
275
340
  });
276
341
 
277
342
  /** Input type: what adapters supply (fields with defaults may be omitted). */
@@ -303,16 +368,6 @@ export function parseServerConfig(input: unknown): ParsedServerConfig {
303
368
  return result.data;
304
369
  }
305
370
 
306
- /**
307
- * Default server config: the result of parsing an empty input. Useful as a
308
- * known-good baseline for tests and for adapters that want to overlay
309
- * per-deployment overrides via object spread.
310
- */
311
- export const DEFAULT_SERVER_CONFIG: ParsedServerConfig = ServerConfigSchema.parse({
312
- serverName: 'irc.example.com',
313
- networkName: 'ServerlessIRCd',
314
- });
315
-
316
371
  /**
317
372
  * Resolves the effective server version for wire output: the configured
318
373
  * value when present, otherwise {@link DEFAULT_SERVER_VERSION}. Used by the
@@ -43,6 +43,18 @@ export interface BroadcastEffect {
43
43
  except?: ConnId;
44
44
  cap?: string;
45
45
  capLines?: RawLine[];
46
+ /**
47
+ * OR-semantics cap list: the recipient receives `lines` iff they
48
+ * negotiated ANY of the listed caps. Used by capabilities whose delivery
49
+ * rule is the union of two caps — e.g. `draft/typing` TAGMSGs reach
50
+ * peers that announced `draft/typing` OR `message-tags` (the latter
51
+ * because every TAGMSG goes to message-tags peers regardless of tag).
52
+ *
53
+ * Mutually exclusive with {@link cap}: set one or the other, not both.
54
+ * When neither is set, dispatch takes the runtime's bulk broadcast fast
55
+ * path.
56
+ */
57
+ caps?: readonly string[];
46
58
  }
47
59
 
48
60
  export interface DisconnectEffect {
@@ -150,11 +162,13 @@ export const Effect = {
150
162
  except?: ConnId,
151
163
  cap?: string,
152
164
  capLines?: RawLine[],
165
+ caps?: readonly string[],
153
166
  ): Effect {
154
167
  const e: BroadcastEffect = { tag: 'Broadcast', chan, lines };
155
168
  if (except !== undefined) e.except = except;
156
169
  if (cap !== undefined) e.cap = cap;
157
170
  if (capLines !== undefined) e.capLines = capLines;
171
+ if (caps !== undefined) e.caps = caps;
158
172
  return e;
159
173
  },
160
174
  disconnect(conn: ConnId, reason?: string): Effect {