serverless-ircd 0.6.0 → 0.8.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 (350) hide show
  1. package/.github/workflows/ci.yml +9 -4
  2. package/.gitmodules +4 -1
  3. package/CHANGELOG.md +480 -22
  4. package/README.md +244 -57
  5. package/apps/aws-stack/package.json +1 -1
  6. package/apps/aws-stack/src/aws-stack.ts +186 -18
  7. package/apps/aws-stack/tests/stack.test.ts +400 -56
  8. package/apps/cf-tcp-container/package.json +1 -1
  9. package/apps/cf-worker/package.json +1 -1
  10. package/apps/cf-worker/src/origin-allowlist.ts +99 -0
  11. package/apps/cf-worker/src/worker.ts +89 -9
  12. package/apps/cf-worker/tests/fixtures/web-dist/index.html +18 -0
  13. package/apps/cf-worker/tests/fixtures/web-dist/webclient/index.html +18 -0
  14. package/apps/cf-worker/tests/origin-allowlist.test.ts +101 -0
  15. package/apps/cf-worker/tests/smoke.test.ts +28 -2
  16. package/apps/cf-worker/tests/ws-origin.test.ts +89 -0
  17. package/apps/cf-worker/wrangler.test.toml +23 -0
  18. package/apps/cf-worker/wrangler.toml +56 -0
  19. package/apps/local-cli/package.json +1 -1
  20. package/apps/local-cli/src/config-loader.ts +8 -0
  21. package/apps/local-cli/src/main.ts +16 -0
  22. package/apps/local-cli/src/server.ts +1 -0
  23. package/apps/local-cli/tests/config-loader.test.ts +14 -0
  24. package/apps/web/landing/index.html +215 -0
  25. package/apps/web/package.json +28 -0
  26. package/apps/web/scripts/build.mjs +132 -0
  27. package/apps/web/src/build-env.ts +43 -0
  28. package/apps/web/src/config-schema.ts +138 -0
  29. package/apps/web/static/config.json +28 -0
  30. package/apps/web/static/config.prod.json +28 -0
  31. package/apps/web/static/config.staging.json +28 -0
  32. package/apps/web/tests/build-env.test.ts +63 -0
  33. package/apps/web/tests/build-smoke.test.ts +103 -0
  34. package/apps/web/tests/config-schema.test.ts +432 -0
  35. package/apps/web/tests/workspace.test.ts +12 -0
  36. package/apps/web/tsconfig.json +10 -0
  37. package/apps/web/upstream/.browserslistrc +3 -0
  38. package/apps/web/upstream/.editorconfig +9 -0
  39. package/apps/web/upstream/.eslintignore +3 -0
  40. package/apps/web/upstream/.eslintrc.js +90 -0
  41. package/apps/web/upstream/.github/FUNDING.yml +12 -0
  42. package/apps/web/upstream/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  43. package/apps/web/upstream/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  44. package/apps/web/upstream/.github/workflows/push.yml +26 -0
  45. package/apps/web/upstream/.github/workflows/release.yml +33 -0
  46. package/apps/web/upstream/.github/workflows/release_manual.yml +55 -0
  47. package/apps/web/upstream/.prettierrc.js +9 -0
  48. package/apps/web/upstream/.stylelintignore +5 -0
  49. package/apps/web/upstream/.stylelintrc.js +21 -0
  50. package/apps/web/upstream/LICENSE +201 -0
  51. package/apps/web/upstream/README.md +76 -0
  52. package/apps/web/upstream/babel.config.js +25 -0
  53. package/apps/web/upstream/crowdin.yml +3 -0
  54. package/apps/web/upstream/index.html +35 -0
  55. package/apps/web/upstream/jest.config.js +7 -0
  56. package/apps/web/upstream/jsdoc.json +22 -0
  57. package/apps/web/upstream/package.json +80 -0
  58. package/apps/web/upstream/scripts/pre-commit +43 -0
  59. package/apps/web/upstream/src/components/App.vue +516 -0
  60. package/apps/web/upstream/src/components/AppSettings.vue +587 -0
  61. package/apps/web/upstream/src/components/AutoComplete.vue +284 -0
  62. package/apps/web/upstream/src/components/Avatar.vue +105 -0
  63. package/apps/web/upstream/src/components/AwayStatusIndicator.vue +77 -0
  64. package/apps/web/upstream/src/components/BufferKey.vue +121 -0
  65. package/apps/web/upstream/src/components/BufferSettings.vue +97 -0
  66. package/apps/web/upstream/src/components/Captcha.vue +85 -0
  67. package/apps/web/upstream/src/components/ChannelBanlist.vue +96 -0
  68. package/apps/web/upstream/src/components/ChannelInfo.vue +159 -0
  69. package/apps/web/upstream/src/components/ChannelInvitelist.vue +276 -0
  70. package/apps/web/upstream/src/components/ChannelList.vue +345 -0
  71. package/apps/web/upstream/src/components/Container.vue +401 -0
  72. package/apps/web/upstream/src/components/ContainerHeader.vue +522 -0
  73. package/apps/web/upstream/src/components/ControlInput.vue +1016 -0
  74. package/apps/web/upstream/src/components/LoadingAnimation.vue +104 -0
  75. package/apps/web/upstream/src/components/MediaViewer.vue +199 -0
  76. package/apps/web/upstream/src/components/MessageInfo.vue +192 -0
  77. package/apps/web/upstream/src/components/MessageList.vue +1028 -0
  78. package/apps/web/upstream/src/components/MessageListAvatar.vue +20 -0
  79. package/apps/web/upstream/src/components/MessageListMessageCompact.vue +361 -0
  80. package/apps/web/upstream/src/components/MessageListMessageInline.vue +257 -0
  81. package/apps/web/upstream/src/components/MessageListMessageModern.vue +430 -0
  82. package/apps/web/upstream/src/components/NetworkSettings.vue +528 -0
  83. package/apps/web/upstream/src/components/Nicklist.vue +309 -0
  84. package/apps/web/upstream/src/components/NicklistUser.vue +181 -0
  85. package/apps/web/upstream/src/components/NotConnected.vue +271 -0
  86. package/apps/web/upstream/src/components/SelfUser.vue +283 -0
  87. package/apps/web/upstream/src/components/ServerSelector.vue +162 -0
  88. package/apps/web/upstream/src/components/ServerView.vue +126 -0
  89. package/apps/web/upstream/src/components/SettingsAdvanced.vue +232 -0
  90. package/apps/web/upstream/src/components/SettingsAliases.vue +121 -0
  91. package/apps/web/upstream/src/components/Sidebar.vue +429 -0
  92. package/apps/web/upstream/src/components/SidebarAboutBuffer.vue +288 -0
  93. package/apps/web/upstream/src/components/SidebarState.vue +145 -0
  94. package/apps/web/upstream/src/components/StartupError.vue +35 -0
  95. package/apps/web/upstream/src/components/StateBrowser.vue +420 -0
  96. package/apps/web/upstream/src/components/StateBrowserBuffer.vue +124 -0
  97. package/apps/web/upstream/src/components/StateBrowserNetwork.vue +823 -0
  98. package/apps/web/upstream/src/components/StateBrowserUsermenu.vue +162 -0
  99. package/apps/web/upstream/src/components/TypingStatusIndicator.vue +34 -0
  100. package/apps/web/upstream/src/components/TypingUsersList.vue +56 -0
  101. package/apps/web/upstream/src/components/UrlEmbed.vue +149 -0
  102. package/apps/web/upstream/src/components/UserBox.vue +728 -0
  103. package/apps/web/upstream/src/components/inputtools/Emoji.vue +67 -0
  104. package/apps/web/upstream/src/components/inputtools/TextStyle.vue +142 -0
  105. package/apps/web/upstream/src/components/startups/CommonLayout.vue +208 -0
  106. package/apps/web/upstream/src/components/startups/CustomServer.vue +495 -0
  107. package/apps/web/upstream/src/components/startups/KiwiBnc.vue +361 -0
  108. package/apps/web/upstream/src/components/startups/Personal.vue +317 -0
  109. package/apps/web/upstream/src/components/startups/Welcome.vue +544 -0
  110. package/apps/web/upstream/src/components/startups/ZncLogin.vue +229 -0
  111. package/apps/web/upstream/src/components/utils/InputConfirm.vue +68 -0
  112. package/apps/web/upstream/src/components/utils/InputPrompt.vue +118 -0
  113. package/apps/web/upstream/src/components/utils/InputText.vue +172 -0
  114. package/apps/web/upstream/src/components/utils/IrcInput.vue +579 -0
  115. package/apps/web/upstream/src/components/utils/PluginWrapper.vue +26 -0
  116. package/apps/web/upstream/src/components/utils/TabbedView.vue +149 -0
  117. package/apps/web/upstream/src/components/utils/TransitionExpand.vue +97 -0
  118. package/apps/web/upstream/src/helpers/Colours.js +128 -0
  119. package/apps/web/upstream/src/helpers/IrcdDiffs.js +26 -0
  120. package/apps/web/upstream/src/helpers/Md5.js +193 -0
  121. package/apps/web/upstream/src/helpers/Misc.js +401 -0
  122. package/apps/web/upstream/src/helpers/TextFormatting.js +237 -0
  123. package/apps/web/upstream/src/libs/AliasRewriter.js +157 -0
  124. package/apps/web/upstream/src/libs/AudioManager.js +60 -0
  125. package/apps/web/upstream/src/libs/BouncerMiddleware.js +247 -0
  126. package/apps/web/upstream/src/libs/BouncerProvider.js +551 -0
  127. package/apps/web/upstream/src/libs/ChathistoryMiddleware.js +153 -0
  128. package/apps/web/upstream/src/libs/ConfigLoader.js +94 -0
  129. package/apps/web/upstream/src/libs/EmojiProvider.js +46 -0
  130. package/apps/web/upstream/src/libs/GlobalApi.js +346 -0
  131. package/apps/web/upstream/src/libs/IPC.js +51 -0
  132. package/apps/web/upstream/src/libs/InputHandler.js +921 -0
  133. package/apps/web/upstream/src/libs/IrcClient.js +1506 -0
  134. package/apps/web/upstream/src/libs/Logger.js +71 -0
  135. package/apps/web/upstream/src/libs/Message.js +169 -0
  136. package/apps/web/upstream/src/libs/MessageFormatter.js +364 -0
  137. package/apps/web/upstream/src/libs/MessageParser.js +249 -0
  138. package/apps/web/upstream/src/libs/Notifications.js +78 -0
  139. package/apps/web/upstream/src/libs/ServerConnection.js +270 -0
  140. package/apps/web/upstream/src/libs/ServerSession.js +102 -0
  141. package/apps/web/upstream/src/libs/SoundBleep.js +20 -0
  142. package/apps/web/upstream/src/libs/StatePersistence.js +90 -0
  143. package/apps/web/upstream/src/libs/ThemeManager.js +128 -0
  144. package/apps/web/upstream/src/libs/TypingMiddleware.js +105 -0
  145. package/apps/web/upstream/src/libs/WindowTitle.js +63 -0
  146. package/apps/web/upstream/src/libs/batchedAdd.js +74 -0
  147. package/apps/web/upstream/src/libs/bufferTools.js +177 -0
  148. package/apps/web/upstream/src/libs/polyfill/Element.closest.js +18 -0
  149. package/apps/web/upstream/src/libs/renderers/Html.js +113 -0
  150. package/apps/web/upstream/src/libs/settingTools.js +31 -0
  151. package/apps/web/upstream/src/libs/state/BufferState.js +703 -0
  152. package/apps/web/upstream/src/libs/state/NetworkState.js +158 -0
  153. package/apps/web/upstream/src/libs/state/UserState.js +89 -0
  154. package/apps/web/upstream/src/libs/state/common.js +26 -0
  155. package/apps/web/upstream/src/libs/state.js +961 -0
  156. package/apps/web/upstream/src/libs/storage/Local.js +51 -0
  157. package/apps/web/upstream/src/main.js +566 -0
  158. package/apps/web/upstream/src/res/autocompleteCommands.js +31 -0
  159. package/apps/web/upstream/src/res/configTemplates.js +373 -0
  160. package/apps/web/upstream/src/res/globalStyle.css +277 -0
  161. package/apps/web/upstream/src/res/kiwiLoadingLogo.png +0 -0
  162. package/apps/web/upstream/src/res/locales/app.af-ZA.po +1145 -0
  163. package/apps/web/upstream/src/res/locales/app.ar-SA.po +1145 -0
  164. package/apps/web/upstream/src/res/locales/app.bg-BG.po +1145 -0
  165. package/apps/web/upstream/src/res/locales/app.bs-BA.po +1145 -0
  166. package/apps/web/upstream/src/res/locales/app.ca-ES.po +1145 -0
  167. package/apps/web/upstream/src/res/locales/app.cs-CZ.po +1145 -0
  168. package/apps/web/upstream/src/res/locales/app.da-DK.po +1145 -0
  169. package/apps/web/upstream/src/res/locales/app.de-DE.po +1145 -0
  170. package/apps/web/upstream/src/res/locales/app.dev.po +1246 -0
  171. package/apps/web/upstream/src/res/locales/app.el-GR.po +1145 -0
  172. package/apps/web/upstream/src/res/locales/app.en-US.po +1145 -0
  173. package/apps/web/upstream/src/res/locales/app.es-419.po +1145 -0
  174. package/apps/web/upstream/src/res/locales/app.es-AR.po +1145 -0
  175. package/apps/web/upstream/src/res/locales/app.es-EM.po +536 -0
  176. package/apps/web/upstream/src/res/locales/app.es-ES.po +1145 -0
  177. package/apps/web/upstream/src/res/locales/app.es-US.po +1145 -0
  178. package/apps/web/upstream/src/res/locales/app.eu-ES.po +1145 -0
  179. package/apps/web/upstream/src/res/locales/app.fi-FI.po +1145 -0
  180. package/apps/web/upstream/src/res/locales/app.fr-FR.po +1145 -0
  181. package/apps/web/upstream/src/res/locales/app.gl-ES.po +1145 -0
  182. package/apps/web/upstream/src/res/locales/app.he-IL.po +1145 -0
  183. package/apps/web/upstream/src/res/locales/app.hi-IN.po +1145 -0
  184. package/apps/web/upstream/src/res/locales/app.hu-HU.po +1145 -0
  185. package/apps/web/upstream/src/res/locales/app.id-ID.po +1145 -0
  186. package/apps/web/upstream/src/res/locales/app.it-IT.po +1145 -0
  187. package/apps/web/upstream/src/res/locales/app.ja-JP.po +1145 -0
  188. package/apps/web/upstream/src/res/locales/app.ko-KR.po +1145 -0
  189. package/apps/web/upstream/src/res/locales/app.nl-NL.po +1145 -0
  190. package/apps/web/upstream/src/res/locales/app.no-NO.po +1145 -0
  191. package/apps/web/upstream/src/res/locales/app.pl-PL.po +1145 -0
  192. package/apps/web/upstream/src/res/locales/app.pt-BR.po +1145 -0
  193. package/apps/web/upstream/src/res/locales/app.pt-PT.po +1145 -0
  194. package/apps/web/upstream/src/res/locales/app.ro-RO.po +1145 -0
  195. package/apps/web/upstream/src/res/locales/app.ru-RU.po +1145 -0
  196. package/apps/web/upstream/src/res/locales/app.sl-SI.po +1145 -0
  197. package/apps/web/upstream/src/res/locales/app.sq-AL.po +1145 -0
  198. package/apps/web/upstream/src/res/locales/app.sr-SP.po +1145 -0
  199. package/apps/web/upstream/src/res/locales/app.sv-SE.po +1145 -0
  200. package/apps/web/upstream/src/res/locales/app.tr-TR.po +1145 -0
  201. package/apps/web/upstream/src/res/locales/app.uk-UA.po +1145 -0
  202. package/apps/web/upstream/src/res/locales/app.vi-VN.po +1145 -0
  203. package/apps/web/upstream/src/res/locales/app.zh-CN.po +1145 -0
  204. package/apps/web/upstream/src/res/locales/app.zh-TW.po +1145 -0
  205. package/apps/web/upstream/src/res/localesList.json +27 -0
  206. package/apps/web/upstream/src/res/logo.png +0 -0
  207. package/apps/web/upstream/src/thirdparty/about.html +43 -0
  208. package/apps/web/upstream/src/thirdparty/index.js +5 -0
  209. package/apps/web/upstream/src/thirdparty/kiwiirccom.vue +185 -0
  210. package/apps/web/upstream/static/config.json +28 -0
  211. package/apps/web/upstream/static/emoticons/smile.png +0 -0
  212. package/apps/web/upstream/static/favicon.png +0 -0
  213. package/apps/web/upstream/static/highlight.mp3 +0 -0
  214. package/apps/web/upstream/static/highlight.ogg +0 -0
  215. package/apps/web/upstream/static/locales/.gitignore +2 -0
  216. package/apps/web/upstream/static/plugins/customise.html.example +10 -0
  217. package/apps/web/upstream/static/themes/coffee/theme.css +145 -0
  218. package/apps/web/upstream/static/themes/common/base.css +1064 -0
  219. package/apps/web/upstream/static/themes/dark/theme.css +196 -0
  220. package/apps/web/upstream/static/themes/default/theme.css +92 -0
  221. package/apps/web/upstream/static/themes/elite/theme.css +248 -0
  222. package/apps/web/upstream/static/themes/grayfox/theme.css +143 -0
  223. package/apps/web/upstream/static/themes/nightswatch/theme.css +367 -0
  224. package/apps/web/upstream/static/themes/osprey/theme.css +115 -0
  225. package/apps/web/upstream/static/themes/radioactive/theme.css +1211 -0
  226. package/apps/web/upstream/static/themes/sky/theme.css +107 -0
  227. package/apps/web/upstream/tests/unit/BatchAdd.spec.js +174 -0
  228. package/apps/web/upstream/tests/unit/MessageParser.spec.js +125 -0
  229. package/apps/web/upstream/tests/unit/Misc.spec.js +24 -0
  230. package/apps/web/upstream/tests/unit/NetworkState.spec.js +58 -0
  231. package/apps/web/upstream/tests/unit/StartupError.spec.js +20 -0
  232. package/apps/web/upstream/vue.config.js +150 -0
  233. package/apps/web/upstream/yarn.lock +9767 -0
  234. package/apps/web/vitest.config.ts +13 -0
  235. package/biome.json +1 -0
  236. package/docs/AWS-Deployment.md +21 -8
  237. package/docs/Cloudflare-Deployment-Guide.md +22 -6
  238. package/docs/Home.md +1 -0
  239. package/docs/PlanExtensions.md +113 -3
  240. package/docs/Release-Process.md +23 -13
  241. package/docs/Services.md +546 -0
  242. package/docs/WebClientGuide.md +536 -0
  243. package/package.json +2 -2
  244. package/packages/aws-adapter/package.json +1 -1
  245. package/packages/aws-adapter/src/aws-runtime.ts +5 -0
  246. package/packages/aws-adapter/src/cdk-table-defs.ts +6 -0
  247. package/packages/aws-adapter/src/config-loader.ts +11 -0
  248. package/packages/aws-adapter/src/connection-counter.ts +89 -0
  249. package/packages/aws-adapter/src/dynamo-services-store.ts +649 -0
  250. package/packages/aws-adapter/src/handlers/connect.ts +55 -51
  251. package/packages/aws-adapter/src/handlers/default.ts +36 -4
  252. package/packages/aws-adapter/src/handlers/index.ts +15 -0
  253. package/packages/aws-adapter/src/handlers/nlb-stream.ts +15 -0
  254. package/packages/aws-adapter/src/handlers/sweeper.ts +5 -1
  255. package/packages/aws-adapter/src/index.ts +4 -0
  256. package/packages/aws-adapter/src/stats.ts +6 -1
  257. package/packages/aws-adapter/src/tables.ts +34 -4
  258. package/packages/aws-adapter/tests/aws-harness.ts +3 -0
  259. package/packages/aws-adapter/tests/config-loader.test.ts +8 -0
  260. package/packages/aws-adapter/tests/connect.test.ts +158 -32
  261. package/packages/aws-adapter/tests/connection-counter.test.ts +127 -0
  262. package/packages/aws-adapter/tests/dynamo-services-store-dynamo.test.ts +183 -0
  263. package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +568 -0
  264. package/packages/aws-adapter/tests/handlers.test.ts +105 -3
  265. package/packages/aws-adapter/tests/tables.test.ts +6 -1
  266. package/packages/cf-adapter/package.json +1 -1
  267. package/packages/cf-adapter/src/config-loader.ts +11 -0
  268. package/packages/cf-adapter/src/connection-do.ts +112 -2
  269. package/packages/cf-adapter/src/d1-services-store.ts +703 -0
  270. package/packages/cf-adapter/src/env.ts +8 -0
  271. package/packages/cf-adapter/src/index.ts +5 -0
  272. package/packages/cf-adapter/tests/cf-harness.ts +12 -1
  273. package/packages/cf-adapter/tests/config-loader.test.ts +19 -0
  274. package/packages/cf-adapter/tests/connection-do-nickserv-d1.test.ts +128 -0
  275. package/packages/cf-adapter/tests/connection-do.test.ts +150 -2
  276. package/packages/cf-adapter/tests/d1-services-store.test.ts +582 -0
  277. package/packages/cf-adapter/tests/serialize.test.ts +1 -0
  278. package/packages/in-memory-runtime/package.json +1 -1
  279. package/packages/irc-core/package.json +1 -1
  280. package/packages/irc-core/reports/mutation/mutation.html +342 -0
  281. package/packages/irc-core/scripts/generate-build-info.mjs +26 -5
  282. package/packages/irc-core/src/commands/account-auth.ts +172 -0
  283. package/packages/irc-core/src/commands/chanserv.ts +882 -0
  284. package/packages/irc-core/src/commands/hostserv.ts +487 -0
  285. package/packages/irc-core/src/commands/index.ts +12 -0
  286. package/packages/irc-core/src/commands/join.ts +164 -8
  287. package/packages/irc-core/src/commands/markread.ts +202 -0
  288. package/packages/irc-core/src/commands/memoserv.ts +319 -0
  289. package/packages/irc-core/src/commands/mode.ts +96 -4
  290. package/packages/irc-core/src/commands/nickserv.ts +390 -0
  291. package/packages/irc-core/src/commands/oper.ts +18 -1
  292. package/packages/irc-core/src/commands/operserv.ts +346 -0
  293. package/packages/irc-core/src/commands/pre-away.ts +3 -1
  294. package/packages/irc-core/src/commands/privmsg.ts +42 -0
  295. package/packages/irc-core/src/commands/read-marker.ts +8 -8
  296. package/packages/irc-core/src/commands/registration.ts +61 -6
  297. package/packages/irc-core/src/commands/sasl.ts +18 -49
  298. package/packages/irc-core/src/commands/tagmsg.ts +41 -6
  299. package/packages/irc-core/src/commands/topic.ts +37 -0
  300. package/packages/irc-core/src/config.ts +36 -5
  301. package/packages/irc-core/src/effects.ts +56 -1
  302. package/packages/irc-core/src/ports.ts +1653 -84
  303. package/packages/irc-core/src/protocol/numerics.ts +8 -0
  304. package/packages/irc-core/src/state/channel.ts +21 -1
  305. package/packages/irc-core/src/state/connection.ts +25 -1
  306. package/packages/irc-core/src/types.ts +48 -12
  307. package/packages/irc-core/tests/batch.test.ts +15 -0
  308. package/packages/irc-core/tests/commands/chanserv.test.ts +1668 -0
  309. package/packages/irc-core/tests/commands/chathistory.test.ts +6 -0
  310. package/packages/irc-core/tests/commands/hostserv.test.ts +935 -0
  311. package/packages/irc-core/tests/commands/join.test.ts +393 -1
  312. package/packages/irc-core/tests/commands/markread.test.ts +361 -0
  313. package/packages/irc-core/tests/commands/memoserv.test.ts +654 -0
  314. package/packages/irc-core/tests/commands/mode.test.ts +381 -2
  315. package/packages/irc-core/tests/commands/nickserv.test.ts +807 -0
  316. package/packages/irc-core/tests/commands/oper.test.ts +13 -0
  317. package/packages/irc-core/tests/commands/operserv.test.ts +656 -0
  318. package/packages/irc-core/tests/commands/privmsg.test.ts +147 -0
  319. package/packages/irc-core/tests/commands/read-marker.test.ts +28 -28
  320. package/packages/irc-core/tests/commands/registration.test.ts +788 -14
  321. package/packages/irc-core/tests/commands/sasl.test.ts +185 -12
  322. package/packages/irc-core/tests/commands/server-info.test.ts +9 -5
  323. package/packages/irc-core/tests/commands/tagmsg.test.ts +73 -33
  324. package/packages/irc-core/tests/commands/topic.test.ts +94 -2
  325. package/packages/irc-core/tests/commands/unified-account.test.ts +416 -0
  326. package/packages/irc-core/tests/config.test.ts +49 -5
  327. package/packages/irc-core/tests/effects.test.ts +19 -0
  328. package/packages/irc-core/tests/message-store.test.ts +63 -0
  329. package/packages/irc-core/tests/numerics.test.ts +13 -0
  330. package/packages/irc-core/tests/parser.test.ts +109 -0
  331. package/packages/irc-core/tests/persistent-services-store.test.ts +582 -0
  332. package/packages/irc-core/tests/services-store.test.ts +1289 -0
  333. package/packages/irc-core/tests/state/channel.test.ts +3 -0
  334. package/packages/irc-server/package.json +1 -1
  335. package/packages/irc-server/src/actor.ts +71 -16
  336. package/packages/irc-server/src/dispatch.ts +94 -7
  337. package/packages/irc-server/src/routing.ts +19 -0
  338. package/packages/irc-server/tests/actor.test.ts +623 -12
  339. package/packages/irc-server/tests/dispatch.test.ts +270 -2
  340. package/packages/irc-server/tests/routing.test.ts +6 -0
  341. package/packages/irc-test-support/package.json +1 -1
  342. package/packages/irc-test-support/src/in-memory-harness.ts +29 -3
  343. package/packages/irc-test-support/src/index.ts +1 -0
  344. package/packages/irc-test-support/src/scenarios.ts +24 -2
  345. package/packages/irc-test-support/tests/in-memory-harness.test.ts +32 -0
  346. package/tools/ci-hardening/package.json +1 -1
  347. package/tools/load-test/package.json +1 -1
  348. package/tools/tcp-ws-forwarder/package.json +1 -1
  349. package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +2 -2
  350. package/packages/irc-core/tests/read-marker-store.test.ts +0 -108
@@ -0,0 +1,487 @@
1
+ /**
2
+ * Pure reducer for the HostServ services pseudo-client.
3
+ *
4
+ * HostServ is invoked via `PRIVMSG HostServ :<command>`. The actor routes
5
+ * any `PRIVMSG` whose target matches {@link HOSTSERV_NICK}
6
+ * (case-insensitive) to {@link hostservReducer} instead of the normal
7
+ * user-target reducer.
8
+ *
9
+ * Supported subcommands:
10
+ * - `ON` — applies the per-account vhost (set via REQUEST or oper SET)
11
+ * to the connection's visible host, persists the prior host so OFF can
12
+ * restore it, and emits `CHGHOST` (IRCv3 `chghost`) to every peer the
13
+ * user shares a channel with. Requires the connection to be identified.
14
+ * - `OFF` — restores the real (pre-vhost) host and emits `CHGHOST` so
15
+ * peers see the revert. Requires identification.
16
+ * - `REQUEST <vhost>` — proposes a vhost for the account. Behaviour
17
+ * depends on {@link ServerConfig.hostservAutoApproveVhosts}: when the
18
+ * knob is on (the default) the vhost is recorded against the account
19
+ * immediately (legacy auto-approve); when the knob is off the request
20
+ * is queued for oper review via {@link ServicesStore.requestVhost}
21
+ * and the requester is told it is pending. Requires identification.
22
+ * - `SET <account> <vhost>` — oper-only. Records (or, with the literal
23
+ * `*` argument, clears) the vhost for `account` directly without
24
+ * going through REQUEST. The target account must be a registered nick.
25
+ * - `APPROVE <account>` (alias `ACTIVATE`) — oper-only. Activates the
26
+ * pending vhost request recorded via queue-mode REQUEST, replying to
27
+ * the oper and best-effort NOTICE-ing the requester when online.
28
+ * - `REJECT <account>` — oper-only. Drops the pending vhost request,
29
+ * replying to the oper and best-effort NOTICE-ing the requester when
30
+ * online.
31
+ * - `LIST` — oper-only. Enumerates every pending vhost request.
32
+ *
33
+ * The reducer is a pure `Reducer<ConnectionState>`: all side effects are
34
+ * emitted as {@link Effect}s. The CHGHOST fanout reuses the
35
+ * {@link emitChghost} broadcast helper so the wire shape stays consistent
36
+ * with SETNAME and any other user/host mutation.
37
+ */
38
+
39
+ import { Effect } from '../effects.js';
40
+ import type { Effect as EffectType, RawLine } from '../effects.js';
41
+ import type { ServicesStore } from '../ports.js';
42
+ import type { ConnectionState } from '../state/connection.js';
43
+ import type { Ctx, Reducer } from '../types.js';
44
+ import { emitChghost } from './chghost.js';
45
+
46
+ /** Canonical HostServ pseudo-client nick. */
47
+ export const HOSTSERV_NICK = 'HostServ';
48
+
49
+ /** Services hostmask fragment used by every services pseudo-client. */
50
+ const SERVICES_HOST = 'services';
51
+
52
+ /** Sentinel value used by `SET <account> *` to clear a recorded vhost. */
53
+ const CLEAR_VHOST_SENTINEL = '*';
54
+
55
+ /**
56
+ * Grammar for a HostServ vhost. Permits the dotted-label form used by
57
+ * mainstream IRC services (each label bounded by alphanumerics, hyphens
58
+ * permitted in the interior). Rejects whitespace, IRC protocol metachars,
59
+ * and empty labels. Capped at 63 characters per label (DNS label limit)
60
+ * and a total of 253 characters (DNS hostname limit).
61
+ */
62
+ const VHOST_RE =
63
+ /^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$/u;
64
+
65
+ /** Returns true iff `vhost` is a syntactically valid HostServ vhost. */
66
+ function isValidVhost(vhost: string): boolean {
67
+ return VHOST_RE.test(vhost);
68
+ }
69
+
70
+ /**
71
+ * Reducer for `PRIVMSG HostServ :<subcommand> <args…>`.
72
+ * Authority: the invoking connection's {@link ConnectionState}.
73
+ */
74
+ export const hostservReducer: Reducer<ConnectionState> = (state, msg, ctx) => {
75
+ state.lastSeen = ctx.clock.now();
76
+
77
+ const effects: EffectType[] = [];
78
+
79
+ if (ctx.services === undefined) {
80
+ effects.push(notice(state, 'Services are not available on this server.'));
81
+ return { state, effects };
82
+ }
83
+
84
+ const body = msg.params[1] ?? '';
85
+ const tokens = body.split(/\s+/u).filter((t) => t.length > 0);
86
+ if (tokens.length === 0) {
87
+ effects.push(helpNotice(state));
88
+ return { state, effects };
89
+ }
90
+
91
+ const sub = tokens[0]?.toUpperCase();
92
+ const args = tokens.slice(1);
93
+
94
+ switch (sub) {
95
+ case 'ON':
96
+ return handleOn(state, ctx, effects);
97
+ case 'OFF':
98
+ return handleOff(state, ctx, effects);
99
+ case 'REQUEST':
100
+ return handleRequest(state, args, ctx, effects);
101
+ case 'SET':
102
+ return handleSet(state, args, ctx, effects);
103
+ case 'APPROVE':
104
+ case 'ACTIVATE':
105
+ return handleApprove(state, args, ctx, effects);
106
+ case 'REJECT':
107
+ return handleReject(state, args, ctx, effects);
108
+ case 'LIST':
109
+ return handleList(state, ctx, effects);
110
+ default:
111
+ effects.push(unknownNotice(state));
112
+ return { state, effects };
113
+ }
114
+ };
115
+
116
+ // ============================================================================
117
+ // ON
118
+ // ============================================================================
119
+
120
+ /**
121
+ * Handles `ON`. Applies the connection's per-account vhost (looked up from
122
+ * the bound {@link ServicesStore}) to {@link ConnectionState.host}, records
123
+ * the prior host on {@link ConnectionState.preVhostHost} so OFF can restore
124
+ * it, and broadcasts `CHGHOST` to every shared-channel peer.
125
+ */
126
+ function handleOn(
127
+ state: ConnectionState,
128
+ ctx: Ctx,
129
+ effects: EffectType[],
130
+ ): { state: ConnectionState; effects: EffectType[] } {
131
+ if (state.account === undefined) {
132
+ effects.push(notice(state, 'You must identify before enabling a vhost.'));
133
+ return { state, effects };
134
+ }
135
+
136
+ if (state.vhostActive === true) {
137
+ effects.push(notice(state, 'Your vhost is already active.'));
138
+ return { state, effects };
139
+ }
140
+
141
+ const services = ctx.services as ServicesStore;
142
+ const vhost = services.getVhost(state.account);
143
+ if (vhost === undefined) {
144
+ effects.push(notice(state, 'No vhost is assigned to your account.'));
145
+ return { state, effects };
146
+ }
147
+
148
+ applyVhost(state, vhost, effects);
149
+ effects.push(notice(state, 'Your vhost is now active.'));
150
+ return { state, effects };
151
+ }
152
+
153
+ // ============================================================================
154
+ // OFF
155
+ // ============================================================================
156
+
157
+ /**
158
+ * Handles `OFF`. Restores the real (pre-vhost) host from
159
+ * {@link ConnectionState.preVhostHost} and broadcasts `CHGHOST`.
160
+ */
161
+ function handleOff(
162
+ state: ConnectionState,
163
+ _ctx: Ctx,
164
+ effects: EffectType[],
165
+ ): { state: ConnectionState; effects: EffectType[] } {
166
+ if (state.account === undefined) {
167
+ effects.push(notice(state, 'You must identify before changing your vhost.'));
168
+ return { state, effects };
169
+ }
170
+
171
+ if (state.vhostActive !== true) {
172
+ effects.push(notice(state, 'No vhost is currently active.'));
173
+ return { state, effects };
174
+ }
175
+
176
+ restoreRealHost(state, effects);
177
+ effects.push(notice(state, 'Your vhost has been disabled.'));
178
+ return { state, effects };
179
+ }
180
+
181
+ // ============================================================================
182
+ // REQUEST
183
+ // ============================================================================
184
+
185
+ /**
186
+ * Handles `REQUEST <vhost>`. Behaviour is selected by
187
+ * {@link ServerConfig.hostservAutoApproveVhosts}:
188
+ * - `true` (auto-approve) — the vhost is recorded against the account
189
+ * immediately (the path that shipped before the oper queue landed). The
190
+ * user is notified that the vhost has been requested and approved.
191
+ * - `false` or omitted (the default — queue mode) — the request is
192
+ * queued for oper review via {@link ServicesStore.requestVhost}; the
193
+ * vhost is NOT activated and no `CHGHOST` is emitted. The user is
194
+ * notified that the request is pending oper approval.
195
+ *
196
+ * Identified-only — otherwise a guest could plant a vhost (or a pending
197
+ * request) against a registered account they do not own.
198
+ */
199
+ function handleRequest(
200
+ state: ConnectionState,
201
+ args: string[],
202
+ ctx: Ctx,
203
+ effects: EffectType[],
204
+ ): { state: ConnectionState; effects: EffectType[] } {
205
+ if (state.account === undefined) {
206
+ effects.push(notice(state, 'You must identify before requesting a vhost.'));
207
+ return { state, effects };
208
+ }
209
+
210
+ const vhost = args[0];
211
+ if (vhost === undefined) {
212
+ effects.push(notice(state, 'Syntax: REQUEST <vhost>'));
213
+ return { state, effects };
214
+ }
215
+
216
+ if (!isValidVhost(vhost)) {
217
+ effects.push(notice(state, `Invalid vhost: ${vhost}`));
218
+ return { state, effects };
219
+ }
220
+
221
+ const services = ctx.services as ServicesStore;
222
+ if (ctx.serverConfig.hostservAutoApproveVhosts !== true) {
223
+ services.requestVhost(state.account, vhost);
224
+ effects.push(notice(state, `Vhost ${vhost} is pending oper approval.`));
225
+ return { state, effects };
226
+ }
227
+
228
+ services.setVhost(state.account, vhost);
229
+ effects.push(notice(state, `Vhost ${vhost} has been requested and approved.`));
230
+ return { state, effects };
231
+ }
232
+
233
+ // ============================================================================
234
+ // SET (oper-only)
235
+ // ============================================================================
236
+
237
+ /**
238
+ * Handles `SET <account> <vhost>` (oper-only). Records the vhost against
239
+ * the named account directly, bypassing REQUEST. The literal `*` clears
240
+ * the recorded vhost. The target account must already be a registered
241
+ * nick; SET does not auto-register.
242
+ */
243
+ function handleSet(
244
+ state: ConnectionState,
245
+ args: string[],
246
+ ctx: Ctx,
247
+ effects: EffectType[],
248
+ ): { state: ConnectionState; effects: EffectType[] } {
249
+ if (!state.userModes.oper) {
250
+ effects.push(notice(state, 'You must be an oper to use SET.'));
251
+ return { state, effects };
252
+ }
253
+
254
+ const account = args[0];
255
+ const vhost = args[1];
256
+ if (account === undefined || vhost === undefined) {
257
+ effects.push(notice(state, 'Syntax: SET <account> <vhost>'));
258
+ return { state, effects };
259
+ }
260
+
261
+ const services = ctx.services as ServicesStore;
262
+ if (!services.isRegisteredNick(account)) {
263
+ effects.push(notice(state, `Nick ${account} is not registered.`));
264
+ return { state, effects };
265
+ }
266
+
267
+ if (vhost === CLEAR_VHOST_SENTINEL) {
268
+ services.setVhost(account, '');
269
+ effects.push(notice(state, `Vhost for ${account} has been cleared.`));
270
+ return { state, effects };
271
+ }
272
+
273
+ if (!isValidVhost(vhost)) {
274
+ effects.push(notice(state, `Invalid vhost: ${vhost}`));
275
+ return { state, effects };
276
+ }
277
+
278
+ services.setVhost(account, vhost);
279
+ effects.push(notice(state, `Vhost for ${account} is now set to ${vhost}.`));
280
+ return { state, effects };
281
+ }
282
+
283
+ // ============================================================================
284
+ // APPROVE / ACTIVATE (oper-only)
285
+ // ============================================================================
286
+
287
+ /**
288
+ * Handles `APPROVE <account>` (alias `ACTIVATE`). Activates the pending
289
+ * vhost request recorded via queue-mode REQUEST: removes the pending
290
+ * entry, calls {@link ServicesStore.setVhost} to activate the vhost, and
291
+ * replies to the oper. A best-effort `SendToNick` NOTICE is emitted to
292
+ * the requester when they are online — delivery is owned by the actor
293
+ * layer (the reducer cannot mutate another connection's host directly).
294
+ * The requester is told to run `HostServ ON` to apply the vhost to their
295
+ * active connection.
296
+ */
297
+ function handleApprove(
298
+ state: ConnectionState,
299
+ args: string[],
300
+ ctx: Ctx,
301
+ effects: EffectType[],
302
+ ): { state: ConnectionState; effects: EffectType[] } {
303
+ if (!state.userModes.oper) {
304
+ effects.push(notice(state, 'You must be an oper to use APPROVE.'));
305
+ return { state, effects };
306
+ }
307
+
308
+ const account = args[0];
309
+ if (account === undefined) {
310
+ effects.push(notice(state, 'Syntax: APPROVE <account>'));
311
+ return { state, effects };
312
+ }
313
+
314
+ const services = ctx.services as ServicesStore;
315
+ const vhost = services.approveVhost(account);
316
+ if (vhost === undefined) {
317
+ effects.push(notice(state, `No pending vhost request for ${account}.`));
318
+ return { state, effects };
319
+ }
320
+
321
+ effects.push(notice(state, `Vhost ${vhost} for ${account} has been approved and activated.`));
322
+ effects.push(
323
+ Effect.sendToNick(account, state.id, [
324
+ servicesRawNotice(
325
+ HOSTSERV_NICK,
326
+ account,
327
+ `Your vhost ${vhost} has been approved and activated. /msg HostServ ON to apply it.`,
328
+ ),
329
+ ]),
330
+ );
331
+ return { state, effects };
332
+ }
333
+
334
+ // ============================================================================
335
+ // REJECT (oper-only)
336
+ // ============================================================================
337
+
338
+ /**
339
+ * Handles `REJECT <account>`. Drops the pending vhost request without
340
+ * activating it, replies to the oper, and best-effort NOTICEs the
341
+ * requester via `SendToNick` (delivered when they are online, silently
342
+ * dropped otherwise).
343
+ */
344
+ function handleReject(
345
+ state: ConnectionState,
346
+ args: string[],
347
+ ctx: Ctx,
348
+ effects: EffectType[],
349
+ ): { state: ConnectionState; effects: EffectType[] } {
350
+ if (!state.userModes.oper) {
351
+ effects.push(notice(state, 'You must be an oper to use REJECT.'));
352
+ return { state, effects };
353
+ }
354
+
355
+ const account = args[0];
356
+ if (account === undefined) {
357
+ effects.push(notice(state, 'Syntax: REJECT <account>'));
358
+ return { state, effects };
359
+ }
360
+
361
+ const services = ctx.services as ServicesStore;
362
+ const vhost = services.rejectVhost(account);
363
+ if (vhost === undefined) {
364
+ effects.push(notice(state, `No pending vhost request for ${account}.`));
365
+ return { state, effects };
366
+ }
367
+
368
+ effects.push(notice(state, `Vhost request for ${account} has been rejected.`));
369
+ effects.push(
370
+ Effect.sendToNick(account, state.id, [
371
+ servicesRawNotice(
372
+ HOSTSERV_NICK,
373
+ account,
374
+ `Your vhost request for ${vhost} has been rejected.`,
375
+ ),
376
+ ]),
377
+ );
378
+ return { state, effects };
379
+ }
380
+
381
+ // ============================================================================
382
+ // LIST (oper-only)
383
+ // ============================================================================
384
+
385
+ /**
386
+ * Handles `LIST`. Enumerates every pending vhost request as a
387
+ * `<account>: <vhost>` NOTICE line, or a `No pending vhost requests.`
388
+ * NOTICE when the queue is empty.
389
+ */
390
+ function handleList(
391
+ state: ConnectionState,
392
+ ctx: Ctx,
393
+ effects: EffectType[],
394
+ ): { state: ConnectionState; effects: EffectType[] } {
395
+ if (!state.userModes.oper) {
396
+ effects.push(notice(state, 'You must be an oper to use LIST.'));
397
+ return { state, effects };
398
+ }
399
+
400
+ const services = ctx.services as ServicesStore;
401
+ const pending = services.listPendingVhosts();
402
+ if (pending.length === 0) {
403
+ effects.push(notice(state, 'No pending vhost requests.'));
404
+ return { state, effects };
405
+ }
406
+ for (const entry of pending) {
407
+ effects.push(notice(state, `${entry.account}: ${entry.vhost}`));
408
+ }
409
+ return { state, effects };
410
+ }
411
+
412
+ // ============================================================================
413
+ // vhost application helpers
414
+ // ============================================================================
415
+
416
+ /**
417
+ * Applies `vhost` to {@link ConnectionState.host}, records the prior host
418
+ * on {@link ConnectionState.preVhostHost}, flips
419
+ * {@link ConnectionState.vhostActive}, and appends the CHGHOST fanout for
420
+ * every channel the user is joined to. The CHGHOST emission reuses the
421
+ * {@link emitChghost} helper so the wire shape matches every other
422
+ * user/host mutation.
423
+ */
424
+ function applyVhost(state: ConnectionState, vhost: string, effects: EffectType[]): void {
425
+ const oldUser = state.user ?? '';
426
+ const oldHost = state.host ?? '';
427
+ state.preVhostHost = oldHost;
428
+ state.host = vhost;
429
+ state.vhostActive = true;
430
+ effects.push(...emitChghost({ conn: state, oldUser, oldHost, newUser: oldUser, newHost: vhost }));
431
+ }
432
+
433
+ /**
434
+ * Restores {@link ConnectionState.host} from
435
+ * {@link ConnectionState.preVhostHost}, clears the saved host, flips
436
+ * {@link ConnectionState.vhostActive} off, and appends the CHGHOST fanout.
437
+ */
438
+ function restoreRealHost(state: ConnectionState, effects: EffectType[]): void {
439
+ const oldUser = state.user ?? '';
440
+ const oldHost = state.host ?? '';
441
+ const realHost = state.preVhostHost ?? oldHost;
442
+ state.host = realHost;
443
+ state.vhostActive = false;
444
+ // biome-ignore lint/performance/noDelete: exactOptionalPropertyTypes forbids `= undefined`.
445
+ delete state.preVhostHost;
446
+ effects.push(
447
+ ...emitChghost({ conn: state, oldUser, oldHost, newUser: oldUser, newHost: realHost }),
448
+ );
449
+ }
450
+
451
+ // ============================================================================
452
+ // Notice helpers
453
+ // ============================================================================
454
+
455
+ /** Builds a `:HostServ!HostServ@services NOTICE <nick> :<text>` line. */
456
+ function notice(state: ConnectionState, text: string): EffectType {
457
+ return Effect.send(state.id, [servicesNotice(HOSTSERV_NICK, state, text)]);
458
+ }
459
+
460
+ /** Builds a help `:HostServ!HostServ@services NOTICE <nick> :…` line. */
461
+ function helpNotice(state: ConnectionState): EffectType {
462
+ return notice(state, 'Available commands: ON, OFF, REQUEST, SET, APPROVE, REJECT, LIST');
463
+ }
464
+
465
+ function unknownNotice(state: ConnectionState): EffectType {
466
+ return notice(state, 'Unknown command. Available: ON, OFF, REQUEST, SET, APPROVE, REJECT, LIST');
467
+ }
468
+
469
+ /**
470
+ * Formats a services NOTICE line. The source hostmask is
471
+ * `<svc>!<svc>@services` regardless of the deployment's serverName so the
472
+ * pseudo-clients are stable across deployments and easy to identify in
473
+ * client UIs.
474
+ */
475
+ function servicesNotice(svc: string, state: ConnectionState, text: string): RawLine {
476
+ const nick = state.nick ?? '*';
477
+ return { text: `:${svc}!${svc}@${SERVICES_HOST} NOTICE ${nick} :${text}` };
478
+ }
479
+
480
+ /**
481
+ * Formats a services NOTICE line addressed to `recipient` (rather than to
482
+ * the calling connection's nick). Used by APPROVE / REJECT to best-effort
483
+ * NOTICE the requester via `SendToNick` when they are online.
484
+ */
485
+ function servicesRawNotice(svc: string, recipient: string, text: string): RawLine {
486
+ return { text: `:${svc}!${svc}@${SERVICES_HOST} NOTICE ${recipient} :${text}` };
487
+ }
@@ -58,6 +58,7 @@ export {
58
58
  CHATHISTORY_CAP,
59
59
  } from './chathistory.js';
60
60
  export { tagmsgChannelReducer, tagmsgUserReducer } from './tagmsg.js';
61
+ export { markreadChannelReducer } from './markread.js';
61
62
  export {
62
63
  DEFAULT_MULTILINE_MAX_BYTES,
63
64
  MULTILINE_CAP,
@@ -98,3 +99,14 @@ export {
98
99
  persistAway,
99
100
  replayPersistedAway,
100
101
  } from './pre-away.js';
102
+ export { NICKSERV_NICK, nickservReducer } from './nickserv.js';
103
+ export { MEMOSERV_NICK, memoservReducer } from './memoserv.js';
104
+ export {
105
+ CHANSERV_NICK,
106
+ chanservReducer,
107
+ clearServicesModesDelta,
108
+ positiveMlockLetters,
109
+ setRegisteredDelta,
110
+ } from './chanserv.js';
111
+ export { HOSTSERV_NICK, hostservReducer } from './hostserv.js';
112
+ export { OPERSERV_NICK, operservReducer } from './operserv.js';