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,63 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { DEFAULT_CONFIG_FILE, ENV_TO_CONFIG_FILE, resolveConfigFile } from '../src/build-env';
3
+
4
+ // ============================================================================
5
+ // resolveConfigFile — per-env config selection
6
+ // ============================================================================
7
+
8
+ describe('resolveConfigFile', () => {
9
+ it('selects config.staging.json for --env staging', () => {
10
+ const result = resolveConfigFile(['--env', 'staging']);
11
+ expect(result.file).toBe('config.staging.json');
12
+ expect(result.env).toBe('staging');
13
+ });
14
+
15
+ it('selects config.prod.json for --env prod', () => {
16
+ const result = resolveConfigFile(['--env', 'prod']);
17
+ expect(result.file).toBe('config.prod.json');
18
+ expect(result.env).toBe('prod');
19
+ });
20
+
21
+ it('falls back to the default config (staging target) when --env is absent', () => {
22
+ const result = resolveConfigFile([]);
23
+ expect(result.file).toBe(DEFAULT_CONFIG_FILE);
24
+ expect(result.file).toBe('config.json');
25
+ expect(result.env).toBe('default (staging)');
26
+ });
27
+
28
+ it('throws on an unknown --env value', () => {
29
+ expect(() => resolveConfigFile(['--env', 'local'])).toThrowError(/Unknown --env value: local/u);
30
+ });
31
+
32
+ it("throws on a typo'd env value", () => {
33
+ expect(() => resolveConfigFile(['--env', 'production'])).toThrowError(
34
+ /Unknown --env value: production/u,
35
+ );
36
+ expect(() => resolveConfigFile(['--env', 'stg'])).toThrowError(/Unknown --env value: stg/u);
37
+ });
38
+
39
+ it('names the accepted env values in the error message', () => {
40
+ try {
41
+ resolveConfigFile(['--env', 'nope']);
42
+ expect.fail('expected resolveConfigFile to throw');
43
+ } catch (err) {
44
+ const msg = (err as Error).message;
45
+ // Every accepted key is listed so the operator knows the valid set.
46
+ for (const key of Object.keys(ENV_TO_CONFIG_FILE)) {
47
+ expect(msg).toContain(key);
48
+ }
49
+ }
50
+ });
51
+
52
+ it('ignores unrelated argv flags', () => {
53
+ const result = resolveConfigFile(['--verbose', '--env', 'prod', '--dry-run']);
54
+ expect(result.file).toBe('config.prod.json');
55
+ });
56
+
57
+ it('treats a missing value after --env as undefined (falls back to default)', () => {
58
+ // `--env` with no following arg: argv.indexOf('--env') + 1 yields
59
+ // `undefined`, which hits the "no flag" branch.
60
+ const result = resolveConfigFile(['--env']);
61
+ expect(result.file).toBe(DEFAULT_CONFIG_FILE);
62
+ });
63
+ });
@@ -0,0 +1,103 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { parseKiwiConfig } from '../src/config-schema';
6
+
7
+ const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
8
+ const appDist = path.join(pkgRoot, 'dist', 'app');
9
+ const indexPath = path.join(appDist, 'index.html');
10
+ const landingSrc = path.join(pkgRoot, 'landing', 'index.html');
11
+ const landingDist = path.join(pkgRoot, 'dist', 'index.html');
12
+
13
+ // Validates the output of `pnpm --filter web build` (or `build:staging` /
14
+ // `build:prod`). Skipped until the SPA has been built (the Kiwi upstream
15
+ // build is too heavy to run inside the test suite); run the build first to
16
+ // exercise these assertions.
17
+ const built = existsSync(indexPath);
18
+ // The landing page is copied by the same build script but is an independent
19
+ // artifact; gate the build-output assertion on its own presence.
20
+ const landingBuilt = existsSync(landingDist) && statSync(landingDist).size > 0;
21
+
22
+ describe.skipIf(!built)('Kiwi SPA build output (dist/app/)', () => {
23
+ it('emits dist/app/index.html', () => {
24
+ expect(existsSync(indexPath)).toBe(true);
25
+ });
26
+
27
+ it('layers a schema-valid static/config.json into dist/app/static/config.json', () => {
28
+ const configPath = path.join(appDist, 'static', 'config.json');
29
+ expect(existsSync(configPath)).toBe(true);
30
+ // The baked config must clear the Zod schema no matter which --env was
31
+ // selected at build time.
32
+ const cfg = parseKiwiConfig(JSON.parse(readFileSync(configPath, 'utf8')));
33
+ expect(cfg.windowTitle.length).toBeGreaterThan(0);
34
+ expect(cfg.startupOptions.channel.startsWith('#')).toBe(true);
35
+ });
36
+
37
+ it('prefixes every asset reference with /app/', () => {
38
+ const html = readFileSync(indexPath, 'utf8');
39
+ // No bare relative `static/...` references may remain.
40
+ const bareStaticRefs = html.match(/(?:src|href)=["']static\//g) ?? [];
41
+ expect(bareStaticRefs).toEqual([]);
42
+ // At least one asset must be /app/-prefixed (the main bundle).
43
+ const appPrefixed = html.match(/(?:src|href)=["']\/app\/static\//g) ?? [];
44
+ expect(appPrefixed.length).toBeGreaterThan(0);
45
+ });
46
+
47
+ it('ships a hashed JS bundle under dist/app/static/js/', () => {
48
+ const jsDir = path.join(appDist, 'static', 'js');
49
+ expect(existsSync(jsDir)).toBe(true);
50
+ const js = readdirSync(jsDir).filter((f) => f.endsWith('.js'));
51
+ expect(js.length).toBeGreaterThan(0);
52
+ });
53
+ });
54
+
55
+ // The landing page is a static file with no compilation step — the build
56
+ // copies it verbatim to dist/index.html. Validating the source directly means
57
+ // these assertions run in CI without the (heavy) Kiwi build; the build-output
58
+ // describe below verifies the copy is wired up.
59
+ describe('landing page source (landing/index.html)', () => {
60
+ it('exists as a non-empty HTML document', () => {
61
+ expect(existsSync(landingSrc)).toBe(true);
62
+ const html = readFileSync(landingSrc, 'utf8');
63
+ expect(html.length).toBeGreaterThan(0);
64
+ expect(html).toContain('<!doctype html>');
65
+ expect(html).toContain('<title>');
66
+ });
67
+
68
+ it('links to the IRC client at /app/', () => {
69
+ const html = readFileSync(landingSrc, 'utf8');
70
+ expect(html).toMatch(/href=["']\/app\/?["']/);
71
+ });
72
+
73
+ it('links to the project docs', () => {
74
+ const html = readFileSync(landingSrc, 'utf8');
75
+ expect(html).toMatch(/href=["'][^"']*docs\/?["']/);
76
+ });
77
+
78
+ it('links to the source repository', () => {
79
+ const html = readFileSync(landingSrc, 'utf8');
80
+ expect(html.toLowerCase()).toMatch(/github\.com/);
81
+ });
82
+
83
+ it('is responsive (declares a viewport meta tag)', () => {
84
+ const html = readFileSync(landingSrc, 'utf8');
85
+ expect(html).toMatch(/<meta[^>]+name=["']viewport["']/);
86
+ });
87
+
88
+ it('works without JavaScript (no <script> tags — pure static HTML/CSS)', () => {
89
+ const html = readFileSync(landingSrc, 'utf8');
90
+ // The landing page is the project's front door and must render fully
91
+ // without JS (acceptance: "works without JS").
92
+ const scriptTags = html.match(/<script\b/gi) ?? [];
93
+ expect(scriptTags).toEqual([]);
94
+ });
95
+ });
96
+
97
+ describe.skipIf(!landingBuilt)('landing page build output (dist/index.html)', () => {
98
+ it('is a verbatim copy of landing/index.html', () => {
99
+ const src = readFileSync(landingSrc, 'utf8');
100
+ const dist = readFileSync(landingDist, 'utf8');
101
+ expect(dist).toBe(src);
102
+ });
103
+ });
@@ -0,0 +1,432 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { KiwiConfigSchema, parseKiwiConfig } from '../src/config-schema';
6
+
7
+ const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
8
+ const staticDir = path.join(pkgRoot, 'static');
9
+
10
+ function readStaticConfig(name: string): unknown {
11
+ return JSON.parse(readFileSync(path.join(staticDir, name), 'utf8'));
12
+ }
13
+
14
+ // A minimal valid startupOptions payload, extended per test. `direct` + `tls`
15
+ // are included because the Worker is a direct IRC-over-WebSocket server (not
16
+ // a kiwiServer WebIRC gateway), so every real config MUST set both; the
17
+ // schema makes them required to prevent the kiwiServer-gateway regression.
18
+ function validStartupOptions(overrides: Record<string, unknown> = {}): Record<string, unknown> {
19
+ return {
20
+ server: '{{hostname}}',
21
+ direct: true,
22
+ tls: true,
23
+ channel: '#welcome',
24
+ ...overrides,
25
+ };
26
+ }
27
+
28
+ function validConfig(overrides: Record<string, unknown> = {}): Record<string, unknown> {
29
+ return {
30
+ windowTitle: 'ServerlessIRCd',
31
+ startupOptions: validStartupOptions(),
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ // ============================================================================
37
+ // KiwiConfigSchema — required fields (TDD outline: Red #1 — Zod accepts `{}`)
38
+ // ============================================================================
39
+
40
+ describe('KiwiConfigSchema — required fields', () => {
41
+ it('rejects an empty object (missing windowTitle + startupOptions)', () => {
42
+ expect(() => parseKiwiConfig({})).toThrowError(/Invalid Kiwi web config/u);
43
+ });
44
+
45
+ it('rejects a config missing startupOptions', () => {
46
+ expect(() => parseKiwiConfig({ windowTitle: 'ServerlessIRCd' })).toThrowError(
47
+ /startupOptions/u,
48
+ );
49
+ });
50
+
51
+ it('rejects a config missing windowTitle', () => {
52
+ expect(() => parseKiwiConfig({ startupOptions: validStartupOptions() })).toThrowError(
53
+ /windowTitle/u,
54
+ );
55
+ });
56
+
57
+ it('rejects an empty windowTitle', () => {
58
+ expect(() =>
59
+ parseKiwiConfig({ windowTitle: '', startupOptions: validStartupOptions() }),
60
+ ).toThrowError(/windowTitle/u);
61
+ });
62
+
63
+ it('rejects startupOptions missing server (TDD outline #1)', () => {
64
+ expect(() =>
65
+ parseKiwiConfig({ windowTitle: 'x', startupOptions: { channel: '#welcome' } }),
66
+ ).toThrowError(/server/u);
67
+ });
68
+
69
+ it('rejects startupOptions with an empty server', () => {
70
+ expect(() =>
71
+ parseKiwiConfig({
72
+ windowTitle: 'x',
73
+ startupOptions: { server: '', channel: '#welcome' },
74
+ }),
75
+ ).toThrowError(/server/u);
76
+ });
77
+
78
+ it('rejects startupOptions missing channel', () => {
79
+ expect(() =>
80
+ parseKiwiConfig({ windowTitle: 'x', startupOptions: { server: 'irc.x' } }),
81
+ ).toThrowError(/channel/u);
82
+ });
83
+ });
84
+
85
+ // ============================================================================
86
+ // KiwiConfigSchema — port range (TDD outline: Red #3 — `port: 99999` passes)
87
+ // ============================================================================
88
+
89
+ describe('KiwiConfigSchema — port range', () => {
90
+ it('rejects a port above 65535 (TDD outline #3)', () => {
91
+ expect(() =>
92
+ parseKiwiConfig(
93
+ validConfig({
94
+ startupOptions: validStartupOptions({ port: 99999 }),
95
+ }),
96
+ ),
97
+ ).toThrowError(/port/u);
98
+ });
99
+
100
+ it('rejects port 0', () => {
101
+ expect(() =>
102
+ parseKiwiConfig(
103
+ validConfig({
104
+ startupOptions: validStartupOptions({ port: 0 }),
105
+ }),
106
+ ),
107
+ ).toThrowError(/port/u);
108
+ });
109
+
110
+ it('rejects a negative port', () => {
111
+ expect(() =>
112
+ parseKiwiConfig(
113
+ validConfig({
114
+ startupOptions: validStartupOptions({ port: -1 }),
115
+ }),
116
+ ),
117
+ ).toThrowError(/port/u);
118
+ });
119
+
120
+ it('rejects a non-integer port', () => {
121
+ expect(() =>
122
+ parseKiwiConfig(
123
+ validConfig({
124
+ startupOptions: validStartupOptions({ port: 6667.5 }),
125
+ }),
126
+ ),
127
+ ).toThrowError(/port/u);
128
+ });
129
+
130
+ it('accepts port 1 (lower bound)', () => {
131
+ const cfg = parseKiwiConfig(
132
+ validConfig({
133
+ startupOptions: validStartupOptions({ port: 1 }),
134
+ }),
135
+ );
136
+ expect(cfg.startupOptions.port).toBe(1);
137
+ });
138
+
139
+ it('accepts port 65535 (upper bound)', () => {
140
+ const cfg = parseKiwiConfig(
141
+ validConfig({
142
+ startupOptions: validStartupOptions({ port: 65535 }),
143
+ }),
144
+ );
145
+ expect(cfg.startupOptions.port).toBe(65535);
146
+ });
147
+ });
148
+
149
+ // ============================================================================
150
+ // KiwiConfigSchema — channel prefix (TDD outline: Red #5 — `channels: ["foo"]`)
151
+ // ============================================================================
152
+
153
+ describe('KiwiConfigSchema — channel prefix', () => {
154
+ it('rejects a channel without the # prefix (TDD outline #5)', () => {
155
+ expect(() =>
156
+ parseKiwiConfig(
157
+ validConfig({
158
+ startupOptions: validStartupOptions({ channel: 'welcome' }),
159
+ }),
160
+ ),
161
+ ).toThrowError(/channel/u);
162
+ });
163
+
164
+ it('rejects an empty channel', () => {
165
+ expect(() =>
166
+ parseKiwiConfig(
167
+ validConfig({
168
+ startupOptions: validStartupOptions({ channel: '' }),
169
+ }),
170
+ ),
171
+ ).toThrowError(/channel/u);
172
+ });
173
+
174
+ it('accepts a single # channel', () => {
175
+ const cfg = parseKiwiConfig(validConfig());
176
+ expect(cfg.startupOptions.channel).toBe('#welcome');
177
+ });
178
+
179
+ it('accepts a comma-separated channel list when each starts with #', () => {
180
+ const cfg = parseKiwiConfig(
181
+ validConfig({
182
+ startupOptions: validStartupOptions({ channel: '#a,#b' }),
183
+ }),
184
+ );
185
+ expect(cfg.startupOptions.channel).toBe('#a,#b');
186
+ });
187
+ });
188
+
189
+ // ============================================================================
190
+ // KiwiConfigSchema — direct WebSocket mode (bypass the kiwiServer gateway)
191
+ // ============================================================================
192
+ //
193
+ // Kiwi has two connection modes (upstream `src/libs/IrcClient.js:69-82`):
194
+ // • `direct: false` (the Kiwi default) → routes the IRC connection through
195
+ // a server-side WebIRC gateway at `kiwiServer` (default `/webirc/kiwiirc/`)
196
+ // via the ServerSession control channel. The browser first probes
197
+ // `${kiwiServer}/info`; if nothing serves it (our Worker doesn't), the
198
+ // SPA logs a 404 and cannot connect.
199
+ // • `direct: true` → the browser opens a WebSocket straight to
200
+ // `startupOptions.server` (irc-framework's default transport), which is
201
+ // exactly what our Worker serves at `/`.
202
+ //
203
+ // The Worker is NOT a kiwiServer gateway, so every shipped config MUST set
204
+ // `direct: true` (and `tls: true`, since Cloudflare terminates HTTPS and the
205
+ // WS upgrade must be `wss://`). The schema makes both required so a config
206
+ // that regresses to gateway mode fails the build rather than 404-ing at
207
+ // runtime.
208
+
209
+ describe('KiwiConfigSchema — direct WebSocket mode', () => {
210
+ it('rejects startupOptions missing direct (would fall back to the kiwiServer gateway and 404)', () => {
211
+ expect(() =>
212
+ parseKiwiConfig(
213
+ validConfig({
214
+ startupOptions: { server: '{{hostname}}', tls: true, channel: '#welcome' },
215
+ }),
216
+ ),
217
+ ).toThrowError(/direct/u);
218
+ });
219
+
220
+ it('rejects startupOptions missing tls (Cloudflare serves HTTPS; the WS must be wss://)', () => {
221
+ expect(() =>
222
+ parseKiwiConfig(
223
+ validConfig({
224
+ startupOptions: { server: '{{hostname}}', direct: true, channel: '#welcome' },
225
+ }),
226
+ ),
227
+ ).toThrowError(/tls/u);
228
+ });
229
+
230
+ it('accepts direct: true with tls: true', () => {
231
+ const cfg = parseKiwiConfig(validConfig());
232
+ expect(cfg.startupOptions.direct).toBe(true);
233
+ expect(cfg.startupOptions.tls).toBe(true);
234
+ });
235
+ });
236
+
237
+ // ============================================================================
238
+ // KiwiConfigSchema — WS path (direct_path)
239
+ // ============================================================================
240
+ //
241
+ // NOTE: Kiwi reads `startupOptions.direct_path`, NOT `startupOptions.path`
242
+ // (upstream `src/helpers/Misc.js:139`: `connection.direct_path = config.direct_path || ''`).
243
+ // The Worker accepts WS upgrades at `/` (root), so `direct_path` — when
244
+ // supplied — MUST be `/`. It is optional: an empty/absent value lets
245
+ // irc-framework build `wss://host:port` with no path, which the browser
246
+ // normalises to `wss://host:port/` and the Worker accepts.
247
+
248
+ describe('KiwiConfigSchema — WS path (direct_path)', () => {
249
+ it('rejects a direct_path other than / (WS endpoint is at root)', () => {
250
+ expect(() =>
251
+ parseKiwiConfig(
252
+ validConfig({
253
+ startupOptions: validStartupOptions({ direct_path: '/app/' }),
254
+ }),
255
+ ),
256
+ ).toThrowError(/direct_path/u);
257
+ });
258
+
259
+ it('accepts the / direct_path', () => {
260
+ const cfg = parseKiwiConfig(
261
+ validConfig({
262
+ startupOptions: validStartupOptions({ direct_path: '/' }),
263
+ }),
264
+ );
265
+ expect(cfg.startupOptions.direct_path).toBe('/');
266
+ });
267
+
268
+ it('accepts an absent direct_path (irc-framework omits the path; browser defaults to /)', () => {
269
+ const cfg = parseKiwiConfig(validConfig());
270
+ expect(cfg.startupOptions.direct_path).toBeUndefined();
271
+ });
272
+ });
273
+
274
+ // ============================================================================
275
+ // KiwiConfigSchema — passthrough of Kiwi-specific fields
276
+ // ============================================================================
277
+
278
+ describe('KiwiConfigSchema — passthrough', () => {
279
+ it('preserves unknown top-level Kiwi fields (themes, plugins, embedly)', () => {
280
+ const input = validConfig({
281
+ theme: 'Dark',
282
+ themes: [{ name: 'Dark', url: 'static/themes/dark' }],
283
+ embedly: { key: '' },
284
+ plugins: [{ name: 'customise', url: 'static/plugins/customise.html' }],
285
+ });
286
+ const cfg = parseKiwiConfig(input);
287
+ expect(cfg).toHaveProperty('theme', 'Dark');
288
+ expect(cfg).toHaveProperty('embedly');
289
+ });
290
+
291
+ it('preserves unknown startupOptions fields (nick, startupScreen)', () => {
292
+ const input = validConfig({
293
+ startupScreen: 'welcome',
294
+ startupOptions: validStartupOptions({ nick: 'guest-?' }),
295
+ });
296
+ const cfg = parseKiwiConfig(input);
297
+ expect(cfg).toHaveProperty('startupScreen', 'welcome');
298
+ expect(cfg.startupOptions).toHaveProperty('nick', 'guest-?');
299
+ });
300
+ });
301
+
302
+ // ============================================================================
303
+ // parseKiwiConfig — error formatting
304
+ // ============================================================================
305
+
306
+ describe('parseKiwiConfig — error formatting', () => {
307
+ it('throws a single aggregated Error listing every invalid field', () => {
308
+ try {
309
+ parseKiwiConfig({
310
+ windowTitle: '',
311
+ startupOptions: { server: '', port: 99999, channel: 'x' },
312
+ });
313
+ expect.fail('expected parseKiwiConfig to throw');
314
+ } catch (err) {
315
+ expect(err).toBeInstanceOf(Error);
316
+ const msg = String((err as Error).message);
317
+ expect(msg).toMatch(/Invalid Kiwi web config/u);
318
+ // Aggregated: every offending field is named.
319
+ expect(msg).toMatch(/windowTitle/u);
320
+ expect(msg).toMatch(/server/u);
321
+ expect(msg).toMatch(/port/u);
322
+ expect(msg).toMatch(/channel/u);
323
+ }
324
+ });
325
+
326
+ it('rejects non-object inputs', () => {
327
+ expect(() => parseKiwiConfig('hello')).toThrowError(/Invalid Kiwi web config/u);
328
+ expect(() => parseKiwiConfig(null)).toThrowError(/Invalid Kiwi web config/u);
329
+ expect(() => parseKiwiConfig([])).toThrowError(/Invalid Kiwi web config/u);
330
+ });
331
+ });
332
+
333
+ // ============================================================================
334
+ // KiwiConfigSchema — server auto-detect ({{hostname}} template)
335
+ // ============================================================================
336
+ //
337
+ // Kiwi registers a value-replacement pass on every page load (upstream
338
+ // `src/main.js`: `addValueReplacement('hostname', window.location.hostname)`
339
+ // et al.) that substitutes `{{hostname}}` / `{{host}}` / `{{port}}` in any
340
+ // config string at browser load time. Baking `{{hostname}}` into
341
+ // `startupOptions.server` therefore makes the SPA connect to whatever host
342
+ // the Worker served it from — staging, prod, preview, PR deployment — without
343
+ // a per-env hostname baked at build time. This is the deploy-time resolution
344
+ // path; the schema accepts (but no longer requires) a literal hostname so an
345
+ // operator can still pin one for bespoke deployments.
346
+
347
+ describe('KiwiConfigSchema — server auto-detect ({{hostname}} template)', () => {
348
+ it('accepts the {{hostname}} template (Kiwi resolves it to window.location.hostname)', () => {
349
+ const cfg = parseKiwiConfig(
350
+ validConfig({ startupOptions: validStartupOptions({ server: '{{hostname}}' }) }),
351
+ );
352
+ expect(cfg.startupOptions.server).toBe('{{hostname}}');
353
+ });
354
+
355
+ it('accepts the {{host}} template (includes port when non-default)', () => {
356
+ const cfg = parseKiwiConfig(
357
+ validConfig({ startupOptions: validStartupOptions({ server: '{{host}}' }) }),
358
+ );
359
+ expect(cfg.startupOptions.server).toBe('{{host}}');
360
+ });
361
+
362
+ it('still accepts a literal hostname (operator pins a specific target)', () => {
363
+ const cfg = parseKiwiConfig(
364
+ validConfig({ startupOptions: validStartupOptions({ server: 'irc.example.com' }) }),
365
+ );
366
+ expect(cfg.startupOptions.server).toBe('irc.example.com');
367
+ });
368
+ });
369
+
370
+ // ============================================================================
371
+ // Static config files — per-env build matrix
372
+ // ============================================================================
373
+
374
+ describe('static config files', () => {
375
+ it('config.json parses (default = staging target)', () => {
376
+ const cfg = parseKiwiConfig(readStaticConfig('config.json'));
377
+ expect(cfg.startupOptions.server.length).toBeGreaterThan(0);
378
+ expect(cfg.startupOptions.channel.startsWith('#')).toBe(true);
379
+ });
380
+
381
+ it('config.staging.json parses', () => {
382
+ const cfg = parseKiwiConfig(readStaticConfig('config.staging.json'));
383
+ expect(cfg.startupOptions.channel.startsWith('#')).toBe(true);
384
+ });
385
+
386
+ it('config.prod.json parses', () => {
387
+ const cfg = parseKiwiConfig(readStaticConfig('config.prod.json'));
388
+ expect(cfg.startupOptions.channel.startsWith('#')).toBe(true);
389
+ });
390
+
391
+ it('the raw schema also accepts every static config', () => {
392
+ expect(KiwiConfigSchema.safeParse(readStaticConfig('config.json')).success).toBe(true);
393
+ expect(KiwiConfigSchema.safeParse(readStaticConfig('config.staging.json')).success).toBe(true);
394
+ expect(KiwiConfigSchema.safeParse(readStaticConfig('config.prod.json')).success).toBe(true);
395
+ });
396
+
397
+ // Deploy-time resolution: every shipped config uses the {{hostname}} template
398
+ // so the browser resolves `server` from window.location.hostname at load
399
+ // time. This is the whole point of NOT baking a per-env hostname; if any
400
+ // config regresses to a literal host, this assertion fires.
401
+ it('every static config bakes {{hostname}} for server (deploy-time auto-detect)', () => {
402
+ for (const file of ['config.json', 'config.staging.json', 'config.prod.json']) {
403
+ const cfg = parseKiwiConfig(readStaticConfig(file));
404
+ expect(cfg.startupOptions.server, `${file} server`).toBe('{{hostname}}');
405
+ }
406
+ });
407
+
408
+ // The Worker is a direct IRC-over-WebSocket server, NOT a kiwiServer WebIRC
409
+ // gateway. Every shipped config MUST set direct:true + tls:true so Kiwi
410
+ // opens a wss:// WebSocket straight to the Worker instead of probing the
411
+ // (non-existent) /webirc/kiwiirc/info gateway endpoint and 404-ing.
412
+ it('every static config sets direct:true + tls:true (no kiwiServer gateway)', () => {
413
+ for (const file of ['config.json', 'config.staging.json', 'config.prod.json']) {
414
+ const cfg = parseKiwiConfig(readStaticConfig(file));
415
+ expect(cfg.startupOptions.direct, `${file} direct`).toBe(true);
416
+ expect(cfg.startupOptions.tls, `${file} tls`).toBe(true);
417
+ }
418
+ });
419
+
420
+ // Upstream Kiwi ships `static/plugins/customise.html.example` (a template),
421
+ // not `customise.html`. Referencing the un-served `.html` path — which the
422
+ // upstream default config also does — produces a 404 + console error on
423
+ // every page load. We don't customise Kiwi, so no static config may ship a
424
+ // `plugins` block. If plugin support is ever needed, the build must copy
425
+ // `.example` → `.html` (or ship a real plugin file) first.
426
+ it('no static config ships a plugins block (avoids the customise.html 404)', () => {
427
+ for (const file of ['config.json', 'config.staging.json', 'config.prod.json']) {
428
+ const raw = readStaticConfig(file) as Record<string, unknown>;
429
+ expect(raw.plugins, `${file} plugins`).toBeUndefined();
430
+ }
431
+ });
432
+ });
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import pkg from '../package.json' with { type: 'json' };
3
+
4
+ describe('apps/web workspace skeleton', () => {
5
+ it('exposes the @serverless-ircd/web package name', () => {
6
+ expect(pkg.name).toBe('@serverless-ircd/web');
7
+ });
8
+
9
+ it('reserves a build task that the workspace can run', () => {
10
+ expect(typeof pkg.scripts?.build).toBe('string');
11
+ });
12
+ });
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "types": ["node"],
6
+ "composite": false
7
+ },
8
+ "include": ["src/**/*.ts", "tests/**/*.ts"],
9
+ "exclude": ["dist"]
10
+ }
@@ -0,0 +1,3 @@
1
+ > 1%
2
+ last 2 versions
3
+ not ie <= 8
@@ -0,0 +1,9 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ indent_style = space
6
+ indent_size = 4
7
+ end_of_line = lf
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
@@ -0,0 +1,3 @@
1
+ build/*.js
2
+ config/*.js
3
+ tests/unit/coverage/