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
@@ -18,13 +18,14 @@
18
18
  import { caseFold } from '../case-fold.js';
19
19
  import { Effect } from '../effects.js';
20
20
  import type { Effect as EffectType, RawLine } from '../effects.js';
21
- import type { StoredMessage } from '../ports.js';
21
+ import type { PersistedTopic, StoredMessage } from '../ports.js';
22
22
  import { wrapBatch } from '../protocol/batch.js';
23
23
  import { Numerics } from '../protocol/numerics.js';
24
- import { type ChanName, type ChannelState, prefixOf } from '../state/channel.js';
24
+ import { type ChanName, type ChannelState, type ChannelTopic, prefixOf } from '../state/channel.js';
25
25
  import { hostmaskOf } from '../state/connection.js';
26
26
  import type { ConnectionState } from '../state/connection.js';
27
27
  import type { Ctx, Reducer } from '../types.js';
28
+ import { CHANSERV_HOSTMASK, DEFAULT_CHANNEL_LEVELS } from './chanserv.js';
28
29
  import { CHATHISTORY_CAP, buildChathistoryBatch } from './chathistory.js';
29
30
 
30
31
  /**
@@ -226,6 +227,21 @@ export const joinReducer: Reducer<ChannelState> = (state, msg, ctx) => {
226
227
  return { state, effects };
227
228
  }
228
229
 
230
+ // ChanServ +R check: unidentified clients may not join a +R channel.
231
+ if (state.modes.blockUnidentified && ctx.connection.account === undefined) {
232
+ effects.push(
233
+ Effect.send(ctx.connId, [
234
+ numericErr(
235
+ ctx,
236
+ Numerics.ERR_ALLMUSTREGISTER,
237
+ 'You must register with NickServ to join this channel',
238
+ rawName,
239
+ ),
240
+ ]),
241
+ );
242
+ return { state, effects };
243
+ }
244
+
229
245
  // Limit check.
230
246
  if (state.modes.limit !== undefined && state.members.size >= state.modes.limit) {
231
247
  effects.push(
@@ -250,14 +266,38 @@ export const joinReducer: Reducer<ChannelState> = (state, msg, ctx) => {
250
266
  }
251
267
 
252
268
  // All checks passed: add to roster. First joiner gets op per RFC 2812.
253
- const isFirst = state.members.size === 0;
269
+ const wasEmpty = state.members.size === 0;
270
+ // ChanServ ACCESS auto-prefix: when services are bound, the channel is
271
+ // registered, and this is not the RFC 2812 first-joiner case, look up
272
+ // the joiner's access level and apply the matching channel prefix.
273
+ // `chanservPrefix` is non-null only when ChanServ (not the RFC 2812
274
+ // rule) granted the prefix, so the JOIN hook knows to emit a ChanServ
275
+ // MODE broadcast alongside the roster mutation.
276
+ const autoPrefix = wasEmpty ? null : computeChanservAutoPrefix(ctx, state.nameLower);
277
+ const grantOp = wasEmpty || (autoPrefix?.op ?? false);
278
+ const grantVoice = autoPrefix?.voice ?? false;
279
+ const joinerNick = ctx.connection.nick ?? '?';
254
280
  state.members.set(ctx.connId, {
255
281
  conn: ctx.connId,
256
- nick: ctx.connection.nick ?? '?',
257
- op: isFirst,
258
- voice: false,
282
+ nick: joinerNick,
283
+ op: grantOp,
284
+ voice: grantVoice,
259
285
  });
260
286
 
287
+ // ChanServ KEEPTOPIC restore: when the channel was empty immediately
288
+ // before this join and services have a persisted topic snapshot for a
289
+ // keep-topic-enabled registration, restore the topic onto the
290
+ // authoritative ChannelState. The 332/333 numerics are emitted to the
291
+ // joiner below alongside the NAMES list.
292
+ let restoredTopic: ChannelTopic | undefined;
293
+ if (wasEmpty) {
294
+ restoredTopic = restoreKeepTopic(ctx, state.nameLower);
295
+ if (restoredTopic !== undefined) {
296
+ state.topic = restoredTopic;
297
+ effects.push(Effect.applyChannelDelta(state.name, { topic: restoredTopic }));
298
+ }
299
+ }
300
+
261
301
  // Pending invite is consumed on a successful join.
262
302
  if (myNick !== undefined) state.pendingInvites.delete(caseFold('rfc1459', myNick));
263
303
 
@@ -275,13 +315,24 @@ export const joinReducer: Reducer<ChannelState> = (state, msg, ctx) => {
275
315
  {
276
316
  type: 'add',
277
317
  conn: ctx.connId,
278
- nick: ctx.connection.nick ?? '?',
279
- op: isFirst,
318
+ nick: joinerNick,
319
+ op: grantOp,
320
+ voice: grantVoice,
280
321
  },
281
322
  ],
282
323
  }),
283
324
  );
284
325
 
326
+ // ChanServ auto-prefix broadcast: when ChanServ granted op or voice
327
+ // (i.e. NOT the RFC 2812 first-joiner case), emit a
328
+ // `:ChanServ!ChanServ@services MODE #chan +o nick` broadcast so every
329
+ // client in the channel updates its prefix display. Supersedes the
330
+ // implicit voice when op is granted (the channel-prefix grammar
331
+ // already renders `@` for ops).
332
+ if (autoPrefix !== null && (autoPrefix.op || autoPrefix.voice)) {
333
+ effects.push(buildChanservPrefixBroadcast(state.name, joinerNick, autoPrefix));
334
+ }
335
+
285
336
  // IRCv3 draft/chathistory auto-playback: a cap-enabled joiner with a
286
337
  // bound MessageStore receives the recent backlog as a BATCH *before*
287
338
  // the JOIN broadcast, then the per-(connection, channel) "last read"
@@ -328,6 +379,20 @@ export const joinReducer: Reducer<ChannelState> = (state, msg, ctx) => {
328
379
 
329
380
  effects.push(Effect.send(ctx.connId, lines));
330
381
 
382
+ // When ChanServ KEEPTOPIC restored a topic this JOIN, emit the 332/333
383
+ // topic numerics to the joiner so their client renders the restored
384
+ // topic (mirroring the TOPIC read-form reply shape).
385
+ if (restoredTopic !== undefined) {
386
+ effects.push(
387
+ Effect.send(ctx.connId, [
388
+ { text: `:${ctx.serverName} 332 ${nick} ${chanName} :${restoredTopic.text}` },
389
+ {
390
+ text: `:${ctx.serverName} 333 ${nick} ${chanName} ${restoredTopic.setter} ${restoredTopic.setAt}`,
391
+ },
392
+ ]),
393
+ );
394
+ }
395
+
331
396
  return { state, effects };
332
397
  };
333
398
 
@@ -360,3 +425,94 @@ export function handleJoinZero(ctx: Ctx): EffectType[] {
360
425
  ctx.connection.joinedChannels.clear();
361
426
  return effects;
362
427
  }
428
+
429
+ // ============================================================================
430
+ // ChanServ KEEPTOPIC restore
431
+ // ============================================================================
432
+
433
+ /**
434
+ * Returns the persisted topic snapshot to restore on a JOIN that re-creates
435
+ * an empty channel, or `undefined` when:
436
+ * - no services store is bound,
437
+ * - the channel is not registered,
438
+ * - KEEPTOPIC is off, or
439
+ * - no topic snapshot is stored.
440
+ *
441
+ * Reads the per-channel lowercased name from the joiner's `nameLower`
442
+ * field; services-store keys are rfc1459-folded so the lookup is
443
+ * case-insensitive.
444
+ */
445
+ function restoreKeepTopic(ctx: Ctx, nameLower: string): ChannelTopic | undefined {
446
+ const services = ctx.services;
447
+ if (services === undefined) return undefined;
448
+ const rec = services.getChannel(nameLower);
449
+ if (rec === undefined || !rec.keepTopic) return undefined;
450
+ const persisted: PersistedTopic | undefined = services.getChannelTopic(nameLower);
451
+ if (persisted === undefined) return undefined;
452
+ return { text: persisted.text, setter: persisted.setter, setAt: persisted.setAt };
453
+ }
454
+
455
+ // ============================================================================
456
+ // ChanServ ACCESS auto-prefix
457
+ // ============================================================================
458
+
459
+ /**
460
+ * Decides whether ChanServ should auto-grant a channel prefix to the
461
+ * joining connection on a non-empty registered channel.
462
+ *
463
+ * Returns `{ op: true, voice: false }` for the channel founder or any
464
+ * account whose access level meets the founder-configured `AUTOOP`
465
+ * threshold; `{ op: false, voice: true }` for accounts that meet the
466
+ * `AUTOVOICE` threshold; `{ op: false, voice: false }` otherwise.
467
+ *
468
+ * Returns `null` when no services store is bound, the channel is not
469
+ * registered, or the joiner is unidentified — the caller treats all of
470
+ * those as "no ChanServ-derived prefix" and falls back to the RFC 2812
471
+ * first-joiner rule or the plain-roster defaults.
472
+ *
473
+ * Threshold lookup consults the founder-redefined overrides via
474
+ * {@link ServicesStore.getChannelLevel} and falls back to
475
+ * {@link DEFAULT_CHANNEL_LEVELS} for ops the founder has not overridden.
476
+ */
477
+ function computeChanservAutoPrefix(
478
+ ctx: Ctx,
479
+ nameLower: string,
480
+ ): { op: boolean; voice: boolean } | null {
481
+ const services = ctx.services;
482
+ if (services === undefined) return null;
483
+ const rec = services.getChannel(nameLower);
484
+ if (rec === undefined) return null;
485
+ const account = ctx.connection.account;
486
+ if (account === undefined) return null;
487
+ if (caseFold('rfc1459', rec.founder) === caseFold('rfc1459', account)) {
488
+ return { op: true, voice: false };
489
+ }
490
+ const level = services.getChannelAccess(nameLower, account);
491
+ const autoOp = services.getChannelLevel(nameLower, 'AUTOOP') ?? DEFAULT_CHANNEL_LEVELS.AUTOOP;
492
+ const autoVoice =
493
+ services.getChannelLevel(nameLower, 'AUTOVOICE') ?? DEFAULT_CHANNEL_LEVELS.AUTOVOICE;
494
+ if (level >= autoOp) return { op: true, voice: false };
495
+ if (level >= autoVoice) return { op: false, voice: true };
496
+ return { op: false, voice: false };
497
+ }
498
+
499
+ /**
500
+ * Builds the `Broadcast` effect for a ChanServ-emitted channel prefix
501
+ * grant. The line shape mirrors the MODE reducer's ChanServ-MLOCK
502
+ * reassert broadcast so existing clients already special-case
503
+ * `:ChanServ!ChanServ@services MODE …` correctly.
504
+ *
505
+ * Op grants emit `+o` only (the channel-prefix grammar renders `@` for
506
+ * ops, so a redundant `+v` would only confuse client UIs that show both
507
+ * prefixes side-by-side).
508
+ */
509
+ function buildChanservPrefixBroadcast(
510
+ chanName: ChanName,
511
+ nick: string,
512
+ prefix: { op: boolean; voice: boolean },
513
+ ): EffectType {
514
+ const mode = prefix.op ? '+o' : '+v';
515
+ return Effect.broadcast(chanName, [
516
+ { text: `:${CHANSERV_HOSTMASK} MODE ${chanName} ${mode} ${nick}` },
517
+ ]);
518
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Pure reducer for the IRCv3 `draft/read-marker` `MARKREAD` command.
3
+ *
4
+ * Spec (work-in-progress): https://ircv3.net/specs/extensions/read-marker
5
+ *
6
+ * The serverless-ircd deployment stores read markers **by msgid** (see
7
+ * {@link ../commands/read-marker.ts}) so they compose with the chathistory
8
+ * last-read plumbing. The draft `MARKREAD` verb, by contrast, is
9
+ * timestamp-based: a client sends `MARKREAD <target> <timestamp>` where the
10
+ * timestamp is the `time` tag of the last read message. This reducer is the
11
+ * translation layer — it resolves the client timestamp to the most recent
12
+ * retained message at or before it ({@link MessageStore.findAtOrBefore}),
13
+ * advances the msgid-based marker via {@link persistReadMarker}, and replies
14
+ * with the resolved message's timestamp so the client observes a value that
15
+ * corresponds to a real `time` tag.
16
+ *
17
+ * Wire forms handled:
18
+ * - `MARKREAD <target>` (GET) — replies with the stored marker's timestamp
19
+ * (or `*` when none / unresolvable).
20
+ * - `MARKREAD <target> <timestamp>` (SET) — advances the marker subject to
21
+ * monotonicity (the marker never moves backwards) and replies with the
22
+ * resolved timestamp, or with the stored value when the client timestamp
23
+ * is older-or-equal.
24
+ *
25
+ * Errors use the IRCv3 `standard-replies` `FAIL` form (the draft specifies no
26
+ * legacy numerics for `MARKREAD`):
27
+ * - `FAIL MARKREAD NEED_MORE_PARAMS :Missing parameters`
28
+ * - `FAIL MARKREAD INVALID_PARAMS :Invalid parameters`
29
+ *
30
+ * The command is gated behind the `draft/read-marker` capability: a client
31
+ * that did not negotiate it sees `421 ERR_UNKNOWNCOMMAND` (the spec's
32
+ * permitted "unknown command" fallback, matching the `TAGMSG` precedent).
33
+ *
34
+ * Membership mirrors the `TAGMSG` read-marker path: a non-member on a `+n`
35
+ * channel is ignored silently (the user has no marker to set or get).
36
+ */
37
+
38
+ import { Effect } from '../effects.js';
39
+ import type { RawLine } from '../effects.js';
40
+ import { Numerics } from '../protocol/numerics.js';
41
+ import { formatServerTime } from '../protocol/outbound.js';
42
+ import type { ChannelState } from '../state/channel.js';
43
+ import type { Ctx, Reducer } from '../types.js';
44
+ import { READ_MARKER_CAP, persistReadMarker } from './read-marker.js';
45
+
46
+ /** Prefix that introduces the server-time timestamp in a SET param. */
47
+ const TIMESTAMP_PREFIX = 'timestamp=';
48
+
49
+ /** Literal timestamp meaning "no known read marker" (server replies only). */
50
+ const NO_MARKER = '*';
51
+
52
+ /** Formats a numeric error line addressed to the connection's nick (or `*`). */
53
+ function numericErr(ctx: Ctx, code: number, trailing: string, middle: string): RawLine {
54
+ const nick = ctx.connection.nick ?? '*';
55
+ const codeStr = code.toString().padStart(3, '0');
56
+ return { text: [`:${ctx.serverName}`, codeStr, nick, middle, `:${trailing}`].join(' ') };
57
+ }
58
+
59
+ /** Formats a `FAIL MARKREAD <code> :<desc>` standard-reply line. */
60
+ function failReply(ctx: Ctx, code: string, desc: string): RawLine {
61
+ return { text: `:${ctx.serverName} FAIL MARKREAD ${code} :${desc}` };
62
+ }
63
+
64
+ /** Formats the `:<server> MARKREAD <target> <timestamp>` server reply line. */
65
+ function markreadReply(ctx: Ctx, target: string, timestamp: string): RawLine {
66
+ return { text: `:${ctx.serverName} MARKREAD ${target} ${timestamp}` };
67
+ }
68
+
69
+ /**
70
+ * Formats `ms` as the wire timestamp token carried by every server
71
+ * `MARKREAD` reply: `timestamp=<server-time>` (matching the client SET form
72
+ * and the spec examples). `NO_MARKER` (`*`) is returned verbatim by callers
73
+ * and never passes through here.
74
+ */
75
+ function timestampTag(ms: number): string {
76
+ return `${TIMESTAMP_PREFIX}${formatServerTime(ms)}`;
77
+ }
78
+
79
+ /**
80
+ * Parses a SET timestamp param (`timestamp=<iso>`) into epoch ms, or
81
+ * `undefined` when the param is malformed (missing prefix or an unparseable
82
+ * date). The literal `*` is rejected upstream (it is invalid for SET) before
83
+ * this helper runs.
84
+ */
85
+ function parseTimestampParam(raw: string): number | undefined {
86
+ if (!raw.startsWith(TIMESTAMP_PREFIX)) return undefined;
87
+ const value = raw.slice(TIMESTAMP_PREFIX.length);
88
+ if (value.length === 0) return undefined;
89
+ const ms = Date.parse(value);
90
+ return Number.isNaN(ms) ? undefined : ms;
91
+ }
92
+
93
+ /**
94
+ * Resolves the connection's current marker for `chanLower` to its message's
95
+ * epoch-ms time, or `undefined` when there is no marker, no bound message
96
+ * store, or the marker's message is no longer retained. The epoch-ms form
97
+ * drives the monotonicity comparison; {@link markerTimestampTag} formats the
98
+ * wire form for replies.
99
+ */
100
+ function markerTime(
101
+ ctx: Ctx,
102
+ chanLower: string,
103
+ messages: MessageStoreLike | undefined,
104
+ ): number | undefined {
105
+ const msgid = ctx.connection.lastReadMarkers?.get(chanLower);
106
+ if (msgid === undefined || messages === undefined) return undefined;
107
+ return messages.getMsg(chanLower, msgid)?.time;
108
+ }
109
+
110
+ interface MessageStoreLike {
111
+ getMsg(chan: string, msgid: string): { time: number } | undefined;
112
+ findAtOrBefore(chan: string, ts: number): { msgid: string; time: number } | undefined;
113
+ }
114
+
115
+ /** Reducer for `MARKREAD <channel>` (GET) and `MARKREAD <channel> <ts>` (SET). */
116
+ export const markreadChannelReducer: Reducer<ChannelState> = (state, msg, ctx) => {
117
+ ctx.connection.lastSeen = ctx.clock.now();
118
+
119
+ // Cap gate: clients that did not negotiate draft/read-marker see RFC 2812
120
+ // only (the spec's permitted "unknown command" fallback).
121
+ if (!ctx.connection.caps.has(READ_MARKER_CAP)) {
122
+ return {
123
+ state,
124
+ effects: [
125
+ Effect.send(ctx.connId, [
126
+ numericErr(ctx, Numerics.ERR_UNKNOWNCOMMAND, 'Unknown command', 'MARKREAD'),
127
+ ]),
128
+ ],
129
+ };
130
+ }
131
+
132
+ const target = msg.params[0];
133
+ if (target === undefined || target.length === 0) {
134
+ return {
135
+ state,
136
+ effects: [
137
+ Effect.send(ctx.connId, [failReply(ctx, 'NEED_MORE_PARAMS', 'Missing parameters')]),
138
+ ],
139
+ };
140
+ }
141
+
142
+ // Membership: a non-member on a +n channel has no marker to set or get.
143
+ // Silent, mirroring the TAGMSG read-marker path.
144
+ const senderEntry = state.members.get(ctx.connId);
145
+ if (state.modes.noExternal && senderEntry === undefined) {
146
+ return { state, effects: [] };
147
+ }
148
+
149
+ const messages = ctx.messages as MessageStoreLike | undefined;
150
+
151
+ // GET command: reply with the stored marker timestamp (or *).
152
+ if (msg.params.length < 2) {
153
+ const stored = markerTime(ctx, state.nameLower, messages);
154
+ const tag = stored === undefined ? NO_MARKER : timestampTag(stored);
155
+ return { state, effects: [Effect.send(ctx.connId, [markreadReply(ctx, target, tag)])] };
156
+ }
157
+
158
+ // SET command.
159
+ const raw = msg.params[1] as string;
160
+ if (raw === NO_MARKER) {
161
+ // The literal * is reserved for server replies; a client SET MUST NOT use it.
162
+ return {
163
+ state,
164
+ effects: [Effect.send(ctx.connId, [failReply(ctx, 'INVALID_PARAMS', 'Invalid parameters')])],
165
+ };
166
+ }
167
+ const clientMs = parseTimestampParam(raw);
168
+ if (clientMs === undefined) {
169
+ return {
170
+ state,
171
+ effects: [Effect.send(ctx.connId, [failReply(ctx, 'INVALID_PARAMS', 'Invalid parameters')])],
172
+ };
173
+ }
174
+
175
+ // Translate the client timestamp to the most recent retained message at or
176
+ // before it. Without a bound store, or when nothing matches (e.g. the
177
+ // timestamp predates every retained message), there is no msgid to advance
178
+ // to: reply with the current marker and do not mutate state.
179
+ const resolved = messages?.findAtOrBefore(state.nameLower, clientMs);
180
+ if (resolved === undefined) {
181
+ const stored = markerTime(ctx, state.nameLower, messages);
182
+ const tag = stored === undefined ? NO_MARKER : timestampTag(stored);
183
+ return { state, effects: [Effect.send(ctx.connId, [markreadReply(ctx, target, tag)])] };
184
+ }
185
+
186
+ // Monotonicity: the marker only ever advances. A client timestamp that
187
+ // resolves to a message at or before the stored marker is rejected — the
188
+ // server replies with the stored (newer) value and ignores the client value.
189
+ const storedMs = markerTime(ctx, state.nameLower, messages);
190
+ if (storedMs !== undefined && resolved.time <= storedMs) {
191
+ return {
192
+ state,
193
+ effects: [Effect.send(ctx.connId, [markreadReply(ctx, target, timestampTag(storedMs))])],
194
+ };
195
+ }
196
+
197
+ persistReadMarker(ctx, state.nameLower, resolved.msgid);
198
+ return {
199
+ state,
200
+ effects: [Effect.send(ctx.connId, [markreadReply(ctx, target, timestampTag(resolved.time))])],
201
+ };
202
+ };