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
|
@@ -65,9 +65,22 @@ describe('operReducer — successful authentication', () => {
|
|
|
65
65
|
expect(out.state.userModes.oper).toBe(true);
|
|
66
66
|
expect(out.effects).toEqual<EffectType[]>([
|
|
67
67
|
Effect.send('c1', [L(':irc.example.com 381 alice :You are now an IRC operator')]),
|
|
68
|
+
Effect.send('c1', [L(':alice!alice@example.com MODE alice +o')]),
|
|
68
69
|
]);
|
|
69
70
|
});
|
|
70
71
|
|
|
72
|
+
it('emits a MODE +o notification to the connection after granting oper', () => {
|
|
73
|
+
const state = makeState();
|
|
74
|
+
const ctx = makeCtx(state);
|
|
75
|
+
|
|
76
|
+
const out = operReducer(state, oper('alice', 'secret'), ctx);
|
|
77
|
+
|
|
78
|
+
const modeLines = out.effects
|
|
79
|
+
.filter((e): e is Extract<EffectType, { tag: 'Send' }> => e.tag === 'Send')
|
|
80
|
+
.flatMap((e) => e.lines.map((l) => l.text));
|
|
81
|
+
expect(modeLines).toContain(':alice!alice@example.com MODE alice +o');
|
|
82
|
+
});
|
|
83
|
+
|
|
71
84
|
it('matches the second configured credential pair', () => {
|
|
72
85
|
const state = makeState();
|
|
73
86
|
const ctx = makeCtx(state);
|
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
OPERSERV_NICK,
|
|
4
|
+
checkAkillAdmission,
|
|
5
|
+
enforceJupedNick,
|
|
6
|
+
operservReducer,
|
|
7
|
+
} from '../../src/commands/operserv';
|
|
8
|
+
import type { Effect as EffectType } from '../../src/effects';
|
|
9
|
+
import type { ServicesStore } from '../../src/ports';
|
|
10
|
+
import {
|
|
11
|
+
EmptyMotdProvider,
|
|
12
|
+
FakeClock,
|
|
13
|
+
InMemoryServicesStore,
|
|
14
|
+
SequentialIdFactory,
|
|
15
|
+
} from '../../src/ports';
|
|
16
|
+
import type { IrcMessage } from '../../src/protocol/messages';
|
|
17
|
+
import { type ConnectionState, createConnection } from '../../src/state/connection';
|
|
18
|
+
import { type Ctx, type ServerConfig, buildCtx } from '../../src/types';
|
|
19
|
+
|
|
20
|
+
const NOW = 1_700_000_000_000;
|
|
21
|
+
|
|
22
|
+
const serverConfig: ServerConfig = {
|
|
23
|
+
serverName: 'irc.example.com',
|
|
24
|
+
networkName: 'ExampleNet',
|
|
25
|
+
maxChannelsPerUser: 30,
|
|
26
|
+
maxTargetsPerCommand: 10,
|
|
27
|
+
maxListEntries: 50,
|
|
28
|
+
nickLen: 30,
|
|
29
|
+
channelLen: 50,
|
|
30
|
+
topicLen: 390,
|
|
31
|
+
quitMessage: 'Client Quit',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function makeConn(id = 'c1', nick = 'admin'): ConnectionState {
|
|
35
|
+
const s = createConnection({ id, connectedSince: 0 });
|
|
36
|
+
s.nick = nick;
|
|
37
|
+
s.user = nick;
|
|
38
|
+
s.host = 'example.com';
|
|
39
|
+
s.realname = nick;
|
|
40
|
+
s.registration = 'registered';
|
|
41
|
+
return s;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Returns a connection marked as oper (the OperServ precondition). */
|
|
45
|
+
function makeOper(id = 'c1', nick = 'admin'): ConnectionState {
|
|
46
|
+
const s = makeConn(id, nick);
|
|
47
|
+
s.userModes.oper = true;
|
|
48
|
+
return s;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function makeCtx(
|
|
52
|
+
state: ConnectionState,
|
|
53
|
+
services?: ServicesStore,
|
|
54
|
+
clock = new FakeClock(NOW),
|
|
55
|
+
): Ctx {
|
|
56
|
+
return buildCtx({
|
|
57
|
+
serverConfig,
|
|
58
|
+
clock,
|
|
59
|
+
ids: new SequentialIdFactory(),
|
|
60
|
+
motd: EmptyMotdProvider,
|
|
61
|
+
connection: state,
|
|
62
|
+
...(services !== undefined ? { services } : {}),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function privmsg(body: string): IrcMessage {
|
|
67
|
+
return { command: 'PRIVMSG', params: [OPERSERV_NICK, body], tags: {} };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Extracts the text of every Send line emitted by the reducer. */
|
|
71
|
+
function sentTexts(effects: EffectType[]): string[] {
|
|
72
|
+
return effects.flatMap((e) => (e.tag === 'Send' ? e.lines.map((l) => l.text) : []));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Extracts the text of every Disconnect line emitted by the reducer. */
|
|
76
|
+
function disconnectReasons(effects: EffectType[]): string[] {
|
|
77
|
+
return effects.flatMap((e) => (e.tag === 'Disconnect' ? [e.reason ?? ''] : []));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe('OPERSERV_NICK constant', () => {
|
|
81
|
+
it('is the canonical OperServ pseudo-client nick', () => {
|
|
82
|
+
expect(OPERSERV_NICK).toBe('OperServ');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// OperServ precondition — oper-only gate
|
|
88
|
+
// ============================================================================
|
|
89
|
+
|
|
90
|
+
describe('operservReducer — oper gate', () => {
|
|
91
|
+
it('rejects every subcommand from a non-oper with a NOTICE and no state change', () => {
|
|
92
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
93
|
+
const conn = makeConn();
|
|
94
|
+
const ctx = makeCtx(conn, services);
|
|
95
|
+
|
|
96
|
+
const out = operservReducer(conn, privmsg('AKILL ADD *!*@bad.example flooding'), ctx);
|
|
97
|
+
|
|
98
|
+
expect(services.listAkills()).toEqual([]);
|
|
99
|
+
expect(services.listJupes()).toEqual([]);
|
|
100
|
+
expect(sentTexts(out.effects)).toContain(
|
|
101
|
+
':OperServ!OperServ@services NOTICE admin :Permission denied — oper privileges required.',
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('rejects every subcommand when services are not bound', () => {
|
|
106
|
+
const conn = makeOper();
|
|
107
|
+
const ctx = makeCtx(conn);
|
|
108
|
+
|
|
109
|
+
const out = operservReducer(conn, privmsg('AKILL ADD *!*@bad.example flooding'), ctx);
|
|
110
|
+
|
|
111
|
+
expect(sentTexts(out.effects)).toContain(
|
|
112
|
+
':OperServ!OperServ@services NOTICE admin :Services are not available on this server.',
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('returns a help NOTICE when PRIVMSG has no body', () => {
|
|
117
|
+
const conn = makeOper();
|
|
118
|
+
const ctx = makeCtx(conn, new InMemoryServicesStore({ clock: new FakeClock(NOW) }));
|
|
119
|
+
|
|
120
|
+
const out = operservReducer(
|
|
121
|
+
conn,
|
|
122
|
+
{ command: 'PRIVMSG', params: [OPERSERV_NICK], tags: {} },
|
|
123
|
+
ctx,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
expect(sentTexts(out.effects)).toContain(
|
|
127
|
+
':OperServ!OperServ@services NOTICE admin :Available commands: AKILL, JUPE, RAW',
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('returns a help NOTICE when the body is whitespace only', () => {
|
|
132
|
+
const conn = makeOper();
|
|
133
|
+
const ctx = makeCtx(conn, new InMemoryServicesStore({ clock: new FakeClock(NOW) }));
|
|
134
|
+
|
|
135
|
+
const out = operservReducer(conn, privmsg(' '), ctx);
|
|
136
|
+
|
|
137
|
+
expect(sentTexts(out.effects)).toContain(
|
|
138
|
+
':OperServ!OperServ@services NOTICE admin :Available commands: AKILL, JUPE, RAW',
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('rejects an unknown subcommand with a NOTICE', () => {
|
|
143
|
+
const conn = makeOper();
|
|
144
|
+
const ctx = makeCtx(conn, new InMemoryServicesStore({ clock: new FakeClock(NOW) }));
|
|
145
|
+
|
|
146
|
+
const out = operservReducer(conn, privmsg('FROBNICATE'), ctx);
|
|
147
|
+
|
|
148
|
+
expect(sentTexts(out.effects)).toContain(
|
|
149
|
+
':OperServ!OperServ@services NOTICE admin :Unknown command. Available: AKILL, JUPE, RAW',
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('addresses the NOTICE to "*" when the oper connection has no nick yet', () => {
|
|
154
|
+
const conn = createConnection({ id: 'c1', connectedSince: 0 });
|
|
155
|
+
conn.userModes.oper = true;
|
|
156
|
+
const ctx = makeCtx(conn, new InMemoryServicesStore({ clock: new FakeClock(NOW) }));
|
|
157
|
+
|
|
158
|
+
const out = operservReducer(conn, privmsg('FROBNICATE'), ctx);
|
|
159
|
+
|
|
160
|
+
expect(sentTexts(out.effects)).toContain(
|
|
161
|
+
':OperServ!OperServ@services NOTICE * :Unknown command. Available: AKILL, JUPE, RAW',
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ============================================================================
|
|
167
|
+
// AKILL ADD / DEL / LIST
|
|
168
|
+
// ============================================================================
|
|
169
|
+
|
|
170
|
+
describe('operservReducer — AKILL ADD', () => {
|
|
171
|
+
it('records the AKILL mask + reason and confirms via NOTICE', () => {
|
|
172
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
173
|
+
const conn = makeOper();
|
|
174
|
+
const ctx = makeCtx(conn, services);
|
|
175
|
+
|
|
176
|
+
const out = operservReducer(conn, privmsg('AKILL ADD *!*@bad.example.net flooding'), ctx);
|
|
177
|
+
|
|
178
|
+
expect(services.listAkills()).toHaveLength(1);
|
|
179
|
+
expect(services.listAkills()[0]?.mask).toBe('*!*@bad.example.net');
|
|
180
|
+
expect(services.listAkills()[0]?.reason).toBe('flooding');
|
|
181
|
+
expect(sentTexts(out.effects)).toContain(
|
|
182
|
+
':OperServ!OperServ@services NOTICE admin :AKILL *!*@bad.example.net added (reason: flooding).',
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('treats everything after the mask as the reason (multi-word)', () => {
|
|
187
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
188
|
+
const conn = makeOper();
|
|
189
|
+
const ctx = makeCtx(conn, services);
|
|
190
|
+
|
|
191
|
+
operservReducer(conn, privmsg('AKILL ADD *!*@bad.example.net mass flood from net'), ctx);
|
|
192
|
+
|
|
193
|
+
expect(services.listAkills()[0]?.reason).toBe('mass flood from net');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('requires both a mask and a reason (NOTICE on missing reason)', () => {
|
|
197
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
198
|
+
const conn = makeOper();
|
|
199
|
+
const ctx = makeCtx(conn, services);
|
|
200
|
+
|
|
201
|
+
const out = operservReducer(conn, privmsg('AKILL ADD *!*@bad.example.net'), ctx);
|
|
202
|
+
|
|
203
|
+
expect(services.listAkills()).toHaveLength(0);
|
|
204
|
+
expect(sentTexts(out.effects)).toContain(
|
|
205
|
+
':OperServ!OperServ@services NOTICE admin :Syntax: AKILL ADD <mask> <reason>',
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('requires both a mask and a reason (NOTICE on missing mask)', () => {
|
|
210
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
211
|
+
const conn = makeOper();
|
|
212
|
+
const ctx = makeCtx(conn, services);
|
|
213
|
+
|
|
214
|
+
const out = operservReducer(conn, privmsg('AKILL ADD'), ctx);
|
|
215
|
+
|
|
216
|
+
expect(services.listAkills()).toHaveLength(0);
|
|
217
|
+
expect(sentTexts(out.effects)).toContain(
|
|
218
|
+
':OperServ!OperServ@services NOTICE admin :Syntax: AKILL ADD <mask> <reason>',
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('replaces the reason on a duplicate mask', () => {
|
|
223
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
224
|
+
const conn = makeOper();
|
|
225
|
+
const ctx = makeCtx(conn, services);
|
|
226
|
+
|
|
227
|
+
operservReducer(conn, privmsg('AKILL ADD *!*@bad.example.net flooding'), ctx);
|
|
228
|
+
const out = operservReducer(conn, privmsg('AKILL ADD *!*@bad.example.net worse flooding'), ctx);
|
|
229
|
+
|
|
230
|
+
expect(services.listAkills()).toHaveLength(1);
|
|
231
|
+
expect(services.listAkills()[0]?.reason).toBe('worse flooding');
|
|
232
|
+
expect(sentTexts(out.effects)).toContain(
|
|
233
|
+
':OperServ!OperServ@services NOTICE admin :AKILL *!*@bad.example.net added (reason: worse flooding).',
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('operservReducer — AKILL DEL', () => {
|
|
239
|
+
it('removes a matching mask and confirms via NOTICE', () => {
|
|
240
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
241
|
+
services.addAkill('*!*@bad.example.net', 'flooding');
|
|
242
|
+
const conn = makeOper();
|
|
243
|
+
const ctx = makeCtx(conn, services);
|
|
244
|
+
|
|
245
|
+
const out = operservReducer(conn, privmsg('AKILL DEL *!*@bad.example.net'), ctx);
|
|
246
|
+
|
|
247
|
+
expect(services.listAkills()).toEqual([]);
|
|
248
|
+
expect(sentTexts(out.effects)).toContain(
|
|
249
|
+
':OperServ!OperServ@services NOTICE admin :AKILL *!*@bad.example.net removed.',
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('NOTIFIES the oper when the mask is not on the list', () => {
|
|
254
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
255
|
+
services.addAkill('*!*@bad.example.net', 'flooding');
|
|
256
|
+
const conn = makeOper();
|
|
257
|
+
const ctx = makeCtx(conn, services);
|
|
258
|
+
|
|
259
|
+
const out = operservReducer(conn, privmsg('AKILL DEL *!*@other.example.net'), ctx);
|
|
260
|
+
|
|
261
|
+
expect(services.listAkills()).toHaveLength(1);
|
|
262
|
+
expect(sentTexts(out.effects)).toContain(
|
|
263
|
+
':OperServ!OperServ@services NOTICE admin :No AKILL matching *!*@other.example.net.',
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('requires a mask argument', () => {
|
|
268
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
269
|
+
const conn = makeOper();
|
|
270
|
+
const ctx = makeCtx(conn, services);
|
|
271
|
+
|
|
272
|
+
const out = operservReducer(conn, privmsg('AKILL DEL'), ctx);
|
|
273
|
+
|
|
274
|
+
expect(sentTexts(out.effects)).toContain(
|
|
275
|
+
':OperServ!OperServ@services NOTICE admin :Syntax: AKILL DEL <mask>',
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('matches the mask case-insensitively', () => {
|
|
280
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
281
|
+
services.addAkill('*!*@Bad.Example.Net', 'flooding');
|
|
282
|
+
const conn = makeOper();
|
|
283
|
+
const ctx = makeCtx(conn, services);
|
|
284
|
+
|
|
285
|
+
operservReducer(conn, privmsg('AKILL DEL *!*@bad.example.net'), ctx);
|
|
286
|
+
|
|
287
|
+
expect(services.listAkills()).toEqual([]);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('operservReducer — AKILL LIST', () => {
|
|
292
|
+
it('returns an empty-list NOTICE on a fresh store', () => {
|
|
293
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
294
|
+
const conn = makeOper();
|
|
295
|
+
const ctx = makeCtx(conn, services);
|
|
296
|
+
|
|
297
|
+
const out = operservReducer(conn, privmsg('AKILL LIST'), ctx);
|
|
298
|
+
|
|
299
|
+
expect(sentTexts(out.effects)).toContain(
|
|
300
|
+
':OperServ!OperServ@services NOTICE admin :No AKILLs are currently set.',
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('enumerates every recorded AKILL with mask + reason', () => {
|
|
305
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
306
|
+
services.addAkill('*!*@a.example', 'flooding');
|
|
307
|
+
services.addAkill('bot*!*@*', 'bot flood');
|
|
308
|
+
const conn = makeOper();
|
|
309
|
+
const ctx = makeCtx(conn, services);
|
|
310
|
+
|
|
311
|
+
const out = operservReducer(conn, privmsg('AKILL LIST'), ctx);
|
|
312
|
+
|
|
313
|
+
const texts = sentTexts(out.effects);
|
|
314
|
+
expect(texts).toContain(':OperServ!OperServ@services NOTICE admin :*!*@a.example — flooding');
|
|
315
|
+
expect(texts).toContain(':OperServ!OperServ@services NOTICE admin :bot*!*@* — bot flood');
|
|
316
|
+
expect(texts).toContain(':OperServ!OperServ@services NOTICE admin :End of AKILL list.');
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe('operservReducer — AKILL (unknown action)', () => {
|
|
321
|
+
it('NOTIFIES the oper on an unknown AKILL action', () => {
|
|
322
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
323
|
+
const conn = makeOper();
|
|
324
|
+
const ctx = makeCtx(conn, services);
|
|
325
|
+
|
|
326
|
+
const out = operservReducer(conn, privmsg('AKILL FROB'), ctx);
|
|
327
|
+
|
|
328
|
+
expect(sentTexts(out.effects)).toContain(
|
|
329
|
+
':OperServ!OperServ@services NOTICE admin :Syntax: AKILL ADD|DEL|LIST [args]',
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('NOTIFIES the oper on a bare AKILL with no action', () => {
|
|
334
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
335
|
+
const conn = makeOper();
|
|
336
|
+
const ctx = makeCtx(conn, services);
|
|
337
|
+
|
|
338
|
+
const out = operservReducer(conn, privmsg('AKILL'), ctx);
|
|
339
|
+
|
|
340
|
+
expect(sentTexts(out.effects)).toContain(
|
|
341
|
+
':OperServ!OperServ@services NOTICE admin :Syntax: AKILL ADD|DEL|LIST [args]',
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// ============================================================================
|
|
347
|
+
// JUPE / UNJUPE / LIST
|
|
348
|
+
// ============================================================================
|
|
349
|
+
|
|
350
|
+
describe('operservReducer — JUPE', () => {
|
|
351
|
+
it('records the jupe and confirms via NOTICE', () => {
|
|
352
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
353
|
+
const conn = makeOper();
|
|
354
|
+
const ctx = makeCtx(conn, services);
|
|
355
|
+
|
|
356
|
+
const out = operservReducer(conn, privmsg('JUPE moo'), ctx);
|
|
357
|
+
|
|
358
|
+
expect(services.isJuped('moo')).toBe(true);
|
|
359
|
+
expect(services.getJupe('moo')?.reason).toBe('');
|
|
360
|
+
expect(sentTexts(out.effects)).toContain(
|
|
361
|
+
':OperServ!OperServ@services NOTICE admin :JUPE moo added.',
|
|
362
|
+
);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('captures a reason from the rest of the line', () => {
|
|
366
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
367
|
+
const conn = makeOper();
|
|
368
|
+
const ctx = makeCtx(conn, services);
|
|
369
|
+
|
|
370
|
+
const out = operservReducer(conn, privmsg('JUPE moo blocked by abuse'), ctx);
|
|
371
|
+
|
|
372
|
+
expect(services.getJupe('moo')?.reason).toBe('blocked by abuse');
|
|
373
|
+
expect(sentTexts(out.effects)).toContain(
|
|
374
|
+
':OperServ!OperServ@services NOTICE admin :JUPE moo added (reason: blocked by abuse).',
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('requires a name argument', () => {
|
|
379
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
380
|
+
const conn = makeOper();
|
|
381
|
+
const ctx = makeCtx(conn, services);
|
|
382
|
+
|
|
383
|
+
const out = operservReducer(conn, privmsg('JUPE'), ctx);
|
|
384
|
+
|
|
385
|
+
expect(services.listJupes()).toEqual([]);
|
|
386
|
+
expect(sentTexts(out.effects)).toContain(
|
|
387
|
+
':OperServ!OperServ@services NOTICE admin :Syntax: JUPE <nick> [reason]',
|
|
388
|
+
);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('replaces the reason on a duplicate name', () => {
|
|
392
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
393
|
+
const conn = makeOper();
|
|
394
|
+
const ctx = makeCtx(conn, services);
|
|
395
|
+
|
|
396
|
+
operservReducer(conn, privmsg('JUPE moo one'), ctx);
|
|
397
|
+
operservReducer(conn, privmsg('JUPE moo two'), ctx);
|
|
398
|
+
|
|
399
|
+
expect(services.listJupes()).toHaveLength(1);
|
|
400
|
+
expect(services.getJupe('moo')?.reason).toBe('two');
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
describe('operservReducer — UNJUPE', () => {
|
|
405
|
+
it('removes a jupe and confirms via NOTICE', () => {
|
|
406
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
407
|
+
services.addJupe('moo', 'blocked');
|
|
408
|
+
const conn = makeOper();
|
|
409
|
+
const ctx = makeCtx(conn, services);
|
|
410
|
+
|
|
411
|
+
const out = operservReducer(conn, privmsg('UNJUPE moo'), ctx);
|
|
412
|
+
|
|
413
|
+
expect(services.isJuped('moo')).toBe(false);
|
|
414
|
+
expect(sentTexts(out.effects)).toContain(
|
|
415
|
+
':OperServ!OperServ@services NOTICE admin :JUPE moo removed.',
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('NOTIFIES the oper when the name is not juped', () => {
|
|
420
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
421
|
+
services.addJupe('moo', 'blocked');
|
|
422
|
+
const conn = makeOper();
|
|
423
|
+
const ctx = makeCtx(conn, services);
|
|
424
|
+
|
|
425
|
+
const out = operservReducer(conn, privmsg('UNJUPE bar'), ctx);
|
|
426
|
+
|
|
427
|
+
expect(services.isJuped('moo')).toBe(true);
|
|
428
|
+
expect(sentTexts(out.effects)).toContain(
|
|
429
|
+
':OperServ!OperServ@services NOTICE admin :No JUPE matching bar.',
|
|
430
|
+
);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('requires a name argument', () => {
|
|
434
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
435
|
+
const conn = makeOper();
|
|
436
|
+
const ctx = makeCtx(conn, services);
|
|
437
|
+
|
|
438
|
+
const out = operservReducer(conn, privmsg('UNJUPE'), ctx);
|
|
439
|
+
|
|
440
|
+
expect(sentTexts(out.effects)).toContain(
|
|
441
|
+
':OperServ!OperServ@services NOTICE admin :Syntax: UNJUPE <nick>',
|
|
442
|
+
);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// ============================================================================
|
|
447
|
+
// RAW
|
|
448
|
+
// ============================================================================
|
|
449
|
+
|
|
450
|
+
describe('operservReducer — RAW', () => {
|
|
451
|
+
it('echoes the raw line back to the oper via a Send effect', () => {
|
|
452
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
453
|
+
const conn = makeOper();
|
|
454
|
+
const ctx = makeCtx(conn, services);
|
|
455
|
+
|
|
456
|
+
const out = operservReducer(conn, privmsg('RAW :irc.example.com 001 alice :Welcome'), ctx);
|
|
457
|
+
|
|
458
|
+
// The actor interprets the Send as a raw injection. The oper should
|
|
459
|
+
// observe the line they asked the server to emit.
|
|
460
|
+
const sent = out.effects.find(
|
|
461
|
+
(e) =>
|
|
462
|
+
e.tag === 'Send' && e.to === conn.id && e.lines.some((l) => !l.text.includes('NOTICE')),
|
|
463
|
+
);
|
|
464
|
+
expect(sent).toBeDefined();
|
|
465
|
+
if (sent === undefined) return;
|
|
466
|
+
if (sent.tag !== 'Send') return;
|
|
467
|
+
expect(sent.lines.map((l) => l.text)).toContain(':irc.example.com 001 alice :Welcome');
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('emits a confirmation NOTICE so the oper sees the line was accepted', () => {
|
|
471
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
472
|
+
const conn = makeOper();
|
|
473
|
+
const ctx = makeCtx(conn, services);
|
|
474
|
+
|
|
475
|
+
const out = operservReducer(conn, privmsg('RAW :server NOTICE * :test'), ctx);
|
|
476
|
+
|
|
477
|
+
expect(sentTexts(out.effects)).toContain(
|
|
478
|
+
':OperServ!OperServ@services NOTICE admin :RAW line accepted and logged.',
|
|
479
|
+
);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('requires a non-empty line argument', () => {
|
|
483
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
484
|
+
const conn = makeOper();
|
|
485
|
+
const ctx = makeCtx(conn, services);
|
|
486
|
+
|
|
487
|
+
const out = operservReducer(conn, privmsg('RAW'), ctx);
|
|
488
|
+
|
|
489
|
+
expect(sentTexts(out.effects)).toContain(
|
|
490
|
+
':OperServ!OperServ@services NOTICE admin :Syntax: RAW <line>',
|
|
491
|
+
);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('rejects RAW from a non-oper with the oper-gate NOTICE', () => {
|
|
495
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
496
|
+
const conn = makeConn();
|
|
497
|
+
const ctx = makeCtx(conn, services);
|
|
498
|
+
|
|
499
|
+
const out = operservReducer(conn, privmsg('RAW :server NOTICE * :test'), ctx);
|
|
500
|
+
|
|
501
|
+
expect(sentTexts(out.effects)).toContain(
|
|
502
|
+
':OperServ!OperServ@services NOTICE admin :Permission denied — oper privileges required.',
|
|
503
|
+
);
|
|
504
|
+
// No raw Send effect from a non-oper — the requested line must not be
|
|
505
|
+
// echoed back. Only the rejection NOTICE should be observed.
|
|
506
|
+
const raw = out.effects.find(
|
|
507
|
+
(e) => e.tag === 'Send' && e.lines.some((l) => l.text === ':server NOTICE * :test'),
|
|
508
|
+
);
|
|
509
|
+
expect(raw).toBeUndefined();
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// ============================================================================
|
|
514
|
+
// Admission hook — checkAkillAdmission
|
|
515
|
+
// ============================================================================
|
|
516
|
+
|
|
517
|
+
describe('checkAkillAdmission', () => {
|
|
518
|
+
it('returns an empty effect list when no services store is bound', () => {
|
|
519
|
+
const conn = makeConn();
|
|
520
|
+
const ctx = makeCtx(conn);
|
|
521
|
+
|
|
522
|
+
const effects = checkAkillAdmission(conn, ctx);
|
|
523
|
+
|
|
524
|
+
expect(effects).toEqual([]);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('returns an empty effect list when the connection has no hostmask yet', () => {
|
|
528
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
529
|
+
services.addAkill('*!*@bad.example.net', 'flooding');
|
|
530
|
+
const conn = createConnection({ id: 'c1', connectedSince: 0 });
|
|
531
|
+
const ctx = makeCtx(conn, services);
|
|
532
|
+
|
|
533
|
+
const effects = checkAkillAdmission(conn, ctx);
|
|
534
|
+
|
|
535
|
+
expect(effects).toEqual([]);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('returns an empty effect list when no AKILL matches the hostmask', () => {
|
|
539
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
540
|
+
services.addAkill('*!*@bad.example.net', 'flooding');
|
|
541
|
+
const conn = makeConn();
|
|
542
|
+
conn.user = 'alice';
|
|
543
|
+
conn.host = 'good.example.net';
|
|
544
|
+
const ctx = makeCtx(conn, services);
|
|
545
|
+
|
|
546
|
+
const effects = checkAkillAdmission(conn, ctx);
|
|
547
|
+
|
|
548
|
+
expect(effects).toEqual([]);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('emits a Disconnect effect when an AKILL mask matches the hostmask', () => {
|
|
552
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
553
|
+
services.addAkill('*!*@bad.example.net', 'flooding');
|
|
554
|
+
const conn = makeConn();
|
|
555
|
+
conn.user = 'spammer';
|
|
556
|
+
conn.host = 'bad.example.net';
|
|
557
|
+
const ctx = makeCtx(conn, services);
|
|
558
|
+
|
|
559
|
+
const effects = checkAkillAdmission(conn, ctx);
|
|
560
|
+
|
|
561
|
+
const reasons = disconnectReasons(effects);
|
|
562
|
+
expect(reasons).toHaveLength(1);
|
|
563
|
+
expect(reasons[0]).toContain('AKILL');
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('NOTIFIES the connection with the matching AKILL reason before disconnecting', () => {
|
|
567
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
568
|
+
services.addAkill('*!*@bad.example.net', 'flooding');
|
|
569
|
+
const conn = makeConn();
|
|
570
|
+
conn.user = 'spammer';
|
|
571
|
+
conn.host = 'bad.example.net';
|
|
572
|
+
const ctx = makeCtx(conn, services);
|
|
573
|
+
|
|
574
|
+
const effects = checkAkillAdmission(conn, ctx);
|
|
575
|
+
|
|
576
|
+
expect(sentTexts(effects)).toContain(
|
|
577
|
+
':OperServ!OperServ@services NOTICE admin :You are banned from this network (AKILL: flooding).',
|
|
578
|
+
);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('returns the first matching AKILL when several masks could match', () => {
|
|
582
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
583
|
+
services.addAkill('*!*@a.example', 'first');
|
|
584
|
+
services.addAkill('*!*@bad.example.net', 'second');
|
|
585
|
+
const conn = makeConn();
|
|
586
|
+
conn.user = 'spammer';
|
|
587
|
+
conn.host = 'bad.example.net';
|
|
588
|
+
const ctx = makeCtx(conn, services);
|
|
589
|
+
|
|
590
|
+
const effects = checkAkillAdmission(conn, ctx);
|
|
591
|
+
|
|
592
|
+
expect(sentTexts(effects)).toContain(
|
|
593
|
+
':OperServ!OperServ@services NOTICE admin :You are banned from this network (AKILL: second).',
|
|
594
|
+
);
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// ============================================================================
|
|
599
|
+
// JUPE enforcement hook — enforceJupedNick
|
|
600
|
+
// ============================================================================
|
|
601
|
+
|
|
602
|
+
describe('enforceJupedNick', () => {
|
|
603
|
+
it('returns an empty effect list when no services store is bound', () => {
|
|
604
|
+
const conn = makeConn();
|
|
605
|
+
const ctx = makeCtx(conn);
|
|
606
|
+
|
|
607
|
+
const effects = enforceJupedNick(conn, 'moo', ctx);
|
|
608
|
+
|
|
609
|
+
expect(effects).toEqual([]);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('returns an empty effect list when the nick is not juped', () => {
|
|
613
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
614
|
+
const conn = makeConn();
|
|
615
|
+
const ctx = makeCtx(conn, services);
|
|
616
|
+
|
|
617
|
+
const effects = enforceJupedNick(conn, 'moo', ctx);
|
|
618
|
+
|
|
619
|
+
expect(effects).toEqual([]);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('emits a 432 ERR_ERRONEUSNICKNAME when the nick is juped', () => {
|
|
623
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
624
|
+
services.addJupe('moo', 'blocked');
|
|
625
|
+
const conn = makeConn();
|
|
626
|
+
const ctx = makeCtx(conn, services);
|
|
627
|
+
|
|
628
|
+
const effects = enforceJupedNick(conn, 'moo', ctx);
|
|
629
|
+
|
|
630
|
+
const texts = sentTexts(effects);
|
|
631
|
+
expect(texts.some((t) => / 432 /.test(t))).toBe(true);
|
|
632
|
+
expect(texts.some((t) => t.includes('moo'))).toBe(true);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('matches the jupe case-insensitively', () => {
|
|
636
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
637
|
+
services.addJupe('Moo', 'blocked');
|
|
638
|
+
const conn = makeConn();
|
|
639
|
+
const ctx = makeCtx(conn, services);
|
|
640
|
+
|
|
641
|
+
const effects = enforceJupedNick(conn, 'moo', ctx);
|
|
642
|
+
|
|
643
|
+
expect(sentTexts(effects).some((t) => / 432 /.test(t))).toBe(true);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it('returns an empty effect list when the nick is undefined', () => {
|
|
647
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
648
|
+
services.addJupe('moo', 'blocked');
|
|
649
|
+
const conn = createConnection({ id: 'c1', connectedSince: 0 });
|
|
650
|
+
const ctx = makeCtx(conn, services);
|
|
651
|
+
|
|
652
|
+
const effects = enforceJupedNick(conn, undefined, ctx);
|
|
653
|
+
|
|
654
|
+
expect(effects).toEqual([]);
|
|
655
|
+
});
|
|
656
|
+
});
|