serverless-ircd 0.6.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 (247) hide show
  1. package/.github/workflows/ci.yml +6 -1
  2. package/.gitmodules +3 -0
  3. package/CHANGELOG.md +214 -0
  4. package/README.md +110 -23
  5. package/apps/aws-stack/package.json +1 -1
  6. package/apps/cf-tcp-container/package.json +1 -1
  7. package/apps/cf-worker/package.json +1 -1
  8. package/apps/cf-worker/src/origin-allowlist.ts +99 -0
  9. package/apps/cf-worker/src/worker.ts +89 -9
  10. package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +18 -0
  11. package/apps/cf-worker/tests/fixtures/web-dist/index.html +18 -0
  12. package/apps/cf-worker/tests/origin-allowlist.test.ts +101 -0
  13. package/apps/cf-worker/tests/smoke.test.ts +28 -2
  14. package/apps/cf-worker/tests/ws-origin.test.ts +89 -0
  15. package/apps/cf-worker/wrangler.test.toml +23 -0
  16. package/apps/cf-worker/wrangler.toml +54 -0
  17. package/apps/local-cli/package.json +1 -1
  18. package/apps/web/landing/index.html +217 -0
  19. package/apps/web/package.json +28 -0
  20. package/apps/web/scripts/build.mjs +125 -0
  21. package/apps/web/src/build-env.ts +43 -0
  22. package/apps/web/src/config-schema.ts +138 -0
  23. package/apps/web/static/config.json +28 -0
  24. package/apps/web/static/config.prod.json +28 -0
  25. package/apps/web/static/config.staging.json +28 -0
  26. package/apps/web/tests/build-env.test.ts +63 -0
  27. package/apps/web/tests/build-smoke.test.ts +103 -0
  28. package/apps/web/tests/config-schema.test.ts +432 -0
  29. package/apps/web/tests/workspace.test.ts +12 -0
  30. package/apps/web/tsconfig.json +10 -0
  31. package/apps/web/upstream/.browserslistrc +3 -0
  32. package/apps/web/upstream/.editorconfig +9 -0
  33. package/apps/web/upstream/.eslintignore +3 -0
  34. package/apps/web/upstream/.eslintrc.js +90 -0
  35. package/apps/web/upstream/.github/FUNDING.yml +12 -0
  36. package/apps/web/upstream/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  37. package/apps/web/upstream/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  38. package/apps/web/upstream/.github/workflows/push.yml +26 -0
  39. package/apps/web/upstream/.github/workflows/release.yml +33 -0
  40. package/apps/web/upstream/.github/workflows/release_manual.yml +55 -0
  41. package/apps/web/upstream/.prettierrc.js +9 -0
  42. package/apps/web/upstream/.stylelintignore +5 -0
  43. package/apps/web/upstream/.stylelintrc.js +21 -0
  44. package/apps/web/upstream/LICENSE +201 -0
  45. package/apps/web/upstream/README.md +76 -0
  46. package/apps/web/upstream/babel.config.js +25 -0
  47. package/apps/web/upstream/crowdin.yml +3 -0
  48. package/apps/web/upstream/index.html +35 -0
  49. package/apps/web/upstream/jest.config.js +7 -0
  50. package/apps/web/upstream/jsdoc.json +22 -0
  51. package/apps/web/upstream/package.json +80 -0
  52. package/apps/web/upstream/scripts/pre-commit +43 -0
  53. package/apps/web/upstream/src/components/App.vue +516 -0
  54. package/apps/web/upstream/src/components/AppSettings.vue +587 -0
  55. package/apps/web/upstream/src/components/AutoComplete.vue +284 -0
  56. package/apps/web/upstream/src/components/Avatar.vue +105 -0
  57. package/apps/web/upstream/src/components/AwayStatusIndicator.vue +77 -0
  58. package/apps/web/upstream/src/components/BufferKey.vue +121 -0
  59. package/apps/web/upstream/src/components/BufferSettings.vue +97 -0
  60. package/apps/web/upstream/src/components/Captcha.vue +85 -0
  61. package/apps/web/upstream/src/components/ChannelBanlist.vue +96 -0
  62. package/apps/web/upstream/src/components/ChannelInfo.vue +159 -0
  63. package/apps/web/upstream/src/components/ChannelInvitelist.vue +276 -0
  64. package/apps/web/upstream/src/components/ChannelList.vue +345 -0
  65. package/apps/web/upstream/src/components/Container.vue +401 -0
  66. package/apps/web/upstream/src/components/ContainerHeader.vue +522 -0
  67. package/apps/web/upstream/src/components/ControlInput.vue +1016 -0
  68. package/apps/web/upstream/src/components/LoadingAnimation.vue +104 -0
  69. package/apps/web/upstream/src/components/MediaViewer.vue +199 -0
  70. package/apps/web/upstream/src/components/MessageInfo.vue +192 -0
  71. package/apps/web/upstream/src/components/MessageList.vue +1028 -0
  72. package/apps/web/upstream/src/components/MessageListAvatar.vue +20 -0
  73. package/apps/web/upstream/src/components/MessageListMessageCompact.vue +361 -0
  74. package/apps/web/upstream/src/components/MessageListMessageInline.vue +257 -0
  75. package/apps/web/upstream/src/components/MessageListMessageModern.vue +430 -0
  76. package/apps/web/upstream/src/components/NetworkSettings.vue +528 -0
  77. package/apps/web/upstream/src/components/Nicklist.vue +309 -0
  78. package/apps/web/upstream/src/components/NicklistUser.vue +181 -0
  79. package/apps/web/upstream/src/components/NotConnected.vue +271 -0
  80. package/apps/web/upstream/src/components/SelfUser.vue +283 -0
  81. package/apps/web/upstream/src/components/ServerSelector.vue +162 -0
  82. package/apps/web/upstream/src/components/ServerView.vue +126 -0
  83. package/apps/web/upstream/src/components/SettingsAdvanced.vue +232 -0
  84. package/apps/web/upstream/src/components/SettingsAliases.vue +121 -0
  85. package/apps/web/upstream/src/components/Sidebar.vue +429 -0
  86. package/apps/web/upstream/src/components/SidebarAboutBuffer.vue +288 -0
  87. package/apps/web/upstream/src/components/SidebarState.vue +145 -0
  88. package/apps/web/upstream/src/components/StartupError.vue +35 -0
  89. package/apps/web/upstream/src/components/StateBrowser.vue +420 -0
  90. package/apps/web/upstream/src/components/StateBrowserBuffer.vue +124 -0
  91. package/apps/web/upstream/src/components/StateBrowserNetwork.vue +823 -0
  92. package/apps/web/upstream/src/components/StateBrowserUsermenu.vue +162 -0
  93. package/apps/web/upstream/src/components/TypingStatusIndicator.vue +34 -0
  94. package/apps/web/upstream/src/components/TypingUsersList.vue +56 -0
  95. package/apps/web/upstream/src/components/UrlEmbed.vue +149 -0
  96. package/apps/web/upstream/src/components/UserBox.vue +728 -0
  97. package/apps/web/upstream/src/components/inputtools/Emoji.vue +67 -0
  98. package/apps/web/upstream/src/components/inputtools/TextStyle.vue +142 -0
  99. package/apps/web/upstream/src/components/startups/CommonLayout.vue +208 -0
  100. package/apps/web/upstream/src/components/startups/CustomServer.vue +495 -0
  101. package/apps/web/upstream/src/components/startups/KiwiBnc.vue +361 -0
  102. package/apps/web/upstream/src/components/startups/Personal.vue +317 -0
  103. package/apps/web/upstream/src/components/startups/Welcome.vue +544 -0
  104. package/apps/web/upstream/src/components/startups/ZncLogin.vue +229 -0
  105. package/apps/web/upstream/src/components/utils/InputConfirm.vue +68 -0
  106. package/apps/web/upstream/src/components/utils/InputPrompt.vue +118 -0
  107. package/apps/web/upstream/src/components/utils/InputText.vue +172 -0
  108. package/apps/web/upstream/src/components/utils/IrcInput.vue +579 -0
  109. package/apps/web/upstream/src/components/utils/PluginWrapper.vue +26 -0
  110. package/apps/web/upstream/src/components/utils/TabbedView.vue +149 -0
  111. package/apps/web/upstream/src/components/utils/TransitionExpand.vue +97 -0
  112. package/apps/web/upstream/src/helpers/Colours.js +128 -0
  113. package/apps/web/upstream/src/helpers/IrcdDiffs.js +26 -0
  114. package/apps/web/upstream/src/helpers/Md5.js +193 -0
  115. package/apps/web/upstream/src/helpers/Misc.js +401 -0
  116. package/apps/web/upstream/src/helpers/TextFormatting.js +237 -0
  117. package/apps/web/upstream/src/libs/AliasRewriter.js +157 -0
  118. package/apps/web/upstream/src/libs/AudioManager.js +60 -0
  119. package/apps/web/upstream/src/libs/BouncerMiddleware.js +247 -0
  120. package/apps/web/upstream/src/libs/BouncerProvider.js +551 -0
  121. package/apps/web/upstream/src/libs/ChathistoryMiddleware.js +153 -0
  122. package/apps/web/upstream/src/libs/ConfigLoader.js +94 -0
  123. package/apps/web/upstream/src/libs/EmojiProvider.js +46 -0
  124. package/apps/web/upstream/src/libs/GlobalApi.js +346 -0
  125. package/apps/web/upstream/src/libs/IPC.js +51 -0
  126. package/apps/web/upstream/src/libs/InputHandler.js +921 -0
  127. package/apps/web/upstream/src/libs/IrcClient.js +1506 -0
  128. package/apps/web/upstream/src/libs/Logger.js +71 -0
  129. package/apps/web/upstream/src/libs/Message.js +169 -0
  130. package/apps/web/upstream/src/libs/MessageFormatter.js +364 -0
  131. package/apps/web/upstream/src/libs/MessageParser.js +249 -0
  132. package/apps/web/upstream/src/libs/Notifications.js +78 -0
  133. package/apps/web/upstream/src/libs/ServerConnection.js +270 -0
  134. package/apps/web/upstream/src/libs/ServerSession.js +102 -0
  135. package/apps/web/upstream/src/libs/SoundBleep.js +20 -0
  136. package/apps/web/upstream/src/libs/StatePersistence.js +90 -0
  137. package/apps/web/upstream/src/libs/ThemeManager.js +128 -0
  138. package/apps/web/upstream/src/libs/TypingMiddleware.js +105 -0
  139. package/apps/web/upstream/src/libs/WindowTitle.js +63 -0
  140. package/apps/web/upstream/src/libs/batchedAdd.js +74 -0
  141. package/apps/web/upstream/src/libs/bufferTools.js +177 -0
  142. package/apps/web/upstream/src/libs/polyfill/Element.closest.js +18 -0
  143. package/apps/web/upstream/src/libs/renderers/Html.js +113 -0
  144. package/apps/web/upstream/src/libs/settingTools.js +31 -0
  145. package/apps/web/upstream/src/libs/state/BufferState.js +703 -0
  146. package/apps/web/upstream/src/libs/state/NetworkState.js +158 -0
  147. package/apps/web/upstream/src/libs/state/UserState.js +89 -0
  148. package/apps/web/upstream/src/libs/state/common.js +26 -0
  149. package/apps/web/upstream/src/libs/state.js +961 -0
  150. package/apps/web/upstream/src/libs/storage/Local.js +51 -0
  151. package/apps/web/upstream/src/main.js +566 -0
  152. package/apps/web/upstream/src/res/autocompleteCommands.js +31 -0
  153. package/apps/web/upstream/src/res/configTemplates.js +373 -0
  154. package/apps/web/upstream/src/res/globalStyle.css +277 -0
  155. package/apps/web/upstream/src/res/kiwiLoadingLogo.png +0 -0
  156. package/apps/web/upstream/src/res/locales/app.af-ZA.po +1145 -0
  157. package/apps/web/upstream/src/res/locales/app.ar-SA.po +1145 -0
  158. package/apps/web/upstream/src/res/locales/app.bg-BG.po +1145 -0
  159. package/apps/web/upstream/src/res/locales/app.bs-BA.po +1145 -0
  160. package/apps/web/upstream/src/res/locales/app.ca-ES.po +1145 -0
  161. package/apps/web/upstream/src/res/locales/app.cs-CZ.po +1145 -0
  162. package/apps/web/upstream/src/res/locales/app.da-DK.po +1145 -0
  163. package/apps/web/upstream/src/res/locales/app.de-DE.po +1145 -0
  164. package/apps/web/upstream/src/res/locales/app.dev.po +1246 -0
  165. package/apps/web/upstream/src/res/locales/app.el-GR.po +1145 -0
  166. package/apps/web/upstream/src/res/locales/app.en-US.po +1145 -0
  167. package/apps/web/upstream/src/res/locales/app.es-419.po +1145 -0
  168. package/apps/web/upstream/src/res/locales/app.es-AR.po +1145 -0
  169. package/apps/web/upstream/src/res/locales/app.es-EM.po +536 -0
  170. package/apps/web/upstream/src/res/locales/app.es-ES.po +1145 -0
  171. package/apps/web/upstream/src/res/locales/app.es-US.po +1145 -0
  172. package/apps/web/upstream/src/res/locales/app.eu-ES.po +1145 -0
  173. package/apps/web/upstream/src/res/locales/app.fi-FI.po +1145 -0
  174. package/apps/web/upstream/src/res/locales/app.fr-FR.po +1145 -0
  175. package/apps/web/upstream/src/res/locales/app.gl-ES.po +1145 -0
  176. package/apps/web/upstream/src/res/locales/app.he-IL.po +1145 -0
  177. package/apps/web/upstream/src/res/locales/app.hi-IN.po +1145 -0
  178. package/apps/web/upstream/src/res/locales/app.hu-HU.po +1145 -0
  179. package/apps/web/upstream/src/res/locales/app.id-ID.po +1145 -0
  180. package/apps/web/upstream/src/res/locales/app.it-IT.po +1145 -0
  181. package/apps/web/upstream/src/res/locales/app.ja-JP.po +1145 -0
  182. package/apps/web/upstream/src/res/locales/app.ko-KR.po +1145 -0
  183. package/apps/web/upstream/src/res/locales/app.nl-NL.po +1145 -0
  184. package/apps/web/upstream/src/res/locales/app.no-NO.po +1145 -0
  185. package/apps/web/upstream/src/res/locales/app.pl-PL.po +1145 -0
  186. package/apps/web/upstream/src/res/locales/app.pt-BR.po +1145 -0
  187. package/apps/web/upstream/src/res/locales/app.pt-PT.po +1145 -0
  188. package/apps/web/upstream/src/res/locales/app.ro-RO.po +1145 -0
  189. package/apps/web/upstream/src/res/locales/app.ru-RU.po +1145 -0
  190. package/apps/web/upstream/src/res/locales/app.sl-SI.po +1145 -0
  191. package/apps/web/upstream/src/res/locales/app.sq-AL.po +1145 -0
  192. package/apps/web/upstream/src/res/locales/app.sr-SP.po +1145 -0
  193. package/apps/web/upstream/src/res/locales/app.sv-SE.po +1145 -0
  194. package/apps/web/upstream/src/res/locales/app.tr-TR.po +1145 -0
  195. package/apps/web/upstream/src/res/locales/app.uk-UA.po +1145 -0
  196. package/apps/web/upstream/src/res/locales/app.vi-VN.po +1145 -0
  197. package/apps/web/upstream/src/res/locales/app.zh-CN.po +1145 -0
  198. package/apps/web/upstream/src/res/locales/app.zh-TW.po +1145 -0
  199. package/apps/web/upstream/src/res/localesList.json +27 -0
  200. package/apps/web/upstream/src/res/logo.png +0 -0
  201. package/apps/web/upstream/src/thirdparty/about.html +43 -0
  202. package/apps/web/upstream/src/thirdparty/index.js +5 -0
  203. package/apps/web/upstream/src/thirdparty/kiwiirccom.vue +185 -0
  204. package/apps/web/upstream/static/config.json +28 -0
  205. package/apps/web/upstream/static/emoticons/smile.png +0 -0
  206. package/apps/web/upstream/static/favicon.png +0 -0
  207. package/apps/web/upstream/static/highlight.mp3 +0 -0
  208. package/apps/web/upstream/static/highlight.ogg +0 -0
  209. package/apps/web/upstream/static/locales/.gitignore +2 -0
  210. package/apps/web/upstream/static/plugins/customise.html.example +10 -0
  211. package/apps/web/upstream/static/themes/coffee/theme.css +145 -0
  212. package/apps/web/upstream/static/themes/common/base.css +1064 -0
  213. package/apps/web/upstream/static/themes/dark/theme.css +196 -0
  214. package/apps/web/upstream/static/themes/default/theme.css +92 -0
  215. package/apps/web/upstream/static/themes/elite/theme.css +248 -0
  216. package/apps/web/upstream/static/themes/grayfox/theme.css +143 -0
  217. package/apps/web/upstream/static/themes/nightswatch/theme.css +367 -0
  218. package/apps/web/upstream/static/themes/osprey/theme.css +115 -0
  219. package/apps/web/upstream/static/themes/radioactive/theme.css +1211 -0
  220. package/apps/web/upstream/static/themes/sky/theme.css +107 -0
  221. package/apps/web/upstream/tests/unit/BatchAdd.spec.js +174 -0
  222. package/apps/web/upstream/tests/unit/MessageParser.spec.js +125 -0
  223. package/apps/web/upstream/tests/unit/Misc.spec.js +24 -0
  224. package/apps/web/upstream/tests/unit/NetworkState.spec.js +58 -0
  225. package/apps/web/upstream/tests/unit/StartupError.spec.js +20 -0
  226. package/apps/web/upstream/vue.config.js +150 -0
  227. package/apps/web/upstream/yarn.lock +9767 -0
  228. package/apps/web/vitest.config.ts +13 -0
  229. package/biome.json +1 -0
  230. package/docs/Home.md +1 -0
  231. package/docs/WebClientGuide.md +535 -0
  232. package/package.json +2 -2
  233. package/packages/aws-adapter/package.json +1 -1
  234. package/packages/cf-adapter/package.json +1 -1
  235. package/packages/cf-adapter/tests/cf-harness.ts +12 -1
  236. package/packages/in-memory-runtime/package.json +1 -1
  237. package/packages/irc-core/package.json +1 -1
  238. package/packages/irc-core/reports/mutation/mutation.html +342 -0
  239. package/packages/irc-core/tests/batch.test.ts +15 -0
  240. package/packages/irc-core/tests/numerics.test.ts +13 -0
  241. package/packages/irc-core/tests/parser.test.ts +109 -0
  242. package/packages/irc-server/package.json +1 -1
  243. package/packages/irc-test-support/package.json +1 -1
  244. package/packages/irc-test-support/src/scenarios.ts +24 -2
  245. package/tools/ci-hardening/package.json +1 -1
  246. package/tools/load-test/package.json +1 -1
  247. package/tools/tcp-ws-forwarder/package.json +1 -1
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'node',
6
+ include: ['tests/**/*.test.ts'],
7
+ coverage: {
8
+ provider: 'v8',
9
+ include: ['src/**/*.ts'],
10
+ reporter: ['text', 'html', 'json-summary'],
11
+ },
12
+ },
13
+ });
package/biome.json CHANGED
@@ -13,6 +13,7 @@
13
13
  "**/coverage-self-test/**",
14
14
  "**/node_modules/**",
15
15
  "**/.turbo/**",
16
+ "apps/web/upstream/**",
16
17
  "pnpm-lock.yaml",
17
18
  "cdk.out/",
18
19
  "*.template.json"
package/docs/Home.md CHANGED
@@ -7,6 +7,7 @@ Welcome to the Wiki.
7
7
  - [AWS TCP+TLS Deployment Guide](AWS-TCP-Deployment)
8
8
  - [Cloudflare Deployment Guide](Cloudflare-Deployment-Guide)
9
9
  - [Cloudflare TCP+TLS Deployment Guide](Cloudflare-TCP-Deployment)
10
+ - [Web Client Guide](WebClientGuide)
10
11
  - [Observability](Observability)
11
12
  - [IRCv3 WebSocket Extension Plan](PlanIRCv3Websocket)
12
13
  - [Web Client Plan](PlanWebClient)
@@ -0,0 +1,535 @@
1
+ # Web Client Guide
2
+
3
+ End-to-end contributor/operator doc for the ServerlessIRCd web client: a
4
+ vendored **Kiwi IRC** SPA served at `/app/` and a static project landing
5
+ page served at `/` by the Cloudflare Worker. The browser opens a native
6
+ `wss://` WebSocket straight to the Worker's existing IRC-over-WebSocket
7
+ endpoint — no proxy, gateway, or transport adaptation in the hot path.
8
+
9
+ Cross-reference: `PLAN.md` §6.1 (CF mapping), `docs/PlanWebClient.md`
10
+ (design), `docs/Cloudflare-Deployment-Guide.md` (Worker deploy),
11
+ `apps/cf-worker/wrangler.toml`, `apps/web/`.
12
+
13
+ **Acceptance criterion (TICKET-139):** a new contributor can
14
+ `git submodule update --init`, `pnpm install`,
15
+ `pnpm --filter web build:staging`, and `wrangler deploy --env staging`
16
+ following only this doc.
17
+
18
+ ---
19
+
20
+ ## 1. How the pieces fit
21
+
22
+ ```
23
+ ┌──────────────────────────────────────────────────────────────┐
24
+ │ Browser │
25
+ │ GET / → landing page (project front door) │
26
+ │ GET /app/ → Kiwi IRC SPA (index.html + JS bundle) │
27
+ │ wss://.../ → one IRC message per WS text frame │
28
+ └──────────────────────────────────────────────────────────────┘
29
+ │ │
30
+ │ HTTPS (static assets) │ wss (IRC-over-WebSocket)
31
+ ▼ ▼
32
+ ┌──────────────────────────────────────────────────────────────┐
33
+ │ apps/cf-worker (Cloudflare Worker) │
34
+ │ fetch(request, env): │
35
+ │ Upgrade: websocket → Origin check → ConnectionDO │
36
+ │ GET /health → plaintext liveness string │
37
+ │ else → env.ASSETS (apps/web/dist) │
38
+ └──────────────────────────────────────────────────────────────┘
39
+ ```
40
+
41
+ The Worker is the single origin for the landing page, the SPA, **and**
42
+ the WebSocket. Same-origin by default — which is what makes the CSWSH
43
+ defense in §5 almost zero-config. A Cloudflare Pages deployment of the
44
+ same `dist/` is a documented alternative for prod (see §7).
45
+
46
+ Path layout on the Worker:
47
+
48
+ | Path | Served by | Content |
49
+ |------------|--------------------------|----------------------------------|
50
+ | `/` | `[assets]` binding | `dist/index.html` (landing page) |
51
+ | `/app/` | `[assets]` binding | `dist/app/index.html` (Kiwi SPA) |
52
+ | `/app/static/...` | `[assets]` binding | Kiwi JS/CSS/themes + baked config |
53
+ | `/health` | `worker.ts` handler | Plaintext liveness string |
54
+ | `/` (WS upgrade) | `worker.ts` → `ConnectionDO` | IRC session |
55
+
56
+ The WS endpoint and the landing page share path `/`. The Worker runs
57
+ first for every request (`run_worker_first = true` in `wrangler.toml`)
58
+ so the `Upgrade: websocket` check fires before the asset platform
59
+ serves `/` — otherwise an upgrade would get a `200 OK` landing page
60
+ instead of a `101` switch.
61
+
62
+ ---
63
+
64
+ ## 2. Prerequisites
65
+
66
+ | Requirement | Version / detail |
67
+ |---|---|
68
+ | Node.js | ≥ 24 (matches CI; `engines.node` in root `package.json`) |
69
+ | pnpm | 11.x (`packageManager` pin in root `package.json`) |
70
+ | yarn | Required **only** to build the SPA. Kiwi ships a `yarn.lock`; `corepack enable` provides yarn on any Node. |
71
+ | `wrangler` CLI | Comes from `apps/cf-worker/devDependencies`; no global install needed. |
72
+ | Cloudflare account | For deploy only (see `docs/Cloudflare-Deployment-Guide.md`). Local dev needs nothing. |
73
+
74
+ Confirm the local environment:
75
+
76
+ ```bash
77
+ node --version # v24.x or newer
78
+ corepack enable # lets the pinned pnpm + yarn run
79
+ pnpm --version # 11.x
80
+ ```
81
+
82
+ ---
83
+
84
+ ## 3. Quickstart (clean checkout → deployed staging SPA)
85
+
86
+ ```bash
87
+ # 1. Pull submodules (docs wiki + Kiwi IRC upstream sources).
88
+ git submodule update --init
89
+
90
+ # 2. Install workspace deps.
91
+ pnpm install
92
+
93
+ # 3. Build the SPA + landing page (default config = staging target).
94
+ pnpm --filter web build:staging
95
+ # Equivalent: pnpm --filter @serverless-ircd/web build:staging
96
+ # → apps/web/dist/index.html (landing page)
97
+ # → apps/web/dist/app/index.html (Kiwi SPA)
98
+ # → apps/web/dist/app/static/config.json (baked, env-specific)
99
+
100
+ # 4. Build the rest of the workspace (Worker imports compiled dist/).
101
+ pnpm build
102
+
103
+ # 5. Serve everything locally (Worker + SPA + landing page).
104
+ pnpm --filter @serverless-ircd/cf-worker dev
105
+ # → http://localhost:8787/ landing page
106
+ # → http://localhost:8787/app/ Kiwi SPA
107
+ # → ws://localhost:8787/ IRC-over-WebSocket (same endpoint)
108
+
109
+ # 6. Deploy staging (Worker + assets in one command).
110
+ pnpm deploy:cf:staging
111
+ # → wrangler deploy --env staging
112
+ ```
113
+
114
+ Open `http://localhost:8787/app/` in a browser — Kiwi boots, reads
115
+ `/app/static/config.json`, and opens a `wss://` (or `ws://` in local
116
+ dev) connection to the same origin.
117
+
118
+ > **Forgot the submodule?** `apps/web/scripts/build.mjs` fails fast:
119
+ > `Kiwi upstream missing at .../upstream. Run
120
+ > 'git submodule update --init apps/web/upstream' first.`
121
+
122
+ ---
123
+
124
+ ## 4. The build pipeline (`apps/web/scripts/build.mjs`)
125
+
126
+ The build is an orchestrated layering over the upstream Kiwi build —
127
+ **we own no IRC logic**, only config + asset placement.
128
+
129
+ ### 4.1 Per-environment config matrix
130
+
131
+ | `pnpm ...` command | Reads | Bakes into `dist/app/static/config.json` |
132
+ |---|---|---|
133
+ | `--filter web build` | `static/config.json` | default (staging-shaped) config |
134
+ | `--filter web build:staging` | `static/config.staging.json` | staging config |
135
+ | `--filter web build:prod` | `static/config.prod.json` | prod config |
136
+
137
+ `--env` is parsed by `src/build-env.ts`; an unknown value aborts the
138
+ build with a readable error. The selected file is validated against the
139
+ Zod schema (`src/config-schema.ts`) **before** it is baked, so a
140
+ malformed config fails the build rather than shipping a broken SPA.
141
+
142
+ ### 4.2 What the build does, in order
143
+
144
+ 1. **Verify the submodule** exists (`upstream/`), or fail with the
145
+ hint quoted above.
146
+ 2. **`yarn install --frozen-lockfile`** inside `upstream/` (only if
147
+ `upstream/node_modules` is missing). `COREPACK_ROOT` is set so yarn
148
+ classic doesn't refuse on the pnpm-managed workspace root. On
149
+ Node ≥ 17 the OpenSSL legacy provider is enabled for webpack 5's md4
150
+ hashing.
151
+ 3. **Patch `upstream/vue.config.js`** `publicPath: ''` →
152
+ `publicPath: '/app/'`, run `yarn build`, then **restore** the file
153
+ in `finally` so the submodule stays clean. This is why every hashed
154
+ asset URL comes out `/app/static/js/app.<hash>.js` instead of
155
+ `static/js/...`, which would 404 at root.
156
+ 4. **Copy `upstream/dist/` → `apps/web/dist/app/`.**
157
+ 5. **Rewrite bare `static/...` refs** in the baked `index.html` (e.g.
158
+ the favicon, which webpack doesn't run through `publicPath`) to
159
+ `/app/static/...`.
160
+ 6. **Validate + bake the per-env config**: read
161
+ `static/config.<env>.json`, run `parseKiwiConfig`, re-serialise
162
+ (stable key order), write to `dist/app/static/config.json`.
163
+ 7. **Copy the landing page** `landing/index.html` → `dist/index.html`.
164
+
165
+ Output is reproducible: same checkout + same `--env` → byte-identical
166
+ `dist/` (modulo upstream webpack hash stability).
167
+
168
+ ### 4.3 The config schema (`src/config-schema.ts`)
169
+
170
+ The schema models only the fields ServerlessIRCd cares about; the rest
171
+ of Kiwi's surface is passed through untouched (`.passthrough()`).
172
+
173
+ ```jsonc
174
+ // static/config.staging.json
175
+ {
176
+ "windowTitle": "ServerlessIRCd (staging)",
177
+ "startupOptions": {
178
+ "server": "{{hostname}}", // resolved to window.location.hostname at boot
179
+ "direct": true, // MUST be true — direct WS, no kiwiServer gateway
180
+ "tls": true, // MUST be true — wss:// (page is HTTPS)
181
+ "port": 443,
182
+ "channel": "#welcome",
183
+ "nick": "Guest"
184
+ }
185
+ }
186
+ ```
187
+
188
+ Field notes (see the schema docstrings for the full rationale):
189
+
190
+ - **`startupOptions.server`** — `{{hostname}}` (or `{{host}}` to keep
191
+ a non-default port) is a Kiwi template resolved by the browser to
192
+ `window.location.hostname` at load time. This is what lets one build
193
+ artifact target staging, prod, preview, and PR deploys without a
194
+ per-env hostname. A literal hostname is also accepted.
195
+ - **`startupOptions.direct`** — MUST be `true`. ServerlessIRCd is a
196
+ direct IRC-over-WebSocket server; the Kiwi default (`false`) routes
197
+ through a `kiwiServer` WebIRC gateway that nothing serves here.
198
+ - **`startupOptions.tls`** — MUST be `true` in any HTTPS deployment
199
+ (browsers refuse `ws://` on an HTTPS page).
200
+ - **`startupOptions.direct_path`** — the WS path. Kiwi reads
201
+ `direct_path` (NOT `path`); the Worker exposes its WS upgrade at
202
+ root, so this MUST be `/` if supplied. Optional — when omitted,
203
+ irc-framework builds `wss://host:port` (no path), which the browser
204
+ normalises to `wss://host:port/`.
205
+ - **`startupOptions.channel`** — must begin with `#` (matches
206
+ `CHANTYPES=#`); comma-separated lists are accepted.
207
+
208
+ A malformed config aborts the build with an aggregated error, e.g.:
209
+
210
+ ```
211
+ config.staging.json: Invalid Kiwi web config:
212
+ - startupOptions.port: port must be a valid TCP port (1-65535)
213
+ - startupOptions.channel: must begin with '#'
214
+ ```
215
+
216
+ ### 4.4 Tests
217
+
218
+ ```bash
219
+ pnpm --filter web test # schema + build-env + workspace tests
220
+ pnpm --filter web typecheck
221
+ ```
222
+
223
+ The schema and build-env tests are pure (no Kiwi build needed). The
224
+ build-smoke tests (`tests/build-smoke.test.ts`) assert the **landing
225
+ page** source unconditionally and the `dist/` output only when a build
226
+ is present (`describe.skipIf(!built)`) — the Kiwi build is too heavy
227
+ for the unit suite, so run `pnpm --filter web build` first to exercise
228
+ those assertions.
229
+
230
+ ---
231
+
232
+ ## 5. CSWSH defense and the `WEB_ORIGINS` var
233
+
234
+ WebSocket upgrades do **not** follow the same-origin policy. A
235
+ malicious page can `new WebSocket('wss://irc.example.com/')` from the
236
+ victim's browser and drive an IRC session with the victim's
237
+ credentials (Cross-Site WebSocket Hijacking, CSWSH). The Worker
238
+ rejects upgrades whose `Origin` is not allowed — see
239
+ `apps/cf-worker/src/origin-allowlist.ts`.
240
+
241
+ ### 5.1 Two enforcement modes (evaluated in order)
242
+
243
+ 1. **Explicit allowlist** — when `WEB_ORIGINS` is set, only listed
244
+ origins proceed. Use this for cross-origin deployments (SPA on a
245
+ different domain than the Worker).
246
+ 2. **Auto-derive (same-origin)** — when `WEB_ORIGINS` is unset/empty
247
+ (the default), the Worker compares the browser's `Origin` against
248
+ the request's own origin (`${protocol}//${host}`). If they match,
249
+ the page opening the WS is the same origin as the server → safe.
250
+
251
+ Non-browser clients (curl, WeeChat, the `tcp-ws-forwarder`, scripted
252
+ test harnesses) never send `Origin`; their upgrades pass through
253
+ unchanged in **both** modes, so existing tooling keeps working.
254
+
255
+ ### 5.2 `WEB_ORIGINS` is optional
256
+
257
+ Because mode 2 (same-origin auto-derive) is the default and requires
258
+ zero per-env configuration, **`WEB_ORIGINS` is optional**. Most
259
+ deployments never need to set it: the SPA and the Worker are on the
260
+ same origin, so the auto-derive check passes for every browser and
261
+ rejects every cross-origin attempt. It works for `*.workers.dev`,
262
+ custom domains, and preview URLs alike.
263
+
264
+ Set `WEB_ORIGINS` only when the SPA is served from a **different**
265
+ origin than the Worker (e.g. a Cloudflare Pages deployment — see §7 —
266
+ or a separate static host). It is a comma-separated list of origins,
267
+ lowercased and whitespace-trimmed:
268
+
269
+ ```toml
270
+ # apps/cf-worker/wrangler.toml — cross-origin example
271
+ [vars]
272
+ WEB_ORIGINS = "https://irc.example.com,https://web.example.com"
273
+ ```
274
+
275
+ ```bash
276
+ # or per-deploy via the secret store / dashboard env vars:
277
+ wrangler secret put WEB_ORIGINS --env staging
278
+ ```
279
+
280
+ ### 5.3 Responses
281
+
282
+ | Scenario | Response |
283
+ |---|---|
284
+ | `Origin` allowed (allowlist or same-origin) | `101 Switching Protocols` → `ConnectionDO` |
285
+ | `Origin` header absent (non-browser client) | `101` (unchanged) |
286
+ | `Origin` present but not allowed | `403 Forbidden` (`WebSocket upgrade refused: the request Origin is not allowed.`) |
287
+ | No allowlist AND `Host` undeterminable | `403 Forbidden` (defensive fail-closed; does not fire on Cloudflare, which always sets `Host`) |
288
+
289
+ The 403 path is unit-tested in `apps/cf-worker/tests/ws-origin.test.ts`
290
+ and the pure policy in `origin-allowlist.test.ts`. There is no
291
+ fail-open path.
292
+
293
+ ---
294
+
295
+ ## 6. Local development
296
+
297
+ ### 6.1 Serve the SPA from the Worker (`wrangler dev`)
298
+
299
+ This is the canonical local loop — same-origin, real `workerd`, real
300
+ Durable Objects:
301
+
302
+ ```bash
303
+ pnpm --filter web build # build the SPA + landing page
304
+ pnpm build # build the rest of the workspace
305
+ pnpm --filter @serverless-ircd/cf-worker dev
306
+ ```
307
+
308
+ Then:
309
+
310
+ - `http://localhost:8787/` — landing page.
311
+ - `http://localhost:8787/app/` — Kiwi SPA (reads
312
+ `/app/static/config.json`, opens `ws://localhost:8787/`).
313
+ - `http://localhost:8787/health` — plaintext liveness.
314
+
315
+ Smoke the WS path directly (no browser needed):
316
+
317
+ ```bash
318
+ node apps/cf-worker/scripts/smoke.mjs
319
+ # → replays CONNECT/NICK/USER/JOIN/PRIVMSG/QUIT, asserts 001/376/353/366
320
+ ```
321
+
322
+ ### 6.2 Iterate on the SPA config without rebuilding Kiwi
323
+
324
+ The Kiwi upstream build is the slow step. For a config-only change,
325
+ edit `static/config.staging.json`, then re-run
326
+ `pnpm --filter web build:staging` — step 2 of the build reuses
327
+ `upstream/node_modules` and skips straight to re-baking the config
328
+ (steps 4–6 are fast).
329
+
330
+ ### 6.3 Iterate on the landing page
331
+
332
+ `landing/index.html` is pure static HTML + inline CSS, no JavaScript,
333
+ no build step. Edit it and re-run the build (it is copied verbatim to
334
+ `dist/index.html`); or just preview it directly in a browser — it has
335
+ no runtime dependencies.
336
+
337
+ ---
338
+
339
+ ## 7. Optional: Cloudflare Pages instead of Worker assets
340
+
341
+ The build output (`apps/web/dist/`) is identical for both hosting
342
+ options; the choice is a deploy-time knob, not a build-time one. Use
343
+ Pages when you want to decouple static deploys from Worker deploys
344
+ (e.g. the frontend changes more often than the IRCd), or when prod
345
+ favors a dedicated static edge.
346
+
347
+ ### 7.1 Set up Pages
348
+
349
+ 1. Push the repo (or just `apps/web/dist/`) to a Pages-connected
350
+ branch, or use `wrangler pages deploy apps/web/dist`.
351
+ 2. Set the Pages project's output directory to `apps/web/dist` and the
352
+ build command to `pnpm --filter web build:prod` (plus `pnpm build`
353
+ if the Worker is deployed from the same repo).
354
+ 3. The Pages project is reachable at
355
+ `https://<project>.pages.dev` (or a custom domain).
356
+
357
+ ### 7.2 Allow the Pages origin on the Worker
358
+
359
+ Because the SPA is now on a **different** origin than the Worker, the
360
+ same-origin auto-derive (§5.1 mode 2) will reject the browser's WS
361
+ upgrade. Set `WEB_ORIGINS` on the Worker to the Pages origin:
362
+
363
+ ```toml
364
+ # apps/cf-worker/wrangler.toml
365
+ [vars]
366
+ WEB_ORIGINS = "https://<project>.pages.dev"
367
+ ```
368
+
369
+ The SPA config's `startupOptions.server` still resolves to the
370
+ **Worker's** hostname (where the WS endpoint lives), not the Pages
371
+ hostname — so set it to a literal if you've pinned it, or keep
372
+ `{{hostname}}` only if Pages and the Worker share a domain.
373
+
374
+ ---
375
+
376
+ ## 8. Optional: `webircgateway` SockJS fallback
377
+
378
+ ServerlessIRCd already speaks the wire format browser Kiwi expects:
379
+ native WebSocket text frames, one IRC message per frame, with
380
+ `\r\n`-joined-frame tolerance. The Worker's WS endpoint at `/` is all
381
+ Kiwi needs. **You do not need `webircgateway` for normal operation.**
382
+
383
+ When is it useful? Corporate proxies and some mobile carriers block
384
+ raw `wss://` (or upgrade it in ways that break long-lived frames).
385
+ `webircgateway` provides a SockJS + Kiwi multi-server transport that
386
+ falls back to HTTP polling when raw WS is unavailable. If your users
387
+ report "can't connect from the office Wi-Fi", that's the trigger.
388
+
389
+ The gateway is vendored separately (see the `webircgateway/` submodule
390
+ if present in your checkout). Running it as a sidecar is a deployment
391
+ concern outside this doc's scope; the key point is that it is
392
+ **strictly opt-in**. The default SPA build does not reference it, and
393
+ no `kiwiServer` / gateway URL is baked into the config.
394
+
395
+ To wire it up when needed, point Kiwi's `startupOptions.kiwiServer`
396
+ at the gateway and set `startupOptions.direct: false` — the opposite
397
+ of the ServerlessIRCd default. Document this only for the affected
398
+ deployment; don't change the default config.
399
+
400
+ ---
401
+
402
+ ## 9. Customising the landing page
403
+
404
+ The landing page (`apps/web/landing/index.html`) is the project's front
405
+ door: a visitor hitting `/` learns what ServerlessIRCd is and clicks
406
+ through to `/app/` to launch the client. It is intentionally
407
+ framework-free (pure HTML + inline CSS, no JS) so it renders without
408
+ JavaScript and costs nothing to serve.
409
+
410
+ To customise:
411
+
412
+ - **Copy/branding** — edit the `<header>`, `<main>`, and `<footer>`
413
+ blocks directly. The CSS variables at the top of the `<style>` block
414
+ (`--bg`, `--surface`, `--accent`, …) theme the whole page in one
415
+ place.
416
+ - **Links** — the page links to `/app/` (the SPA), `docs/`, and the
417
+ source repo. Update the repo URLs and the docs path to match your
418
+ deployment.
419
+ - **Accessibility** — keep the declared viewport meta tag and the
420
+ no-`<script>` invariant; both are asserted by
421
+ `tests/build-smoke.test.ts`. The build copies the file verbatim to
422
+ `dist/index.html`, so what you author is exactly what is served.
423
+
424
+ Re-run `pnpm --filter web build` (any `--env`) after edits; the build
425
+ test suite will fail loudly if you accidentally remove the `/app/`
426
+ link, the docs link, or the viewport tag.
427
+
428
+ ---
429
+
430
+ ## 10. Troubleshooting
431
+
432
+ ### 10.1 `Kiwi upstream missing at .../upstream`
433
+
434
+ The submodule wasn't initialised. Run:
435
+
436
+ ```bash
437
+ git submodule update --init apps/web/upstream
438
+ ```
439
+
440
+ The build fails fast with this hint rather than a confusing webpack
441
+ error.
442
+
443
+ ### 10.2 SPA loads but never connects ("connection refused" / spinning)
444
+
445
+ Almost always a config issue. Open the browser devtools Network tab
446
+ and check the WS upgrade:
447
+
448
+ - **`ws://` on an HTTPS page** — `startupOptions.tls` must be `true`
449
+ (browsers refuse insecure WS from secure pages). The default configs
450
+ set it; a hand-edited config may not.
451
+ - **`direct: false`** — Kiwi is probing a `kiwiServer` gateway that
452
+ nothing serves. `startupOptions.direct` MUST be `true`.
453
+ - **Wrong server** — if you pinned a literal `server`, it must be the
454
+ Worker's hostname. `{{hostname}}` resolves to
455
+ `window.location.hostname` and is the safest default.
456
+ - **403 on upgrade** — the `Origin` was rejected (§5). Check
457
+ `WEB_ORIGINS` if you're cross-origin, or confirm the SPA and Worker
458
+ are same-origin.
459
+
460
+ ### 10.3 Asset 404s at `/static/...` (instead of `/app/static/...`)
461
+
462
+ The `publicPath` patch in step 3 of the build didn't take. Confirm
463
+ `upstream/vue.config.js` still contains a `publicPath: ''` literal to
464
+ patch against (the build restores it after building). If upstream
465
+ changed the phrasing, the patch regex in `scripts/build.mjs` needs an
466
+ update.
467
+
468
+ ### 10.4 `200 OK` instead of `101 Switching Protocols` on WS upgrade
469
+
470
+ The asset platform served the landing page for the upgrade request.
471
+ This means `run_worker_first = true` is missing or false in the
472
+ `[assets]` block of `wrangler.toml`. The WS endpoint and the landing
473
+ page share path `/`, so the Worker MUST run first for everything and
474
+ fall through to assets only for non-WS requests.
475
+
476
+ ### 10.5 `Cannot find module '@serverless-ircd/cf-adapter'`
477
+
478
+ The workspace packages weren't built. Run `pnpm build` from the repo
479
+ root before `pnpm --filter web build` or `wrangler dev` — the build
480
+ script and the Worker entry import compiled `dist/` from the workspace
481
+ packages.
482
+
483
+ ### 10.6 Build-smoke tests fail in CI but pass locally
484
+
485
+ The `dist/`-dependent assertions in `tests/build-smoke.test.ts` are
486
+ gated on `existsSync(dist/app/index.html)` and are skipped when the
487
+ SPA hasn't been built. CI runs `pnpm --filter web build` (or
488
+ `pnpm build`) before `pnpm --filter web test`, so the assertions fire
489
+ there. If you run `pnpm --filter web test` locally without building,
490
+ only the landing-page source assertions run — that's expected, not a
491
+ regression.
492
+
493
+ ---
494
+
495
+ ## 11. Quick reference
496
+
497
+ ```bash
498
+ # One-time / per-clone
499
+ corepack enable
500
+ git submodule update --init # docs + apps/web/upstream (Kiwi)
501
+ pnpm install
502
+
503
+ # Build the SPA + landing page (pick the env)
504
+ pnpm --filter web build # default config (staging-shaped)
505
+ pnpm --filter web build:staging # static/config.staging.json
506
+ pnpm --filter web build:prod # static/config.prod.json
507
+
508
+ # Local dev (Worker serves SPA + landing page + WS on one origin)
509
+ pnpm build # build the rest of the workspace
510
+ pnpm --filter @serverless-ircd/cf-worker dev # http://localhost:8787
511
+ node apps/cf-worker/scripts/smoke.mjs # WS smoke
512
+
513
+ # Deploy staging (Worker + assets in one command)
514
+ pnpm deploy:cf:staging # wrangler deploy --env staging
515
+
516
+ # Tests / lint
517
+ pnpm --filter web test
518
+ pnpm --filter web typecheck
519
+ pnpm lint
520
+ ```
521
+
522
+ Key files:
523
+
524
+ | Path | What |
525
+ |---|---|
526
+ | `apps/web/scripts/build.mjs` | Orchestrates the upstream Kiwi build + layers our config. |
527
+ | `apps/web/src/config-schema.ts` | Zod schema for `static/config.*.json` (the only TS we own). |
528
+ | `apps/web/src/build-env.ts` | `--env` → config-file resolution (unit-tested). |
529
+ | `apps/web/static/config.{,staging,prod}.json` | Per-env Kiwi configs (validated at build time). |
530
+ | `apps/web/landing/index.html` | Static landing page (project front door), copied to `dist/index.html`. |
531
+ | `apps/web/tests/build-smoke.test.ts` | Landing-page + build-output assertions. |
532
+ | `apps/cf-worker/src/worker.ts` | Edge handler: WS → `ConnectionDO`, `/health`, else `env.ASSETS`. |
533
+ | `apps/cf-worker/src/origin-allowlist.ts` | CSWSH `Origin` policy (pure, unit-tested). |
534
+ | `apps/cf-worker/wrangler.toml` | `[assets]` binding + per-env blocks + `WEB_ORIGINS` var. |
535
+ | `apps/web/upstream/` | Kiwi IRC source (git submodule, tag-pinned). |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serverless-ircd",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "private": false,
5
5
  "description": "Serverless IRC daemon with a platform-agnostic core and Cloudflare Workers + AWS adapters",
6
6
  "license": "BSD-3-Clause",
@@ -17,7 +17,7 @@
17
17
  "typescript": "^5.9.3",
18
18
  "vite": "^7.3.6",
19
19
  "vitest": "^4.1.10",
20
- "@serverless-ircd/aws-adapter": "0.6.0"
20
+ "@serverless-ircd/aws-adapter": "0.7.0"
21
21
  },
22
22
  "scripts": {
23
23
  "build": "turbo run build",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverless-ircd/aws-adapter",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "private": true,
5
5
  "description": "AWS Lambda + DynamoDB adapter: AwsRuntime implementing IrcRuntime + $connect/$disconnect/$default handlers",
6
6
  "license": "BSD-3-Clause",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverless-ircd/cf-adapter",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "private": true,
5
5
  "description": "Cloudflare Workers adapter: Durable Objects (ConnectionDO, RegistryDO, ChannelDO) and CfRuntime implementing IrcRuntime",
6
6
  "license": "BSD-3-Clause",
@@ -119,7 +119,18 @@ class CfHarness implements IrcHarness {
119
119
  if (opts?.nick !== undefined) {
120
120
  await client.send(`NICK ${opts.nick}`);
121
121
  await client.send(`USER ${opts.nick} 0 * :${opts.nick}`);
122
- await client.waitForNumeric('001', DEFAULT_TIMEOUT_MS);
122
+ // Wait for the FULL registration block, not just 001. Unlike the
123
+ // in-memory runtime (where the whole welcome fires synchronously
124
+ // inside `send`), CF delivers the 001..376 lines asynchronously
125
+ // through the WebSocket. Returning on 001 leaves 002..376 in flight,
126
+ // which can pollute `received` after a test calls `received.length = 0`
127
+ // and trip "no numeric" assertions on slow/loaded runners. The
128
+ // MOTD tailer (376 RPL_ENDOFMOTD, or 422 ERR_NOMOTD when no MOTD is
129
+ // configured) is the last frame of the registration burst.
130
+ await client.waitForLine(
131
+ (l) => numericEquals(l, '376') || numericEquals(l, '422'),
132
+ DEFAULT_TIMEOUT_MS,
133
+ );
123
134
  this.nicks.set(opts.nick.toLowerCase(), client);
124
135
  }
125
136
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverless-ircd/in-memory-runtime",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "private": true,
5
5
  "description": "Single-process reference implementation of the IrcRuntime port, backed by Maps",
6
6
  "license": "BSD-3-Clause",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverless-ircd/irc-core",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "private": true,
5
5
  "description": "Platform-agnostic IRC protocol core: parser, serializer, command reducers, state shapes",
6
6
  "license": "BSD-3-Clause",