serverless-ircd 0.6.0 → 0.8.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 +9 -4
- package/.gitmodules +4 -1
- package/CHANGELOG.md +480 -22
- package/README.md +244 -57
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +186 -18
- package/apps/aws-stack/tests/stack.test.ts +400 -56
- 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/index.html +18 -0
- package/apps/cf-worker/tests/fixtures/web-dist/webclient/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 +56 -0
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +8 -0
- package/apps/local-cli/src/main.ts +16 -0
- package/apps/local-cli/src/server.ts +1 -0
- package/apps/local-cli/tests/config-loader.test.ts +14 -0
- package/apps/web/landing/index.html +215 -0
- package/apps/web/package.json +28 -0
- package/apps/web/scripts/build.mjs +132 -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/AWS-Deployment.md +21 -8
- package/docs/Cloudflare-Deployment-Guide.md +22 -6
- package/docs/Home.md +1 -0
- package/docs/PlanExtensions.md +113 -3
- package/docs/Release-Process.md +23 -13
- package/docs/Services.md +546 -0
- package/docs/WebClientGuide.md +536 -0
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/aws-runtime.ts +5 -0
- package/packages/aws-adapter/src/cdk-table-defs.ts +6 -0
- package/packages/aws-adapter/src/config-loader.ts +11 -0
- package/packages/aws-adapter/src/connection-counter.ts +89 -0
- package/packages/aws-adapter/src/dynamo-services-store.ts +649 -0
- package/packages/aws-adapter/src/handlers/connect.ts +55 -51
- package/packages/aws-adapter/src/handlers/default.ts +36 -4
- package/packages/aws-adapter/src/handlers/index.ts +15 -0
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +15 -0
- package/packages/aws-adapter/src/handlers/sweeper.ts +5 -1
- package/packages/aws-adapter/src/index.ts +4 -0
- package/packages/aws-adapter/src/stats.ts +6 -1
- package/packages/aws-adapter/src/tables.ts +34 -4
- package/packages/aws-adapter/tests/aws-harness.ts +3 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +8 -0
- package/packages/aws-adapter/tests/connect.test.ts +158 -32
- package/packages/aws-adapter/tests/connection-counter.test.ts +127 -0
- package/packages/aws-adapter/tests/dynamo-services-store-dynamo.test.ts +183 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +568 -0
- package/packages/aws-adapter/tests/handlers.test.ts +105 -3
- package/packages/aws-adapter/tests/tables.test.ts +6 -1
- package/packages/cf-adapter/package.json +1 -1
- package/packages/cf-adapter/src/config-loader.ts +11 -0
- package/packages/cf-adapter/src/connection-do.ts +112 -2
- package/packages/cf-adapter/src/d1-services-store.ts +703 -0
- package/packages/cf-adapter/src/env.ts +8 -0
- package/packages/cf-adapter/src/index.ts +5 -0
- package/packages/cf-adapter/tests/cf-harness.ts +12 -1
- package/packages/cf-adapter/tests/config-loader.test.ts +19 -0
- package/packages/cf-adapter/tests/connection-do-nickserv-d1.test.ts +128 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +150 -2
- package/packages/cf-adapter/tests/d1-services-store.test.ts +582 -0
- package/packages/cf-adapter/tests/serialize.test.ts +1 -0
- 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/scripts/generate-build-info.mjs +26 -5
- package/packages/irc-core/src/commands/account-auth.ts +172 -0
- package/packages/irc-core/src/commands/chanserv.ts +882 -0
- package/packages/irc-core/src/commands/hostserv.ts +487 -0
- package/packages/irc-core/src/commands/index.ts +12 -0
- package/packages/irc-core/src/commands/join.ts +164 -8
- package/packages/irc-core/src/commands/markread.ts +202 -0
- package/packages/irc-core/src/commands/memoserv.ts +319 -0
- package/packages/irc-core/src/commands/mode.ts +96 -4
- package/packages/irc-core/src/commands/nickserv.ts +390 -0
- package/packages/irc-core/src/commands/oper.ts +18 -1
- package/packages/irc-core/src/commands/operserv.ts +346 -0
- package/packages/irc-core/src/commands/pre-away.ts +3 -1
- package/packages/irc-core/src/commands/privmsg.ts +42 -0
- package/packages/irc-core/src/commands/read-marker.ts +8 -8
- package/packages/irc-core/src/commands/registration.ts +61 -6
- package/packages/irc-core/src/commands/sasl.ts +18 -49
- package/packages/irc-core/src/commands/tagmsg.ts +41 -6
- package/packages/irc-core/src/commands/topic.ts +37 -0
- package/packages/irc-core/src/config.ts +36 -5
- package/packages/irc-core/src/effects.ts +56 -1
- package/packages/irc-core/src/ports.ts +1653 -84
- package/packages/irc-core/src/protocol/numerics.ts +8 -0
- package/packages/irc-core/src/state/channel.ts +21 -1
- package/packages/irc-core/src/state/connection.ts +25 -1
- package/packages/irc-core/src/types.ts +48 -12
- package/packages/irc-core/tests/batch.test.ts +15 -0
- package/packages/irc-core/tests/commands/chanserv.test.ts +1668 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +6 -0
- package/packages/irc-core/tests/commands/hostserv.test.ts +935 -0
- package/packages/irc-core/tests/commands/join.test.ts +393 -1
- package/packages/irc-core/tests/commands/markread.test.ts +361 -0
- package/packages/irc-core/tests/commands/memoserv.test.ts +654 -0
- package/packages/irc-core/tests/commands/mode.test.ts +381 -2
- package/packages/irc-core/tests/commands/nickserv.test.ts +807 -0
- package/packages/irc-core/tests/commands/oper.test.ts +13 -0
- package/packages/irc-core/tests/commands/operserv.test.ts +656 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +147 -0
- package/packages/irc-core/tests/commands/read-marker.test.ts +28 -28
- package/packages/irc-core/tests/commands/registration.test.ts +788 -14
- package/packages/irc-core/tests/commands/sasl.test.ts +185 -12
- package/packages/irc-core/tests/commands/server-info.test.ts +9 -5
- package/packages/irc-core/tests/commands/tagmsg.test.ts +73 -33
- package/packages/irc-core/tests/commands/topic.test.ts +94 -2
- package/packages/irc-core/tests/commands/unified-account.test.ts +416 -0
- package/packages/irc-core/tests/config.test.ts +49 -5
- package/packages/irc-core/tests/effects.test.ts +19 -0
- package/packages/irc-core/tests/message-store.test.ts +63 -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/persistent-services-store.test.ts +582 -0
- package/packages/irc-core/tests/services-store.test.ts +1289 -0
- package/packages/irc-core/tests/state/channel.test.ts +3 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +71 -16
- package/packages/irc-server/src/dispatch.ts +94 -7
- package/packages/irc-server/src/routing.ts +19 -0
- package/packages/irc-server/tests/actor.test.ts +623 -12
- package/packages/irc-server/tests/dispatch.test.ts +270 -2
- package/packages/irc-server/tests/routing.test.ts +6 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +29 -3
- package/packages/irc-test-support/src/index.ts +1 -0
- package/packages/irc-test-support/src/scenarios.ts +24 -2
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +32 -0
- 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/tools/tcp-ws-forwarder/tests/forwarder.test.ts +2 -2
- package/packages/irc-core/tests/read-marker-store.test.ts +0 -108
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Effect } from '@serverless-ircd/irc-core';
|
|
2
2
|
import { CapturingLogger, LogLevel } from '@serverless-ircd/irc-core';
|
|
3
3
|
import type { ChannelDelta, ConnId, RawLine } from '@serverless-ircd/irc-core';
|
|
4
|
-
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { dispatch } from '../src/dispatch';
|
|
6
6
|
import type { IrcRuntime } from '../src/runtime';
|
|
7
7
|
|
|
@@ -251,6 +251,12 @@ describe('dispatch', () => {
|
|
|
251
251
|
|
|
252
252
|
interface MemberInfo {
|
|
253
253
|
caps: ReadonlySet<string>;
|
|
254
|
+
/**
|
|
255
|
+
* Optional SASL/NickServ account the member is identified as. When set,
|
|
256
|
+
* the fake runtime's `getConnectionInfo` surfaces it so account-scoped
|
|
257
|
+
* broadcast filtering (draft/read-marker privacy) can be exercised.
|
|
258
|
+
*/
|
|
259
|
+
account?: string;
|
|
254
260
|
}
|
|
255
261
|
|
|
256
262
|
/**
|
|
@@ -300,9 +306,11 @@ function capAwareRuntime(members: ReadonlyMap<ConnId, MemberInfo>): {
|
|
|
300
306
|
wallops: false,
|
|
301
307
|
serverNotices: false,
|
|
302
308
|
tls: false,
|
|
309
|
+
registered: false,
|
|
303
310
|
},
|
|
304
311
|
lastSeen: 0,
|
|
305
312
|
connectedSince: 0,
|
|
313
|
+
...(info.account !== undefined ? { account: info.account } : {}),
|
|
306
314
|
};
|
|
307
315
|
},
|
|
308
316
|
async getConnection() {
|
|
@@ -323,6 +331,9 @@ function capAwareRuntime(members: ReadonlyMap<ConnId, MemberInfo>): {
|
|
|
323
331
|
moderated: false,
|
|
324
332
|
secret: false,
|
|
325
333
|
private: false,
|
|
334
|
+
registered: false,
|
|
335
|
+
blockUnidentified: false,
|
|
336
|
+
moderatedIdent: false,
|
|
326
337
|
},
|
|
327
338
|
banMasks: new Set<string>(),
|
|
328
339
|
members: roster,
|
|
@@ -491,6 +502,7 @@ describe('dispatch — Broadcast cap / capLines', () => {
|
|
|
491
502
|
wallops: false,
|
|
492
503
|
serverNotices: false,
|
|
493
504
|
tls: false,
|
|
505
|
+
registered: false,
|
|
494
506
|
},
|
|
495
507
|
lastSeen: 0,
|
|
496
508
|
connectedSince: 0,
|
|
@@ -516,6 +528,9 @@ describe('dispatch — Broadcast cap / capLines', () => {
|
|
|
516
528
|
moderated: false,
|
|
517
529
|
secret: false,
|
|
518
530
|
private: false,
|
|
531
|
+
registered: false,
|
|
532
|
+
blockUnidentified: false,
|
|
533
|
+
moderatedIdent: false,
|
|
519
534
|
},
|
|
520
535
|
banMasks: new Set<string>(),
|
|
521
536
|
members: roster,
|
|
@@ -690,7 +705,183 @@ describe('dispatch — Broadcast caps (OR semantics)', () => {
|
|
|
690
705
|
});
|
|
691
706
|
|
|
692
707
|
// ===========================================================================
|
|
693
|
-
// dispatch — Broadcast
|
|
708
|
+
// dispatch — Broadcast account-scoped recipient filter (draft/read-marker)
|
|
709
|
+
//
|
|
710
|
+
// IRCv3 `draft/read-marker` privacy: a read marker is private to the
|
|
711
|
+
// originator's account and MUST NOT be disclosed to other users. The reducer
|
|
712
|
+
// annotates the mark TAGMSG Broadcast with `account`; dispatch restricts the
|
|
713
|
+
// recipient set to channel members authenticated as that account (case-
|
|
714
|
+
// insensitive), still honouring `except` and the OR-semantics `caps` filter.
|
|
715
|
+
// Unidentified peers (no account) never receive a mark bound to an account.
|
|
716
|
+
// ===========================================================================
|
|
717
|
+
|
|
718
|
+
describe('dispatch — Broadcast account-scoped filter (draft/read-marker)', () => {
|
|
719
|
+
it('delivers ONLY to same-account members; cross-account members are excluded', async () => {
|
|
720
|
+
const members = memberMap([
|
|
721
|
+
['c1', { caps: new Set(['message-tags']), account: 'alice' }], // originator (except)
|
|
722
|
+
['c2', { caps: new Set(['message-tags']), account: 'alice' }], // same account
|
|
723
|
+
['c3', { caps: new Set(['message-tags']), account: 'bob' }], // different account
|
|
724
|
+
]);
|
|
725
|
+
const { runtime, sends, broadcasts } = capAwareRuntime(members);
|
|
726
|
+
|
|
727
|
+
await dispatch(
|
|
728
|
+
[
|
|
729
|
+
Effect.broadcast(
|
|
730
|
+
'#foo',
|
|
731
|
+
[L('@+draft/read-marker=m1 :a!u@h TAGMSG #foo')],
|
|
732
|
+
'c1',
|
|
733
|
+
undefined,
|
|
734
|
+
undefined,
|
|
735
|
+
['message-tags', 'draft/read-marker'],
|
|
736
|
+
'alice',
|
|
737
|
+
),
|
|
738
|
+
],
|
|
739
|
+
runtime,
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
// No fast-path bulk broadcast: dispatch routes per-recipient.
|
|
743
|
+
expect(broadcasts).toHaveLength(0);
|
|
744
|
+
// Only c2 (same account) receives; c3 (different account) is excluded.
|
|
745
|
+
expect(sends.map((s) => s.to).sort()).toEqual(['c2']);
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it('excludes unidentified members (no account) from an account-scoped mark', async () => {
|
|
749
|
+
const members = memberMap([
|
|
750
|
+
['c1', { caps: new Set(['message-tags']), account: 'alice' }], // originator (except)
|
|
751
|
+
['c2', { caps: new Set(['message-tags']) }], // unidentified
|
|
752
|
+
]);
|
|
753
|
+
const { runtime, sends } = capAwareRuntime(members);
|
|
754
|
+
|
|
755
|
+
await dispatch(
|
|
756
|
+
[
|
|
757
|
+
Effect.broadcast(
|
|
758
|
+
'#foo',
|
|
759
|
+
[L('@+draft/read-marker=m1 :a!u@h TAGMSG #foo')],
|
|
760
|
+
'c1',
|
|
761
|
+
undefined,
|
|
762
|
+
undefined,
|
|
763
|
+
['message-tags', 'draft/read-marker'],
|
|
764
|
+
'alice',
|
|
765
|
+
),
|
|
766
|
+
],
|
|
767
|
+
runtime,
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
expect(sends).toHaveLength(0);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it('matches the account case-insensitively (Alice vs alice)', async () => {
|
|
774
|
+
const members = memberMap([
|
|
775
|
+
['c1', { caps: new Set(['message-tags']), account: 'alice' }], // originator (except)
|
|
776
|
+
['c2', { caps: new Set(['message-tags']), account: 'ALICE' }], // same account, diff case
|
|
777
|
+
]);
|
|
778
|
+
const { runtime, sends } = capAwareRuntime(members);
|
|
779
|
+
|
|
780
|
+
await dispatch(
|
|
781
|
+
[
|
|
782
|
+
Effect.broadcast(
|
|
783
|
+
'#foo',
|
|
784
|
+
[L('@+draft/read-marker=m1 :a!u@h TAGMSG #foo')],
|
|
785
|
+
'c1',
|
|
786
|
+
undefined,
|
|
787
|
+
undefined,
|
|
788
|
+
['message-tags', 'draft/read-marker'],
|
|
789
|
+
'alice',
|
|
790
|
+
),
|
|
791
|
+
],
|
|
792
|
+
runtime,
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
expect(sends.map((s) => s.to)).toEqual(['c2']);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it('still applies the OR-caps filter alongside the account filter', async () => {
|
|
799
|
+
const members = memberMap([
|
|
800
|
+
['c1', { caps: new Set(['message-tags']), account: 'alice' }], // originator (except)
|
|
801
|
+
// same account but no relevant cap — excluded by caps filter:
|
|
802
|
+
['c2', { caps: new Set<string>(), account: 'alice' }],
|
|
803
|
+
// same account, draft/read-marker cap — delivered:
|
|
804
|
+
['c3', { caps: new Set(['draft/read-marker']), account: 'alice' }],
|
|
805
|
+
]);
|
|
806
|
+
const { runtime, sends } = capAwareRuntime(members);
|
|
807
|
+
|
|
808
|
+
await dispatch(
|
|
809
|
+
[
|
|
810
|
+
Effect.broadcast(
|
|
811
|
+
'#foo',
|
|
812
|
+
[L('@+draft/read-marker=m1 :a!u@h TAGMSG #foo')],
|
|
813
|
+
'c1',
|
|
814
|
+
undefined,
|
|
815
|
+
undefined,
|
|
816
|
+
['message-tags', 'draft/read-marker'],
|
|
817
|
+
'alice',
|
|
818
|
+
),
|
|
819
|
+
],
|
|
820
|
+
runtime,
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
expect(sends.map((s) => s.to)).toEqual(['c3']);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
it('ignores the account filter when account is unset (channel-wide broadcast)', async () => {
|
|
827
|
+
const members = memberMap([
|
|
828
|
+
['c1', { caps: new Set(['message-tags']), account: 'alice' }],
|
|
829
|
+
['c2', { caps: new Set(['message-tags']), account: 'bob' }],
|
|
830
|
+
['c3', { caps: new Set(['message-tags']), account: 'carol' }],
|
|
831
|
+
]);
|
|
832
|
+
const { runtime, sends } = capAwareRuntime(members);
|
|
833
|
+
|
|
834
|
+
await dispatch(
|
|
835
|
+
[
|
|
836
|
+
Effect.broadcast(
|
|
837
|
+
'#foo',
|
|
838
|
+
[L('@+draft/typing=active :a!u@h TAGMSG #foo')],
|
|
839
|
+
'c1',
|
|
840
|
+
undefined,
|
|
841
|
+
undefined,
|
|
842
|
+
['message-tags', 'draft/typing'],
|
|
843
|
+
),
|
|
844
|
+
],
|
|
845
|
+
runtime,
|
|
846
|
+
);
|
|
847
|
+
|
|
848
|
+
// No account filter → every other member receives regardless of account.
|
|
849
|
+
expect(sends.map((s) => s.to).sort()).toEqual(['c2', 'c3']);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it('delivers to every same-account member regardless of caps when only account is set', async () => {
|
|
853
|
+
// account-only broadcast (no cap / caps): the recipient set is purely
|
|
854
|
+
// account-scoped, so caps play no role — every same-account member
|
|
855
|
+
// receives the line, even with an empty cap set.
|
|
856
|
+
const members = memberMap([
|
|
857
|
+
['c1', { caps: new Set<string>(), account: 'alice' }], // originator (except)
|
|
858
|
+
['c2', { caps: new Set<string>(), account: 'alice' }], // same account, no caps
|
|
859
|
+
['c3', { caps: new Set<string>(), account: 'bob' }], // different account
|
|
860
|
+
]);
|
|
861
|
+
const { runtime, sends, broadcasts } = capAwareRuntime(members);
|
|
862
|
+
|
|
863
|
+
await dispatch(
|
|
864
|
+
[
|
|
865
|
+
Effect.broadcast(
|
|
866
|
+
'#foo',
|
|
867
|
+
[L(':a!u@h TAGMSG #foo')],
|
|
868
|
+
'c1',
|
|
869
|
+
undefined,
|
|
870
|
+
undefined,
|
|
871
|
+
undefined,
|
|
872
|
+
'alice',
|
|
873
|
+
),
|
|
874
|
+
],
|
|
875
|
+
runtime,
|
|
876
|
+
);
|
|
877
|
+
|
|
878
|
+
// No fast-path bulk broadcast: dispatch routes per-recipient.
|
|
879
|
+
expect(broadcasts).toHaveLength(0);
|
|
880
|
+
// c2 (same account, no caps) receives; c3 (different account) is excluded.
|
|
881
|
+
expect(sends.map((s) => s.to)).toEqual(['c2']);
|
|
882
|
+
});
|
|
883
|
+
});
|
|
884
|
+
|
|
694
885
|
//
|
|
695
886
|
// Recipients that did NOT negotiate `message-tags` must never receive a line
|
|
696
887
|
// beginning with `@+<client-tag>`. The dispatch broadcast per-recipient path
|
|
@@ -875,3 +1066,80 @@ describe('dispatch — observability', () => {
|
|
|
875
1066
|
expect(errs[0]?.fields?.err).toBe('string-error');
|
|
876
1067
|
});
|
|
877
1068
|
});
|
|
1069
|
+
|
|
1070
|
+
describe('dispatch — EnforceNick', () => {
|
|
1071
|
+
it('disconnects immediately for the kill policy', async () => {
|
|
1072
|
+
const calls: RecordedCall[] = [];
|
|
1073
|
+
await dispatch([Effect.enforceNick('c2', 'alice', 'kill', 0)], fakeRuntime(calls));
|
|
1074
|
+
expect(calls).toEqual([{ method: 'disconnect', args: ['c2', 'Nick enforced by NickServ'] }]);
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
it('is a no-op for the none policy (no runtime calls)', async () => {
|
|
1078
|
+
const calls: RecordedCall[] = [];
|
|
1079
|
+
await dispatch([Effect.enforceNick('c2', 'alice', 'none', 0)], fakeRuntime(calls));
|
|
1080
|
+
expect(calls).toEqual([]);
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
it('schedules the disconnect after graceMs for ghost via the injected scheduler', async () => {
|
|
1084
|
+
const calls: RecordedCall[] = [];
|
|
1085
|
+
const scheduled: { ms: number }[] = [];
|
|
1086
|
+
let scheduledCallback: (() => void) | undefined;
|
|
1087
|
+
const scheduler = (cb: () => void, ms: number): void => {
|
|
1088
|
+
scheduled.push({ ms });
|
|
1089
|
+
scheduledCallback = cb;
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
await dispatch([Effect.enforceNick('c2', 'alice', 'ghost', 30_000)], fakeRuntime(calls), {
|
|
1093
|
+
scheduler,
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
// Disconnect must NOT have fired yet (still within the grace window).
|
|
1097
|
+
expect(calls).toEqual([]);
|
|
1098
|
+
expect(scheduled).toEqual([{ ms: 30_000 }]);
|
|
1099
|
+
expect(scheduledCallback).toBeTypeOf('function');
|
|
1100
|
+
|
|
1101
|
+
// Flush the grace timer → disconnect fires with the canonical reason.
|
|
1102
|
+
scheduledCallback?.();
|
|
1103
|
+
await Promise.resolve();
|
|
1104
|
+
|
|
1105
|
+
expect(calls).toEqual([{ method: 'disconnect', args: ['c2', 'Nick enforced by NickServ'] }]);
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
it('disconnects immediately for ghost when graceMs is 0', async () => {
|
|
1109
|
+
const calls: RecordedCall[] = [];
|
|
1110
|
+
const scheduler = (): void => {
|
|
1111
|
+
// Should never be called when graceMs is 0.
|
|
1112
|
+
throw new Error('scheduler must not be invoked for graceMs=0');
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
await dispatch([Effect.enforceNick('c2', 'alice', 'ghost', 0)], fakeRuntime(calls), {
|
|
1116
|
+
scheduler,
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
expect(calls).toEqual([{ method: 'disconnect', args: ['c2', 'Nick enforced by NickServ'] }]);
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
it('still disconnects for ghost when no scheduler is supplied (setTimeout fallback)', async () => {
|
|
1123
|
+
// graceMs=0 takes the immediate path, so the missing scheduler is fine.
|
|
1124
|
+
const calls: RecordedCall[] = [];
|
|
1125
|
+
await dispatch([Effect.enforceNick('c2', 'alice', 'ghost', 0)], fakeRuntime(calls));
|
|
1126
|
+
expect(calls).toEqual([{ method: 'disconnect', args: ['c2', 'Nick enforced by NickServ'] }]);
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
it('uses the default setTimeout scheduler when no scheduler is injected (ghost with grace)', async () => {
|
|
1130
|
+
vi.useFakeTimers();
|
|
1131
|
+
try {
|
|
1132
|
+
const calls: RecordedCall[] = [];
|
|
1133
|
+
// dispatch resolves once the timer is scheduled; the disconnect is deferred.
|
|
1134
|
+
await dispatch([Effect.enforceNick('c2', 'alice', 'ghost', 30_000)], fakeRuntime(calls));
|
|
1135
|
+
expect(calls).toEqual([]);
|
|
1136
|
+
|
|
1137
|
+
// Flushing the fake timer fires the scheduled disconnect.
|
|
1138
|
+
await vi.advanceTimersByTimeAsync(30_000);
|
|
1139
|
+
|
|
1140
|
+
expect(calls).toEqual([{ method: 'disconnect', args: ['c2', 'Nick enforced by NickServ'] }]);
|
|
1141
|
+
} finally {
|
|
1142
|
+
vi.useRealTimers();
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
});
|
|
@@ -114,16 +114,22 @@ describe('buildRoutedReducers', () => {
|
|
|
114
114
|
'away',
|
|
115
115
|
'cap',
|
|
116
116
|
'channelMode',
|
|
117
|
+
'chanserv',
|
|
118
|
+
'hostserv',
|
|
117
119
|
'info',
|
|
118
120
|
'invite',
|
|
119
121
|
'join',
|
|
120
122
|
'kick',
|
|
123
|
+
'markreadChannel',
|
|
121
124
|
'motd',
|
|
122
125
|
'names',
|
|
123
126
|
'nick',
|
|
127
|
+
'nickserv',
|
|
128
|
+
'memoserv',
|
|
124
129
|
'noticeChannel',
|
|
125
130
|
'noticeUser',
|
|
126
131
|
'oper',
|
|
132
|
+
'operserv',
|
|
127
133
|
'part',
|
|
128
134
|
'pass',
|
|
129
135
|
'ping',
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
FakeClock,
|
|
22
22
|
type RawLine,
|
|
23
23
|
SequentialIdFactory,
|
|
24
|
+
type ServerConfig,
|
|
24
25
|
StaticMotdProvider,
|
|
25
26
|
createConnection,
|
|
26
27
|
} from '@serverless-ircd/irc-core';
|
|
@@ -44,7 +45,14 @@ const POLL_MS = 10;
|
|
|
44
45
|
/** Default timeout for waitFor* implementations (ms). */
|
|
45
46
|
const DEFAULT_TIMEOUT_MS = 1000;
|
|
46
47
|
|
|
47
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Shared server config used by the harness. Hard-coded — tests assert on it.
|
|
50
|
+
*
|
|
51
|
+
* `serverPassword` is intentionally NOT set here so the default harness
|
|
52
|
+
* leaves the registration gate disabled and every existing scenario
|
|
53
|
+
* registers without `PASS`. Construct the harness with
|
|
54
|
+
* `{ serverPassword: '...' }` to opt a scenario into the gated path.
|
|
55
|
+
*/
|
|
48
56
|
export const HARNESS_SERVER_CONFIG = {
|
|
49
57
|
serverName: 'irc.example.com',
|
|
50
58
|
networkName: 'ExampleNet',
|
|
@@ -57,6 +65,17 @@ export const HARNESS_SERVER_CONFIG = {
|
|
|
57
65
|
quitMessage: 'Client Quit',
|
|
58
66
|
} as const;
|
|
59
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Optional harness configuration. `serverPassword` opts a scenario into
|
|
70
|
+
* the server-password gate: when set, every spawned client must supply
|
|
71
|
+
* the matching `PASS <value>` (or SASL-identify) before `001
|
|
72
|
+
* RPL_WELCOME`. Default unset → gate disabled (existing scenarios
|
|
73
|
+
* unchanged).
|
|
74
|
+
*/
|
|
75
|
+
export interface InMemoryHarnessOptions {
|
|
76
|
+
serverPassword?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
60
79
|
/**
|
|
61
80
|
* Default MOTD lines exposed to every spawned client. The registration
|
|
62
81
|
* welcome block renders these lines as `375`/`372`×N/`376` numerics, and
|
|
@@ -120,6 +139,7 @@ export class InMemoryHarness implements IrcHarness {
|
|
|
120
139
|
private readonly motd = new StaticMotdProvider(HARNESS_MOTD_LINES);
|
|
121
140
|
private readonly clients = new Map<ConnId, TrackedClient>();
|
|
122
141
|
private readonly transportKind: TransportKind;
|
|
142
|
+
private readonly serverConfig: ServerConfig;
|
|
123
143
|
private nextId = 0;
|
|
124
144
|
private closed = false;
|
|
125
145
|
|
|
@@ -130,9 +150,15 @@ export class InMemoryHarness implements IrcHarness {
|
|
|
130
150
|
* {@link TcpByteStreamTransport} so chunk-boundary scenarios exercise
|
|
131
151
|
* real byte-stream framing. One transport instance per client (TCP
|
|
132
152
|
* transports own a per-connection partial-line buffer).
|
|
153
|
+
* @param opts Optional harness-wide config. `serverPassword` opts a
|
|
154
|
+
* scenario into the registration gate (default off).
|
|
133
155
|
*/
|
|
134
|
-
constructor(transportKind: TransportKind = 'ws') {
|
|
156
|
+
constructor(transportKind: TransportKind = 'ws', opts: InMemoryHarnessOptions = {}) {
|
|
135
157
|
this.transportKind = transportKind;
|
|
158
|
+
this.serverConfig =
|
|
159
|
+
opts.serverPassword === undefined
|
|
160
|
+
? HARNESS_SERVER_CONFIG
|
|
161
|
+
: { ...HARNESS_SERVER_CONFIG, serverPassword: opts.serverPassword };
|
|
136
162
|
}
|
|
137
163
|
|
|
138
164
|
async spawnClient(opts?: SpawnClientOptions): Promise<ClientHarness> {
|
|
@@ -162,7 +188,7 @@ export class InMemoryHarness implements IrcHarness {
|
|
|
162
188
|
state,
|
|
163
189
|
runtime: this.runtime,
|
|
164
190
|
channels: this.runtime,
|
|
165
|
-
serverConfig:
|
|
191
|
+
serverConfig: this.serverConfig,
|
|
166
192
|
clock: this.clock,
|
|
167
193
|
ids: this.ids,
|
|
168
194
|
motd: this.motd,
|
|
@@ -48,6 +48,21 @@ export function runIrcScenarios(factories: readonly IrcHarnessFactory[]): void {
|
|
|
48
48
|
return undefined;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Numeric error codes that PRIVMSG/NOTICE rejection paths would emit
|
|
53
|
+
* (401 nick-target only). RFC: NOTICE MUST NOT generate any of these.
|
|
54
|
+
* Used by the NOTICE scenario so a late-arriving MOTD/welcome numeric
|
|
55
|
+
* (e.g. 376) on a slow transport does not false-positive the check.
|
|
56
|
+
*/
|
|
57
|
+
const NOTICE_ERROR_CODES: ReadonlySet<string> = new Set([
|
|
58
|
+
'401', // ERR_NOSUCHNICK
|
|
59
|
+
'403', // ERR_NOSUCHCHANNEL
|
|
60
|
+
'404', // ERR_CANNOTSENDTOCHAN
|
|
61
|
+
'411', // ERR_NORECIPIENT
|
|
62
|
+
'412', // ERR_NOTEXTTOSEND
|
|
63
|
+
'442', // ERR_NOTONCHANNEL
|
|
64
|
+
]);
|
|
65
|
+
|
|
51
66
|
/**
|
|
52
67
|
* Synchronization helper: alice (already in #chan) waits until she sees
|
|
53
68
|
* bob's JOIN broadcast. By the time this resolves, the runtime has also
|
|
@@ -442,8 +457,15 @@ export function runIrcScenarios(factories: readonly IrcHarnessFactory[]): void {
|
|
|
442
457
|
bob.received.length = 0;
|
|
443
458
|
await alice.send('NOTICE #notice :quiet note');
|
|
444
459
|
await bob.waitForLine((l) => l.includes('NOTICE #notice :quiet note'));
|
|
445
|
-
|
|
446
|
-
|
|
460
|
+
// RFC: NOTICE MUST NOT generate an error reply. Check only the
|
|
461
|
+
// NOTICE-relevant error numerics — not any numeric — so a delayed
|
|
462
|
+
// MOTD/welcome line draining through a slow transport (e.g. the CF
|
|
463
|
+
// WebSocket pool under load) does not false-positive the assertion.
|
|
464
|
+
const noticeErrors = alice.received.filter((l) => {
|
|
465
|
+
const code = numericOf(l);
|
|
466
|
+
return code !== undefined && NOTICE_ERROR_CODES.has(code);
|
|
467
|
+
});
|
|
468
|
+
expect(noticeErrors).toEqual([]);
|
|
447
469
|
await harness.close();
|
|
448
470
|
});
|
|
449
471
|
|
|
@@ -228,3 +228,35 @@ describe('InMemoryClientHarness.close', () => {
|
|
|
228
228
|
await h.close();
|
|
229
229
|
});
|
|
230
230
|
});
|
|
231
|
+
|
|
232
|
+
describe('InMemoryHarness — server-password gate opt-in', () => {
|
|
233
|
+
it('rejects registration with 464 + disconnect when serverPassword is set and no PASS is sent', async () => {
|
|
234
|
+
const h = new InMemoryHarness('ws', { serverPassword: 's3cret' });
|
|
235
|
+
const c = await h.spawnClient();
|
|
236
|
+
await c.send('NICK alice');
|
|
237
|
+
await c.send('USER alice 0 * :alice');
|
|
238
|
+
// No PASS → gate fires: 464 then the connection is torn down.
|
|
239
|
+
const line464 = await c.waitForNumeric('464');
|
|
240
|
+
expect(line464).toMatch(/464 alice :Password mismatch/u);
|
|
241
|
+
await h.close();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('completes registration when PASS matches the configured serverPassword', async () => {
|
|
245
|
+
const h = new InMemoryHarness('ws', { serverPassword: 's3cret' });
|
|
246
|
+
const c = await h.spawnClient();
|
|
247
|
+
await c.send('PASS s3cret');
|
|
248
|
+
await c.send('NICK bob');
|
|
249
|
+
await c.send('USER bob 0 * :bob');
|
|
250
|
+
await c.waitForNumeric('001');
|
|
251
|
+
await h.close();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('leaves the gate disabled by default (no serverPassword option)', async () => {
|
|
255
|
+
const h = new InMemoryHarness();
|
|
256
|
+
const c = await h.spawnClient();
|
|
257
|
+
await c.send('NICK carol');
|
|
258
|
+
await c.send('USER carol 0 * :carol');
|
|
259
|
+
await c.waitForNumeric('001');
|
|
260
|
+
await h.close();
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serverless-ircd/tcp-ws-forwarder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Local TCP to ws/wss forwarder so standard IRC clients can reach the WebSocket-only ServerlessIRCd transport",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
@@ -367,8 +367,8 @@ describe('forwarder — TCP→WS framing', () => {
|
|
|
367
367
|
await client.opened();
|
|
368
368
|
client.sendRaw('PING :bare\n');
|
|
369
369
|
|
|
370
|
-
await target.waitForConnection();
|
|
371
|
-
await new Promise<void>((res) =>
|
|
370
|
+
const ws = await target.waitForConnection();
|
|
371
|
+
await new Promise<void>((res) => ws.once('message', () => res()));
|
|
372
372
|
expect(target.frames).toEqual(['PING :bare']);
|
|
373
373
|
});
|
|
374
374
|
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { InMemoryReadMarkerStore, type ReadMarkerStore } from '../src/ports';
|
|
3
|
-
|
|
4
|
-
describe('InMemoryReadMarkerStore — port conformance', () => {
|
|
5
|
-
it('implements the ReadMarkerStore port', () => {
|
|
6
|
-
const store: ReadMarkerStore = new InMemoryReadMarkerStore();
|
|
7
|
-
expect(store).toBeDefined();
|
|
8
|
-
});
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
describe('InMemoryReadMarkerStore — set + get', () => {
|
|
12
|
-
it('returns undefined for an unknown (account, channel) pair', () => {
|
|
13
|
-
const store = new InMemoryReadMarkerStore();
|
|
14
|
-
expect(store.get('alice', '#foo')).toBeUndefined();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('stores and returns the msgid for a known (account, channel) pair', () => {
|
|
18
|
-
const store = new InMemoryReadMarkerStore();
|
|
19
|
-
store.set('alice', '#foo', 'm1');
|
|
20
|
-
expect(store.get('alice', '#foo')).toBe('m1');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('overwrites the marker on a subsequent set for the same pair', () => {
|
|
24
|
-
const store = new InMemoryReadMarkerStore();
|
|
25
|
-
store.set('alice', '#foo', 'm1');
|
|
26
|
-
store.set('alice', '#foo', 'm2');
|
|
27
|
-
expect(store.get('alice', '#foo')).toBe('m2');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('keys channels case-insensitively (rfc1459 fold)', () => {
|
|
31
|
-
const store = new InMemoryReadMarkerStore();
|
|
32
|
-
store.set('alice', '#Foo', 'm1');
|
|
33
|
-
expect(store.get('alice', '#foo')).toBe('m1');
|
|
34
|
-
expect(store.get('alice', '#FOO')).toBe('m1');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('keys accounts case-insensitively (rfc1459 fold)', () => {
|
|
38
|
-
const store = new InMemoryReadMarkerStore();
|
|
39
|
-
store.set('Alice', '#foo', 'm1');
|
|
40
|
-
expect(store.get('alice', '#foo')).toBe('m1');
|
|
41
|
-
expect(store.get('ALICE', '#foo')).toBe('m1');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('keeps markers for distinct accounts on the same channel separate', () => {
|
|
45
|
-
const store = new InMemoryReadMarkerStore();
|
|
46
|
-
store.set('alice', '#foo', 'm1');
|
|
47
|
-
store.set('bob', '#foo', 'm9');
|
|
48
|
-
expect(store.get('alice', '#foo')).toBe('m1');
|
|
49
|
-
expect(store.get('bob', '#foo')).toBe('m9');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('keeps markers for distinct channels on the same account separate', () => {
|
|
53
|
-
const store = new InMemoryReadMarkerStore();
|
|
54
|
-
store.set('alice', '#foo', 'm1');
|
|
55
|
-
store.set('alice', '#bar', 'm2');
|
|
56
|
-
expect(store.get('alice', '#foo')).toBe('m1');
|
|
57
|
-
expect(store.get('alice', '#bar')).toBe('m2');
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('InMemoryReadMarkerStore — forAccount', () => {
|
|
62
|
-
it('returns an empty array for an account with no markers', () => {
|
|
63
|
-
const store = new InMemoryReadMarkerStore();
|
|
64
|
-
expect(store.forAccount('alice')).toEqual([]);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('returns every (channel, msgid) pair recorded for the account', () => {
|
|
68
|
-
const store = new InMemoryReadMarkerStore();
|
|
69
|
-
store.set('alice', '#foo', 'm1');
|
|
70
|
-
store.set('alice', '#bar', 'm2');
|
|
71
|
-
|
|
72
|
-
const entries = store.forAccount('alice');
|
|
73
|
-
expect(entries).toHaveLength(2);
|
|
74
|
-
const byChan = Object.fromEntries(entries.map((e) => [e.channel, e.msgid]));
|
|
75
|
-
expect(byChan['#foo']).toBe('m1');
|
|
76
|
-
expect(byChan['#bar']).toBe('m2');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('reports the account name on each entry', () => {
|
|
80
|
-
const store = new InMemoryReadMarkerStore();
|
|
81
|
-
store.set('alice', '#foo', 'm1');
|
|
82
|
-
const entries = store.forAccount('alice');
|
|
83
|
-
expect(entries[0]?.account).toBe('alice');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('reflects the most recent set when a pair is overwritten', () => {
|
|
87
|
-
const store = new InMemoryReadMarkerStore();
|
|
88
|
-
store.set('alice', '#foo', 'm1');
|
|
89
|
-
store.set('alice', '#foo', 'm2');
|
|
90
|
-
const entries = store.forAccount('alice');
|
|
91
|
-
expect(entries).toHaveLength(1);
|
|
92
|
-
expect(entries[0]?.msgid).toBe('m2');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('does not leak markers belonging to a different account', () => {
|
|
96
|
-
const store = new InMemoryReadMarkerStore();
|
|
97
|
-
store.set('alice', '#foo', 'm1');
|
|
98
|
-
store.set('bob', '#bar', 'm2');
|
|
99
|
-
expect(store.forAccount('alice')).toHaveLength(1);
|
|
100
|
-
expect(store.forAccount('bob')).toHaveLength(1);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('folds the account name case-insensitively on lookup', () => {
|
|
104
|
-
const store = new InMemoryReadMarkerStore();
|
|
105
|
-
store.set('Alice', '#foo', 'm1');
|
|
106
|
-
expect(store.forAccount('ALICE')).toHaveLength(1);
|
|
107
|
-
});
|
|
108
|
-
});
|