serverless-ircd 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +40 -3
- package/.github/workflows/deploy-aws.yml +1 -2
- package/.github/workflows/deploy-cf-tcp.yml +1 -5
- package/.github/workflows/deploy-cf.yml +1 -2
- package/.gitmodules +3 -0
- package/CHANGELOG.md +474 -0
- package/README.md +219 -22
- package/apps/aws-stack/README.md +33 -0
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +74 -9
- package/apps/aws-stack/tests/coverage-command.test.ts +155 -0
- package/apps/aws-stack/tests/stack.test.ts +2 -2
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +129 -0
- package/apps/aws-stack/tests/vitest.setup.ts +18 -0
- package/apps/aws-stack/vitest.config.ts +3 -0
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-tcp-container/src/config-loader.ts +6 -7
- package/apps/cf-tcp-container/src/container-server.ts +3 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +10 -3
- package/apps/cf-tcp-container/wrangler.toml +1 -1
- package/apps/cf-worker/package.json +2 -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/vitest.config.ts +1 -1
- package/apps/cf-worker/wrangler.test.toml +23 -0
- package/apps/cf-worker/wrangler.toml +59 -3
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +6 -6
- package/apps/local-cli/src/main.ts +20 -0
- package/apps/local-cli/src/server.ts +9 -7
- package/apps/local-cli/tests/e2e.test.ts +112 -28
- package/apps/local-cli/tests/tcp.test.ts +71 -19
- package/apps/local-cli/tests/ws-subprotocol.test.ts +12 -7
- 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 +2 -0
- package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
- package/docs/ADR-002-location-of-authority.md +82 -0
- package/docs/ADR-003-durable-object-sharding.md +110 -0
- package/docs/ADR-004-dynamodb-schema.md +96 -0
- package/docs/ADR-005-wss-only-transport-v1.md +83 -0
- package/docs/ADR-006-sasl-mechanism-scope.md +96 -0
- package/docs/ADR-007-deterministic-ports.md +82 -0
- package/docs/ADR-008-monorepo-tooling.md +60 -0
- package/docs/ADR-009-dual-transport-wss-and-irc-tls.md +138 -0
- package/docs/ADR-Index.md +15 -0
- package/docs/AWS-Adapter-Architecture.md +496 -0
- package/docs/AWS-Deployment.md +1275 -0
- package/docs/AWS-TCP-Deployment.md +410 -0
- package/docs/Cloudflare-Deployment-Guide.md +719 -0
- package/docs/Cloudflare-TCP-Deployment.md +510 -0
- package/docs/Home.md +16 -0
- package/docs/Observability.md +87 -0
- package/docs/PlanExtensions.md +812 -0
- package/docs/PlanIRCv3Websocket.md +503 -0
- package/docs/PlanWebClient.md +451 -0
- package/docs/Release-Process.md +443 -0
- package/docs/WebClientGuide.md +535 -0
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/handlers/default.ts +3 -0
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +9 -0
- package/packages/aws-adapter/src/serialize.ts +29 -1
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +49 -8
- package/packages/aws-adapter/tests/account-store.test.ts +4 -7
- package/packages/aws-adapter/tests/aws-harness.ts +5 -2
- package/packages/aws-adapter/tests/aws-integration.test.ts +8 -4
- package/packages/aws-adapter/tests/aws-runtime.test.ts +79 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +11 -2
- package/packages/aws-adapter/tests/connect.test.ts +9 -9
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +3 -4
- package/packages/aws-adapter/tests/global-setup.ts +55 -30
- package/packages/aws-adapter/tests/gone-exception.test.ts +4 -9
- package/packages/aws-adapter/tests/handlers.test.ts +90 -4
- package/packages/aws-adapter/tests/message-store.test.ts +6 -5
- package/packages/aws-adapter/tests/nlb-stream.test.ts +60 -8
- package/packages/cf-adapter/package.json +2 -1
- package/packages/cf-adapter/src/connection-do.ts +3 -0
- package/packages/cf-adapter/src/serialize.ts +25 -4
- package/packages/cf-adapter/tests/cf-harness.ts +12 -1
- package/packages/cf-adapter/tests/cf-runtime.test.ts +269 -0
- package/packages/cf-adapter/tests/channel-do.test.ts +117 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +49 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +91 -0
- package/packages/cf-adapter/tests/d1-account-store.test.ts +48 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +104 -0
- package/packages/cf-adapter/tests/serialize.test.ts +308 -0
- package/packages/cf-adapter/vitest.config.ts +1 -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/src/caps/capabilities.ts +37 -3
- package/packages/irc-core/src/caps/index.ts +1 -0
- package/packages/irc-core/src/caps/sts.ts +84 -0
- package/packages/irc-core/src/commands/account-notify.ts +53 -0
- package/packages/irc-core/src/commands/away.ts +9 -3
- package/packages/irc-core/src/commands/cap.ts +23 -5
- package/packages/irc-core/src/commands/chathistory.ts +40 -11
- package/packages/irc-core/src/commands/index.ts +34 -0
- package/packages/irc-core/src/commands/isupport.ts +53 -0
- package/packages/irc-core/src/commands/mode.ts +8 -1
- package/packages/irc-core/src/commands/monitor.ts +327 -0
- package/packages/irc-core/src/commands/multiline.ts +256 -0
- package/packages/irc-core/src/commands/pre-away.ts +112 -0
- package/packages/irc-core/src/commands/privmsg.ts +19 -12
- package/packages/irc-core/src/commands/read-marker.ts +108 -0
- package/packages/irc-core/src/commands/registration.ts +8 -0
- package/packages/irc-core/src/commands/sasl.ts +24 -1
- package/packages/irc-core/src/commands/tagmsg.ts +71 -1
- package/packages/irc-core/src/commands/whois.ts +5 -0
- package/packages/irc-core/src/config.ts +65 -10
- package/packages/irc-core/src/effects.ts +14 -0
- package/packages/irc-core/src/ports.ts +389 -0
- package/packages/irc-core/src/protocol/index.ts +14 -0
- package/packages/irc-core/src/protocol/labeled-response.ts +133 -0
- package/packages/irc-core/src/protocol/numerics.ts +15 -0
- package/packages/irc-core/src/protocol/outbound.ts +16 -1
- package/packages/irc-core/src/protocol/standard-replies.ts +277 -0
- package/packages/irc-core/src/state/connection.ts +32 -1
- package/packages/irc-core/src/types.ts +112 -0
- package/packages/irc-core/stryker.commands.conf.json +1 -2
- package/packages/irc-core/tests/away-store.test.ts +73 -0
- package/packages/irc-core/tests/batch.test.ts +15 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +119 -2
- package/packages/irc-core/tests/caps/sts.test.ts +123 -0
- package/packages/irc-core/tests/commands/account-notify.test.ts +171 -0
- package/packages/irc-core/tests/commands/away.test.ts +85 -2
- package/packages/irc-core/tests/commands/cap.test.ts +165 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +148 -1
- package/packages/irc-core/tests/commands/echo-message.test.ts +9 -3
- package/packages/irc-core/tests/commands/isupport.test.ts +80 -0
- package/packages/irc-core/tests/commands/mode.test.ts +72 -0
- package/packages/irc-core/tests/commands/monitor.test.ts +411 -0
- package/packages/irc-core/tests/commands/multiline.test.ts +428 -0
- package/packages/irc-core/tests/commands/pre-away.test.ts +241 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +154 -3
- package/packages/irc-core/tests/commands/read-marker.test.ts +211 -0
- package/packages/irc-core/tests/commands/registration.test.ts +64 -0
- package/packages/irc-core/tests/commands/sasl.test.ts +116 -2
- package/packages/irc-core/tests/commands/tagmsg.test.ts +357 -0
- package/packages/irc-core/tests/commands/whois.test.ts +34 -0
- package/packages/irc-core/tests/config.test.ts +103 -13
- package/packages/irc-core/tests/labeled-response.test.ts +181 -0
- package/packages/irc-core/tests/message-tags.test.ts +114 -0
- package/packages/irc-core/tests/monitor-store.test.ts +200 -0
- package/packages/irc-core/tests/numerics.test.ts +13 -0
- package/packages/irc-core/tests/parser.test.ts +109 -0
- package/packages/irc-core/tests/read-marker-store.test.ts +108 -0
- package/packages/irc-core/tests/standard-replies.test.ts +412 -0
- package/packages/irc-core/tests/types.test.ts +27 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +450 -3
- package/packages/irc-server/src/dispatch.ts +108 -16
- package/packages/irc-server/tests/actor.test.ts +1432 -39
- package/packages/irc-server/tests/dispatch.test.ts +263 -2
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/index.ts +6 -0
- package/packages/irc-test-support/src/scenarios.ts +24 -2
- package/packages/irc-test-support/src/test-config.ts +54 -0
- package/packages/irc-test-support/tests/test-config.test.ts +51 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/ci-hardening/tests/ddb-local.test.ts +82 -0
- package/tools/load-test/package.json +33 -0
- package/tools/load-test/src/client.ts +351 -0
- package/tools/load-test/src/config.ts +313 -0
- package/tools/load-test/src/harness.ts +116 -0
- package/tools/load-test/src/main.ts +120 -0
- package/tools/load-test/src/metrics.ts +168 -0
- package/tools/load-test/src/report.ts +106 -0
- package/tools/load-test/tests/client.test.ts +212 -0
- package/tools/load-test/tests/config.test.ts +152 -0
- package/tools/load-test/tests/framing.test.ts +37 -0
- package/tools/load-test/tests/harness.test.ts +165 -0
- package/tools/load-test/tests/metrics.test.ts +174 -0
- package/tools/load-test/tests/report.test.ts +161 -0
- package/tools/load-test/tests/smoke.test.ts +67 -0
- package/tools/load-test/tsconfig.build.json +12 -0
- package/tools/load-test/tsconfig.test.json +10 -0
- package/tools/load-test/vitest.config.ts +29 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI-hardening acceptance test for the aws-stack coverage command.
|
|
3
|
+
*
|
|
4
|
+
* Drives the real `pnpm --filter @serverless-ircd/aws-stack coverage`
|
|
5
|
+
* pipeline in a subprocess and asserts two things the ticket requires:
|
|
6
|
+
*
|
|
7
|
+
* 1. The command exits 0 (no `[ELIFECYCLE]` / OOM crash).
|
|
8
|
+
* 2. The vitest text reporter prints an `All files` coverage row.
|
|
9
|
+
*
|
|
10
|
+
* Before the bundle-stub fix this command exhausted CI memory re-running
|
|
11
|
+
* esbuild for every `NodejsFunction` synth (one per stack-related test,
|
|
12
|
+
* multiplied by 3–4 Lambda constructs each), and died with
|
|
13
|
+
* `[ELIFECYCLE] Command failed with exit code N` before the coverage
|
|
14
|
+
* table could print. The stub (`makeLambda` in `src/aws-stack.ts`,
|
|
15
|
+
* armed by `tests/vitest.setup.ts`) makes the synth a no-op for
|
|
16
|
+
* bundling so the pipeline completes in seconds.
|
|
17
|
+
*
|
|
18
|
+
* Recursion guard: the subprocess inherits
|
|
19
|
+
* `IRC_AWS_COVERAGE_SELF_TEST=1`, which makes this describe block skip
|
|
20
|
+
* itself when re-entered. Without that, the outer coverage run would
|
|
21
|
+
* spawn the coverage command, which spawns the coverage command, … .
|
|
22
|
+
*
|
|
23
|
+
* Slow-path: the subprocess still spins up pnpm + vitest + the v8
|
|
24
|
+
* coverage provider, so the test carries a generous timeout. It is
|
|
25
|
+
* intentionally part of the default `pnpm test` run because it is the
|
|
26
|
+
* end-to-end guard against the coverage pipeline silently regressing
|
|
27
|
+
* back into the bundling loop.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { spawnSync } from 'node:child_process';
|
|
31
|
+
import { describe, expect, it } from 'vitest';
|
|
32
|
+
|
|
33
|
+
/** Skip-self env var handed to the subprocess so it does not re-spawn. */
|
|
34
|
+
const SELF_TEST_FLAG = 'IRC_AWS_COVERAGE_SELF_TEST';
|
|
35
|
+
|
|
36
|
+
/** Hard ceiling for the subprocess; the stubbed suite finishes well under this. */
|
|
37
|
+
const COVERAGE_TIMEOUT_MS = 240_000;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Strips ANSI color / cursor escapes from `combined` so the assertion
|
|
41
|
+
* regexes below can be plain text. Vitest always writes the coverage
|
|
42
|
+
* banner and table through its colorizer even when stdout is piped, so
|
|
43
|
+
* without this the banner reads `% \u001b[2mCoverage report from …`
|
|
44
|
+
* and a literal `/Coverage report from v8/` regex fails to match.
|
|
45
|
+
*/
|
|
46
|
+
function stripAnsi(text: string): string {
|
|
47
|
+
// The ANSI color escape is the literal ESC byte (0x1B) followed by `[`
|
|
48
|
+
// and a parameter list. biome's noControlCharactersInRegex flags the
|
|
49
|
+
// inline `\u001b` literal, so the regex is built from a hex string to
|
|
50
|
+
// make the intent explicit while satisfying the linter.
|
|
51
|
+
const ansiPattern = new RegExp(`[${String.fromCharCode(27)}]\\[[0-9;]*m`, 'g');
|
|
52
|
+
return text.replace(ansiPattern, '');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe.skipIf(process.env[SELF_TEST_FLAG] === '1')(
|
|
56
|
+
'aws-stack coverage command (ci-hardening acceptance)',
|
|
57
|
+
() => {
|
|
58
|
+
it(
|
|
59
|
+
'completes and prints an "All files" coverage row',
|
|
60
|
+
() => {
|
|
61
|
+
// Strip the `npm_package_*` / `npm_config_*` env vars that the outer
|
|
62
|
+
// `pnpm run coverage` sets. When those leak into the spawned pnpm,
|
|
63
|
+
// its `--filter` resolution short-circuits to the current package
|
|
64
|
+
// and fails with `ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT` even though the
|
|
65
|
+
// workspace package exposes the script. The inner pnpm must resolve
|
|
66
|
+
// the filter against the workspace root, not the parent run context.
|
|
67
|
+
//
|
|
68
|
+
// Also strip AI-agent env vars (CLAUDECODE, OPENCODE, …): vitest's
|
|
69
|
+
// std-env dependency detects them and flips the text reporter into
|
|
70
|
+
// `skipFull` mode, which hides the `All files` row this test exists
|
|
71
|
+
// to assert on. CI runners never set these; the strip only matters
|
|
72
|
+
// when the suite is launched from inside an agent shell.
|
|
73
|
+
const cleanedEnv: Record<string, string | undefined> = { ...process.env };
|
|
74
|
+
for (const key of Object.keys(cleanedEnv)) {
|
|
75
|
+
if (
|
|
76
|
+
key.startsWith('npm_package_') ||
|
|
77
|
+
key.startsWith('npm_config_') ||
|
|
78
|
+
key.startsWith('npm_lifecycle_') ||
|
|
79
|
+
key === 'INIT_CWD' ||
|
|
80
|
+
key === 'OPENCODE' ||
|
|
81
|
+
key === 'CLAUDECODE' ||
|
|
82
|
+
key === 'CLAUDE_CODE' ||
|
|
83
|
+
key === 'GEMINI_CLI' ||
|
|
84
|
+
key === 'CODEX_SANDBOX' ||
|
|
85
|
+
key === 'CODEX_THREAD_ID' ||
|
|
86
|
+
key === 'AUGMENT_AGENT' ||
|
|
87
|
+
key === 'GOOSE_PROVIDER'
|
|
88
|
+
) {
|
|
89
|
+
cleanedEnv[key] = '';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
cleanedEnv[SELF_TEST_FLAG] = '1';
|
|
93
|
+
|
|
94
|
+
const result = spawnSync(
|
|
95
|
+
'pnpm',
|
|
96
|
+
[
|
|
97
|
+
'--filter',
|
|
98
|
+
'@serverless-ircd/aws-stack',
|
|
99
|
+
'exec',
|
|
100
|
+
'vitest',
|
|
101
|
+
'run',
|
|
102
|
+
'--coverage',
|
|
103
|
+
// Send the inner run's coverage output to a sibling directory so
|
|
104
|
+
// it cannot race the outer vitest that spawned us (both would
|
|
105
|
+
// otherwise write to `./coverage` and the loser crashes with
|
|
106
|
+
// "Something removed the coverage directory").
|
|
107
|
+
'--coverage.reportsDirectory=coverage-self-test',
|
|
108
|
+
],
|
|
109
|
+
{
|
|
110
|
+
encoding: 'utf8',
|
|
111
|
+
env: cleanedEnv,
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Exit code 0 = no `[ELIFECYCLE]` / early exit. Report the captured
|
|
116
|
+
// output in the assertion message so a CI failure shows what the
|
|
117
|
+
// subprocess actually printed instead of a bare status code.
|
|
118
|
+
if (result.status !== 0) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`coverage command exited with status ${result.status as number | string}\n` +
|
|
121
|
+
`--- stdout ---\n${result.stdout}\n` +
|
|
122
|
+
`--- stderr ---\n${result.stderr}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const combined = stripAnsi(`${result.stdout}\n${result.stderr}`);
|
|
127
|
+
|
|
128
|
+
// The vitest text reporter prefixes its table with this banner; the
|
|
129
|
+
// `All files` summary row follows. Asserting both makes the test
|
|
130
|
+
// robust to incidental wording changes elsewhere in the output.
|
|
131
|
+
expect(combined, 'coverage command must emit the v8 coverage banner').toMatch(
|
|
132
|
+
/Coverage report from v8/,
|
|
133
|
+
);
|
|
134
|
+
expect(combined, 'coverage command must emit an "All files" summary row').toMatch(
|
|
135
|
+
/All files/,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// Belt-and-braces: if the bundle stub ever regresses, the subprocess
|
|
139
|
+
// would flood `Bundling asset TestStack/IrcHandler/Code/Stage…` once
|
|
140
|
+
// per Lambda per test (dozens to hundreds of times) and eventually
|
|
141
|
+
// OOM in CI. With the stub active the count is at most a handful
|
|
142
|
+
// (only the deliberately-ungated production-path test triggers real
|
|
143
|
+
// esbuild runs, and it bounds itself to one stack). Assert the count
|
|
144
|
+
// stays small so a regression fails loudly here instead of looking
|
|
145
|
+
// like a mysterious `[ELIFECYCLE]` exit on the CI runner.
|
|
146
|
+
const bundleCount = (combined.match(/Bundling asset/g) ?? []).length;
|
|
147
|
+
expect(
|
|
148
|
+
bundleCount,
|
|
149
|
+
'coverage command must not loop esbuild bundling across the test synth',
|
|
150
|
+
).toBeLessThan(10);
|
|
151
|
+
},
|
|
152
|
+
COVERAGE_TIMEOUT_MS,
|
|
153
|
+
);
|
|
154
|
+
},
|
|
155
|
+
);
|
|
@@ -483,11 +483,11 @@ describe('IrcAwsStack — server identity props (configurable deploy identity)',
|
|
|
483
483
|
);
|
|
484
484
|
});
|
|
485
485
|
|
|
486
|
-
it('applies
|
|
486
|
+
it('applies a non-placeholder fallback server identity when the props are omitted', () => {
|
|
487
487
|
const { template } = makeTemplate();
|
|
488
488
|
expect(
|
|
489
489
|
findFunction(template, 'IrcHandler').Properties?.Environment?.Variables?.SERVER_NAME,
|
|
490
|
-
).toBe('irc.
|
|
490
|
+
).toBe('irc.localhost');
|
|
491
491
|
expect(
|
|
492
492
|
findFunction(template, 'IrcHandler').Properties?.Environment?.Variables?.NETWORK_NAME,
|
|
493
493
|
).toBe('ExampleNet');
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Red→green driver for the aws-stack test-mode bundle stub.
|
|
3
|
+
*
|
|
4
|
+
* The IrcAwsStack constructs up to four `NodejsFunction` resources. Each
|
|
5
|
+
* one invokes esbuild at synth time to bundle the Lambda handler entry.
|
|
6
|
+
* Repeated across the full `apps/aws-stack` test suite, the bundler ran
|
|
7
|
+
* dozens of times per coverage invocation and exhausted CI memory before
|
|
8
|
+
* the vitest coverage table could be printed (the failure mode behind
|
|
9
|
+
* the coverage-command crash).
|
|
10
|
+
*
|
|
11
|
+
* Setting `IRC_AWS_STACK_TEST_NO_BUNDLE=1` (done once for the whole
|
|
12
|
+
* suite by `tests/vitest.setup.ts`) flips the stack onto a stub code
|
|
13
|
+
* path that emits an inline Lambda zip and skips esbuild entirely,
|
|
14
|
+
* while preserving every other CloudFormation shape the rest of the
|
|
15
|
+
* suite asserts on (runtime, env vars, IAM grants, route wiring, …).
|
|
16
|
+
*
|
|
17
|
+
* The assertions below lock that stub in place. They fail loudly the
|
|
18
|
+
* moment a refactor re-routes the stack back to `NodejsFunction` under
|
|
19
|
+
* test, or drops the env-var guard, so the coverage pipeline cannot
|
|
20
|
+
* silently regress back into an unbounded esbuild loop.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { App } from 'aws-cdk-lib';
|
|
24
|
+
import { Template } from 'aws-cdk-lib/assertions';
|
|
25
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
26
|
+
import { IrcAwsStack } from '../src/aws-stack.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Collects every `AWS::Lambda::Function`'s `Code` block from a synthesized
|
|
30
|
+
* template. Under the stub each Code must be `{ ZipFile: … }` (inline) and
|
|
31
|
+
* must NOT be `{ S3Bucket: …, S3Key: … }` (the shape CDK emits only after
|
|
32
|
+
* esbuild has run).
|
|
33
|
+
*/
|
|
34
|
+
function lambdaCodeBlocks(template: Template): Array<Record<string, unknown>> {
|
|
35
|
+
const fns = template.findResources('AWS::Lambda::Function') as Record<
|
|
36
|
+
string,
|
|
37
|
+
{ Properties?: { Code?: Record<string, unknown> } }
|
|
38
|
+
>;
|
|
39
|
+
return Object.values(fns).map((fn) => fn.Properties?.Code ?? {});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('IrcAwsStack — test-mode bundle stub (no esbuild)', () => {
|
|
43
|
+
it('emits inline (ZipFile) Lambda code for the default wss stack so esbuild is skipped', () => {
|
|
44
|
+
const app = new App();
|
|
45
|
+
const stack = new IrcAwsStack(app, 'TestStack');
|
|
46
|
+
const template = Template.fromStack(stack);
|
|
47
|
+
const codes = lambdaCodeBlocks(template);
|
|
48
|
+
// handler + sweeper + pingChecker.
|
|
49
|
+
expect(codes).toHaveLength(3);
|
|
50
|
+
for (const code of codes) {
|
|
51
|
+
expect(code).toHaveProperty('ZipFile');
|
|
52
|
+
expect(code).not.toHaveProperty('S3Bucket');
|
|
53
|
+
expect(code).not.toHaveProperty('S3Key');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('emits inline (ZipFile) Lambda code for the NLB streaming handler when tcpTlsDomainName is set', () => {
|
|
58
|
+
const app = new App();
|
|
59
|
+
const stack = new IrcAwsStack(app, 'TestStack', {
|
|
60
|
+
environmentName: 'staging',
|
|
61
|
+
tcpTlsDomainName: 'irc.example.com',
|
|
62
|
+
});
|
|
63
|
+
const template = Template.fromStack(stack);
|
|
64
|
+
const codes = lambdaCodeBlocks(template);
|
|
65
|
+
// handler + sweeper + pingChecker + nlbHandler.
|
|
66
|
+
expect(codes).toHaveLength(4);
|
|
67
|
+
for (const code of codes) {
|
|
68
|
+
expect(code).toHaveProperty('ZipFile');
|
|
69
|
+
expect(code).not.toHaveProperty('S3Bucket');
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('preserves the Node 24 runtime on every stubbed Lambda', () => {
|
|
74
|
+
const app = new App();
|
|
75
|
+
const stack = new IrcAwsStack(app, 'TestStack', {
|
|
76
|
+
tcpTlsDomainName: 'irc.example.com',
|
|
77
|
+
});
|
|
78
|
+
const template = Template.fromStack(stack);
|
|
79
|
+
template.allResourcesProperties('AWS::Lambda::Function', {
|
|
80
|
+
Runtime: 'nodejs24.x',
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Production-path coverage. The stub above is the test-only fast path; the
|
|
87
|
+
* `NodejsFunction` branch in `makeLambda` is what every real `cdk synth`
|
|
88
|
+
* takes. Exercising it once here covers the otherwise-unreached return and
|
|
89
|
+
* asserts the production Code shape (`S3Bucket` / asset-backed) so a
|
|
90
|
+
* refactor that accidentally drops the production path fails loudly.
|
|
91
|
+
*
|
|
92
|
+
* Bounded to a single synth (one stack, no `tcpTlsDomainName`) so esbuild
|
|
93
|
+
* runs at most three times — enough to cover the branch without
|
|
94
|
+
* re-introducing the unbounded bundling loop the stub exists to dodge.
|
|
95
|
+
*/
|
|
96
|
+
describe('IrcAwsStack — production bundling path (no stub)', () => {
|
|
97
|
+
let savedFlag: string | undefined;
|
|
98
|
+
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
savedFlag = process.env.IRC_AWS_STACK_TEST_NO_BUNDLE;
|
|
101
|
+
process.env.IRC_AWS_STACK_TEST_NO_BUNDLE = '';
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
afterEach(() => {
|
|
105
|
+
if (savedFlag !== undefined) {
|
|
106
|
+
process.env.IRC_AWS_STACK_TEST_NO_BUNDLE = savedFlag;
|
|
107
|
+
} else {
|
|
108
|
+
process.env.IRC_AWS_STACK_TEST_NO_BUNDLE = '';
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('uses NodejsFunction (S3-bundled asset) when the stub env var is unset', () => {
|
|
113
|
+
const app = new App();
|
|
114
|
+
const stack = new IrcAwsStack(app, 'TestStack');
|
|
115
|
+
const template = Template.fromStack(stack);
|
|
116
|
+
const fns = template.findResources('AWS::Lambda::Function') as Record<
|
|
117
|
+
string,
|
|
118
|
+
{ Properties?: { Code?: Record<string, unknown> } }
|
|
119
|
+
>;
|
|
120
|
+
const codes = Object.values(fns).map((fn) => fn.Properties?.Code ?? {});
|
|
121
|
+
expect(codes).toHaveLength(3);
|
|
122
|
+
for (const code of codes) {
|
|
123
|
+
// Production synth emits an S3-backed asset (esbuild ran). No
|
|
124
|
+
// `ZipFile` should appear on any production Lambda.
|
|
125
|
+
expect(code).toHaveProperty('S3Bucket');
|
|
126
|
+
expect(code).not.toHaveProperty('ZipFile');
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest setup for the aws-stack test suite.
|
|
3
|
+
*
|
|
4
|
+
* Runs once per test file before any test body executes. Sets the flag
|
|
5
|
+
* consumed by `makeLambda()` in `src/aws-stack.ts` to route every Lambda
|
|
6
|
+
* construct onto the inline-zip stub, so synthesizing the stack under
|
|
7
|
+
* test does NOT invoke esbuild. Without this, the ~80 stack-related
|
|
8
|
+
* tests each triggered one esbuild bundle per Lambda (4 functions × 80
|
|
9
|
+
* synths = 320+ bundles per coverage run), which exhausted CI memory
|
|
10
|
+
* before the vitest coverage table could print.
|
|
11
|
+
*
|
|
12
|
+
* The stub preserves every CloudFormation shape the rest of the suite
|
|
13
|
+
* asserts on (runtime, env vars, IAM grants, route wiring, …) while
|
|
14
|
+
* making the bundling step a no-op. Production synth (`bin/aws.ts`) is
|
|
15
|
+
* driven by `cdk synth` / the CDK CLI, never via vitest, so it cannot
|
|
16
|
+
* accidentally inherit this env var.
|
|
17
|
+
*/
|
|
18
|
+
process.env.IRC_AWS_STACK_TEST_NO_BUNDLE = '1';
|
|
@@ -4,6 +4,9 @@ export default defineConfig({
|
|
|
4
4
|
test: {
|
|
5
5
|
include: ['tests/**/*.test.ts'],
|
|
6
6
|
exclude: ['cdk.out/**', 'dist/**', 'node_modules/**'],
|
|
7
|
+
setupFiles: ['./tests/vitest.setup.ts'],
|
|
8
|
+
testTimeout: 60_000,
|
|
9
|
+
fileParallelism: false,
|
|
7
10
|
coverage: {
|
|
8
11
|
provider: 'v8',
|
|
9
12
|
all: false,
|
|
@@ -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",
|
|
@@ -58,14 +58,13 @@ const DEFAULT_TCP_PORT = 6667;
|
|
|
58
58
|
const DEFAULT_TCP_HOST = '0.0.0.0';
|
|
59
59
|
const DEFAULT_SNAPSHOT_INTERVAL_MS = 60_000;
|
|
60
60
|
|
|
61
|
-
/** Default server name / network when env vars are missing. */
|
|
62
|
-
const DEFAULT_SERVER_NAME = 'irc.example.com';
|
|
63
|
-
const DEFAULT_NETWORK_NAME = 'ExampleNet';
|
|
64
|
-
|
|
65
61
|
/**
|
|
66
62
|
* Loads and validates the IRC server config plus container-specific knobs
|
|
67
63
|
* from a platform env. Throws on an invalid IRC config (bad port number,
|
|
68
|
-
* missing required fields, etc.) so boot fails fast.
|
|
64
|
+
* missing required fields, etc.) so boot fails fast. `SERVER_NAME` and
|
|
65
|
+
* `NETWORK_NAME` are required — omitting them produces a readable error
|
|
66
|
+
* pointing at the missing knob rather than silently advertising a
|
|
67
|
+
* placeholder identity.
|
|
69
68
|
*/
|
|
70
69
|
export function loadContainerConfig(env: ContainerConfigEnv): ContainerConfig {
|
|
71
70
|
const server = parseServerConfig(buildServerInput(env));
|
|
@@ -89,8 +88,8 @@ function parseIntOrDefault(raw: string | undefined, def: number): number {
|
|
|
89
88
|
|
|
90
89
|
function buildServerInput(env: ContainerConfigEnv): Record<string, unknown> {
|
|
91
90
|
const input: Record<string, unknown> = {
|
|
92
|
-
serverName: env.SERVER_NAME
|
|
93
|
-
networkName: env.NETWORK_NAME
|
|
91
|
+
serverName: env.SERVER_NAME,
|
|
92
|
+
networkName: env.NETWORK_NAME,
|
|
94
93
|
};
|
|
95
94
|
if (env.SERVER_VERSION !== undefined) input.serverVersion = env.SERVER_VERSION;
|
|
96
95
|
if (env.CREATED_AT !== undefined) input.createdAt = parseCreatedAt(env.CREATED_AT);
|
|
@@ -286,6 +286,9 @@ function attachConnection(
|
|
|
286
286
|
history,
|
|
287
287
|
logger,
|
|
288
288
|
transport: new TcpByteStreamTransport(),
|
|
289
|
+
// Spectrum terminates TLS at the edge before forwarding plaintext TCP
|
|
290
|
+
// to this origin, so every connection is secure → user mode `S`.
|
|
291
|
+
secure: true,
|
|
289
292
|
});
|
|
290
293
|
|
|
291
294
|
runtime.registerConnection(
|
|
@@ -40,9 +40,16 @@ describe('config-loader — IRC server config', () => {
|
|
|
40
40
|
]);
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
it('
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
it('throws a readable error naming serverName when SERVER_NAME is unset', () => {
|
|
44
|
+
expect(() =>
|
|
45
|
+
loadContainerConfig({ NETWORK_NAME: 'TestNet' } as ContainerConfigEnv),
|
|
46
|
+
).toThrowError(/serverName/u);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('throws a readable error naming networkName when NETWORK_NAME is unset', () => {
|
|
50
|
+
expect(() =>
|
|
51
|
+
loadContainerConfig({ SERVER_NAME: 'irc.test' } as ContainerConfigEnv),
|
|
52
|
+
).toThrowError(/networkName/u);
|
|
46
53
|
});
|
|
47
54
|
});
|
|
48
55
|
|
|
@@ -47,7 +47,7 @@ new_sqlite_classes = ["IrcTcpOrigin"]
|
|
|
47
47
|
|
|
48
48
|
# --- Environment ---
|
|
49
49
|
[vars]
|
|
50
|
-
SERVER_NAME = "irc.
|
|
50
|
+
SERVER_NAME = "irc.your-domain.invalid"
|
|
51
51
|
NETWORK_NAME = "ExampleNet"
|
|
52
52
|
MOTD_LINES = "Welcome to the ServerlessIRCd TCP origin."
|
|
53
53
|
TCP_PORT = "6667"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serverless-ircd/cf-worker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Cloudflare Worker deploy glue: WebSocket edge entry point + DO bindings + wrangler pipeline",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"@cloudflare/vitest-pool-workers": "0.18.6",
|
|
28
28
|
"@cloudflare/workers-types": "^5.20260714.1",
|
|
29
29
|
"@types/ws": "^8.5.13",
|
|
30
|
+
"@vitest/coverage-istanbul": "^4.1.0",
|
|
30
31
|
"@vitest/coverage-v8": "^4.1.0",
|
|
31
32
|
"rimraf": "^6.0.0",
|
|
32
33
|
"typescript": "^5.6.0",
|
|
@@ -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
|
+
}
|
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
* • Cross-DO coordination (registry + channel) is owned by
|
|
14
14
|
* `ConnectionDO` and `CfRuntime`; the worker never touches it.
|
|
15
15
|
*
|
|
16
|
+
* Request routing (non-WS):
|
|
17
|
+
* • `/health` — plaintext human-readable liveness string (the previous
|
|
18
|
+
* catch-all response, now scoped to an explicit path so the root URL
|
|
19
|
+
* is free for the landing page).
|
|
20
|
+
* • everything else — falls through to the `[assets]` binding, which
|
|
21
|
+
* serves the Kiwi SPA at `/app/` and the landing page at `/` from
|
|
22
|
+
* `apps/web/dist`. The asset layer is configured in `wrangler.toml`.
|
|
23
|
+
*
|
|
16
24
|
* The DO classes are re-exported so `wrangler.toml`'s
|
|
17
25
|
* `durable_objects.bindings.class_name` can resolve them in this
|
|
18
26
|
* module's scope (wrangler requires the class to be exported from the
|
|
@@ -29,6 +37,7 @@ import {
|
|
|
29
37
|
type Env,
|
|
30
38
|
RegistryDO,
|
|
31
39
|
} from '@serverless-ircd/cf-adapter';
|
|
40
|
+
import { decideOrigin, parseWebOrigins } from './origin-allowlist';
|
|
32
41
|
|
|
33
42
|
// Re-exported for wrangler DO binding resolution. The class names below
|
|
34
43
|
// MUST match `class_name` in `wrangler.toml`.
|
|
@@ -36,9 +45,68 @@ export { ChannelDO, ChannelRegistryDO, ConnectionDO, RegistryDO };
|
|
|
36
45
|
export type { Env };
|
|
37
46
|
|
|
38
47
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
48
|
+
* Production {@link Env} for the worker entry. Extends the cf-adapter
|
|
49
|
+
* {@link Env} with the `ASSETS` binding (the static-asset `Fetcher` for
|
|
50
|
+
* `/app/` SPA + `/` landing page). The cf-adapter's internal types stay
|
|
51
|
+
* asset-agnostic; only the edge entry knows about the asset layer.
|
|
52
|
+
*/
|
|
53
|
+
export interface WorkerEnv extends Env {
|
|
54
|
+
/**
|
|
55
|
+
* Static-asset `Fetcher` backed by `apps/web/dist` (production) or
|
|
56
|
+
* `tests/fixtures/web-dist` (test). Non-WS, non-`/health` requests are
|
|
57
|
+
* forwarded here so the asset layer serves the SPA and landing page
|
|
58
|
+
* without the worker inspecting asset paths.
|
|
59
|
+
*/
|
|
60
|
+
ASSETS: Fetcher;
|
|
61
|
+
/**
|
|
62
|
+
* Optional comma-separated allowlist of web origins permitted to open
|
|
63
|
+
* WebSocket upgrades (CSWSH defense — browsers send `Origin` on every
|
|
64
|
+
* cross-origin upgrade; WebSocket upgrades do not otherwise enforce
|
|
65
|
+
* same-origin).
|
|
66
|
+
*
|
|
67
|
+
* When set, only listed origins proceed. When unset/empty (the default),
|
|
68
|
+
* the Worker auto-derives the expected origin from the request's own
|
|
69
|
+
* `Host` header (same-origin enforcement) — no per-env configuration
|
|
70
|
+
* needed. Set this only for cross-origin deployments where the SPA is
|
|
71
|
+
* served from a different domain than the Worker.
|
|
72
|
+
*
|
|
73
|
+
* Non-browser clients (curl, WeeChat, the `tcp-ws-forwarder`) omit
|
|
74
|
+
* `Origin` and pass through unchanged regardless of mode.
|
|
75
|
+
*/
|
|
76
|
+
WEB_ORIGINS: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Human-readable liveness string surfaced at `GET /health`. Previously the
|
|
81
|
+
* catch-all response for every non-WS request; now scoped to an explicit
|
|
82
|
+
* path so `/` can serve the landing page from the `[assets]` binding.
|
|
83
|
+
*/
|
|
84
|
+
const HEALTH_BODY =
|
|
85
|
+
'ServerlessIRCd (Cloudflare Worker). Connect via WebSocket to begin an IRC session.';
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Response body sent when a browser-sent `Origin` is not allowed.
|
|
89
|
+
* Mentioned in `docs/web-client.md` as the canonical CSWSH rejection.
|
|
90
|
+
*/
|
|
91
|
+
const ORIGIN_DENIED_BODY = 'WebSocket upgrade refused: the request Origin is not allowed.';
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Response body sent when neither the allowlist nor the request origin could
|
|
95
|
+
* be determined. In practice this never fires on Cloudflare (the platform
|
|
96
|
+
* always sets `Host`), but the pure policy function keeps the case for
|
|
97
|
+
* defensive completeness.
|
|
98
|
+
*/
|
|
99
|
+
const ORIGIN_UNCONFIGURED_BODY =
|
|
100
|
+
'WebSocket upgrades refused: could not determine the request origin and WEB_ORIGINS is not configured.';
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Default edge handler.
|
|
104
|
+
*
|
|
105
|
+
* Routing order:
|
|
106
|
+
* 1. WebSocket upgrade → CSWSH `Origin` check, then per-connection
|
|
107
|
+
* `ConnectionDO`.
|
|
108
|
+
* 2. `GET /health` → plaintext liveness response.
|
|
109
|
+
* 3. everything else → `env.ASSETS` (Kiwi SPA at `/app/`, landing at `/`).
|
|
42
110
|
*
|
|
43
111
|
* The connection id is allocated here via `crypto.randomUUID()` (the
|
|
44
112
|
* Web Crypto API is sync-available in Workers for RFC-4122 v4 UUIDs).
|
|
@@ -47,19 +115,31 @@ export type { Env };
|
|
|
47
115
|
* it before falling back to a fresh uuid.
|
|
48
116
|
*/
|
|
49
117
|
export default {
|
|
50
|
-
async fetch(request: Request, env:
|
|
118
|
+
async fetch(request: Request, env: WorkerEnv): Promise<Response> {
|
|
51
119
|
if (request.headers.get('Upgrade') === 'websocket') {
|
|
120
|
+
const allowlist = parseWebOrigins(env.WEB_ORIGINS);
|
|
121
|
+
const url = new URL(request.url);
|
|
122
|
+
const requestOrigin = `${url.protocol}//${url.host}`;
|
|
123
|
+
const decision = decideOrigin(request.headers.get('Origin'), allowlist, requestOrigin);
|
|
124
|
+
if (decision !== 'allow') {
|
|
125
|
+
const body = decision === 'fail-closed' ? ORIGIN_UNCONFIGURED_BODY : ORIGIN_DENIED_BODY;
|
|
126
|
+
return new Response(body, {
|
|
127
|
+
status: 403,
|
|
128
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
129
|
+
});
|
|
130
|
+
}
|
|
52
131
|
const connId = crypto.randomUUID();
|
|
53
132
|
const id = env.CONNECTION_DO.idFromName(connId);
|
|
54
133
|
const stub = env.CONNECTION_DO.get(id);
|
|
55
134
|
return stub.fetch(request);
|
|
56
135
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
{
|
|
136
|
+
const url = new URL(request.url);
|
|
137
|
+
if (url.pathname === '/health') {
|
|
138
|
+
return new Response(HEALTH_BODY, {
|
|
60
139
|
status: 200,
|
|
61
140
|
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
62
|
-
}
|
|
63
|
-
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return env.ASSETS.fetch(request);
|
|
64
144
|
},
|
|
65
145
|
};
|