serverless-ircd 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (394) hide show
  1. package/.github/workflows/ci.yml +40 -3
  2. package/.github/workflows/deploy-aws.yml +1 -2
  3. package/.github/workflows/deploy-cf-tcp.yml +1 -5
  4. package/.github/workflows/deploy-cf.yml +1 -2
  5. package/.gitmodules +3 -0
  6. package/CHANGELOG.md +474 -0
  7. package/README.md +219 -22
  8. package/apps/aws-stack/README.md +33 -0
  9. package/apps/aws-stack/package.json +1 -1
  10. package/apps/aws-stack/src/aws-stack.ts +74 -9
  11. package/apps/aws-stack/tests/coverage-command.test.ts +155 -0
  12. package/apps/aws-stack/tests/stack.test.ts +2 -2
  13. package/apps/aws-stack/tests/synth-no-bundle.test.ts +129 -0
  14. package/apps/aws-stack/tests/vitest.setup.ts +18 -0
  15. package/apps/aws-stack/vitest.config.ts +3 -0
  16. package/apps/cf-tcp-container/package.json +1 -1
  17. package/apps/cf-tcp-container/src/config-loader.ts +6 -7
  18. package/apps/cf-tcp-container/src/container-server.ts +3 -0
  19. package/apps/cf-tcp-container/tests/config-loader.test.ts +10 -3
  20. package/apps/cf-tcp-container/wrangler.toml +1 -1
  21. package/apps/cf-worker/package.json +2 -1
  22. package/apps/cf-worker/src/origin-allowlist.ts +99 -0
  23. package/apps/cf-worker/src/worker.ts +89 -9
  24. package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +18 -0
  25. package/apps/cf-worker/tests/fixtures/web-dist/index.html +18 -0
  26. package/apps/cf-worker/tests/origin-allowlist.test.ts +101 -0
  27. package/apps/cf-worker/tests/smoke.test.ts +28 -2
  28. package/apps/cf-worker/tests/ws-origin.test.ts +89 -0
  29. package/apps/cf-worker/vitest.config.ts +1 -1
  30. package/apps/cf-worker/wrangler.test.toml +23 -0
  31. package/apps/cf-worker/wrangler.toml +59 -3
  32. package/apps/local-cli/package.json +1 -1
  33. package/apps/local-cli/src/config-loader.ts +6 -6
  34. package/apps/local-cli/src/main.ts +20 -0
  35. package/apps/local-cli/src/server.ts +9 -7
  36. package/apps/local-cli/tests/e2e.test.ts +112 -28
  37. package/apps/local-cli/tests/tcp.test.ts +71 -19
  38. package/apps/local-cli/tests/ws-subprotocol.test.ts +12 -7
  39. package/apps/web/landing/index.html +217 -0
  40. package/apps/web/package.json +28 -0
  41. package/apps/web/scripts/build.mjs +125 -0
  42. package/apps/web/src/build-env.ts +43 -0
  43. package/apps/web/src/config-schema.ts +138 -0
  44. package/apps/web/static/config.json +28 -0
  45. package/apps/web/static/config.prod.json +28 -0
  46. package/apps/web/static/config.staging.json +28 -0
  47. package/apps/web/tests/build-env.test.ts +63 -0
  48. package/apps/web/tests/build-smoke.test.ts +103 -0
  49. package/apps/web/tests/config-schema.test.ts +432 -0
  50. package/apps/web/tests/workspace.test.ts +12 -0
  51. package/apps/web/tsconfig.json +10 -0
  52. package/apps/web/upstream/.browserslistrc +3 -0
  53. package/apps/web/upstream/.editorconfig +9 -0
  54. package/apps/web/upstream/.eslintignore +3 -0
  55. package/apps/web/upstream/.eslintrc.js +90 -0
  56. package/apps/web/upstream/.github/FUNDING.yml +12 -0
  57. package/apps/web/upstream/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  58. package/apps/web/upstream/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  59. package/apps/web/upstream/.github/workflows/push.yml +26 -0
  60. package/apps/web/upstream/.github/workflows/release.yml +33 -0
  61. package/apps/web/upstream/.github/workflows/release_manual.yml +55 -0
  62. package/apps/web/upstream/.prettierrc.js +9 -0
  63. package/apps/web/upstream/.stylelintignore +5 -0
  64. package/apps/web/upstream/.stylelintrc.js +21 -0
  65. package/apps/web/upstream/LICENSE +201 -0
  66. package/apps/web/upstream/README.md +76 -0
  67. package/apps/web/upstream/babel.config.js +25 -0
  68. package/apps/web/upstream/crowdin.yml +3 -0
  69. package/apps/web/upstream/index.html +35 -0
  70. package/apps/web/upstream/jest.config.js +7 -0
  71. package/apps/web/upstream/jsdoc.json +22 -0
  72. package/apps/web/upstream/package.json +80 -0
  73. package/apps/web/upstream/scripts/pre-commit +43 -0
  74. package/apps/web/upstream/src/components/App.vue +516 -0
  75. package/apps/web/upstream/src/components/AppSettings.vue +587 -0
  76. package/apps/web/upstream/src/components/AutoComplete.vue +284 -0
  77. package/apps/web/upstream/src/components/Avatar.vue +105 -0
  78. package/apps/web/upstream/src/components/AwayStatusIndicator.vue +77 -0
  79. package/apps/web/upstream/src/components/BufferKey.vue +121 -0
  80. package/apps/web/upstream/src/components/BufferSettings.vue +97 -0
  81. package/apps/web/upstream/src/components/Captcha.vue +85 -0
  82. package/apps/web/upstream/src/components/ChannelBanlist.vue +96 -0
  83. package/apps/web/upstream/src/components/ChannelInfo.vue +159 -0
  84. package/apps/web/upstream/src/components/ChannelInvitelist.vue +276 -0
  85. package/apps/web/upstream/src/components/ChannelList.vue +345 -0
  86. package/apps/web/upstream/src/components/Container.vue +401 -0
  87. package/apps/web/upstream/src/components/ContainerHeader.vue +522 -0
  88. package/apps/web/upstream/src/components/ControlInput.vue +1016 -0
  89. package/apps/web/upstream/src/components/LoadingAnimation.vue +104 -0
  90. package/apps/web/upstream/src/components/MediaViewer.vue +199 -0
  91. package/apps/web/upstream/src/components/MessageInfo.vue +192 -0
  92. package/apps/web/upstream/src/components/MessageList.vue +1028 -0
  93. package/apps/web/upstream/src/components/MessageListAvatar.vue +20 -0
  94. package/apps/web/upstream/src/components/MessageListMessageCompact.vue +361 -0
  95. package/apps/web/upstream/src/components/MessageListMessageInline.vue +257 -0
  96. package/apps/web/upstream/src/components/MessageListMessageModern.vue +430 -0
  97. package/apps/web/upstream/src/components/NetworkSettings.vue +528 -0
  98. package/apps/web/upstream/src/components/Nicklist.vue +309 -0
  99. package/apps/web/upstream/src/components/NicklistUser.vue +181 -0
  100. package/apps/web/upstream/src/components/NotConnected.vue +271 -0
  101. package/apps/web/upstream/src/components/SelfUser.vue +283 -0
  102. package/apps/web/upstream/src/components/ServerSelector.vue +162 -0
  103. package/apps/web/upstream/src/components/ServerView.vue +126 -0
  104. package/apps/web/upstream/src/components/SettingsAdvanced.vue +232 -0
  105. package/apps/web/upstream/src/components/SettingsAliases.vue +121 -0
  106. package/apps/web/upstream/src/components/Sidebar.vue +429 -0
  107. package/apps/web/upstream/src/components/SidebarAboutBuffer.vue +288 -0
  108. package/apps/web/upstream/src/components/SidebarState.vue +145 -0
  109. package/apps/web/upstream/src/components/StartupError.vue +35 -0
  110. package/apps/web/upstream/src/components/StateBrowser.vue +420 -0
  111. package/apps/web/upstream/src/components/StateBrowserBuffer.vue +124 -0
  112. package/apps/web/upstream/src/components/StateBrowserNetwork.vue +823 -0
  113. package/apps/web/upstream/src/components/StateBrowserUsermenu.vue +162 -0
  114. package/apps/web/upstream/src/components/TypingStatusIndicator.vue +34 -0
  115. package/apps/web/upstream/src/components/TypingUsersList.vue +56 -0
  116. package/apps/web/upstream/src/components/UrlEmbed.vue +149 -0
  117. package/apps/web/upstream/src/components/UserBox.vue +728 -0
  118. package/apps/web/upstream/src/components/inputtools/Emoji.vue +67 -0
  119. package/apps/web/upstream/src/components/inputtools/TextStyle.vue +142 -0
  120. package/apps/web/upstream/src/components/startups/CommonLayout.vue +208 -0
  121. package/apps/web/upstream/src/components/startups/CustomServer.vue +495 -0
  122. package/apps/web/upstream/src/components/startups/KiwiBnc.vue +361 -0
  123. package/apps/web/upstream/src/components/startups/Personal.vue +317 -0
  124. package/apps/web/upstream/src/components/startups/Welcome.vue +544 -0
  125. package/apps/web/upstream/src/components/startups/ZncLogin.vue +229 -0
  126. package/apps/web/upstream/src/components/utils/InputConfirm.vue +68 -0
  127. package/apps/web/upstream/src/components/utils/InputPrompt.vue +118 -0
  128. package/apps/web/upstream/src/components/utils/InputText.vue +172 -0
  129. package/apps/web/upstream/src/components/utils/IrcInput.vue +579 -0
  130. package/apps/web/upstream/src/components/utils/PluginWrapper.vue +26 -0
  131. package/apps/web/upstream/src/components/utils/TabbedView.vue +149 -0
  132. package/apps/web/upstream/src/components/utils/TransitionExpand.vue +97 -0
  133. package/apps/web/upstream/src/helpers/Colours.js +128 -0
  134. package/apps/web/upstream/src/helpers/IrcdDiffs.js +26 -0
  135. package/apps/web/upstream/src/helpers/Md5.js +193 -0
  136. package/apps/web/upstream/src/helpers/Misc.js +401 -0
  137. package/apps/web/upstream/src/helpers/TextFormatting.js +237 -0
  138. package/apps/web/upstream/src/libs/AliasRewriter.js +157 -0
  139. package/apps/web/upstream/src/libs/AudioManager.js +60 -0
  140. package/apps/web/upstream/src/libs/BouncerMiddleware.js +247 -0
  141. package/apps/web/upstream/src/libs/BouncerProvider.js +551 -0
  142. package/apps/web/upstream/src/libs/ChathistoryMiddleware.js +153 -0
  143. package/apps/web/upstream/src/libs/ConfigLoader.js +94 -0
  144. package/apps/web/upstream/src/libs/EmojiProvider.js +46 -0
  145. package/apps/web/upstream/src/libs/GlobalApi.js +346 -0
  146. package/apps/web/upstream/src/libs/IPC.js +51 -0
  147. package/apps/web/upstream/src/libs/InputHandler.js +921 -0
  148. package/apps/web/upstream/src/libs/IrcClient.js +1506 -0
  149. package/apps/web/upstream/src/libs/Logger.js +71 -0
  150. package/apps/web/upstream/src/libs/Message.js +169 -0
  151. package/apps/web/upstream/src/libs/MessageFormatter.js +364 -0
  152. package/apps/web/upstream/src/libs/MessageParser.js +249 -0
  153. package/apps/web/upstream/src/libs/Notifications.js +78 -0
  154. package/apps/web/upstream/src/libs/ServerConnection.js +270 -0
  155. package/apps/web/upstream/src/libs/ServerSession.js +102 -0
  156. package/apps/web/upstream/src/libs/SoundBleep.js +20 -0
  157. package/apps/web/upstream/src/libs/StatePersistence.js +90 -0
  158. package/apps/web/upstream/src/libs/ThemeManager.js +128 -0
  159. package/apps/web/upstream/src/libs/TypingMiddleware.js +105 -0
  160. package/apps/web/upstream/src/libs/WindowTitle.js +63 -0
  161. package/apps/web/upstream/src/libs/batchedAdd.js +74 -0
  162. package/apps/web/upstream/src/libs/bufferTools.js +177 -0
  163. package/apps/web/upstream/src/libs/polyfill/Element.closest.js +18 -0
  164. package/apps/web/upstream/src/libs/renderers/Html.js +113 -0
  165. package/apps/web/upstream/src/libs/settingTools.js +31 -0
  166. package/apps/web/upstream/src/libs/state/BufferState.js +703 -0
  167. package/apps/web/upstream/src/libs/state/NetworkState.js +158 -0
  168. package/apps/web/upstream/src/libs/state/UserState.js +89 -0
  169. package/apps/web/upstream/src/libs/state/common.js +26 -0
  170. package/apps/web/upstream/src/libs/state.js +961 -0
  171. package/apps/web/upstream/src/libs/storage/Local.js +51 -0
  172. package/apps/web/upstream/src/main.js +566 -0
  173. package/apps/web/upstream/src/res/autocompleteCommands.js +31 -0
  174. package/apps/web/upstream/src/res/configTemplates.js +373 -0
  175. package/apps/web/upstream/src/res/globalStyle.css +277 -0
  176. package/apps/web/upstream/src/res/kiwiLoadingLogo.png +0 -0
  177. package/apps/web/upstream/src/res/locales/app.af-ZA.po +1145 -0
  178. package/apps/web/upstream/src/res/locales/app.ar-SA.po +1145 -0
  179. package/apps/web/upstream/src/res/locales/app.bg-BG.po +1145 -0
  180. package/apps/web/upstream/src/res/locales/app.bs-BA.po +1145 -0
  181. package/apps/web/upstream/src/res/locales/app.ca-ES.po +1145 -0
  182. package/apps/web/upstream/src/res/locales/app.cs-CZ.po +1145 -0
  183. package/apps/web/upstream/src/res/locales/app.da-DK.po +1145 -0
  184. package/apps/web/upstream/src/res/locales/app.de-DE.po +1145 -0
  185. package/apps/web/upstream/src/res/locales/app.dev.po +1246 -0
  186. package/apps/web/upstream/src/res/locales/app.el-GR.po +1145 -0
  187. package/apps/web/upstream/src/res/locales/app.en-US.po +1145 -0
  188. package/apps/web/upstream/src/res/locales/app.es-419.po +1145 -0
  189. package/apps/web/upstream/src/res/locales/app.es-AR.po +1145 -0
  190. package/apps/web/upstream/src/res/locales/app.es-EM.po +536 -0
  191. package/apps/web/upstream/src/res/locales/app.es-ES.po +1145 -0
  192. package/apps/web/upstream/src/res/locales/app.es-US.po +1145 -0
  193. package/apps/web/upstream/src/res/locales/app.eu-ES.po +1145 -0
  194. package/apps/web/upstream/src/res/locales/app.fi-FI.po +1145 -0
  195. package/apps/web/upstream/src/res/locales/app.fr-FR.po +1145 -0
  196. package/apps/web/upstream/src/res/locales/app.gl-ES.po +1145 -0
  197. package/apps/web/upstream/src/res/locales/app.he-IL.po +1145 -0
  198. package/apps/web/upstream/src/res/locales/app.hi-IN.po +1145 -0
  199. package/apps/web/upstream/src/res/locales/app.hu-HU.po +1145 -0
  200. package/apps/web/upstream/src/res/locales/app.id-ID.po +1145 -0
  201. package/apps/web/upstream/src/res/locales/app.it-IT.po +1145 -0
  202. package/apps/web/upstream/src/res/locales/app.ja-JP.po +1145 -0
  203. package/apps/web/upstream/src/res/locales/app.ko-KR.po +1145 -0
  204. package/apps/web/upstream/src/res/locales/app.nl-NL.po +1145 -0
  205. package/apps/web/upstream/src/res/locales/app.no-NO.po +1145 -0
  206. package/apps/web/upstream/src/res/locales/app.pl-PL.po +1145 -0
  207. package/apps/web/upstream/src/res/locales/app.pt-BR.po +1145 -0
  208. package/apps/web/upstream/src/res/locales/app.pt-PT.po +1145 -0
  209. package/apps/web/upstream/src/res/locales/app.ro-RO.po +1145 -0
  210. package/apps/web/upstream/src/res/locales/app.ru-RU.po +1145 -0
  211. package/apps/web/upstream/src/res/locales/app.sl-SI.po +1145 -0
  212. package/apps/web/upstream/src/res/locales/app.sq-AL.po +1145 -0
  213. package/apps/web/upstream/src/res/locales/app.sr-SP.po +1145 -0
  214. package/apps/web/upstream/src/res/locales/app.sv-SE.po +1145 -0
  215. package/apps/web/upstream/src/res/locales/app.tr-TR.po +1145 -0
  216. package/apps/web/upstream/src/res/locales/app.uk-UA.po +1145 -0
  217. package/apps/web/upstream/src/res/locales/app.vi-VN.po +1145 -0
  218. package/apps/web/upstream/src/res/locales/app.zh-CN.po +1145 -0
  219. package/apps/web/upstream/src/res/locales/app.zh-TW.po +1145 -0
  220. package/apps/web/upstream/src/res/localesList.json +27 -0
  221. package/apps/web/upstream/src/res/logo.png +0 -0
  222. package/apps/web/upstream/src/thirdparty/about.html +43 -0
  223. package/apps/web/upstream/src/thirdparty/index.js +5 -0
  224. package/apps/web/upstream/src/thirdparty/kiwiirccom.vue +185 -0
  225. package/apps/web/upstream/static/config.json +28 -0
  226. package/apps/web/upstream/static/emoticons/smile.png +0 -0
  227. package/apps/web/upstream/static/favicon.png +0 -0
  228. package/apps/web/upstream/static/highlight.mp3 +0 -0
  229. package/apps/web/upstream/static/highlight.ogg +0 -0
  230. package/apps/web/upstream/static/locales/.gitignore +2 -0
  231. package/apps/web/upstream/static/plugins/customise.html.example +10 -0
  232. package/apps/web/upstream/static/themes/coffee/theme.css +145 -0
  233. package/apps/web/upstream/static/themes/common/base.css +1064 -0
  234. package/apps/web/upstream/static/themes/dark/theme.css +196 -0
  235. package/apps/web/upstream/static/themes/default/theme.css +92 -0
  236. package/apps/web/upstream/static/themes/elite/theme.css +248 -0
  237. package/apps/web/upstream/static/themes/grayfox/theme.css +143 -0
  238. package/apps/web/upstream/static/themes/nightswatch/theme.css +367 -0
  239. package/apps/web/upstream/static/themes/osprey/theme.css +115 -0
  240. package/apps/web/upstream/static/themes/radioactive/theme.css +1211 -0
  241. package/apps/web/upstream/static/themes/sky/theme.css +107 -0
  242. package/apps/web/upstream/tests/unit/BatchAdd.spec.js +174 -0
  243. package/apps/web/upstream/tests/unit/MessageParser.spec.js +125 -0
  244. package/apps/web/upstream/tests/unit/Misc.spec.js +24 -0
  245. package/apps/web/upstream/tests/unit/NetworkState.spec.js +58 -0
  246. package/apps/web/upstream/tests/unit/StartupError.spec.js +20 -0
  247. package/apps/web/upstream/vue.config.js +150 -0
  248. package/apps/web/upstream/yarn.lock +9767 -0
  249. package/apps/web/vitest.config.ts +13 -0
  250. package/biome.json +2 -0
  251. package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
  252. package/docs/ADR-002-location-of-authority.md +82 -0
  253. package/docs/ADR-003-durable-object-sharding.md +110 -0
  254. package/docs/ADR-004-dynamodb-schema.md +96 -0
  255. package/docs/ADR-005-wss-only-transport-v1.md +83 -0
  256. package/docs/ADR-006-sasl-mechanism-scope.md +96 -0
  257. package/docs/ADR-007-deterministic-ports.md +82 -0
  258. package/docs/ADR-008-monorepo-tooling.md +60 -0
  259. package/docs/ADR-009-dual-transport-wss-and-irc-tls.md +138 -0
  260. package/docs/ADR-Index.md +15 -0
  261. package/docs/AWS-Adapter-Architecture.md +496 -0
  262. package/docs/AWS-Deployment.md +1275 -0
  263. package/docs/AWS-TCP-Deployment.md +410 -0
  264. package/docs/Cloudflare-Deployment-Guide.md +719 -0
  265. package/docs/Cloudflare-TCP-Deployment.md +510 -0
  266. package/docs/Home.md +16 -0
  267. package/docs/Observability.md +87 -0
  268. package/docs/PlanExtensions.md +812 -0
  269. package/docs/PlanIRCv3Websocket.md +503 -0
  270. package/docs/PlanWebClient.md +451 -0
  271. package/docs/Release-Process.md +443 -0
  272. package/docs/WebClientGuide.md +535 -0
  273. package/package.json +2 -2
  274. package/packages/aws-adapter/package.json +1 -1
  275. package/packages/aws-adapter/src/handlers/default.ts +3 -0
  276. package/packages/aws-adapter/src/handlers/nlb-stream.ts +9 -0
  277. package/packages/aws-adapter/src/serialize.ts +29 -1
  278. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +49 -8
  279. package/packages/aws-adapter/tests/account-store.test.ts +4 -7
  280. package/packages/aws-adapter/tests/aws-harness.ts +5 -2
  281. package/packages/aws-adapter/tests/aws-integration.test.ts +8 -4
  282. package/packages/aws-adapter/tests/aws-runtime.test.ts +79 -0
  283. package/packages/aws-adapter/tests/config-loader.test.ts +11 -2
  284. package/packages/aws-adapter/tests/connect.test.ts +9 -9
  285. package/packages/aws-adapter/tests/disconnect-fanout.test.ts +3 -4
  286. package/packages/aws-adapter/tests/global-setup.ts +55 -30
  287. package/packages/aws-adapter/tests/gone-exception.test.ts +4 -9
  288. package/packages/aws-adapter/tests/handlers.test.ts +90 -4
  289. package/packages/aws-adapter/tests/message-store.test.ts +6 -5
  290. package/packages/aws-adapter/tests/nlb-stream.test.ts +60 -8
  291. package/packages/cf-adapter/package.json +2 -1
  292. package/packages/cf-adapter/src/connection-do.ts +3 -0
  293. package/packages/cf-adapter/src/serialize.ts +25 -4
  294. package/packages/cf-adapter/tests/cf-harness.ts +12 -1
  295. package/packages/cf-adapter/tests/cf-runtime.test.ts +269 -0
  296. package/packages/cf-adapter/tests/channel-do.test.ts +117 -0
  297. package/packages/cf-adapter/tests/config-loader.test.ts +49 -0
  298. package/packages/cf-adapter/tests/connection-do-pure.test.ts +91 -0
  299. package/packages/cf-adapter/tests/d1-account-store.test.ts +48 -0
  300. package/packages/cf-adapter/tests/registry-do.test.ts +104 -0
  301. package/packages/cf-adapter/tests/serialize.test.ts +308 -0
  302. package/packages/cf-adapter/vitest.config.ts +1 -1
  303. package/packages/in-memory-runtime/package.json +1 -1
  304. package/packages/irc-core/package.json +1 -1
  305. package/packages/irc-core/reports/mutation/mutation.html +342 -0
  306. package/packages/irc-core/src/caps/capabilities.ts +37 -3
  307. package/packages/irc-core/src/caps/index.ts +1 -0
  308. package/packages/irc-core/src/caps/sts.ts +84 -0
  309. package/packages/irc-core/src/commands/account-notify.ts +53 -0
  310. package/packages/irc-core/src/commands/away.ts +9 -3
  311. package/packages/irc-core/src/commands/cap.ts +23 -5
  312. package/packages/irc-core/src/commands/chathistory.ts +40 -11
  313. package/packages/irc-core/src/commands/index.ts +34 -0
  314. package/packages/irc-core/src/commands/isupport.ts +53 -0
  315. package/packages/irc-core/src/commands/mode.ts +8 -1
  316. package/packages/irc-core/src/commands/monitor.ts +327 -0
  317. package/packages/irc-core/src/commands/multiline.ts +256 -0
  318. package/packages/irc-core/src/commands/pre-away.ts +112 -0
  319. package/packages/irc-core/src/commands/privmsg.ts +19 -12
  320. package/packages/irc-core/src/commands/read-marker.ts +108 -0
  321. package/packages/irc-core/src/commands/registration.ts +8 -0
  322. package/packages/irc-core/src/commands/sasl.ts +24 -1
  323. package/packages/irc-core/src/commands/tagmsg.ts +71 -1
  324. package/packages/irc-core/src/commands/whois.ts +5 -0
  325. package/packages/irc-core/src/config.ts +65 -10
  326. package/packages/irc-core/src/effects.ts +14 -0
  327. package/packages/irc-core/src/ports.ts +389 -0
  328. package/packages/irc-core/src/protocol/index.ts +14 -0
  329. package/packages/irc-core/src/protocol/labeled-response.ts +133 -0
  330. package/packages/irc-core/src/protocol/numerics.ts +15 -0
  331. package/packages/irc-core/src/protocol/outbound.ts +16 -1
  332. package/packages/irc-core/src/protocol/standard-replies.ts +277 -0
  333. package/packages/irc-core/src/state/connection.ts +32 -1
  334. package/packages/irc-core/src/types.ts +112 -0
  335. package/packages/irc-core/stryker.commands.conf.json +1 -2
  336. package/packages/irc-core/tests/away-store.test.ts +73 -0
  337. package/packages/irc-core/tests/batch.test.ts +15 -0
  338. package/packages/irc-core/tests/caps/capabilities.test.ts +119 -2
  339. package/packages/irc-core/tests/caps/sts.test.ts +123 -0
  340. package/packages/irc-core/tests/commands/account-notify.test.ts +171 -0
  341. package/packages/irc-core/tests/commands/away.test.ts +85 -2
  342. package/packages/irc-core/tests/commands/cap.test.ts +165 -0
  343. package/packages/irc-core/tests/commands/chathistory.test.ts +148 -1
  344. package/packages/irc-core/tests/commands/echo-message.test.ts +9 -3
  345. package/packages/irc-core/tests/commands/isupport.test.ts +80 -0
  346. package/packages/irc-core/tests/commands/mode.test.ts +72 -0
  347. package/packages/irc-core/tests/commands/monitor.test.ts +411 -0
  348. package/packages/irc-core/tests/commands/multiline.test.ts +428 -0
  349. package/packages/irc-core/tests/commands/pre-away.test.ts +241 -0
  350. package/packages/irc-core/tests/commands/privmsg.test.ts +154 -3
  351. package/packages/irc-core/tests/commands/read-marker.test.ts +211 -0
  352. package/packages/irc-core/tests/commands/registration.test.ts +64 -0
  353. package/packages/irc-core/tests/commands/sasl.test.ts +116 -2
  354. package/packages/irc-core/tests/commands/tagmsg.test.ts +357 -0
  355. package/packages/irc-core/tests/commands/whois.test.ts +34 -0
  356. package/packages/irc-core/tests/config.test.ts +103 -13
  357. package/packages/irc-core/tests/labeled-response.test.ts +181 -0
  358. package/packages/irc-core/tests/message-tags.test.ts +114 -0
  359. package/packages/irc-core/tests/monitor-store.test.ts +200 -0
  360. package/packages/irc-core/tests/numerics.test.ts +13 -0
  361. package/packages/irc-core/tests/parser.test.ts +109 -0
  362. package/packages/irc-core/tests/read-marker-store.test.ts +108 -0
  363. package/packages/irc-core/tests/standard-replies.test.ts +412 -0
  364. package/packages/irc-core/tests/types.test.ts +27 -0
  365. package/packages/irc-server/package.json +1 -1
  366. package/packages/irc-server/src/actor.ts +450 -3
  367. package/packages/irc-server/src/dispatch.ts +108 -16
  368. package/packages/irc-server/tests/actor.test.ts +1432 -39
  369. package/packages/irc-server/tests/dispatch.test.ts +263 -2
  370. package/packages/irc-test-support/package.json +1 -1
  371. package/packages/irc-test-support/src/index.ts +6 -0
  372. package/packages/irc-test-support/src/scenarios.ts +24 -2
  373. package/packages/irc-test-support/src/test-config.ts +54 -0
  374. package/packages/irc-test-support/tests/test-config.test.ts +51 -0
  375. package/tools/ci-hardening/package.json +1 -1
  376. package/tools/ci-hardening/tests/ddb-local.test.ts +82 -0
  377. package/tools/load-test/package.json +33 -0
  378. package/tools/load-test/src/client.ts +351 -0
  379. package/tools/load-test/src/config.ts +313 -0
  380. package/tools/load-test/src/harness.ts +116 -0
  381. package/tools/load-test/src/main.ts +120 -0
  382. package/tools/load-test/src/metrics.ts +168 -0
  383. package/tools/load-test/src/report.ts +106 -0
  384. package/tools/load-test/tests/client.test.ts +212 -0
  385. package/tools/load-test/tests/config.test.ts +152 -0
  386. package/tools/load-test/tests/framing.test.ts +37 -0
  387. package/tools/load-test/tests/harness.test.ts +165 -0
  388. package/tools/load-test/tests/metrics.test.ts +174 -0
  389. package/tools/load-test/tests/report.test.ts +161 -0
  390. package/tools/load-test/tests/smoke.test.ts +67 -0
  391. package/tools/load-test/tsconfig.build.json +12 -0
  392. package/tools/load-test/tsconfig.test.json +10 -0
  393. package/tools/load-test/vitest.config.ts +29 -0
  394. package/tools/tcp-ws-forwarder/package.json +1 -1
@@ -0,0 +1,108 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { InMemoryReadMarkerStore, type ReadMarkerStore } from '../src/ports';
3
+
4
+ describe('InMemoryReadMarkerStore — port conformance', () => {
5
+ it('implements the ReadMarkerStore port', () => {
6
+ const store: ReadMarkerStore = new InMemoryReadMarkerStore();
7
+ expect(store).toBeDefined();
8
+ });
9
+ });
10
+
11
+ describe('InMemoryReadMarkerStore — set + get', () => {
12
+ it('returns undefined for an unknown (account, channel) pair', () => {
13
+ const store = new InMemoryReadMarkerStore();
14
+ expect(store.get('alice', '#foo')).toBeUndefined();
15
+ });
16
+
17
+ it('stores and returns the msgid for a known (account, channel) pair', () => {
18
+ const store = new InMemoryReadMarkerStore();
19
+ store.set('alice', '#foo', 'm1');
20
+ expect(store.get('alice', '#foo')).toBe('m1');
21
+ });
22
+
23
+ it('overwrites the marker on a subsequent set for the same pair', () => {
24
+ const store = new InMemoryReadMarkerStore();
25
+ store.set('alice', '#foo', 'm1');
26
+ store.set('alice', '#foo', 'm2');
27
+ expect(store.get('alice', '#foo')).toBe('m2');
28
+ });
29
+
30
+ it('keys channels case-insensitively (rfc1459 fold)', () => {
31
+ const store = new InMemoryReadMarkerStore();
32
+ store.set('alice', '#Foo', 'm1');
33
+ expect(store.get('alice', '#foo')).toBe('m1');
34
+ expect(store.get('alice', '#FOO')).toBe('m1');
35
+ });
36
+
37
+ it('keys accounts case-insensitively (rfc1459 fold)', () => {
38
+ const store = new InMemoryReadMarkerStore();
39
+ store.set('Alice', '#foo', 'm1');
40
+ expect(store.get('alice', '#foo')).toBe('m1');
41
+ expect(store.get('ALICE', '#foo')).toBe('m1');
42
+ });
43
+
44
+ it('keeps markers for distinct accounts on the same channel separate', () => {
45
+ const store = new InMemoryReadMarkerStore();
46
+ store.set('alice', '#foo', 'm1');
47
+ store.set('bob', '#foo', 'm9');
48
+ expect(store.get('alice', '#foo')).toBe('m1');
49
+ expect(store.get('bob', '#foo')).toBe('m9');
50
+ });
51
+
52
+ it('keeps markers for distinct channels on the same account separate', () => {
53
+ const store = new InMemoryReadMarkerStore();
54
+ store.set('alice', '#foo', 'm1');
55
+ store.set('alice', '#bar', 'm2');
56
+ expect(store.get('alice', '#foo')).toBe('m1');
57
+ expect(store.get('alice', '#bar')).toBe('m2');
58
+ });
59
+ });
60
+
61
+ describe('InMemoryReadMarkerStore — forAccount', () => {
62
+ it('returns an empty array for an account with no markers', () => {
63
+ const store = new InMemoryReadMarkerStore();
64
+ expect(store.forAccount('alice')).toEqual([]);
65
+ });
66
+
67
+ it('returns every (channel, msgid) pair recorded for the account', () => {
68
+ const store = new InMemoryReadMarkerStore();
69
+ store.set('alice', '#foo', 'm1');
70
+ store.set('alice', '#bar', 'm2');
71
+
72
+ const entries = store.forAccount('alice');
73
+ expect(entries).toHaveLength(2);
74
+ const byChan = Object.fromEntries(entries.map((e) => [e.channel, e.msgid]));
75
+ expect(byChan['#foo']).toBe('m1');
76
+ expect(byChan['#bar']).toBe('m2');
77
+ });
78
+
79
+ it('reports the account name on each entry', () => {
80
+ const store = new InMemoryReadMarkerStore();
81
+ store.set('alice', '#foo', 'm1');
82
+ const entries = store.forAccount('alice');
83
+ expect(entries[0]?.account).toBe('alice');
84
+ });
85
+
86
+ it('reflects the most recent set when a pair is overwritten', () => {
87
+ const store = new InMemoryReadMarkerStore();
88
+ store.set('alice', '#foo', 'm1');
89
+ store.set('alice', '#foo', 'm2');
90
+ const entries = store.forAccount('alice');
91
+ expect(entries).toHaveLength(1);
92
+ expect(entries[0]?.msgid).toBe('m2');
93
+ });
94
+
95
+ it('does not leak markers belonging to a different account', () => {
96
+ const store = new InMemoryReadMarkerStore();
97
+ store.set('alice', '#foo', 'm1');
98
+ store.set('bob', '#bar', 'm2');
99
+ expect(store.forAccount('alice')).toHaveLength(1);
100
+ expect(store.forAccount('bob')).toHaveLength(1);
101
+ });
102
+
103
+ it('folds the account name case-insensitively on lookup', () => {
104
+ const store = new InMemoryReadMarkerStore();
105
+ store.set('Alice', '#foo', 'm1');
106
+ expect(store.forAccount('ALICE')).toHaveLength(1);
107
+ });
108
+ });
@@ -0,0 +1,412 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { RawLine } from '../src/effects';
3
+ import { Numerics } from '../src/protocol/numerics';
4
+ import { numericToStandardReply, toStandardReply } from '../src/protocol/standard-replies';
5
+
6
+ const L = (text: string): RawLine => ({ text });
7
+
8
+ const CAPS = new Set<string>(['standard-replies']);
9
+ const NO_CAPS = new Set<string>();
10
+
11
+ const SERVER = 'irc.example.com';
12
+
13
+ /**
14
+ * Builds a numeric line in the canonical `:server CODE nick [middle] :trailing`
15
+ * form this server emits.
16
+ */
17
+ function numericLine(
18
+ code: number,
19
+ nick: string,
20
+ middle: string | undefined,
21
+ trailing: string,
22
+ ): RawLine {
23
+ const codeStr = code.toString().padStart(3, '0');
24
+ const parts = [`:${SERVER}`, codeStr, nick];
25
+ if (middle !== undefined) parts.push(middle);
26
+ parts.push(`:${trailing}`);
27
+ return L(parts.join(' '));
28
+ }
29
+
30
+ // ============================================================================
31
+ // toStandardReply — 461 ERR_NEEDMOREPARAMS (the seed case)
32
+ // ============================================================================
33
+
34
+ describe('toStandardReply — 461 ERR_NEEDMOREPARAMS', () => {
35
+ it('rewrites 461 to FAIL <command> INVALID_PARAMS :description for a cap-enabled client', () => {
36
+ // Arrange: the canonical 461 form emitted by every reducer that ships a
37
+ // command name in the middle slot. The cap-enabled client should see
38
+ // the structured FAIL form instead.
39
+ const input = numericLine(
40
+ Numerics.ERR_NEEDMOREPARAMS,
41
+ 'alice',
42
+ 'JOIN',
43
+ 'Not enough parameters',
44
+ );
45
+
46
+ // Act
47
+ const out = toStandardReply(input, CAPS);
48
+
49
+ // Assert: middle (JOIN) becomes the FAIL COMMAND; the trailing text is
50
+ // preserved verbatim as the description. The nick token is dropped —
51
+ // standard replies are addressed by routing, not by the nick field.
52
+ expect(out.text).toBe(`:${SERVER} FAIL JOIN INVALID_PARAMS :Not enough parameters`);
53
+ });
54
+ });
55
+
56
+ // ============================================================================
57
+ // toStandardReply — 403 ERR_NOSUCHCHANNEL
58
+ // ============================================================================
59
+
60
+ describe('toStandardReply — 403 ERR_NOSUCHCHANNEL', () => {
61
+ it('rewrites 403 to FAIL JOIN NO_SUCH_CHANNEL <channel> :description', () => {
62
+ const input = numericLine(
63
+ Numerics.ERR_NOSUCHCHANNEL,
64
+ 'alice',
65
+ '#nonexistent',
66
+ 'No such channel',
67
+ );
68
+
69
+ const out = toStandardReply(input, CAPS);
70
+
71
+ // The numeric's middle (the bad channel name) is emitted as the
72
+ // standard-reply context, and the reducer's command (best known as
73
+ // JOIN here — the most common caller) is the FAIL COMMAND.
74
+ expect(out.text).toBe(`:${SERVER} FAIL JOIN NO_SUCH_CHANNEL #nonexistent :No such channel`);
75
+ });
76
+ });
77
+
78
+ // ============================================================================
79
+ // toStandardReply — 432 ERR_ERRONEUSNICKNAME
80
+ // ============================================================================
81
+
82
+ describe('toStandardReply — 432 ERR_ERRONEUSNICKNAME', () => {
83
+ it('rewrites 432 to FAIL NICK INVALID_NICK <nick> :description', () => {
84
+ const input = numericLine(
85
+ Numerics.ERR_ERRONEUSNICKNAME,
86
+ 'alice',
87
+ 'b@dnick',
88
+ 'Erroneous nickname',
89
+ );
90
+
91
+ const out = toStandardReply(input, CAPS);
92
+
93
+ expect(out.text).toBe(`:${SERVER} FAIL NICK INVALID_NICK b@dnick :Erroneous nickname`);
94
+ });
95
+ });
96
+
97
+ // ============================================================================
98
+ // toStandardReply — 433 ERR_NICKNAMEINUSE
99
+ // ============================================================================
100
+
101
+ describe('toStandardReply — 433 ERR_NICKNAMEINUSE', () => {
102
+ it('rewrites 433 to FAIL NICK NICKNAME_IN_USE <nick> :description', () => {
103
+ const input = numericLine(
104
+ Numerics.ERR_NICKNAMEINUSE,
105
+ 'alice',
106
+ 'bob',
107
+ 'Nickname is already in use',
108
+ );
109
+
110
+ const out = toStandardReply(input, CAPS);
111
+
112
+ expect(out.text).toBe(`:${SERVER} FAIL NICK NICKNAME_IN_USE bob :Nickname is already in use`);
113
+ });
114
+ });
115
+
116
+ // ============================================================================
117
+ // toStandardReply — 904 ERR_SASLFAIL (SASL failure)
118
+ // ============================================================================
119
+
120
+ describe('toStandardReply — 904 ERR_SASLFAIL', () => {
121
+ it('rewrites 904 to FAIL AUTHENTICATE SASL_FAILED :description', () => {
122
+ const input = numericLine(
123
+ Numerics.ERR_SASLFAIL,
124
+ 'alice',
125
+ undefined,
126
+ 'SASL authentication failed',
127
+ );
128
+
129
+ const out = toStandardReply(input, CAPS);
130
+
131
+ // 904 has no middle parameter; the structured form carries only the
132
+ // fixed AUTHENTICATE command and the SASL_FAILED code.
133
+ expect(out.text).toBe(`:${SERVER} FAIL AUTHENTICATE SASL_FAILED :SASL authentication failed`);
134
+ });
135
+ });
136
+
137
+ // ============================================================================
138
+ // toStandardReply — legacy client gating (no standard-replies cap)
139
+ // ============================================================================
140
+
141
+ describe('toStandardReply — legacy client gating', () => {
142
+ it('returns the unchanged 461 numeric when the recipient lacks the cap', () => {
143
+ const input = numericLine(
144
+ Numerics.ERR_NEEDMOREPARAMS,
145
+ 'alice',
146
+ 'JOIN',
147
+ 'Not enough parameters',
148
+ );
149
+
150
+ // Same line, same object identity — the helper short-circuits before
151
+ // touching the table when the cap is absent.
152
+ const out = toStandardReply(input, NO_CAPS);
153
+
154
+ expect(out).toBe(input);
155
+ expect(out.text).toBe(`:${SERVER} 461 alice JOIN :Not enough parameters`);
156
+ });
157
+
158
+ it('returns the unchanged 403 numeric when the recipient lacks the cap', () => {
159
+ const input = numericLine(Numerics.ERR_NOSUCHCHANNEL, 'alice', '#nope', 'No such channel');
160
+
161
+ const out = toStandardReply(input, NO_CAPS);
162
+
163
+ expect(out).toBe(input);
164
+ expect(out.text).toBe(`:${SERVER} 403 alice #nope :No such channel`);
165
+ });
166
+
167
+ it('returns the unchanged 433 numeric when the recipient lacks the cap', () => {
168
+ const input = numericLine(
169
+ Numerics.ERR_NICKNAMEINUSE,
170
+ 'alice',
171
+ 'bob',
172
+ 'Nickname is already in use',
173
+ );
174
+
175
+ const out = toStandardReply(input, NO_CAPS);
176
+
177
+ expect(out).toBe(input);
178
+ expect(out.text).toBe(`:${SERVER} 433 alice bob :Nickname is already in use`);
179
+ });
180
+
181
+ it('returns the unchanged 904 numeric when the recipient lacks the cap', () => {
182
+ const input = numericLine(
183
+ Numerics.ERR_SASLFAIL,
184
+ 'alice',
185
+ undefined,
186
+ 'SASL authentication failed',
187
+ );
188
+
189
+ const out = toStandardReply(input, NO_CAPS);
190
+
191
+ expect(out).toBe(input);
192
+ expect(out.text).toBe(`:${SERVER} 904 alice :SASL authentication failed`);
193
+ });
194
+ });
195
+
196
+ // ============================================================================
197
+ // toStandardReply — non-curated numerics pass through unchanged
198
+ // ============================================================================
199
+
200
+ describe('toStandardReply — non-curated numerics pass through', () => {
201
+ it('passes a 372 RPL_MOTD line through unchanged for a cap-enabled client', () => {
202
+ // Numeric-only families (MOTD lines, NAMES, the welcome block, …) are
203
+ // intentionally left on the numeric form per the spec.
204
+ const input = numericLine(Numerics.RPL_MOTD, 'alice', undefined, '- Welcome to ExampleNet');
205
+
206
+ const out = toStandardReply(input, CAPS);
207
+
208
+ expect(out).toBe(input);
209
+ });
210
+
211
+ it('passes a 001 RPL_WELCOME line through unchanged for a cap-enabled client', () => {
212
+ const input = numericLine(Numerics.RPL_WELCOME, 'alice', undefined, 'Welcome to the network');
213
+
214
+ const out = toStandardReply(input, CAPS);
215
+
216
+ expect(out).toBe(input);
217
+ });
218
+
219
+ it('passes a 353 RPL_NAMREPLY line through unchanged for a cap-enabled client', () => {
220
+ // NAMES replies carry two middle tokens (sigil + channel); they are
221
+ // not in the curated set and must be delivered verbatim.
222
+ const input = numericLine(Numerics.RPL_NAMREPLY, 'alice', '= #foo', 'alice bob @carol');
223
+
224
+ const out = toStandardReply(input, CAPS);
225
+
226
+ expect(out).toBe(input);
227
+ });
228
+ });
229
+
230
+ // ============================================================================
231
+ // toStandardReply — non-numeric lines pass through unchanged
232
+ // ============================================================================
233
+
234
+ describe('toStandardReply — non-numeric lines pass through', () => {
235
+ it('passes a PRIVMSG line through unchanged for a cap-enabled client', () => {
236
+ const input = L(':alice!alice@example.com PRIVMSG #foo :hi');
237
+
238
+ const out = toStandardReply(input, CAPS);
239
+
240
+ expect(out).toBe(input);
241
+ });
242
+
243
+ it('passes a FAIL line already emitted by a reducer through unchanged', () => {
244
+ // Defensive: a reducer that already emits the structured form must not
245
+ // be re-translated. The leading token after the prefix is `FAIL`, not
246
+ // a three-digit numeric, so the parser rejects it.
247
+ const input = L(`:${SERVER} FAIL JOIN INVALID_PARAMS :Not enough parameters`);
248
+
249
+ const out = toStandardReply(input, CAPS);
250
+
251
+ expect(out).toBe(input);
252
+ });
253
+
254
+ it('passes a line without a server prefix through unchanged', () => {
255
+ // e.g. a server-to-server PING carrying no leading `:`. Defensive.
256
+ const input = L('PING :ping.example.com');
257
+
258
+ const out = toStandardReply(input, CAPS);
259
+
260
+ expect(out).toBe(input);
261
+ });
262
+ });
263
+
264
+ // ============================================================================
265
+ // toStandardReply — defensive parsing (malformed numeric lines)
266
+ // ============================================================================
267
+
268
+ describe('toStandardReply — malformed numeric lines pass through', () => {
269
+ it('passes a bare server prefix through unchanged (no space, no code)', () => {
270
+ // `:irc.example.com` alone — every reducer emits at least one trailing
271
+ // token, so this never ships in practice; defensive.
272
+ const input = L(`:${SERVER}`);
273
+
274
+ const out = toStandardReply(input, CAPS);
275
+
276
+ expect(out).toBe(input);
277
+ });
278
+
279
+ it('passes a server prefix with a single trailing token through unchanged', () => {
280
+ // `:irc.example.com 001` — no second space, so no nick and no trailing.
281
+ const input = L(`:${SERVER} 001`);
282
+
283
+ const out = toStandardReply(input, CAPS);
284
+
285
+ expect(out).toBe(input);
286
+ });
287
+
288
+ it('passes a server prefix with a non-numeric command token through unchanged', () => {
289
+ // `:server PING …` — second token is not three digits, so this is not
290
+ // a numeric line; pass through verbatim.
291
+ const input = L(`:${SERVER} PING :ping`);
292
+
293
+ const out = toStandardReply(input, CAPS);
294
+
295
+ expect(out).toBe(input);
296
+ });
297
+
298
+ it('passes a curated numeric line with no nick token through unchanged', () => {
299
+ // `:server 461 :trailing` — no nick between code and trailing. Every
300
+ // reducer emits the nick, so this never ships; defensive.
301
+ const input = L(`:${SERVER} 461 :Not enough parameters`);
302
+
303
+ const out = toStandardReply(input, CAPS);
304
+
305
+ expect(out).toBe(input);
306
+ });
307
+
308
+ it('passes a numeric line with trailing whitespace after the code through unchanged', () => {
309
+ // `:server 001 ` — secondSpace is found but afterCode is empty. The
310
+ // reducer never emits this; defensive.
311
+ const input = L(`:${SERVER} 001 `);
312
+
313
+ const out = toStandardReply(input, CAPS);
314
+
315
+ expect(out).toBe(input);
316
+ });
317
+
318
+ it('passes a numeric line whose head collapses to empty after trim through unchanged', () => {
319
+ // `:server 001 :trailing` — multiple spaces after the code, then the
320
+ // trailing param. The nick slot is empty after trimming; defensive.
321
+ const input = L(`:${SERVER} 001 :trailing`);
322
+
323
+ const out = toStandardReply(input, CAPS);
324
+
325
+ expect(out).toBe(input);
326
+ });
327
+
328
+ it('passes a curated numeric line with no trailing parameter through unchanged', () => {
329
+ // `:server 461 alice JOIN` — no `:trailing`. The structured reply form
330
+ // requires a description, so the translator bails and the bare numeric
331
+ // ships verbatim (defensive).
332
+ const input = L(`:${SERVER} 461 alice JOIN`);
333
+
334
+ const out = toStandardReply(input, CAPS);
335
+
336
+ expect(out).toBe(input);
337
+ });
338
+
339
+ it('passes a curated numeric line whose command-from-middle has no middle through unchanged', () => {
340
+ // 461 normally carries the failed command name as its middle param.
341
+ // When the middle is missing, the translator cannot infer the COMMAND
342
+ // token (required by the spec), so it bails and ships the numeric
343
+ // verbatim. Mirrors how some legacy daemons emit `461 nick :msg`.
344
+ const input = L(`:${SERVER} 461 alice :Not enough parameters`);
345
+
346
+ const out = toStandardReply(input, CAPS);
347
+
348
+ expect(out).toBe(input);
349
+ });
350
+
351
+ it('passes a curated context-bearing numeric line with no middle through unchanged', () => {
352
+ // 403 normally carries the bad channel name as its middle param. When
353
+ // the middle is missing, no context token is available, so the FAIL
354
+ // form degrades to `FAIL JOIN NO_SUCH_CHANNEL :description` (no context).
355
+ const input = L(`:${SERVER} 403 alice :No such channel`);
356
+
357
+ const out = toStandardReply(input, CAPS);
358
+
359
+ // The structured form is still emitted; only the context slot is
360
+ // elided when the middle is missing.
361
+ expect(out.text).toBe(`:${SERVER} FAIL JOIN NO_SUCH_CHANNEL :No such channel`);
362
+ });
363
+ });
364
+
365
+ // ============================================================================
366
+ // numericToStandardReply — table data
367
+ // ============================================================================
368
+
369
+ describe('numericToStandardReply — curated table', () => {
370
+ it('maps every curated numeric from the ticket to a FAIL', () => {
371
+ const curated: ReadonlyArray<[number, string]> = [
372
+ [Numerics.ERR_NEEDMOREPARAMS, 'INVALID_PARAMS'],
373
+ [Numerics.ERR_NOSUCHCHANNEL, 'NO_SUCH_CHANNEL'],
374
+ [Numerics.ERR_ERRONEUSNICKNAME, 'INVALID_NICK'],
375
+ [Numerics.ERR_NICKNAMEINUSE, 'NICKNAME_IN_USE'],
376
+ [Numerics.ERR_SASLFAIL, 'SASL_FAILED'],
377
+ ];
378
+ for (const [code, expected] of curated) {
379
+ const mapping = numericToStandardReply.get(code);
380
+ expect(mapping).toBeDefined();
381
+ expect(mapping?.kind).toBe('FAIL');
382
+ expect(mapping?.code).toBe(expected);
383
+ }
384
+ });
385
+
386
+ it('leaves a non-curated error numeric unmapped', () => {
387
+ expect(numericToStandardReply.has(Numerics.ERR_NOSUCHNICK)).toBe(false);
388
+ expect(numericToStandardReply.has(Numerics.RPL_WELCOME)).toBe(false);
389
+ expect(numericToStandardReply.has(Numerics.ERR_PASSWDMISMATCH)).toBe(false);
390
+ });
391
+
392
+ it('declares a command strategy for every mapping (no ambiguous rows)', () => {
393
+ // Invariant: every row resolves a COMMAND. When commandFromMiddle is
394
+ // false, a fixed `command` MUST be supplied — otherwise the helper
395
+ // would have to bail at runtime.
396
+ for (const mapping of numericToStandardReply.values()) {
397
+ if (mapping.commandFromMiddle) {
398
+ expect(mapping.contextFromMiddle).toBe(false);
399
+ } else {
400
+ expect(mapping.command).toBeDefined();
401
+ }
402
+ }
403
+ });
404
+
405
+ it('never declares both commandFromMiddle and contextFromMiddle simultaneously', () => {
406
+ // The two strategies consume the same middle slot; they are mutually
407
+ // exclusive by construction.
408
+ for (const mapping of numericToStandardReply.values()) {
409
+ expect(mapping.commandFromMiddle && mapping.contextFromMiddle).toBe(false);
410
+ }
411
+ });
412
+ });
@@ -131,6 +131,33 @@ describe('Ctx / Reducer types', () => {
131
131
  const ctx = makeCtx();
132
132
  expect(ctx.history).toBeUndefined();
133
133
  });
134
+
135
+ it('threads an inbound label through buildCtx onto ctx.label', () => {
136
+ const ctx = buildCtx({
137
+ serverConfig: {
138
+ serverName: 'irc.example.com',
139
+ networkName: 'ExampleNet',
140
+ maxChannelsPerUser: 30,
141
+ maxTargetsPerCommand: 10,
142
+ maxListEntries: 50,
143
+ nickLen: 30,
144
+ channelLen: 50,
145
+ topicLen: 390,
146
+ quitMessage: 'Client Quit',
147
+ },
148
+ clock: new FakeClock(1_000),
149
+ ids: new SequentialIdFactory(),
150
+ motd: EmptyMotdProvider,
151
+ connection: createConnection({ id: 'c1', connectedSince: 0 }),
152
+ label: 'foo',
153
+ });
154
+ expect(ctx.label).toBe('foo');
155
+ });
156
+
157
+ it('omits ctx.label when no label is supplied', () => {
158
+ const ctx = makeCtx();
159
+ expect(ctx.label).toBeUndefined();
160
+ });
134
161
  });
135
162
 
136
163
  describe('ServerConfig shape', () => {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverless-ircd/irc-server",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "private": true,
5
5
  "description": "Orchestration layer: IrcRuntime port, dispatch interpreter, ConnectionActor",
6
6
  "license": "BSD-3-Clause",