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,217 @@
1
+ <!doctype html>
2
+ <!--
3
+ ServerlessIRCd — static landing page (project front door).
4
+
5
+ Served at `/` by the Worker `[assets]` binding (apps/cf-worker/wrangler.toml).
6
+ The build (`scripts/build.mjs`) copies this file verbatim to `dist/index.html`.
7
+ The Kiwi IRC SPA lives at `/app/`; this page links through to it plus the
8
+ project docs and source repo. Pure static HTML + inline CSS, no JavaScript.
9
+ -->
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="utf-8" />
13
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
14
+ <title>ServerlessIRCd — Serverless IRC Daemon</title>
15
+ <meta
16
+ name="description"
17
+ content="A serverless IRC daemon: one TypeScript core, running on Cloudflare Workers or AWS."
18
+ />
19
+ <style>
20
+ :root {
21
+ --bg: #0d1117;
22
+ --surface: #161b22;
23
+ --border: #30363d;
24
+ --text: #e6edf3;
25
+ --muted: #8b949e;
26
+ --accent: #58a6ff;
27
+ --accent-hover: #79c0ff;
28
+ --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
29
+ "Liberation Mono", monospace;
30
+ --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,
31
+ sans-serif;
32
+ }
33
+ * {
34
+ box-sizing: border-box;
35
+ }
36
+ html {
37
+ -webkit-text-size-adjust: 100%;
38
+ }
39
+ body {
40
+ margin: 0;
41
+ font-family: var(--sans);
42
+ line-height: 1.6;
43
+ color: var(--text);
44
+ background-color: var(--bg);
45
+ display: flex;
46
+ flex-direction: column;
47
+ min-height: 100vh;
48
+ }
49
+ .container {
50
+ width: 100%;
51
+ max-width: 720px;
52
+ margin: 0 auto;
53
+ padding: 0 20px;
54
+ }
55
+ header {
56
+ padding: 32px 0 16px;
57
+ border-bottom: 1px solid var(--border);
58
+ }
59
+ header h1 {
60
+ margin: 0;
61
+ font-size: 1.75rem;
62
+ letter-spacing: -0.02em;
63
+ }
64
+ header h1 .irc {
65
+ color: var(--accent);
66
+ font-family: var(--mono);
67
+ }
68
+ header p {
69
+ margin: 8px 0 0;
70
+ color: var(--muted);
71
+ font-size: 1.05rem;
72
+ }
73
+ main {
74
+ flex: 1 0 auto;
75
+ padding: 32px 0;
76
+ }
77
+ main p {
78
+ font-size: 1.05rem;
79
+ }
80
+ .cta {
81
+ display: flex;
82
+ flex-wrap: wrap;
83
+ gap: 12px;
84
+ margin: 28px 0;
85
+ }
86
+ .cta a {
87
+ display: inline-block;
88
+ padding: 12px 22px;
89
+ border-radius: 8px;
90
+ text-decoration: none;
91
+ font-weight: 600;
92
+ font-size: 1.02rem;
93
+ border: 1px solid var(--border);
94
+ transition: background-color 0.15s, border-color 0.15s;
95
+ }
96
+ .cta a.primary {
97
+ background-color: var(--accent);
98
+ color: #0d1117;
99
+ border-color: var(--accent);
100
+ }
101
+ .cta a.primary:hover {
102
+ background-color: var(--accent-hover);
103
+ border-color: var(--accent-hover);
104
+ }
105
+ .cta a.secondary {
106
+ background-color: var(--surface);
107
+ color: var(--text);
108
+ }
109
+ .cta a.secondary:hover {
110
+ border-color: var(--accent);
111
+ }
112
+ ul.features {
113
+ list-style: none;
114
+ padding: 0;
115
+ margin: 24px 0;
116
+ display: grid;
117
+ gap: 10px;
118
+ }
119
+ ul.features li {
120
+ padding-left: 24px;
121
+ position: relative;
122
+ color: var(--text);
123
+ }
124
+ ul.features li::before {
125
+ content: "\203A";
126
+ position: absolute;
127
+ left: 0;
128
+ color: var(--accent);
129
+ font-weight: 700;
130
+ }
131
+ code {
132
+ font-family: var(--mono);
133
+ background-color: var(--surface);
134
+ padding: 2px 6px;
135
+ border-radius: 4px;
136
+ font-size: 0.9em;
137
+ }
138
+ .links {
139
+ margin-top: 28px;
140
+ padding: 20px 0;
141
+ border-top: 1px solid var(--border);
142
+ display: flex;
143
+ flex-wrap: wrap;
144
+ gap: 8px 24px;
145
+ }
146
+ .links a {
147
+ color: var(--accent);
148
+ text-decoration: none;
149
+ }
150
+ .links a:hover {
151
+ text-decoration: underline;
152
+ }
153
+ footer {
154
+ flex-shrink: 0;
155
+ padding: 20px 0;
156
+ border-top: 1px solid var(--border);
157
+ color: var(--muted);
158
+ font-size: 0.9rem;
159
+ }
160
+ footer a {
161
+ color: var(--muted);
162
+ text-decoration: none;
163
+ }
164
+ footer a:hover {
165
+ color: var(--accent);
166
+ }
167
+ </style>
168
+ </head>
169
+ <body>
170
+ <header>
171
+ <div class="container">
172
+ <h1>Serverless<span class="irc">IRCd</span></h1>
173
+ <p>A serverless IRC daemon. One TypeScript core, two serverless substrates.</p>
174
+ </div>
175
+ </header>
176
+
177
+ <main>
178
+ <div class="container">
179
+ <p>
180
+ ServerlessIRCd is an IRC server where the protocol logic lives in a
181
+ pure, platform-agnostic core, and two thin adapters run it on
182
+ <strong>Cloudflare Workers</strong> (Durable Objects) or
183
+ <strong>AWS</strong> (API Gateway WebSockets + Lambda + DynamoDB).
184
+ The same code, unchanged, on both.
185
+ </p>
186
+
187
+ <ul class="features">
188
+ <li>Pure-reducer protocol core — every command is a trivial unit test</li>
189
+ <li>Dual transport: WebSocket <em>and</em> <code>irc+tls</code> on port 6697</li>
190
+ <li>Full IRCv3 surface: message-tags, chathistory, SASL, labeled-response, and more</li>
191
+ <li>Authoritative state in one place per datum — races are structurally impossible</li>
192
+ </ul>
193
+
194
+ <div class="cta">
195
+ <a class="primary" href="/app/">Launch Web Client</a>
196
+ <a class="secondary" href="docs/">Read the Docs</a>
197
+ </div>
198
+
199
+ <div class="links">
200
+ <a href="https://github.com/fowlmouth/ServerlessIRCd">Source Code (GitHub)</a>
201
+ <a href="https://github.com/fowlmouth/ServerlessIRCd/issues">Issue Tracker</a>
202
+ <a href="/app/">Web IRC Client</a>
203
+ </div>
204
+ </div>
205
+ </main>
206
+
207
+ <footer>
208
+ <div class="container">
209
+ <p>
210
+ ServerlessIRCd &middot;
211
+ <a href="https://github.com/fowlmouth/ServerlessIRCd">GitHub</a> &middot;
212
+ BSD-3-Clause License
213
+ </p>
214
+ </div>
215
+ </footer>
216
+ </body>
217
+ </html>
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@serverless-ircd/web",
3
+ "version": "0.7.0",
4
+ "private": true,
5
+ "description": "Web client workspace: Kiwi IRC SPA served at /app/ and project landing page at /",
6
+ "license": "BSD-3-Clause",
7
+ "type": "module",
8
+ "scripts": {
9
+ "build": "node scripts/build.mjs",
10
+ "build:staging": "node scripts/build.mjs --env staging",
11
+ "build:prod": "node scripts/build.mjs --env prod",
12
+ "typecheck": "tsc -p tsconfig.json --noEmit",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "coverage": "vitest run --coverage",
16
+ "clean": "rimraf dist coverage .turbo"
17
+ },
18
+ "dependencies": {
19
+ "zod": "^3.25.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^26.1.1",
23
+ "@vitest/coverage-v8": "^4.1.0",
24
+ "rimraf": "^6.0.0",
25
+ "typescript": "^5.6.0",
26
+ "vitest": "^4.1.0"
27
+ }
28
+ }
@@ -0,0 +1,125 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { resolveConfigFile } from '../src/build-env.ts';
7
+ import { parseKiwiConfig } from '../src/config-schema.ts';
8
+
9
+ const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
+ const upstream = path.resolve(pkgRoot, 'upstream');
11
+ const dist = path.resolve(pkgRoot, 'dist');
12
+ const appDist = path.join(dist, 'app');
13
+ const staticDir = path.join(pkgRoot, 'static');
14
+
15
+ // The SPA is served at /app/ on the Worker (the [assets] binding arrives in a
16
+ // later ticket). All hashed asset URLs must be /app/-prefixed so they load
17
+ // from /app/static/... rather than /static/... (which would 404 at root).
18
+ const PUBLIC_PATH = '/app/';
19
+
20
+ // Yarn runs from a pnpm lifecycle script. Kiwi v1.7.1's package.json has no
21
+ // `packageManager` field, so yarn classic walks up and finds the workspace
22
+ // root's `pnpm@...` pin and refuses to run. COREPACK_ROOT tells yarn it is
23
+ // corepack-managed and skips that check. --ignore-engines lets the upstream
24
+ // tree resolve on Node newer than a few transitive deps declare
25
+ // (e.g. @achrinza/node-ipc). On Node >= 17, webpack 5's md4 hashing needs the
26
+ // OpenSSL legacy provider.
27
+ function yarnEnv() {
28
+ const major = Number.parseInt(process.versions.node.split('.')[0] ?? '0', 10);
29
+ const existing = process.env.NODE_OPTIONS ?? '';
30
+ const nodeOptions =
31
+ major >= 17 && !existing.includes('openssl-legacy-provider')
32
+ ? `${existing} --openssl-legacy-provider`.trim()
33
+ : existing;
34
+ return { ...process.env, COREPACK_ROOT: upstream, NODE_OPTIONS: nodeOptions || undefined };
35
+ }
36
+
37
+ function runYarn(args) {
38
+ execFileSync('yarn', args, { cwd: upstream, stdio: 'inherit', env: yarnEnv() });
39
+ }
40
+
41
+ if (!existsSync(upstream)) {
42
+ throw new Error(
43
+ `Kiwi upstream missing at ${upstream}. Run \`git submodule update --init apps/web/upstream\` first.`,
44
+ );
45
+ }
46
+
47
+ // 1. Install upstream deps. Kiwi ships a yarn.lock; --frozen-lockfile gives a
48
+ // reproducible tree. Contributors get yarn via `corepack enable`.
49
+ if (!existsSync(path.join(upstream, 'node_modules'))) {
50
+ runYarn(['install', '--frozen-lockfile', '--ignore-engines']);
51
+ }
52
+
53
+ // 2. Build with publicPath=/app/. Upstream pins publicPath:'' (relative); we
54
+ // temporarily patch vue.config.js so webpack emits /app/-prefixed asset
55
+ // URLs and the runtime public_path matches the mount point. Restored in
56
+ // `finally` so the submodule stays clean.
57
+ const vueConfigPath = path.join(upstream, 'vue.config.js');
58
+ const originalVueConfig = await readFile(vueConfigPath, 'utf8');
59
+ try {
60
+ const patched = originalVueConfig.replace(/publicPath:\s*''/, `publicPath: '${PUBLIC_PATH}'`);
61
+ if (patched === originalVueConfig) {
62
+ throw new Error('Could not locate `publicPath` in upstream vue.config.js; patching failed.');
63
+ }
64
+ await writeFile(vueConfigPath, patched);
65
+ runYarn(['build']);
66
+ } finally {
67
+ await writeFile(vueConfigPath, originalVueConfig);
68
+ }
69
+
70
+ // 3. Copy the built SPA into dist/app/.
71
+ await rm(appDist, { recursive: true, force: true });
72
+ await mkdir(appDist, { recursive: true });
73
+ await cp(path.join(upstream, 'dist'), appDist, { recursive: true });
74
+
75
+ // 3b. Webpack applies publicPath to the bundles it emits, but template-hardcoded
76
+ // refs (e.g. the favicon) stay relative. Prefix every bare `static/...`
77
+ // reference so all asset URLs resolve to /app/static/... when served at /app/.
78
+ const indexDest = path.join(appDist, 'index.html');
79
+ let indexHtml = await readFile(indexDest, 'utf8');
80
+ indexHtml = indexHtml.replace(/((?:src|href)=["'])(static\/)/g, `$1${PUBLIC_PATH}$2`);
81
+ await writeFile(indexDest, indexHtml);
82
+
83
+ // 4. Layer our per-env static/config.*.json over the upstream default. The
84
+ // selected file is parsed against the Zod schema (`src/config-schema.ts`)
85
+ // so missing fields, out-of-range ports, and non-`#` channels abort the
86
+ // build here with a readable error. The validated JSON is re-serialised
87
+ // (stable key order) so the baked config is identical to the schema output.
88
+ const { file: configFileName, env: configEnv } = resolveConfigFile(process.argv.slice(2));
89
+ const configSrcPath = path.join(staticDir, configFileName);
90
+ if (!existsSync(configSrcPath)) {
91
+ throw new Error(
92
+ `Config file for env "${configEnv}" not found at ${configSrcPath}. Expected one of: config.json, config.staging.json, config.prod.json.`,
93
+ );
94
+ }
95
+ const configRaw = await readFile(configSrcPath, 'utf8');
96
+ let configParsed;
97
+ try {
98
+ configParsed = parseKiwiConfig(JSON.parse(configRaw));
99
+ } catch (err) {
100
+ // Re-throw with the file name prepended so the build error names the
101
+ // offending env config, not just the schema path.
102
+ throw new Error(`${configFileName}: ${err.message}`, { cause: err });
103
+ }
104
+ await writeFile(
105
+ path.join(appDist, 'static', 'config.json'),
106
+ `${JSON.stringify(configParsed, null, 2)}\n`,
107
+ 'utf8',
108
+ );
109
+
110
+ // 5. Landing page: copy the static landing page (landing/index.html) to
111
+ // dist/index.html so the Worker `[assets]` binding serves it at `/`. Pure
112
+ // static HTML + inline CSS (no JS); links to /app/ (the SPA), docs/, and
113
+ // the source repo.
114
+ await mkdir(dist, { recursive: true });
115
+ const landingSrc = path.join(pkgRoot, 'landing', 'index.html');
116
+ if (!existsSync(landingSrc)) {
117
+ throw new Error(
118
+ `Landing page source missing at ${landingSrc}. Expected apps/web/landing/index.html.`,
119
+ );
120
+ }
121
+ await cp(landingSrc, path.join(dist, 'index.html'));
122
+
123
+ console.log(
124
+ `Kiwi SPA built -> dist/app/ (publicPath=${PUBLIC_PATH}, config=${configFileName}, env=${configEnv})`,
125
+ );
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Per-env config-file resolution for the SPA build (`scripts/build.mjs`).
3
+ *
4
+ * Extracted from the build script so the `--env` → config-file mapping is
5
+ * unit-testable without running the (slow) Kiwi upstream build. The build
6
+ * script delegates here and then validates the selected file against
7
+ * {@link parseKiwiConfig} before baking it into `dist/app/static/config.json`.
8
+ */
9
+
10
+ /** argv values accepted by `--env`; mapped to a `static/config.<env>.json` file. */
11
+ export const ENV_TO_CONFIG_FILE = {
12
+ staging: 'config.staging.json',
13
+ prod: 'config.prod.json',
14
+ } as const;
15
+
16
+ export type BuildEnv = keyof typeof ENV_TO_CONFIG_FILE;
17
+
18
+ /** Default config file baked when `--env` is not supplied (staging target). */
19
+ export const DEFAULT_CONFIG_FILE = 'config.json';
20
+
21
+ /**
22
+ * Resolves which `static/config.*.json` the build should bake from the
23
+ * `--env <staging|prod>` CLI flag.
24
+ *
25
+ * @param argv the CLI args after the script name (typically `process.argv.slice(2)`)
26
+ * @returns `{ file, env }` where `file` is the basename under `static/` and
27
+ * `env` is a human-readable label for the build log.
28
+ * @throws {Error} when `--env` is supplied with an unknown value.
29
+ */
30
+ export function resolveConfigFile(argv: string[]): { file: string; env: string } {
31
+ const envIndex = argv.indexOf('--env');
32
+ const env = envIndex !== -1 ? argv[envIndex + 1] : undefined;
33
+ if (env === undefined) {
34
+ return { file: DEFAULT_CONFIG_FILE, env: 'default (staging)' };
35
+ }
36
+ const file = ENV_TO_CONFIG_FILE[env as BuildEnv];
37
+ if (file === undefined) {
38
+ throw new Error(
39
+ `Unknown --env value: ${env}. Expected one of: ${Object.keys(ENV_TO_CONFIG_FILE).join(', ')}.`,
40
+ );
41
+ }
42
+ return { file, env };
43
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Kiwi IRC web-client config schema (Zod) — the single source of truth for
3
+ * what the per-environment `static/config.{staging,prod,}.json` files must
4
+ * supply to drive the embedded Kiwi SPA.
5
+ *
6
+ * `build.mjs` runs {@link parseKiwiConfig} against the selected env config
7
+ * before baking it into `dist/app/static/config.json`, so a malformed config
8
+ * fails the build with a readable aggregated error rather than shipping a
9
+ * broken SPA. The schema only models the fields ServerlessIRCd cares about
10
+ * (server, port, channel, WS path); the rest of Kiwi's surface is passed
11
+ * through untouched.
12
+ *
13
+ * Hosting layout reminder:
14
+ * - The Worker's WebSocket endpoint is at `/` (root), so Kiwi's
15
+ * `startupOptions.path` — when supplied — MUST be `/`.
16
+ * - The SPA itself is served from `/app/` by the Worker `[assets]`
17
+ * binding (a separate concern handled by the public-path patch in
18
+ * `build.mjs`); that is independent of the WS path modelled here.
19
+ */
20
+
21
+ import { z } from 'zod';
22
+
23
+ /**
24
+ * Regex for a single IRC channel name as advertised by this deployment.
25
+ * `CHANTYPES=#` is the default (`packages/irc-core/src/config.ts`), so the
26
+ * auto-join channel must begin with `#`. Comma-separated lists are accepted
27
+ * (Kiwi's `channel` field may hold several at once); each member is validated
28
+ * implicitly because the leading `#` gate is on the whole string.
29
+ */
30
+ const CHANNEL_PATTERN = /^#/u;
31
+
32
+ /**
33
+ * Schema for Kiwi's `startupOptions` block. Only the ServerlessIRCd-relevant
34
+ * fields are modelled; the object is passthrough so Kiwi's many other knobs
35
+ * (nick, gecos, ircd password, SASL, ...) survive the round-trip.
36
+ */
37
+ export const KiwiStartupOptionsSchema = z
38
+ .object({
39
+ /**
40
+ * IRC server hostname the browser client connects to (the Worker's public
41
+ * hostname, or whatever reverse-proxy fronts it). Required — a missing or
42
+ * empty value is the most common misconfiguration and breaks the SPA at
43
+ * first paint.
44
+ *
45
+ * Deploy-time auto-detect: bake the Kiwi template `{{hostname}}` (or
46
+ * `{{host}}` to include a non-default port) and the browser resolves it
47
+ * to `window.location.hostname` at load time, so the same build artifact
48
+ * targets staging, prod, preview, and PR deployments without a per-env
49
+ * hostname. Kiwi registers the value-replacement pass unconditionally on
50
+ * every page load (upstream `src/main.js`). A literal hostname is still
51
+ * accepted for operators who want to pin a specific target.
52
+ */
53
+ server: z.string().min(1),
54
+ /**
55
+ * IRC server port. Optional; when omitted, irc-framework selects 443 for
56
+ * `tls: true` and 80 for `tls: false` (upstream `Welcome.vue:437-444`).
57
+ * When supplied it must be a valid TCP port (1-65535); out-of-range values
58
+ * produce a confusing "connection refused" at runtime.
59
+ */
60
+ port: z.number().int().min(1).max(65535).optional(),
61
+ /**
62
+ * Auto-join channel(s). Must begin with `#` to match `CHANTYPES`. May be a
63
+ * comma-separated list (`#a,#b`); Kiwi splits and joins each.
64
+ */
65
+ channel: z.string().regex(CHANNEL_PATTERN),
66
+ /**
67
+ * Direct-WebSocket mode. MUST be `true` for ServerlessIRCd: the Worker is
68
+ * a direct IRC-over-WebSocket server, not a kiwiServer WebIRC gateway. If
69
+ * `false` (the Kiwi default), Kiwi routes the connection through the
70
+ * gateway at `kiwiServer` (default `/webirc/kiwiirc/`) and probes
71
+ * `${kiwiServer}/info`; nothing serves that on the Worker, so the SPA
72
+ * 404s and cannot connect. With `true`, irc-framework opens a WebSocket
73
+ * straight to `server`:`port` (`IrcClient.js:79-82`).
74
+ */
75
+ direct: z.boolean(),
76
+ /**
77
+ * TLS for the IRC connection. MUST be `true` on Cloudflare: the Worker is
78
+ * served over HTTPS, so the WS upgrade must be `wss://` (browsers refuse
79
+ * `ws://` on an HTTPS page). irc-framework's WS transport prefixes the
80
+ * address with `wss://` when `tls` is truthy (`transports/websocket.js:65`).
81
+ */
82
+ tls: z.boolean(),
83
+ /**
84
+ * WebSocket path on the server. Kiwi reads `direct_path` (NOT `path`) —
85
+ * upstream `helpers/Misc.js:139` copies `config.direct_path` into the
86
+ * connection info; a `path` field is silently ignored. The Worker exposes
87
+ * its WS upgrade at root (`worker.ts` routes the upgrade before any path
88
+ * dispatch), so any path other than `/` would miss the handler. Optional
89
+ * — when omitted irc-framework builds `wss://host:port` with no path,
90
+ * which the browser normalises to `wss://host:port/`.
91
+ */
92
+ direct_path: z.literal('/').optional(),
93
+ })
94
+ .passthrough();
95
+
96
+ /**
97
+ * Top-level Kiwi config schema. `windowTitle` is the one field every
98
+ * ServerlessIRCd deployment MUST set (it is what the browser tab shows and
99
+ * what the build-smoke test asserts against); everything else at the top
100
+ * level (theme, themes, embedly, plugins, startupScreen, ...) is passed
101
+ * through verbatim so Kiwi's full configuration surface remains available.
102
+ */
103
+ export const KiwiConfigSchema = z
104
+ .object({
105
+ windowTitle: z.string().min(1),
106
+ startupOptions: KiwiStartupOptionsSchema,
107
+ })
108
+ .passthrough();
109
+
110
+ /** Parsed Kiwi config: all modelled fields populated, plus any passthroughs. */
111
+ export type KiwiConfig = z.infer<typeof KiwiConfigSchema>;
112
+
113
+ /** Parsed `startupOptions` block. */
114
+ export type KiwiStartupOptions = z.infer<typeof KiwiStartupOptionsSchema>;
115
+
116
+ /**
117
+ * Parses a raw config value (typically the JSON-decoded contents of a
118
+ * `static/config.*.json` file) against {@link KiwiConfigSchema}, throwing a
119
+ * single aggregated {@link Error} on failure.
120
+ *
121
+ * `build.mjs` calls this exactly once per build, against the env-selected
122
+ * config file, so the build fails fast with every offending field named
123
+ * rather than shipping a broken SPA. The thrown message starts with
124
+ * "Invalid Kiwi web config" and lists one bullet per Zod issue.
125
+ *
126
+ * @throws {Error} when `input` fails schema validation.
127
+ */
128
+ export function parseKiwiConfig(input: unknown): KiwiConfig {
129
+ const result = KiwiConfigSchema.safeParse(input);
130
+ if (!result.success) {
131
+ const lines = result.error.issues.map((issue) => {
132
+ const key = issue.path.length > 0 ? issue.path.join('.') : '<root>';
133
+ return ` - ${key}: ${issue.message}`;
134
+ });
135
+ throw new Error(`Invalid Kiwi web config:\n${lines.join('\n')}`);
136
+ }
137
+ return result.data;
138
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "windowTitle": "ServerlessIRCd",
3
+ "startupScreen": "welcome",
4
+ "restricted": false,
5
+ "theme": "Dark",
6
+ "themes": [
7
+ { "name": "Default", "url": "static/themes/default" },
8
+ { "name": "Dark", "url": "static/themes/dark" },
9
+ { "name": "Coffee", "url": "static/themes/coffee" },
10
+ { "name": "GrayFox", "url": "static/themes/grayfox" },
11
+ { "name": "Nightswatch", "url": "static/themes/nightswatch" },
12
+ { "name": "Osprey", "url": "static/themes/osprey" },
13
+ { "name": "Radioactive", "url": "static/themes/radioactive" },
14
+ { "name": "Sky", "url": "static/themes/sky" },
15
+ { "name": "Elite", "url": "static/themes/elite" }
16
+ ],
17
+ "startupOptions": {
18
+ "server": "{{hostname}}",
19
+ "direct": true,
20
+ "tls": true,
21
+ "port": 443,
22
+ "channel": "#welcome",
23
+ "nick": "Guest"
24
+ },
25
+ "embedly": {
26
+ "key": ""
27
+ }
28
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "windowTitle": "ServerlessIRCd",
3
+ "startupScreen": "welcome",
4
+ "restricted": false,
5
+ "theme": "Dark",
6
+ "themes": [
7
+ { "name": "Default", "url": "static/themes/default" },
8
+ { "name": "Dark", "url": "static/themes/dark" },
9
+ { "name": "Coffee", "url": "static/themes/coffee" },
10
+ { "name": "GrayFox", "url": "static/themes/grayfox" },
11
+ { "name": "Nightswatch", "url": "static/themes/nightswatch" },
12
+ { "name": "Osprey", "url": "static/themes/osprey" },
13
+ { "name": "Radioactive", "url": "static/themes/radioactive" },
14
+ { "name": "Sky", "url": "static/themes/sky" },
15
+ { "name": "Elite", "url": "static/themes/elite" }
16
+ ],
17
+ "startupOptions": {
18
+ "server": "{{hostname}}",
19
+ "direct": true,
20
+ "tls": true,
21
+ "port": 443,
22
+ "channel": "#welcome",
23
+ "nick": "Guest"
24
+ },
25
+ "embedly": {
26
+ "key": ""
27
+ }
28
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "windowTitle": "ServerlessIRCd (staging)",
3
+ "startupScreen": "welcome",
4
+ "restricted": false,
5
+ "theme": "Dark",
6
+ "themes": [
7
+ { "name": "Default", "url": "static/themes/default" },
8
+ { "name": "Dark", "url": "static/themes/dark" },
9
+ { "name": "Coffee", "url": "static/themes/coffee" },
10
+ { "name": "GrayFox", "url": "static/themes/grayfox" },
11
+ { "name": "Nightswatch", "url": "static/themes/nightswatch" },
12
+ { "name": "Osprey", "url": "static/themes/osprey" },
13
+ { "name": "Radioactive", "url": "static/themes/radioactive" },
14
+ { "name": "Sky", "url": "static/themes/sky" },
15
+ { "name": "Elite", "url": "static/themes/elite" }
16
+ ],
17
+ "startupOptions": {
18
+ "server": "{{hostname}}",
19
+ "direct": true,
20
+ "tls": true,
21
+ "port": 443,
22
+ "channel": "#welcome",
23
+ "nick": "Guest"
24
+ },
25
+ "embedly": {
26
+ "key": ""
27
+ }
28
+ }