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
@@ -80,7 +80,12 @@ describe('local-cli e2e', () => {
80
80
  let server: LocalServer;
81
81
 
82
82
  beforeAll(async () => {
83
- server = await startLocalServer({ port: 0, hostname: '127.0.0.1' });
83
+ server = await startLocalServer({
84
+ port: 0,
85
+ hostname: '127.0.0.1',
86
+ serverName: 'irc.local.example.com',
87
+ networkName: 'LocalNet',
88
+ });
84
89
  });
85
90
 
86
91
  afterAll(async () => {
@@ -103,15 +108,15 @@ describe('local-cli e2e', () => {
103
108
 
104
109
  await alice.send('NICK alice');
105
110
  await alice.send('USER alice 0 * :Alice');
106
- await alice.waitFor((l) => l.startsWith(':irc.example.com 001 '));
111
+ await alice.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
107
112
 
108
113
  await bob.send('NICK bob');
109
114
  await bob.send('USER bob 0 * :Bob');
110
- await bob.waitFor((l) => l.startsWith(':irc.example.com 001 '));
115
+ await bob.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
111
116
 
112
117
  await alice.send('JOIN #e2e');
113
118
  await alice.waitFor((l) => l.includes('JOIN #e2e'));
114
- await alice.waitFor((l) => l.startsWith(':irc.example.com 353 '));
119
+ await alice.waitFor((l) => l.startsWith(':irc.local.example.com 353 '));
115
120
 
116
121
  await bob.send('JOIN #e2e');
117
122
  // Bob sees his own JOIN + NAMES.
@@ -130,7 +135,7 @@ describe('local-cli e2e', () => {
130
135
  await client.opened();
131
136
  await client.send('NICK charlie');
132
137
  await client.send('USER charlie 0 * :Charlie');
133
- await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
138
+ await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
134
139
 
135
140
  await client.send('PING :tok-12345');
136
141
  await client.waitFor((l) => l === 'PONG :tok-12345');
@@ -143,7 +148,7 @@ describe('local-cli e2e', () => {
143
148
  await client.opened();
144
149
  await client.send('NICK dave');
145
150
  await client.send('USER dave 0 * :Dave');
146
- await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
151
+ await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
147
152
 
148
153
  await client.send('BOGUSCMD');
149
154
  await client.waitFor((l) => l.includes('421') && l.includes('BOGUSCMD'));
@@ -156,7 +161,7 @@ describe('local-cli e2e', () => {
156
161
  await client.opened();
157
162
  await client.send('NICK eve');
158
163
  await client.send('USER eve 0 * :Eve');
159
- await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
164
+ await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
160
165
 
161
166
  await client.send('QUIT :bye');
162
167
  // The disconnect effect should close the socket.
@@ -166,18 +171,42 @@ describe('local-cli e2e', () => {
166
171
  await next.opened();
167
172
  await next.send('NICK eve');
168
173
  await next.send('USER eve 0 * :Eve2');
169
- await next.waitFor((l) => l.startsWith(':irc.example.com 001 '), 2000);
174
+ await next.waitFor((l) => l.startsWith(':irc.local.example.com 001 '), 2000);
170
175
  await next.close();
171
176
  });
172
177
  });
173
178
 
174
179
  describe('local-cli error paths', () => {
175
180
  it('rejects startLocalServer when the port is already in use', async () => {
176
- const first = await startLocalServer({ port: 0, hostname: '127.0.0.1' });
177
- await expect(startLocalServer({ port: first.port, hostname: '127.0.0.1' })).rejects.toThrow();
181
+ const first = await startLocalServer({
182
+ port: 0,
183
+ hostname: '127.0.0.1',
184
+ serverName: 'irc.local.example.com',
185
+ networkName: 'LocalNet',
186
+ });
187
+ await expect(
188
+ startLocalServer({
189
+ port: first.port,
190
+ hostname: '127.0.0.1',
191
+ serverName: 'irc.local.example.com',
192
+ networkName: 'LocalNet',
193
+ }),
194
+ ).rejects.toThrow();
178
195
  await first.close();
179
196
  });
180
197
 
198
+ it('rejects when serverName is omitted so the placeholder never leaks', async () => {
199
+ await expect(startLocalServer({ port: 0, hostname: '127.0.0.1' })).rejects.toThrowError(
200
+ /serverName/u,
201
+ );
202
+ });
203
+
204
+ it('rejects when networkName is omitted', async () => {
205
+ await expect(
206
+ startLocalServer({ port: 0, hostname: '127.0.0.1', serverName: 'irc.local.example.com' }),
207
+ ).rejects.toThrowError(/networkName/u);
208
+ });
209
+
181
210
  it('validates the supplied config through the shared ServerConfigSchema at boot', async () => {
182
211
  // nickLen=0 is rejected by the schema (must be a positive integer).
183
212
  await expect(
@@ -192,7 +221,12 @@ describe('local-cli error paths', () => {
192
221
  });
193
222
 
194
223
  it('logs and recovers from a per-socket error without crashing the server', async () => {
195
- const srv = await startLocalServer({ port: 0, hostname: '127.0.0.1' });
224
+ const srv = await startLocalServer({
225
+ port: 0,
226
+ hostname: '127.0.0.1',
227
+ serverName: 'irc.local.example.com',
228
+ networkName: 'LocalNet',
229
+ });
196
230
  const client = new TestClient(`ws://127.0.0.1:${srv.port}/`);
197
231
  await client.opened();
198
232
  // Synthesize a socket-level error on the SERVER-side ws via the
@@ -218,20 +252,22 @@ describe('local-cli error paths', () => {
218
252
  const srv = await startLocalServer({
219
253
  port: 0,
220
254
  hostname: '127.0.0.1',
255
+ serverName: 'irc.local.example.com',
256
+ networkName: 'LocalNet',
221
257
  motdLines: ['first line of motd', 'second line of motd'],
222
258
  });
223
259
  const client = new TestClient(`ws://127.0.0.1:${srv.port}/`);
224
260
  await client.opened();
225
261
  await client.send('NICK karl');
226
262
  await client.send('USER karl 0 * :Karl');
227
- await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
263
+ await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
228
264
  // Drain registration burst so the explicit MOTD responses below aren't
229
265
  // confused with the placeholder MOTD emitted at registration.
230
266
  client.received.length = 0;
231
267
  await client.send('MOTD');
232
268
  // Wait for end-of-MOTD so all 372 lines are buffered in `received`.
233
- await client.waitFor((l) => l.startsWith(':irc.example.com 376 karl'));
234
- const motdBody = client.received.filter((l) => l.startsWith(':irc.example.com 372 karl'));
269
+ await client.waitFor((l) => l.startsWith(':irc.local.example.com 376 karl'));
270
+ const motdBody = client.received.filter((l) => l.startsWith(':irc.local.example.com 372 karl'));
235
271
  expect(motdBody.some((l) => l.includes('first line of motd'))).toBe(true);
236
272
  expect(motdBody.some((l) => l.includes('second line of motd'))).toBe(true);
237
273
  await client.close();
@@ -239,7 +275,11 @@ describe('local-cli error paths', () => {
239
275
  });
240
276
 
241
277
  it('defaults hostname to 127.0.0.1 when omitted', async () => {
242
- const srv = await startLocalServer({ port: 0 });
278
+ const srv = await startLocalServer({
279
+ port: 0,
280
+ serverName: 'irc.local.example.com',
281
+ networkName: 'LocalNet',
282
+ });
243
283
  expect(srv.hostname).toBe('127.0.0.1');
244
284
  // Sanity-check that the server is actually reachable on the default.
245
285
  const client = new TestClient(`ws://127.0.0.1:${srv.port}/`);
@@ -249,7 +289,12 @@ describe('local-cli error paths', () => {
249
289
  });
250
290
 
251
291
  it('close() forcibly closes any still-open client sockets', async () => {
252
- const srv = await startLocalServer({ port: 0, hostname: '127.0.0.1' });
292
+ const srv = await startLocalServer({
293
+ port: 0,
294
+ hostname: '127.0.0.1',
295
+ serverName: 'irc.local.example.com',
296
+ networkName: 'LocalNet',
297
+ });
253
298
  const client = new TestClient(`ws://127.0.0.1:${srv.port}/`);
254
299
  await client.opened();
255
300
  // Close the server WITHOUT first closing the client — exercises the
@@ -260,14 +305,24 @@ describe('local-cli error paths', () => {
260
305
  });
261
306
 
262
307
  it('close() is a no-op when no connections are live', async () => {
263
- const srv = await startLocalServer({ port: 0, hostname: '127.0.0.1' });
308
+ const srv = await startLocalServer({
309
+ port: 0,
310
+ hostname: '127.0.0.1',
311
+ serverName: 'irc.local.example.com',
312
+ networkName: 'LocalNet',
313
+ });
264
314
  await srv.close();
265
315
  // Closing again should not throw (idempotent).
266
316
  await expect(srv.close()).resolves.toBeUndefined();
267
317
  });
268
318
 
269
319
  it('logs an actor failure without crashing the server (Error path)', async () => {
270
- const srv = await startLocalServer({ port: 0, hostname: '127.0.0.1' });
320
+ const srv = await startLocalServer({
321
+ port: 0,
322
+ hostname: '127.0.0.1',
323
+ serverName: 'irc.local.example.com',
324
+ networkName: 'LocalNet',
325
+ });
271
326
  // Force the runtime's `send` to throw a real Error so the actor's
272
327
  // reducer pipeline rejects with an Error instance — exercises
273
328
  // `stringifyErr`'s `instanceof Error` branch and `logError`.
@@ -291,14 +346,19 @@ describe('local-cli error paths', () => {
291
346
  await next.opened();
292
347
  await next.send('NICK grace');
293
348
  await next.send('USER grace 0 * :Grace');
294
- await next.waitFor((l) => l.startsWith(':irc.example.com 001 '), 2000);
349
+ await next.waitFor((l) => l.startsWith(':irc.local.example.com 001 '), 2000);
295
350
  await next.close();
296
351
  await client.close();
297
352
  await srv.close();
298
353
  });
299
354
 
300
355
  it('logs a non-Error throw from the actor pipeline (defensive path)', async () => {
301
- const srv = await startLocalServer({ port: 0, hostname: '127.0.0.1' });
356
+ const srv = await startLocalServer({
357
+ port: 0,
358
+ hostname: '127.0.0.1',
359
+ serverName: 'irc.local.example.com',
360
+ networkName: 'LocalNet',
361
+ });
302
362
  const original = srv.runtime.send.bind(srv.runtime);
303
363
  srv.runtime.send = async () => {
304
364
  // Throw a non-Error value to exercise stringifyErr's else branch.
@@ -316,12 +376,17 @@ describe('local-cli error paths', () => {
316
376
  });
317
377
 
318
378
  it('cleans up when a client terminates abruptly (server-side error path)', async () => {
319
- const srv = await startLocalServer({ port: 0, hostname: '127.0.0.1' });
379
+ const srv = await startLocalServer({
380
+ port: 0,
381
+ hostname: '127.0.0.1',
382
+ serverName: 'irc.local.example.com',
383
+ networkName: 'LocalNet',
384
+ });
320
385
  const client = new TestClient(`ws://127.0.0.1:${srv.port}/`);
321
386
  await client.opened();
322
387
  await client.send('NICK ivan');
323
388
  await client.send('USER ivan 0 * :Ivan');
324
- await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
389
+ await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
325
390
  const connId = await srv.runtime.lookupNick('ivan');
326
391
  expect(connId).not.toBeNull();
327
392
  expect(srv.runtime.hasConnection(connId as string)).toBe(true);
@@ -338,12 +403,17 @@ describe('local-cli error paths', () => {
338
403
  });
339
404
 
340
405
  it('delivers an ERROR notice when a reducer supplies a disconnect reason', async () => {
341
- const srv = await startLocalServer({ port: 0, hostname: '127.0.0.1' });
406
+ const srv = await startLocalServer({
407
+ port: 0,
408
+ hostname: '127.0.0.1',
409
+ serverName: 'irc.local.example.com',
410
+ networkName: 'LocalNet',
411
+ });
342
412
  const client = new TestClient(`ws://127.0.0.1:${srv.port}/`);
343
413
  await client.opened();
344
414
  await client.send('NICK heidi');
345
415
  await client.send('USER heidi 0 * :Heidi');
346
- await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
416
+ await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
347
417
 
348
418
  // Call the runtime's disconnect directly with a reason (simulates a
349
419
  // future flood-control / oper-kill effect). The runtime takes a ConnId;
@@ -370,12 +440,17 @@ describe('local-cli chathistory end-to-end', () => {
370
440
  client.send(`NICK ${nick}\r\n`);
371
441
  client.send(`USER ${nick} 0 * :${nick}\r\n`);
372
442
  client.send('CAP END\r\n');
373
- await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
443
+ await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
374
444
  return client;
375
445
  }
376
446
 
377
447
  it('records a PRIVMSG and replays it via CHATHISTORY LATEST in a BATCH', async () => {
378
- const srv = await startLocalServer({ port: 0, hostname: '127.0.0.1' });
448
+ const srv = await startLocalServer({
449
+ port: 0,
450
+ hostname: '127.0.0.1',
451
+ serverName: 'irc.local.example.com',
452
+ networkName: 'LocalNet',
453
+ });
379
454
  const alice = await registerWithCaps(srv, 'ch-alice');
380
455
 
381
456
  alice.send('JOIN #chathist-e2e\r\n');
@@ -395,7 +470,12 @@ describe('local-cli chathistory end-to-end', () => {
395
470
  });
396
471
 
397
472
  it('prepends a chathistory BATCH on rejoin after messages were sent', async () => {
398
- const srv = await startLocalServer({ port: 0, hostname: '127.0.0.1' });
473
+ const srv = await startLocalServer({
474
+ port: 0,
475
+ hostname: '127.0.0.1',
476
+ serverName: 'irc.local.example.com',
477
+ networkName: 'LocalNet',
478
+ });
399
479
 
400
480
  // Seed history as alice: join + send a message that bob has missed.
401
481
  const alice = await registerWithCaps(srv, 'ch-seed');
@@ -429,6 +509,8 @@ describe('local-cli SASL PLAIN end-to-end', () => {
429
509
  const srv = await startLocalServer({
430
510
  port: 0,
431
511
  hostname: '127.0.0.1',
512
+ serverName: 'irc.local.example.com',
513
+ networkName: 'LocalNet',
432
514
  saslAccounts: [{ username: 'sasl-alice', password: 's3cret' }],
433
515
  });
434
516
 
@@ -445,7 +527,7 @@ describe('local-cli SASL PLAIN end-to-end', () => {
445
527
  client.send('NICK sasl-alice\r\n');
446
528
  client.send('USER sasl-alice 0 * :Sasl Alice\r\n');
447
529
  client.send('CAP END\r\n');
448
- await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
530
+ await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
449
531
  await client.close();
450
532
  await srv.close();
451
533
  });
@@ -454,6 +536,8 @@ describe('local-cli SASL PLAIN end-to-end', () => {
454
536
  const srv = await startLocalServer({
455
537
  port: 0,
456
538
  hostname: '127.0.0.1',
539
+ serverName: 'irc.local.example.com',
540
+ networkName: 'LocalNet',
457
541
  saslAccounts: [{ username: 'sasl-bob', password: 'right' }],
458
542
  });
459
543
 
@@ -92,7 +92,13 @@ class TcpTestClient extends EventEmitter {
92
92
 
93
93
  describe('local-cli TCP transport — startLocalServer tcpPort option', () => {
94
94
  it('exposes tcpPort and testTcpSockets when tcpPort is provided', async () => {
95
- const srv = await startLocalServer({ port: 0, tcpPort: 0, hostname: '127.0.0.1' });
95
+ const srv = await startLocalServer({
96
+ port: 0,
97
+ tcpPort: 0,
98
+ hostname: '127.0.0.1',
99
+ serverName: 'irc.local.example.com',
100
+ networkName: 'LocalNet',
101
+ });
96
102
  expect(srv.tcpPort).toBeGreaterThan(0);
97
103
  expect(srv.testTcpSockets).toBeDefined();
98
104
  expect(srv.testTcpSockets?.size).toBe(0);
@@ -100,16 +106,33 @@ describe('local-cli TCP transport — startLocalServer tcpPort option', () => {
100
106
  });
101
107
 
102
108
  it('omits tcpPort / testTcpSockets when tcpPort is not provided', async () => {
103
- const srv = await startLocalServer({ port: 0, hostname: '127.0.0.1' });
109
+ const srv = await startLocalServer({
110
+ port: 0,
111
+ hostname: '127.0.0.1',
112
+ serverName: 'irc.local.example.com',
113
+ networkName: 'LocalNet',
114
+ });
104
115
  expect(srv.tcpPort).toBeUndefined();
105
116
  expect(srv.testTcpSockets).toBeUndefined();
106
117
  await srv.close();
107
118
  });
108
119
 
109
120
  it('rejects when the TCP port is already in use', async () => {
110
- const first = await startLocalServer({ port: 0, tcpPort: 0, hostname: '127.0.0.1' });
121
+ const first = await startLocalServer({
122
+ port: 0,
123
+ tcpPort: 0,
124
+ hostname: '127.0.0.1',
125
+ serverName: 'irc.local.example.com',
126
+ networkName: 'LocalNet',
127
+ });
111
128
  await expect(
112
- startLocalServer({ port: 0, tcpPort: first.tcpPort, hostname: '127.0.0.1' }),
129
+ startLocalServer({
130
+ port: 0,
131
+ tcpPort: first.tcpPort,
132
+ hostname: '127.0.0.1',
133
+ serverName: 'irc.local.example.com',
134
+ networkName: 'LocalNet',
135
+ }),
113
136
  ).rejects.toThrow();
114
137
  await first.close();
115
138
  });
@@ -119,7 +142,13 @@ describe('local-cli TCP transport — IRC flows', () => {
119
142
  let server: LocalServer;
120
143
 
121
144
  beforeAll(async () => {
122
- server = await startLocalServer({ port: 0, tcpPort: 0, hostname: '127.0.0.1' });
145
+ server = await startLocalServer({
146
+ port: 0,
147
+ tcpPort: 0,
148
+ hostname: '127.0.0.1',
149
+ serverName: 'irc.local.example.com',
150
+ networkName: 'LocalNet',
151
+ });
123
152
  });
124
153
 
125
154
  afterAll(async () => {
@@ -141,11 +170,11 @@ describe('local-cli TCP transport — IRC flows', () => {
141
170
 
142
171
  await alice.send('NICK alice');
143
172
  await alice.send('USER alice 0 * :Alice');
144
- await alice.waitFor((l) => l.startsWith(':irc.example.com 001 '));
173
+ await alice.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
145
174
 
146
175
  await bob.send('NICK bob');
147
176
  await bob.send('USER bob 0 * :Bob');
148
- await bob.waitFor((l) => l.startsWith(':irc.example.com 001 '));
177
+ await bob.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
149
178
 
150
179
  await alice.send('JOIN #tcp-e2e');
151
180
  await alice.waitFor((l) => l.includes('JOIN #tcp-e2e'));
@@ -164,7 +193,7 @@ describe('local-cli TCP transport — IRC flows', () => {
164
193
  await client.opened();
165
194
  await client.send('NICK pingUser');
166
195
  await client.send('USER pingUser 0 * :Ping');
167
- await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
196
+ await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
168
197
 
169
198
  await client.send('PING :tcp-tok');
170
199
  await client.waitFor((l) => l === 'PONG :tcp-tok');
@@ -176,7 +205,7 @@ describe('local-cli TCP transport — IRC flows', () => {
176
205
  await client.opened();
177
206
  await client.send('NICK splitter');
178
207
  await client.send('USER splitter 0 * :Splitter');
179
- await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
208
+ await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
180
209
 
181
210
  // Split a PING line mid-token across two writes. The server's LineScanner
182
211
  // must buffer the partial line and emit one PONG when the terminator lands.
@@ -191,7 +220,7 @@ describe('local-cli TCP transport — IRC flows', () => {
191
220
  await client.opened();
192
221
  // NICK and USER bundled into one write, CRLF-separated.
193
222
  await client.writeRaw('NICK bundler\r\nUSER bundler 0 * :Bundler\r\n');
194
- await client.waitFor((l) => l.startsWith(':irc.example.com 001 bundler'));
223
+ await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 bundler'));
195
224
  await client.close();
196
225
  });
197
226
 
@@ -200,7 +229,7 @@ describe('local-cli TCP transport — IRC flows', () => {
200
229
  await client.opened();
201
230
  await client.send('NICK termUser');
202
231
  await client.send('USER termUser 0 * :Term');
203
- await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
232
+ await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
204
233
 
205
234
  const connId = await server.runtime.lookupNick('termUser');
206
235
  expect(connId).not.toBeNull();
@@ -217,7 +246,7 @@ describe('local-cli TCP transport — IRC flows', () => {
217
246
  await client.opened();
218
247
  await client.send('NICK reasonUser');
219
248
  await client.send('USER reasonUser 0 * :Reason');
220
- await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
249
+ await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
221
250
 
222
251
  // Synthesize a flood-control / oper-kill style disconnect with a reason.
223
252
  const connId = await server.runtime.lookupNick('reasonUser');
@@ -247,7 +276,7 @@ describe('local-cli TCP transport — IRC flows', () => {
247
276
  await next.opened();
248
277
  await next.send('NICK tcpRecover');
249
278
  await next.send('USER tcpRecover 0 * :TcpRecover');
250
- await next.waitFor((l) => l.startsWith(':irc.example.com 001 '), 2000);
279
+ await next.waitFor((l) => l.startsWith(':irc.local.example.com 001 '), 2000);
251
280
  await next.close();
252
281
  await client.close();
253
282
  });
@@ -255,7 +284,13 @@ describe('local-cli TCP transport — IRC flows', () => {
255
284
 
256
285
  describe('local-cli TCP × WS cross-transport interop', () => {
257
286
  it('delivers a TCP PRIVMSG to a WS client in the same channel', async () => {
258
- const srv = await startLocalServer({ port: 0, tcpPort: 0, hostname: '127.0.0.1' });
287
+ const srv = await startLocalServer({
288
+ port: 0,
289
+ tcpPort: 0,
290
+ hostname: '127.0.0.1',
291
+ serverName: 'irc.local.example.com',
292
+ networkName: 'LocalNet',
293
+ });
259
294
 
260
295
  // WS side.
261
296
  const wsClient = new WebSocket(`ws://127.0.0.1:${srv.port}/`);
@@ -288,7 +323,7 @@ describe('local-cli TCP × WS cross-transport interop', () => {
288
323
 
289
324
  wsSend('NICK wsUser');
290
325
  wsSend('USER wsUser 0 * :WS');
291
- await wsWaitFor((l) => l.startsWith(':irc.example.com 001 '));
326
+ await wsWaitFor((l) => l.startsWith(':irc.local.example.com 001 '));
292
327
  wsSend('JOIN #interop');
293
328
  await wsWaitFor((l) => l.includes('JOIN #interop'));
294
329
 
@@ -297,7 +332,7 @@ describe('local-cli TCP × WS cross-transport interop', () => {
297
332
  await tcp.opened();
298
333
  await tcp.send('NICK tcpUser');
299
334
  await tcp.send('USER tcpUser 0 * :TCP');
300
- await tcp.waitFor((l) => l.startsWith(':irc.example.com 001 '));
335
+ await tcp.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
301
336
  await tcp.send('JOIN #interop');
302
337
  await tcp.waitFor((l) => l.includes('JOIN #interop'));
303
338
 
@@ -315,7 +350,13 @@ describe('local-cli TCP × WS cross-transport interop', () => {
315
350
 
316
351
  describe('local-cli TCP transport — shutdown', () => {
317
352
  it('close() tears down the TCP server and live TCP sockets', async () => {
318
- const srv = await startLocalServer({ port: 0, tcpPort: 0, hostname: '127.0.0.1' });
353
+ const srv = await startLocalServer({
354
+ port: 0,
355
+ tcpPort: 0,
356
+ hostname: '127.0.0.1',
357
+ serverName: 'irc.local.example.com',
358
+ networkName: 'LocalNet',
359
+ });
319
360
  const client = new TcpTestClient('127.0.0.1', srv.tcpPort as number);
320
361
  await client.opened();
321
362
  // Closing the server destroys the server-side socket; the client side
@@ -327,12 +368,23 @@ describe('local-cli TCP transport — shutdown', () => {
327
368
  });
328
369
 
329
370
  it('close() is a no-op for TCP when tcpPort was never enabled', async () => {
330
- const srv = await startLocalServer({ port: 0, hostname: '127.0.0.1' });
371
+ const srv = await startLocalServer({
372
+ port: 0,
373
+ hostname: '127.0.0.1',
374
+ serverName: 'irc.local.example.com',
375
+ networkName: 'LocalNet',
376
+ });
331
377
  await expect(srv.close()).resolves.toBeUndefined();
332
378
  });
333
379
 
334
380
  it('logs and recovers from a per-TCP-socket error without crashing the server', async () => {
335
- const srv = await startLocalServer({ port: 0, tcpPort: 0, hostname: '127.0.0.1' });
381
+ const srv = await startLocalServer({
382
+ port: 0,
383
+ tcpPort: 0,
384
+ hostname: '127.0.0.1',
385
+ serverName: 'irc.local.example.com',
386
+ networkName: 'LocalNet',
387
+ });
336
388
  const client = new TcpTestClient('127.0.0.1', srv.tcpPort as number);
337
389
  await client.opened();
338
390
  // Wait for the server-side 'connection' handler to register the socket
@@ -92,7 +92,12 @@ describe('local-cli IRCv3 WebSocket subprotocol', () => {
92
92
  let server: LocalServer;
93
93
 
94
94
  beforeAll(async () => {
95
- server = await startLocalServer({ port: 0, hostname: '127.0.0.1' });
95
+ server = await startLocalServer({
96
+ port: 0,
97
+ hostname: '127.0.0.1',
98
+ serverName: 'irc.local.example.com',
99
+ networkName: 'LocalNet',
100
+ });
96
101
  });
97
102
 
98
103
  afterAll(async () => {
@@ -121,7 +126,7 @@ describe('local-cli IRCv3 WebSocket subprotocol', () => {
121
126
  await client.opened();
122
127
  await client.send('NICK spec-bin');
123
128
  await client.send('USER spec-bin 0 * :Spec Bin');
124
- const welcome = await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
129
+ const welcome = await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
125
130
  expect(welcome).toContain('spec-bin');
126
131
 
127
132
  await client.send('PING :bin-tok');
@@ -172,7 +177,7 @@ describe('local-cli IRCv3 WebSocket subprotocol', () => {
172
177
  // One bare line per WS message (no trailing CRLF).
173
178
  await client.send('NICK spec-alice');
174
179
  await client.send('USER spec-alice 0 * :Spec Alice');
175
- const welcome = await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
180
+ const welcome = await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
176
181
  expect(welcome).toContain('spec-alice');
177
182
 
178
183
  // Every received message must be a single IRC line with no trailing CRLF
@@ -189,7 +194,7 @@ describe('local-cli IRCv3 WebSocket subprotocol', () => {
189
194
  await client.opened();
190
195
  await client.send('NICK spec-bob');
191
196
  await client.send('USER spec-bob 0 * :Spec Bob');
192
- await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
197
+ await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
193
198
 
194
199
  // The welcome sequence is many numerics (001..005, 375/372/376). In spec
195
200
  // mode each arrives as its own WS message; a legacy batch would have
@@ -204,7 +209,7 @@ describe('local-cli IRCv3 WebSocket subprotocol', () => {
204
209
  await client.opened();
205
210
  await client.send('NICK spec-ping');
206
211
  await client.send('USER spec-ping 0 * :Spec Ping');
207
- await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
212
+ await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
208
213
 
209
214
  await client.send('PING :spec-tok');
210
215
  const pong = await client.waitFor((l) => l === 'PONG :spec-tok');
@@ -219,7 +224,7 @@ describe('local-cli IRCv3 WebSocket subprotocol', () => {
219
224
  await client.send('USER spec-budget 0 * :Spec Budget');
220
225
  // Drain the entire registration burst (001..376) so delayed numerics
221
226
  // don't pollute the post-overlong-message assertion below.
222
- await client.waitFor((l) => l.startsWith(':irc.example.com 376 '));
227
+ await client.waitFor((l) => l.startsWith(':irc.local.example.com 376 '));
223
228
 
224
229
  client.received.length = 0;
225
230
  // 511-byte payload: `PRIVMSG #x :` (12 bytes) + 499 chars = 511 total.
@@ -244,7 +249,7 @@ describe('local-cli IRCv3 WebSocket subprotocol', () => {
244
249
  await client.opened();
245
250
  await client.send(`NICK ${nick}`);
246
251
  await client.send(`USER ${nick} 0 * :Spec Disc`);
247
- await client.waitFor((l) => l.startsWith(':irc.example.com 001 '));
252
+ await client.waitFor((l) => l.startsWith(':irc.local.example.com 001 '));
248
253
 
249
254
  const connId = await server.runtime.lookupNick(nick);
250
255
  expect(connId).not.toBeNull();