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.
- package/.github/workflows/ci.yml +6 -1
- package/.gitmodules +3 -0
- package/CHANGELOG.md +214 -0
- package/README.md +110 -23
- package/apps/aws-stack/package.json +1 -1
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/src/origin-allowlist.ts +99 -0
- package/apps/cf-worker/src/worker.ts +89 -9
- package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +18 -0
- package/apps/cf-worker/tests/fixtures/web-dist/index.html +18 -0
- package/apps/cf-worker/tests/origin-allowlist.test.ts +101 -0
- package/apps/cf-worker/tests/smoke.test.ts +28 -2
- package/apps/cf-worker/tests/ws-origin.test.ts +89 -0
- package/apps/cf-worker/wrangler.test.toml +23 -0
- package/apps/cf-worker/wrangler.toml +54 -0
- package/apps/local-cli/package.json +1 -1
- package/apps/web/landing/index.html +217 -0
- package/apps/web/package.json +28 -0
- package/apps/web/scripts/build.mjs +125 -0
- package/apps/web/src/build-env.ts +43 -0
- package/apps/web/src/config-schema.ts +138 -0
- package/apps/web/static/config.json +28 -0
- package/apps/web/static/config.prod.json +28 -0
- package/apps/web/static/config.staging.json +28 -0
- package/apps/web/tests/build-env.test.ts +63 -0
- package/apps/web/tests/build-smoke.test.ts +103 -0
- package/apps/web/tests/config-schema.test.ts +432 -0
- package/apps/web/tests/workspace.test.ts +12 -0
- package/apps/web/tsconfig.json +10 -0
- package/apps/web/upstream/.browserslistrc +3 -0
- package/apps/web/upstream/.editorconfig +9 -0
- package/apps/web/upstream/.eslintignore +3 -0
- package/apps/web/upstream/.eslintrc.js +90 -0
- package/apps/web/upstream/.github/FUNDING.yml +12 -0
- package/apps/web/upstream/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- package/apps/web/upstream/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/apps/web/upstream/.github/workflows/push.yml +26 -0
- package/apps/web/upstream/.github/workflows/release.yml +33 -0
- package/apps/web/upstream/.github/workflows/release_manual.yml +55 -0
- package/apps/web/upstream/.prettierrc.js +9 -0
- package/apps/web/upstream/.stylelintignore +5 -0
- package/apps/web/upstream/.stylelintrc.js +21 -0
- package/apps/web/upstream/LICENSE +201 -0
- package/apps/web/upstream/README.md +76 -0
- package/apps/web/upstream/babel.config.js +25 -0
- package/apps/web/upstream/crowdin.yml +3 -0
- package/apps/web/upstream/index.html +35 -0
- package/apps/web/upstream/jest.config.js +7 -0
- package/apps/web/upstream/jsdoc.json +22 -0
- package/apps/web/upstream/package.json +80 -0
- package/apps/web/upstream/scripts/pre-commit +43 -0
- package/apps/web/upstream/src/components/App.vue +516 -0
- package/apps/web/upstream/src/components/AppSettings.vue +587 -0
- package/apps/web/upstream/src/components/AutoComplete.vue +284 -0
- package/apps/web/upstream/src/components/Avatar.vue +105 -0
- package/apps/web/upstream/src/components/AwayStatusIndicator.vue +77 -0
- package/apps/web/upstream/src/components/BufferKey.vue +121 -0
- package/apps/web/upstream/src/components/BufferSettings.vue +97 -0
- package/apps/web/upstream/src/components/Captcha.vue +85 -0
- package/apps/web/upstream/src/components/ChannelBanlist.vue +96 -0
- package/apps/web/upstream/src/components/ChannelInfo.vue +159 -0
- package/apps/web/upstream/src/components/ChannelInvitelist.vue +276 -0
- package/apps/web/upstream/src/components/ChannelList.vue +345 -0
- package/apps/web/upstream/src/components/Container.vue +401 -0
- package/apps/web/upstream/src/components/ContainerHeader.vue +522 -0
- package/apps/web/upstream/src/components/ControlInput.vue +1016 -0
- package/apps/web/upstream/src/components/LoadingAnimation.vue +104 -0
- package/apps/web/upstream/src/components/MediaViewer.vue +199 -0
- package/apps/web/upstream/src/components/MessageInfo.vue +192 -0
- package/apps/web/upstream/src/components/MessageList.vue +1028 -0
- package/apps/web/upstream/src/components/MessageListAvatar.vue +20 -0
- package/apps/web/upstream/src/components/MessageListMessageCompact.vue +361 -0
- package/apps/web/upstream/src/components/MessageListMessageInline.vue +257 -0
- package/apps/web/upstream/src/components/MessageListMessageModern.vue +430 -0
- package/apps/web/upstream/src/components/NetworkSettings.vue +528 -0
- package/apps/web/upstream/src/components/Nicklist.vue +309 -0
- package/apps/web/upstream/src/components/NicklistUser.vue +181 -0
- package/apps/web/upstream/src/components/NotConnected.vue +271 -0
- package/apps/web/upstream/src/components/SelfUser.vue +283 -0
- package/apps/web/upstream/src/components/ServerSelector.vue +162 -0
- package/apps/web/upstream/src/components/ServerView.vue +126 -0
- package/apps/web/upstream/src/components/SettingsAdvanced.vue +232 -0
- package/apps/web/upstream/src/components/SettingsAliases.vue +121 -0
- package/apps/web/upstream/src/components/Sidebar.vue +429 -0
- package/apps/web/upstream/src/components/SidebarAboutBuffer.vue +288 -0
- package/apps/web/upstream/src/components/SidebarState.vue +145 -0
- package/apps/web/upstream/src/components/StartupError.vue +35 -0
- package/apps/web/upstream/src/components/StateBrowser.vue +420 -0
- package/apps/web/upstream/src/components/StateBrowserBuffer.vue +124 -0
- package/apps/web/upstream/src/components/StateBrowserNetwork.vue +823 -0
- package/apps/web/upstream/src/components/StateBrowserUsermenu.vue +162 -0
- package/apps/web/upstream/src/components/TypingStatusIndicator.vue +34 -0
- package/apps/web/upstream/src/components/TypingUsersList.vue +56 -0
- package/apps/web/upstream/src/components/UrlEmbed.vue +149 -0
- package/apps/web/upstream/src/components/UserBox.vue +728 -0
- package/apps/web/upstream/src/components/inputtools/Emoji.vue +67 -0
- package/apps/web/upstream/src/components/inputtools/TextStyle.vue +142 -0
- package/apps/web/upstream/src/components/startups/CommonLayout.vue +208 -0
- package/apps/web/upstream/src/components/startups/CustomServer.vue +495 -0
- package/apps/web/upstream/src/components/startups/KiwiBnc.vue +361 -0
- package/apps/web/upstream/src/components/startups/Personal.vue +317 -0
- package/apps/web/upstream/src/components/startups/Welcome.vue +544 -0
- package/apps/web/upstream/src/components/startups/ZncLogin.vue +229 -0
- package/apps/web/upstream/src/components/utils/InputConfirm.vue +68 -0
- package/apps/web/upstream/src/components/utils/InputPrompt.vue +118 -0
- package/apps/web/upstream/src/components/utils/InputText.vue +172 -0
- package/apps/web/upstream/src/components/utils/IrcInput.vue +579 -0
- package/apps/web/upstream/src/components/utils/PluginWrapper.vue +26 -0
- package/apps/web/upstream/src/components/utils/TabbedView.vue +149 -0
- package/apps/web/upstream/src/components/utils/TransitionExpand.vue +97 -0
- package/apps/web/upstream/src/helpers/Colours.js +128 -0
- package/apps/web/upstream/src/helpers/IrcdDiffs.js +26 -0
- package/apps/web/upstream/src/helpers/Md5.js +193 -0
- package/apps/web/upstream/src/helpers/Misc.js +401 -0
- package/apps/web/upstream/src/helpers/TextFormatting.js +237 -0
- package/apps/web/upstream/src/libs/AliasRewriter.js +157 -0
- package/apps/web/upstream/src/libs/AudioManager.js +60 -0
- package/apps/web/upstream/src/libs/BouncerMiddleware.js +247 -0
- package/apps/web/upstream/src/libs/BouncerProvider.js +551 -0
- package/apps/web/upstream/src/libs/ChathistoryMiddleware.js +153 -0
- package/apps/web/upstream/src/libs/ConfigLoader.js +94 -0
- package/apps/web/upstream/src/libs/EmojiProvider.js +46 -0
- package/apps/web/upstream/src/libs/GlobalApi.js +346 -0
- package/apps/web/upstream/src/libs/IPC.js +51 -0
- package/apps/web/upstream/src/libs/InputHandler.js +921 -0
- package/apps/web/upstream/src/libs/IrcClient.js +1506 -0
- package/apps/web/upstream/src/libs/Logger.js +71 -0
- package/apps/web/upstream/src/libs/Message.js +169 -0
- package/apps/web/upstream/src/libs/MessageFormatter.js +364 -0
- package/apps/web/upstream/src/libs/MessageParser.js +249 -0
- package/apps/web/upstream/src/libs/Notifications.js +78 -0
- package/apps/web/upstream/src/libs/ServerConnection.js +270 -0
- package/apps/web/upstream/src/libs/ServerSession.js +102 -0
- package/apps/web/upstream/src/libs/SoundBleep.js +20 -0
- package/apps/web/upstream/src/libs/StatePersistence.js +90 -0
- package/apps/web/upstream/src/libs/ThemeManager.js +128 -0
- package/apps/web/upstream/src/libs/TypingMiddleware.js +105 -0
- package/apps/web/upstream/src/libs/WindowTitle.js +63 -0
- package/apps/web/upstream/src/libs/batchedAdd.js +74 -0
- package/apps/web/upstream/src/libs/bufferTools.js +177 -0
- package/apps/web/upstream/src/libs/polyfill/Element.closest.js +18 -0
- package/apps/web/upstream/src/libs/renderers/Html.js +113 -0
- package/apps/web/upstream/src/libs/settingTools.js +31 -0
- package/apps/web/upstream/src/libs/state/BufferState.js +703 -0
- package/apps/web/upstream/src/libs/state/NetworkState.js +158 -0
- package/apps/web/upstream/src/libs/state/UserState.js +89 -0
- package/apps/web/upstream/src/libs/state/common.js +26 -0
- package/apps/web/upstream/src/libs/state.js +961 -0
- package/apps/web/upstream/src/libs/storage/Local.js +51 -0
- package/apps/web/upstream/src/main.js +566 -0
- package/apps/web/upstream/src/res/autocompleteCommands.js +31 -0
- package/apps/web/upstream/src/res/configTemplates.js +373 -0
- package/apps/web/upstream/src/res/globalStyle.css +277 -0
- package/apps/web/upstream/src/res/kiwiLoadingLogo.png +0 -0
- package/apps/web/upstream/src/res/locales/app.af-ZA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ar-SA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.bg-BG.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.bs-BA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ca-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.cs-CZ.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.da-DK.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.de-DE.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.dev.po +1246 -0
- package/apps/web/upstream/src/res/locales/app.el-GR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.en-US.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-419.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-AR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-EM.po +536 -0
- package/apps/web/upstream/src/res/locales/app.es-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-US.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.eu-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.fi-FI.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.fr-FR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.gl-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.he-IL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.hi-IN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.hu-HU.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.id-ID.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.it-IT.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ja-JP.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ko-KR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.nl-NL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.no-NO.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pl-PL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pt-BR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pt-PT.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ro-RO.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ru-RU.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sl-SI.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sq-AL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sr-SP.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sv-SE.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.tr-TR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.uk-UA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.vi-VN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.zh-CN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.zh-TW.po +1145 -0
- package/apps/web/upstream/src/res/localesList.json +27 -0
- package/apps/web/upstream/src/res/logo.png +0 -0
- package/apps/web/upstream/src/thirdparty/about.html +43 -0
- package/apps/web/upstream/src/thirdparty/index.js +5 -0
- package/apps/web/upstream/src/thirdparty/kiwiirccom.vue +185 -0
- package/apps/web/upstream/static/config.json +28 -0
- package/apps/web/upstream/static/emoticons/smile.png +0 -0
- package/apps/web/upstream/static/favicon.png +0 -0
- package/apps/web/upstream/static/highlight.mp3 +0 -0
- package/apps/web/upstream/static/highlight.ogg +0 -0
- package/apps/web/upstream/static/locales/.gitignore +2 -0
- package/apps/web/upstream/static/plugins/customise.html.example +10 -0
- package/apps/web/upstream/static/themes/coffee/theme.css +145 -0
- package/apps/web/upstream/static/themes/common/base.css +1064 -0
- package/apps/web/upstream/static/themes/dark/theme.css +196 -0
- package/apps/web/upstream/static/themes/default/theme.css +92 -0
- package/apps/web/upstream/static/themes/elite/theme.css +248 -0
- package/apps/web/upstream/static/themes/grayfox/theme.css +143 -0
- package/apps/web/upstream/static/themes/nightswatch/theme.css +367 -0
- package/apps/web/upstream/static/themes/osprey/theme.css +115 -0
- package/apps/web/upstream/static/themes/radioactive/theme.css +1211 -0
- package/apps/web/upstream/static/themes/sky/theme.css +107 -0
- package/apps/web/upstream/tests/unit/BatchAdd.spec.js +174 -0
- package/apps/web/upstream/tests/unit/MessageParser.spec.js +125 -0
- package/apps/web/upstream/tests/unit/Misc.spec.js +24 -0
- package/apps/web/upstream/tests/unit/NetworkState.spec.js +58 -0
- package/apps/web/upstream/tests/unit/StartupError.spec.js +20 -0
- package/apps/web/upstream/vue.config.js +150 -0
- package/apps/web/upstream/yarn.lock +9767 -0
- package/apps/web/vitest.config.ts +13 -0
- package/biome.json +1 -0
- package/docs/Home.md +1 -0
- package/docs/WebClientGuide.md +535 -0
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/cf-adapter/package.json +1 -1
- package/packages/cf-adapter/tests/cf-harness.ts +12 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/reports/mutation/mutation.html +342 -0
- package/packages/irc-core/tests/batch.test.ts +15 -0
- package/packages/irc-core/tests/numerics.test.ts +13 -0
- package/packages/irc-core/tests/parser.test.ts +109 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/scenarios.ts +24 -2
- package/tools/ci-hardening/package.json +1 -1
- package/tools/load-test/package.json +1 -1
- package/tools/tcp-ws-forwarder/package.json +1 -1
package/.github/workflows/ci.yml
CHANGED
|
@@ -151,7 +151,12 @@ jobs:
|
|
|
151
151
|
run: pnpm install --frozen-lockfile
|
|
152
152
|
|
|
153
153
|
- name: Build workspace (irc-core mutation targets)
|
|
154
|
-
|
|
154
|
+
# Scope the build to irc-core and its workspace deps only. The
|
|
155
|
+
# mutation spot-check mutates irc-core/protocol and irc-core/commands,
|
|
156
|
+
# so building the rest of the workspace (notably @serverless-ircd/web,
|
|
157
|
+
# which spawns `yarn` to build the Kiwi upstream SPA) is unnecessary
|
|
158
|
+
# and fails on runner images that don't ship yarn.
|
|
159
|
+
run: pnpm turbo run build --filter=@serverless-ircd/irc-core...
|
|
155
160
|
|
|
156
161
|
- name: Stryker spot-check on irc-core/protocol
|
|
157
162
|
# Mutation score threshold >=80% is enforced by the
|
package/.gitmodules
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,220 @@ For the release process itself — versioning policy, pre-release checklist,
|
|
|
10
10
|
cutting a tag, rolling back — see [`docs/release.md`](docs/release.md).
|
|
11
11
|
Cross-reference `progress.md` / `tickets.md` for per-ticket detail.
|
|
12
12
|
|
|
13
|
+
## [0.7.0] - 2026-08-04
|
|
14
|
+
|
|
15
|
+
The headline is the **web client (Phase 14)**: a vendored **Kiwi IRC**
|
|
16
|
+
SPA served at `/app/` and a static project landing page served at `/`
|
|
17
|
+
directly by the Cloudflare Worker, with **Cross-Site WebSocket
|
|
18
|
+
Hijacking (CSWSH) defense** wired into the WS upgrade path. The browser
|
|
19
|
+
opens a native `wss://` straight to the existing IRC-over-WebSocket
|
|
20
|
+
endpoint — no proxy, gateway, or transport adaptation in the hot path.
|
|
21
|
+
Alongside it: a round of **test hardening** (mutation-killing tests for
|
|
22
|
+
`irc-core`'s parser / batch / numerics, and CF-harness / scenario fixes
|
|
23
|
+
for slow transports).
|
|
24
|
+
|
|
25
|
+
### Added — Web client (`@serverless-ircd/web`, `@serverless-ircd/cf-worker`)
|
|
26
|
+
|
|
27
|
+
Six Phase 14 tickets light up the project's browser surface. The Worker
|
|
28
|
+
becomes the single origin for the landing page, the SPA, **and** the
|
|
29
|
+
WebSocket. Same-origin by default — which is what makes the CSWSH
|
|
30
|
+
defense almost zero-config.
|
|
31
|
+
|
|
32
|
+
- **`apps/web` workspace package (`@serverless-ircd/web`)**: a new
|
|
33
|
+
pnpm workspace package owning the Kiwi build orchestration and config
|
|
34
|
+
schema. The package layout reserves `dist/app/` for the Kiwi SPA
|
|
35
|
+
(served at `/app/`) and `dist/index.html` for the landing page
|
|
36
|
+
(served at `/`). No `@serverless-ircd/*` runtime dependency —
|
|
37
|
+
Kiwi talks to the Worker over plain WebSocket.
|
|
38
|
+
- **Kiwi IRC vendored as a git submodule** (`apps/web/upstream/`,
|
|
39
|
+
tag-pinned): `scripts/build.mjs` patches the upstream `publicPath`
|
|
40
|
+
to `/app/`, runs `yarn build` inside the submodule, copies
|
|
41
|
+
`upstream/dist/` → `apps/web/dist/app/`, then layers our validated
|
|
42
|
+
`static/config.json` over the upstream default. The patch runs in
|
|
43
|
+
`finally` so the submodule stays clean; every hashed asset URL comes
|
|
44
|
+
out `/app/static/...` instead of root-relative. `.gitmodules` updated;
|
|
45
|
+
`biome.json` ignores `apps/web/upstream/**`. `git submodule update
|
|
46
|
+
--init` is now part of the contributor quickstart.
|
|
47
|
+
- **Per-env config matrix + Zod schema** (`apps/web/src/config-schema.ts`,
|
|
48
|
+
`apps/web/src/build-env.ts`): `static/config.json`,
|
|
49
|
+
`static/config.staging.json`, `static/config.prod.json` are validated
|
|
50
|
+
against a Zod schema **before** being baked into
|
|
51
|
+
`dist/app/static/config.json`. `pnpm --filter web build` accepts a
|
|
52
|
+
`--env <staging|prod>` flag (unit-tested in isolation from the slow
|
|
53
|
+
Kiwi build). A malformed config (missing `server`, out-of-range
|
|
54
|
+
`port`, non-`#` channel, `direct: false`, …) aborts the build with
|
|
55
|
+
an aggregated, field-listing error rather than shipping a broken SPA.
|
|
56
|
+
The Kiwi `startupOptions.server` uses the `{{hostname}}` template
|
|
57
|
+
(resolved by the browser to `window.location.hostname` at load time),
|
|
58
|
+
so **one build artifact targets staging, prod, and preview deploys
|
|
59
|
+
without a per-env hostname**.
|
|
60
|
+
- **Cloudflare Worker `[assets]` binding** (`apps/cf-worker/wrangler.toml`,
|
|
61
|
+
`apps/cf-worker/src/worker.ts`): the non-WS, non-`/health` branch of
|
|
62
|
+
the worker's `fetch` falls through to `env.ASSETS`, which serves
|
|
63
|
+
`apps/web/dist`. Per-env (`[env.staging.assets]`) blocks ship so
|
|
64
|
+
`wrangler deploy --env staging` deploys the Worker and the SPA in
|
|
65
|
+
one command. `run_worker_first = true` is set so WS upgrades aren't
|
|
66
|
+
intercepted by the asset platform serving `dist/index.html` at `/`
|
|
67
|
+
with a `200 OK` (the WS endpoint and the landing page share path
|
|
68
|
+
`/`; only the Worker can tell them apart).
|
|
69
|
+
- **Static landing page** (`apps/web/landing/index.html`): framework-
|
|
70
|
+
free HTML + inline CSS, no JavaScript. The project's front door —
|
|
71
|
+
explains what ServerlessIRCd is, links to `/app/` (the IRC client),
|
|
72
|
+
the docs, and the source repo. The build copies it verbatim to
|
|
73
|
+
`dist/index.html`. Renders without JS; responsive; the build-smoke
|
|
74
|
+
suite asserts the `/app/` link, the docs link, and the viewport meta
|
|
75
|
+
tag so they cannot regress.
|
|
76
|
+
- **`/health` is an explicit route**: the previous catch-all plaintext
|
|
77
|
+
liveness string now lives at `GET /health` so `/` is free for the
|
|
78
|
+
landing page. WS upgrades still route to `ConnectionDO`.
|
|
79
|
+
|
|
80
|
+
### Added — CSWSH defense (`@serverless-ircd/cf-worker`)
|
|
81
|
+
|
|
82
|
+
WebSocket upgrades do **not** follow the same-origin policy. A
|
|
83
|
+
malicious page can `new WebSocket('wss://irc.example.com/')` from the
|
|
84
|
+
victim's browser and drive an IRC session with the victim's
|
|
85
|
+
credentials. The Worker now rejects upgrades whose `Origin` is not
|
|
86
|
+
allowed — see `apps/cf-worker/src/origin-allowlist.ts` and
|
|
87
|
+
`docs/WebClientGuide.md` §5.
|
|
88
|
+
|
|
89
|
+
- **Two enforcement modes, evaluated in order:**
|
|
90
|
+
1. **Explicit allowlist** — when the `WEB_ORIGINS` var is set, only
|
|
91
|
+
listed origins proceed. Use this for cross-origin deployments
|
|
92
|
+
(SPA on a different domain than the Worker, e.g. Cloudflare
|
|
93
|
+
Pages).
|
|
94
|
+
2. **Auto-derive (same-origin)** — when `WEB_ORIGINS` is unset/empty
|
|
95
|
+
(the default), the Worker compares the browser's `Origin` against
|
|
96
|
+
the request's own origin (`${protocol}//${host}`). If they match,
|
|
97
|
+
the page opening the WS is the same origin as the server → safe.
|
|
98
|
+
- **`WEB_ORIGINS` is optional.** Because mode 2 is the default and
|
|
99
|
+
requires zero per-env configuration, most deployments never need to
|
|
100
|
+
set it: the SPA and the Worker are on the same origin. It works for
|
|
101
|
+
`*.workers.dev`, custom domains, and preview URLs alike.
|
|
102
|
+
- **Non-browser clients pass through unchanged.** curl, WeeChat, the
|
|
103
|
+
`tcp-ws-forwarder`, and scripted test harnesses never send `Origin`;
|
|
104
|
+
their upgrades are allowed in both modes so existing tooling keeps
|
|
105
|
+
working.
|
|
106
|
+
- **No fail-open path.** A browser-sent `Origin` that is not allowed
|
|
107
|
+
→ `403 Forbidden`. The extreme edge case (no allowlist AND
|
|
108
|
+
`Host` undeterminable, which never fires on Cloudflare) is
|
|
109
|
+
fail-closed `403`. The pure policy is exhaustively unit-tested in
|
|
110
|
+
`apps/cf-worker/tests/origin-allowlist.test.ts`; the worker
|
|
111
|
+
integration is covered by `tests/ws-origin.test.ts`.
|
|
112
|
+
|
|
113
|
+
### Added — Documentation (`docs/WebClientGuide.md`)
|
|
114
|
+
|
|
115
|
+
End-to-end contributor/operator doc covering: how the SPA, landing
|
|
116
|
+
page, and WS endpoint fit on one origin; prerequisites (incl. `yarn`
|
|
117
|
+
for the Kiwi build only); the quickstart (`git submodule update --init`
|
|
118
|
+
→ `pnpm --filter web build:staging` → `pnpm deploy:cf:staging`); a
|
|
119
|
+
walkthrough of the build pipeline and the per-env config matrix; the
|
|
120
|
+
CSWSH rationale and the optional `WEB_ORIGINS` var; local dev
|
|
121
|
+
(`wrangler dev` serves everything on `http://localhost:8787`); an
|
|
122
|
+
optional Cloudflare Pages alternative for cross-origin prod deploys;
|
|
123
|
+
the optional `webircgateway` SockJS fallback (strictly opt-in, not
|
|
124
|
+
needed for normal operation); landing-page customisation; and a
|
|
125
|
+
troubleshooting section. The acceptance criterion is that a new
|
|
126
|
+
contributor can deploy a staging SPA following only this doc.
|
|
127
|
+
|
|
128
|
+
### Added — Test hardening (`@serverless-ircd/irc-core`, `@serverless-ircd/cf-adapter`, `@serverless-ircd/irc-test-support`)
|
|
129
|
+
|
|
130
|
+
- **Mutation-killing tests for `irc-core`**: the parser, batch, and
|
|
131
|
+
numerics suites were expanded to kill surviving Stryker mutants —
|
|
132
|
+
command-token anchors, strict source-prefix shape, malformed-input
|
|
133
|
+
diagnostic text, middle-param whitespace collapsing,
|
|
134
|
+
`unescapeTagValue` boundary conditions; empty-batch framing metadata
|
|
135
|
+
surviving elision; and `numericToName` round-trip parity against the
|
|
136
|
+
registry. A generated Stryker mutation report is committed under
|
|
137
|
+
`packages/irc-core/reports/mutation/`.
|
|
138
|
+
- **CF harness / scenario fixes for slow transports**: the CF test
|
|
139
|
+
harness now waits for the end of the registration burst (`376` /
|
|
140
|
+
`422`) instead of `001`, so late MOTD frames do not pollute
|
|
141
|
+
`received` after a test clears it. The NOTICE scenario now only
|
|
142
|
+
rejects NOTICE-relevant error numerics (rather than any numeric),
|
|
143
|
+
preventing delayed welcome / MOTD lines from false-positiving the
|
|
144
|
+
RFC no-error-replies check. No production code changed.
|
|
145
|
+
|
|
146
|
+
### Changed
|
|
147
|
+
|
|
148
|
+
- **Cloudflare Worker routing.** `/health` is the new liveness route;
|
|
149
|
+
the root `/` now serves the landing page from the `[assets]`
|
|
150
|
+
binding. WS upgrades still route to `ConnectionDO`. Operators who
|
|
151
|
+
scraped the root `/` for the plaintext health string must update
|
|
152
|
+
to `/health`.
|
|
153
|
+
|
|
154
|
+
### ⚠️ Migration required
|
|
155
|
+
|
|
156
|
+
- **New optional runtime var: `WEB_ORIGINS`.** Optional — same-origin
|
|
157
|
+
auto-derive is the default. Set it only for cross-origin SPA
|
|
158
|
+
deployments (SPA on Cloudflare Pages, Worker serving the WS only,
|
|
159
|
+
etc.):
|
|
160
|
+
```toml
|
|
161
|
+
# apps/cf-worker/wrangler.toml
|
|
162
|
+
[vars]
|
|
163
|
+
WEB_ORIGINS = "https://irc.example.com,https://web.example.com"
|
|
164
|
+
```
|
|
165
|
+
- **New build prerequisite for the SPA.** Building `apps/web` needs
|
|
166
|
+
the Kiwi submodule (`git submodule update --init apps/web/upstream`)
|
|
167
|
+
and `yarn` (provided by `corepack enable`). The CF deploy workflow
|
|
168
|
+
runs `pnpm build` (which builds the SPA + landing page) before
|
|
169
|
+
`wrangler deploy`. If you deploy manually and skip the SPA build,
|
|
170
|
+
the Worker still works but `/app/` and `/` return 404 from the asset
|
|
171
|
+
layer.
|
|
172
|
+
- **`/health` is the new liveness route.** Operators who scraped `/`
|
|
173
|
+
for the plaintext health string must update to `/health`.
|
|
174
|
+
- **New workspace package.** `apps/web` is a new private package
|
|
175
|
+
(`@serverless-ircd/web`); `pnpm install` picks it up automatically
|
|
176
|
+
from the existing `pnpm-workspace.yaml` glob.
|
|
177
|
+
- **No persisted-state schema change.** `PERSISTED_STATE_VERSION`
|
|
178
|
+
stays `1`. **Code rollback across this release is safe** — the
|
|
179
|
+
web-client work adds the `[assets]` binding and the `/health` route
|
|
180
|
+
but touches no Durable Object state.
|
|
181
|
+
- No toolchain change. Node ≥ 24 / pnpm 11 (from v0.4.0) still apply.
|
|
182
|
+
|
|
183
|
+
### Known limitations
|
|
184
|
+
|
|
185
|
+
- **Phase 14 is partial.** The Playwright headless-browser e2e
|
|
186
|
+
against the deployed staging frontend remains pending
|
|
187
|
+
— the SPA is exercised via the existing WS smoke
|
|
188
|
+
(`scripts/smoke.mjs`) and the new `apps/web` unit / build-smoke
|
|
189
|
+
suite, but no browser-driven end-to-end test runs in CI yet.
|
|
190
|
+
- **The web client is Cloudflare-only.** The `[assets]` binding is a
|
|
191
|
+
Cloudflare Worker feature; AWS deployments continue to require an
|
|
192
|
+
external static host for the SPA (and must add the SPA's origin to
|
|
193
|
+
the Worker-style `WEB_ORIGINS` equivalent if/when an AWS-side
|
|
194
|
+
CSWSH check lands).
|
|
195
|
+
- Same transport limits as v0.6.0: the **CF TCP path requires
|
|
196
|
+
Cloudflare Spectrum (Enterprise tier)** plus a stateful Container
|
|
197
|
+
origin; the **AWS TCP path** uses NLB + Lambda streaming and is
|
|
198
|
+
subject to Lambda idle-timeout / stream-duration limits. The
|
|
199
|
+
WebSocket path remains the zero-extra-deps default.
|
|
200
|
+
- **mTLS (for SASL `EXTERNAL`) requires a custom domain + uploaded
|
|
201
|
+
CA/trust-store** on both platforms and is not enabled by default.
|
|
202
|
+
- **Client compatibility sweep** (WeeChat / HexChat / IRCCloud /
|
|
203
|
+
TheLounge / matrix-IRC bridge) remains pending.
|
|
204
|
+
- **Coverage hardening is partial.** `aws-adapter` and `aws-stack`
|
|
205
|
+
clear the 90% gate (v0.6.0); the remaining packages
|
|
206
|
+
(`cf-adapter`, `local-cli`, `load-test`, `cf-tcp-container`,
|
|
207
|
+
`tcp-ws-forwarder`, `irc-test-support`, and now `web`) are **above
|
|
208
|
+
the gate but below 100%** — follow-ups drive each to 100%.
|
|
209
|
+
- The IRC services Phase 13 work (NickServ / ChanServ / HostServ /
|
|
210
|
+
OperServ / MemoServ) is pending; `draft/read-marker` and
|
|
211
|
+
`draft/pre-away` ship with in-memory reference stores, so marker /
|
|
212
|
+
away persistence does not survive an adapter restart until a
|
|
213
|
+
services backend lands.
|
|
214
|
+
|
|
215
|
+
### Security
|
|
216
|
+
|
|
217
|
+
- **CSWSH defense added.** WebSocket upgrades from browsers now
|
|
218
|
+
require an allowed `Origin`. Same-origin auto-derive is the default
|
|
219
|
+
(zero config); explicit `WEB_ORIGINS` allowlist for cross-origin
|
|
220
|
+
setups. Non-browser clients (no `Origin`) pass through unchanged.
|
|
221
|
+
There is no fail-open path. See `docs/WebClientGuide.md` §5. No
|
|
222
|
+
other auth path changed (SASL `PLAIN` / `EXTERNAL`, oper gate,
|
|
223
|
+
admission limits, and cloaking are unchanged from v0.6.0).
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
13
227
|
## [0.6.0] - 2026-08-03
|
|
14
228
|
|
|
15
229
|
The headline is the **IRCv3 extension sweep (Phase 12)**: ten negotiated
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ platform-agnostic core, and two thin adapters run it on **Cloudflare Workers**
|
|
|
6
6
|
|
|
7
7
|
One TypeScript codebase. Two serverless substrates.
|
|
8
8
|
|
|
9
|
-
> **Status:** **v0.
|
|
9
|
+
> **Status:** **v0.7.0 (preview).** The pure protocol core, the
|
|
10
10
|
> `IrcRuntime` port + in-memory runtime, a runnable local CLI server,
|
|
11
11
|
> the **Cloudflare Workers** adapter, and the **AWS** (API Gateway
|
|
12
12
|
> WebSocket + Lambda + DynamoDB + CDK) adapter are all functional and
|
|
@@ -19,21 +19,24 @@ One TypeScript codebase. Two serverless substrates.
|
|
|
19
19
|
> deferred IRC verbs (`KILL`, `REHASH`, `LUSERS` + `STATS`, `TRACE`,
|
|
20
20
|
> `WALLOPS`, `SETNAME`) all land, and the S2S (`CONNECT` / `SQUIT` /
|
|
21
21
|
> `LINKS`) and obsolete RFC 2812 (`SERVICE` / `SUMMON` / `USERS`) verbs
|
|
22
|
-
> are formally dropped.
|
|
22
|
+
> are formally dropped. v0.6.0 landed the **IRCv3 extension sweep
|
|
23
23
|
> (Phase 12)** — ten caps (`account-notify`, `msgid`,
|
|
24
24
|
> `standard-replies`, `MONITOR`, `labeled-response`, `sts`,
|
|
25
25
|
> `draft/typing`, `draft/multiline`, `draft/read-marker`,
|
|
26
26
|
> `draft/pre-away`), the ISUPPORT tokens for the new caps, and the
|
|
27
|
-
> read-only user mode `S` (TLS connected)
|
|
28
|
-
>
|
|
29
|
-
>
|
|
30
|
-
>
|
|
31
|
-
>
|
|
32
|
-
>
|
|
33
|
-
>
|
|
34
|
-
>
|
|
35
|
-
>
|
|
36
|
-
>
|
|
27
|
+
> read-only user mode `S` (TLS connected). **v0.7.0 lands the web
|
|
28
|
+
> client (Phase 14)**: a vendored **Kiwi IRC** SPA served at `/app/`
|
|
29
|
+
> and a static landing page at `/` directly by the Cloudflare Worker,
|
|
30
|
+
> with **Cross-Site WebSocket Hijacking (CSWSH) defense** wired into
|
|
31
|
+
> the WS upgrade path (same-origin auto-derive by default, optional
|
|
32
|
+
> `WEB_ORIGINS` allowlist for cross-origin deploys) — the browser
|
|
33
|
+
> opens a native `wss://` straight to the existing IRC-over-WebSocket
|
|
34
|
+
> endpoint. Remaining 0.x work: the client compatibility sweep
|
|
35
|
+
> (WeeChat / HexChat / IRCCloud / TheLounge), the Playwright
|
|
36
|
+
> browser-driven e2e for the SPA, IRC services (NickServ / ChanServ /
|
|
37
|
+
> HostServ / OperServ / MemoServ), and driving the remaining
|
|
38
|
+
> sub-100% packages to full coverage. See `CHANGELOG.md` for the
|
|
39
|
+
> per-release manifests.
|
|
37
40
|
|
|
38
41
|
---
|
|
39
42
|
|
|
@@ -134,9 +137,10 @@ ServerlessIRCd/
|
|
|
134
137
|
│ ├── cf-adapter/ Durable Objects + CfRuntime
|
|
135
138
|
│ └── aws-adapter/ Lambda + DynamoDB + AwsRuntime
|
|
136
139
|
├── apps/
|
|
137
|
-
│ ├── cf-worker/ worker entry, DO migrations, bindings
|
|
140
|
+
│ ├── cf-worker/ worker entry, DO migrations, bindings, [assets] + CSWSH gate
|
|
138
141
|
│ ├── cf-tcp-container/ Spectrum + Container origin for irc+tls :6697
|
|
139
142
|
│ ├── aws-stack/ CDK stack (APIGW WS + NLB + Lambda streaming + DynamoDB)
|
|
143
|
+
│ ├── web/ vendored Kiwi IRC SPA (→ /app/) + static landing page (→ /)
|
|
140
144
|
│ └── local-cli/ runnable WS + TCP server using in-memory-runtime
|
|
141
145
|
├── tools/
|
|
142
146
|
│ ├── tcp-ws-forwarder/ local TCP↔ws/wss bridge for stock IRC clients
|
|
@@ -174,6 +178,7 @@ pin is resolved automatically:
|
|
|
174
178
|
```bash
|
|
175
179
|
corepack enable # lets the pinned pnpm@11 run on any Node ≥ 24
|
|
176
180
|
|
|
181
|
+
git submodule update --init # docs + apps/web Kiwi upstream sources
|
|
177
182
|
pnpm install # install workspace deps
|
|
178
183
|
|
|
179
184
|
pnpm build # build all packages (turbo)
|
|
@@ -191,6 +196,14 @@ pnpm mutation # Stryker spot-check on irc-core (protocol + commands)
|
|
|
191
196
|
pnpm clean # remove dist/coverage/.turbo + node_modules
|
|
192
197
|
```
|
|
193
198
|
|
|
199
|
+
The web client (`apps/web`) vendors Kiwi IRC as a git submodule. Building it
|
|
200
|
+
needs the upstream sources (`git submodule update --init apps/web/upstream`)
|
|
201
|
+
and `yarn` (Kiwi ships a `yarn.lock`; `corepack enable` provides it):
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
pnpm --filter web build # builds the Kiwi SPA into apps/web/dist/app/ (/app/)
|
|
205
|
+
```
|
|
206
|
+
|
|
194
207
|
Coverage reports are written to `packages/*/coverage/`. CI (`.github/workflows/ci.yml`)
|
|
195
208
|
runs lint, typecheck, the coverage gate, the parametrized contract suite, and a
|
|
196
209
|
Stryker mutation spot-check on every push and pull request. Coverage thresholds
|
|
@@ -364,6 +377,66 @@ report).
|
|
|
364
377
|
|
|
365
378
|
---
|
|
366
379
|
|
|
380
|
+
## Web client (`apps/web`)
|
|
381
|
+
|
|
382
|
+
A vendored **Kiwi IRC** SPA served at `/app/` and a static project
|
|
383
|
+
landing page served at `/`, both baked out of `apps/web/dist/` and
|
|
384
|
+
served directly by the Cloudflare Worker via its `[assets]` binding.
|
|
385
|
+
The browser opens a native `wss://` straight to the Worker's existing
|
|
386
|
+
IRC-over-WebSocket endpoint — no proxy, gateway, or transport
|
|
387
|
+
adaptation in the hot path. See `docs/WebClientGuide.md` for the
|
|
388
|
+
end-to-end contributor/operator guide.
|
|
389
|
+
|
|
390
|
+
Build the SPA + landing page (needs the submodule + `yarn`, provided by
|
|
391
|
+
`corepack enable`):
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
git submodule update --init apps/web/upstream # one-time per clone
|
|
395
|
+
pnpm --filter web build:staging # or :prod / default
|
|
396
|
+
# → apps/web/dist/index.html (landing page, served at /)
|
|
397
|
+
# → apps/web/dist/app/index.html (Kiwi SPA, served at /app/)
|
|
398
|
+
# → apps/web/dist/app/static/config.json (baked, env-specific)
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Then run everything locally (Worker serves SPA + landing page + WS on
|
|
402
|
+
one origin):
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
pnpm build # workspace packages
|
|
406
|
+
pnpm --filter @serverless-ircd/cf-worker dev # http://localhost:8787
|
|
407
|
+
# / → landing page
|
|
408
|
+
# /app/ → Kiwi SPA (opens ws://localhost:8787/)
|
|
409
|
+
# /health → plaintext liveness
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Deploy staging (Worker + assets in one command):
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
pnpm deploy:cf:staging # wrangler deploy --env staging
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### WebSocket Origin policy (CSWSH defense)
|
|
419
|
+
|
|
420
|
+
WebSocket upgrades do **not** follow the same-origin policy, so the
|
|
421
|
+
Worker rejects browser upgrades whose `Origin` is not allowed — see
|
|
422
|
+
`apps/cf-worker/src/origin-allowlist.ts` and the SPA guide §5. Two
|
|
423
|
+
modes, evaluated in order:
|
|
424
|
+
|
|
425
|
+
1. **Explicit allowlist** — set `WEB_ORIGINS` (comma-separated) for
|
|
426
|
+
cross-origin deploys (SPA on a different domain than the Worker,
|
|
427
|
+
e.g. Cloudflare Pages). Only listed origins proceed.
|
|
428
|
+
2. **Same-origin auto-derive (the default)** — when `WEB_ORIGINS` is
|
|
429
|
+
unset/empty, the Worker compares the browser's `Origin` against the
|
|
430
|
+
request's own origin. Match → proceed; mismatch → `403 Forbidden`.
|
|
431
|
+
|
|
432
|
+
`WEB_ORIGINS` is **optional** — same-origin auto-derive needs zero
|
|
433
|
+
per-env config and works for `*.workers.dev`, custom domains, and
|
|
434
|
+
preview URLs alike. Non-browser clients (curl, WeeChat, the
|
|
435
|
+
`tcp-ws-forwarder`, scripted harnesses) never send `Origin` and pass
|
|
436
|
+
through unchanged in both modes.
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
367
440
|
## WebSocket transport (IRCv3 subprotocols)
|
|
368
441
|
|
|
369
442
|
The WebSocket entry points (the local CLI, the Cloudflare Worker, and the
|
|
@@ -484,14 +557,19 @@ cleanly. Per-release manifests live in `CHANGELOG.md`.
|
|
|
484
557
|
backends (D1 / DynamoDB) ship alongside; until then
|
|
485
558
|
`draft/read-marker` and `draft/pre-away` use in-memory reference
|
|
486
559
|
stores and do not survive an adapter restart.
|
|
487
|
-
- **Phase 14 — Web client.**
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
560
|
+
- **Phase 14 — Web client.** 🔄 partial as of **v0.7.0**: a vendored
|
|
561
|
+
Kiwi IRC SPA is served at `/app/` and a static landing page at `/`
|
|
562
|
+
by the Cloudflare Worker via an `[assets]` binding, with a Zod-
|
|
563
|
+
validated per-env config matrix and **CSWSH defense** (same-origin
|
|
564
|
+
auto-derive + optional `WEB_ORIGINS` allowlist). The Playwright
|
|
565
|
+
headless-browser e2e is the remaining open ticket.
|
|
566
|
+
See `docs/WebClientGuide.md` for the contributor/operator guide.
|
|
567
|
+
- **Coverage hardening.** 🔄 partial as of **v0.7.0**: `aws-adapter`
|
|
568
|
+
and `aws-stack` clear the 90% gate (v0.6.0); the CF packages run
|
|
569
|
+
under istanbul (v0.6.0). The remaining packages (`cf-adapter`,
|
|
570
|
+
`local-cli`, `load-test`, `cf-tcp-container`, `tcp-ws-forwarder`,
|
|
571
|
+
`irc-test-support`, `web`) are **above the gate but below 100%** —
|
|
572
|
+
follow-ups drive each to 100%.
|
|
495
573
|
|
|
496
574
|
---
|
|
497
575
|
|
|
@@ -563,13 +641,22 @@ bridges a stock TCP client to a deployed WebSocket endpoint.
|
|
|
563
641
|
`irc+tls :6697`, SASL `EXTERNAL`/mTLS, WHOWAS + remaining verbs, D1 account
|
|
564
642
|
store, Node 24/pnpm 11), v0.5.0 (deferred IRC verbs `KILL` / `REHASH`
|
|
565
643
|
/ `LUSERS` + `STATS` / `TRACE` / `WALLOPS` / `SETNAME`, plus the formal
|
|
566
|
-
drop of the S2S and obsolete RFC 2812 verbs),
|
|
644
|
+
drop of the S2S and obsolete RFC 2812 verbs), v0.6.0 (the IRCv3
|
|
567
645
|
extension sweep — `account-notify`, `msgid`, `standard-replies`,
|
|
568
646
|
`MONITOR`, `labeled-response`, `sts`, `draft/typing`, `draft/multiline`,
|
|
569
647
|
`draft/read-marker`, `draft/pre-away`, plus user mode `S`; the
|
|
570
648
|
required-`serverName` gate; `tools/load-test`; aws-adapter / aws-stack
|
|
571
649
|
coverage unblocks; CF packages moved to istanbul; deploys now
|
|
572
|
-
manual-only).
|
|
650
|
+
manual-only), and **v0.7.0** (the web client — vendored Kiwi IRC SPA
|
|
651
|
+
at `/app/`, static landing page at `/`, Cloudflare Worker `[assets]`
|
|
652
|
+
binding, per-env Zod-validated config matrix, and CSWSH defense via
|
|
653
|
+
same-origin auto-derive + optional `WEB_ORIGINS`; `/health` becomes
|
|
654
|
+
an explicit route; mutation-killing tests for `irc-core`'s parser /
|
|
655
|
+
batch / numerics).
|
|
656
|
+
- `docs/WebClientGuide.md` — end-to-end contributor/operator doc for the
|
|
657
|
+
web client: build pipeline, per-env config matrix, CSWSH rationale and
|
|
658
|
+
the optional `WEB_ORIGINS` var, local dev, optional Cloudflare Pages
|
|
659
|
+
alternative, and troubleshooting.
|
|
573
660
|
- `docs/Cloudflare-TCP-Deployment.md` and `docs/AWS-TCP-Deployment.md` —
|
|
574
661
|
end-to-end guides for the `:6697` TCP+TLS variants (Spectrum/Container on
|
|
575
662
|
CF, NLB + Lambda streaming on AWS), including mTLS trust-store setup.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serverless-ircd/cf-tcp-container",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Cloudflare Container TCP origin for the irc+tls :6697 transport — Spectrum terminates TLS at the edge, this container runs the IRC core over plaintext TCP",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket `Origin` policy — Cross-Site WebSocket Hijacking (CSWSH) defense.
|
|
3
|
+
*
|
|
4
|
+
* Browsers send an `Origin` header on every cross-origin WebSocket upgrade
|
|
5
|
+
* (RFC 6454); WebSocket upgrades do not otherwise enforce same-origin.
|
|
6
|
+
* Without a check, a malicious page can open a WS to the IRC server from
|
|
7
|
+
* the victim's browser and drive the session with the victim's credentials.
|
|
8
|
+
*
|
|
9
|
+
* Two enforcement modes, evaluated in order:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Explicit allowlist** — when `WEB_ORIGINS` is set, only listed
|
|
12
|
+
* origins proceed. Use this for cross-origin deployments (SPA on a
|
|
13
|
+
* different domain than the Worker).
|
|
14
|
+
*
|
|
15
|
+
* 2. **Auto-derive (same-origin)** — when `WEB_ORIGINS` is unset/empty,
|
|
16
|
+
* the Worker compares the browser's `Origin` against the request's own
|
|
17
|
+
* origin (`${protocol}//${host}`). If they match, the page that opened
|
|
18
|
+
* the WebSocket is the same origin as the server → safe. This is the
|
|
19
|
+
* default and requires zero per-env configuration; it works for any
|
|
20
|
+
* deployment hostname (`*.workers.dev`, custom domains, preview URLs).
|
|
21
|
+
*
|
|
22
|
+
* Non-browser clients (curl, WeeChat, the `tcp-ws-forwarder`, scripted test
|
|
23
|
+
* harnesses) never send `Origin`; their upgrades are allowed through unchanged
|
|
24
|
+
* so existing tooling keeps working.
|
|
25
|
+
*
|
|
26
|
+
* The policy is split into two pure functions so the logic is exhaustively
|
|
27
|
+
* unit-testable; the worker's `fetch` handler is the only integration site
|
|
28
|
+
* (see `tests/ws-origin.test.ts`).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Outcome of evaluating an upgrade's `Origin` header.
|
|
33
|
+
*
|
|
34
|
+
* • `'allow'` — proceed: the origin is allowlisted or same-origin,
|
|
35
|
+
* or the header is absent (non-browser client).
|
|
36
|
+
* • `'deny'` — reject (`403`): a browser sent an `Origin` that is
|
|
37
|
+
* not allowlisted and not same-origin.
|
|
38
|
+
* • `'fail-closed'` — reject (`403`): neither the allowlist nor the
|
|
39
|
+
* request origin could be determined (extreme edge
|
|
40
|
+
* case — Cloudflare always sets `Host`).
|
|
41
|
+
*/
|
|
42
|
+
export type OriginDecision = 'allow' | 'deny' | 'fail-closed';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse the comma-separated `WEB_ORIGINS` var into a normalized set of
|
|
46
|
+
* lowercased origins. Whitespace-only and empty entries are dropped so
|
|
47
|
+
* trailing commas / stray spaces don't pollute the set. An empty/undefined
|
|
48
|
+
* var yields an empty set, which {@link decideOrigin} treats as "fall back
|
|
49
|
+
* to auto-derive" (NOT fail-closed).
|
|
50
|
+
*
|
|
51
|
+
* Origins are lowercased wholesale for case-insensitive matching; the
|
|
52
|
+
* scheme and host are case-insensitive per RFC 3986, and lowercasing the
|
|
53
|
+
* whole string is the simplest normalization that handles the real-world
|
|
54
|
+
* browser-emitted form (`https://Example.com` → `https://example.com`).
|
|
55
|
+
*/
|
|
56
|
+
export function parseWebOrigins(raw: string | undefined): Set<string> {
|
|
57
|
+
return new Set(
|
|
58
|
+
(raw ?? '')
|
|
59
|
+
.split(',')
|
|
60
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
61
|
+
.filter((entry) => entry.length > 0),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Decide whether a WebSocket upgrade's `Origin` header is permitted.
|
|
67
|
+
*
|
|
68
|
+
* @param originHeader the raw `Origin` header value (`null` when absent).
|
|
69
|
+
* @param allowlist the parsed {@link parseWebOrigins} set. When non-empty,
|
|
70
|
+
* it takes precedence over auto-derive (explicit operator
|
|
71
|
+
* override for cross-origin setups).
|
|
72
|
+
* @param requestOrigin the request's own origin (`${protocol}//${host}`),
|
|
73
|
+
* used for same-origin auto-derive when the allowlist is
|
|
74
|
+
* empty. `null` if the origin cannot be determined.
|
|
75
|
+
* @returns the {@link OriginDecision}; see the type docstring.
|
|
76
|
+
*/
|
|
77
|
+
export function decideOrigin(
|
|
78
|
+
originHeader: string | null,
|
|
79
|
+
allowlist: Set<string>,
|
|
80
|
+
requestOrigin: string | null,
|
|
81
|
+
): OriginDecision {
|
|
82
|
+
// Missing Origin = non-browser client (curl, WeeChat, tcp-ws-forwarder).
|
|
83
|
+
if (originHeader === null || originHeader === '') return 'allow';
|
|
84
|
+
|
|
85
|
+
const origin = originHeader.trim().toLowerCase();
|
|
86
|
+
|
|
87
|
+
// Mode 1: explicit allowlist takes precedence.
|
|
88
|
+
if (allowlist.size > 0) {
|
|
89
|
+
return allowlist.has(origin) ? 'allow' : 'deny';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Mode 2: auto-derive — compare Origin against the request's own origin.
|
|
93
|
+
if (requestOrigin !== null && requestOrigin !== '') {
|
|
94
|
+
return origin === requestOrigin.trim().toLowerCase() ? 'allow' : 'deny';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Neither allowlist nor request origin available — fail-closed.
|
|
98
|
+
return 'fail-closed';
|
|
99
|
+
}
|