serverless-ircd 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +40 -3
- package/.github/workflows/deploy-aws.yml +1 -2
- package/.github/workflows/deploy-cf-tcp.yml +1 -5
- package/.github/workflows/deploy-cf.yml +1 -2
- package/.gitmodules +3 -0
- package/CHANGELOG.md +474 -0
- package/README.md +219 -22
- package/apps/aws-stack/README.md +33 -0
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +74 -9
- package/apps/aws-stack/tests/coverage-command.test.ts +155 -0
- package/apps/aws-stack/tests/stack.test.ts +2 -2
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +129 -0
- package/apps/aws-stack/tests/vitest.setup.ts +18 -0
- package/apps/aws-stack/vitest.config.ts +3 -0
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-tcp-container/src/config-loader.ts +6 -7
- package/apps/cf-tcp-container/src/container-server.ts +3 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +10 -3
- package/apps/cf-tcp-container/wrangler.toml +1 -1
- package/apps/cf-worker/package.json +2 -1
- package/apps/cf-worker/src/origin-allowlist.ts +99 -0
- package/apps/cf-worker/src/worker.ts +89 -9
- package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +18 -0
- package/apps/cf-worker/tests/fixtures/web-dist/index.html +18 -0
- package/apps/cf-worker/tests/origin-allowlist.test.ts +101 -0
- package/apps/cf-worker/tests/smoke.test.ts +28 -2
- package/apps/cf-worker/tests/ws-origin.test.ts +89 -0
- package/apps/cf-worker/vitest.config.ts +1 -1
- package/apps/cf-worker/wrangler.test.toml +23 -0
- package/apps/cf-worker/wrangler.toml +59 -3
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +6 -6
- package/apps/local-cli/src/main.ts +20 -0
- package/apps/local-cli/src/server.ts +9 -7
- package/apps/local-cli/tests/e2e.test.ts +112 -28
- package/apps/local-cli/tests/tcp.test.ts +71 -19
- package/apps/local-cli/tests/ws-subprotocol.test.ts +12 -7
- package/apps/web/landing/index.html +217 -0
- package/apps/web/package.json +28 -0
- package/apps/web/scripts/build.mjs +125 -0
- package/apps/web/src/build-env.ts +43 -0
- package/apps/web/src/config-schema.ts +138 -0
- package/apps/web/static/config.json +28 -0
- package/apps/web/static/config.prod.json +28 -0
- package/apps/web/static/config.staging.json +28 -0
- package/apps/web/tests/build-env.test.ts +63 -0
- package/apps/web/tests/build-smoke.test.ts +103 -0
- package/apps/web/tests/config-schema.test.ts +432 -0
- package/apps/web/tests/workspace.test.ts +12 -0
- package/apps/web/tsconfig.json +10 -0
- package/apps/web/upstream/.browserslistrc +3 -0
- package/apps/web/upstream/.editorconfig +9 -0
- package/apps/web/upstream/.eslintignore +3 -0
- package/apps/web/upstream/.eslintrc.js +90 -0
- package/apps/web/upstream/.github/FUNDING.yml +12 -0
- package/apps/web/upstream/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- package/apps/web/upstream/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/apps/web/upstream/.github/workflows/push.yml +26 -0
- package/apps/web/upstream/.github/workflows/release.yml +33 -0
- package/apps/web/upstream/.github/workflows/release_manual.yml +55 -0
- package/apps/web/upstream/.prettierrc.js +9 -0
- package/apps/web/upstream/.stylelintignore +5 -0
- package/apps/web/upstream/.stylelintrc.js +21 -0
- package/apps/web/upstream/LICENSE +201 -0
- package/apps/web/upstream/README.md +76 -0
- package/apps/web/upstream/babel.config.js +25 -0
- package/apps/web/upstream/crowdin.yml +3 -0
- package/apps/web/upstream/index.html +35 -0
- package/apps/web/upstream/jest.config.js +7 -0
- package/apps/web/upstream/jsdoc.json +22 -0
- package/apps/web/upstream/package.json +80 -0
- package/apps/web/upstream/scripts/pre-commit +43 -0
- package/apps/web/upstream/src/components/App.vue +516 -0
- package/apps/web/upstream/src/components/AppSettings.vue +587 -0
- package/apps/web/upstream/src/components/AutoComplete.vue +284 -0
- package/apps/web/upstream/src/components/Avatar.vue +105 -0
- package/apps/web/upstream/src/components/AwayStatusIndicator.vue +77 -0
- package/apps/web/upstream/src/components/BufferKey.vue +121 -0
- package/apps/web/upstream/src/components/BufferSettings.vue +97 -0
- package/apps/web/upstream/src/components/Captcha.vue +85 -0
- package/apps/web/upstream/src/components/ChannelBanlist.vue +96 -0
- package/apps/web/upstream/src/components/ChannelInfo.vue +159 -0
- package/apps/web/upstream/src/components/ChannelInvitelist.vue +276 -0
- package/apps/web/upstream/src/components/ChannelList.vue +345 -0
- package/apps/web/upstream/src/components/Container.vue +401 -0
- package/apps/web/upstream/src/components/ContainerHeader.vue +522 -0
- package/apps/web/upstream/src/components/ControlInput.vue +1016 -0
- package/apps/web/upstream/src/components/LoadingAnimation.vue +104 -0
- package/apps/web/upstream/src/components/MediaViewer.vue +199 -0
- package/apps/web/upstream/src/components/MessageInfo.vue +192 -0
- package/apps/web/upstream/src/components/MessageList.vue +1028 -0
- package/apps/web/upstream/src/components/MessageListAvatar.vue +20 -0
- package/apps/web/upstream/src/components/MessageListMessageCompact.vue +361 -0
- package/apps/web/upstream/src/components/MessageListMessageInline.vue +257 -0
- package/apps/web/upstream/src/components/MessageListMessageModern.vue +430 -0
- package/apps/web/upstream/src/components/NetworkSettings.vue +528 -0
- package/apps/web/upstream/src/components/Nicklist.vue +309 -0
- package/apps/web/upstream/src/components/NicklistUser.vue +181 -0
- package/apps/web/upstream/src/components/NotConnected.vue +271 -0
- package/apps/web/upstream/src/components/SelfUser.vue +283 -0
- package/apps/web/upstream/src/components/ServerSelector.vue +162 -0
- package/apps/web/upstream/src/components/ServerView.vue +126 -0
- package/apps/web/upstream/src/components/SettingsAdvanced.vue +232 -0
- package/apps/web/upstream/src/components/SettingsAliases.vue +121 -0
- package/apps/web/upstream/src/components/Sidebar.vue +429 -0
- package/apps/web/upstream/src/components/SidebarAboutBuffer.vue +288 -0
- package/apps/web/upstream/src/components/SidebarState.vue +145 -0
- package/apps/web/upstream/src/components/StartupError.vue +35 -0
- package/apps/web/upstream/src/components/StateBrowser.vue +420 -0
- package/apps/web/upstream/src/components/StateBrowserBuffer.vue +124 -0
- package/apps/web/upstream/src/components/StateBrowserNetwork.vue +823 -0
- package/apps/web/upstream/src/components/StateBrowserUsermenu.vue +162 -0
- package/apps/web/upstream/src/components/TypingStatusIndicator.vue +34 -0
- package/apps/web/upstream/src/components/TypingUsersList.vue +56 -0
- package/apps/web/upstream/src/components/UrlEmbed.vue +149 -0
- package/apps/web/upstream/src/components/UserBox.vue +728 -0
- package/apps/web/upstream/src/components/inputtools/Emoji.vue +67 -0
- package/apps/web/upstream/src/components/inputtools/TextStyle.vue +142 -0
- package/apps/web/upstream/src/components/startups/CommonLayout.vue +208 -0
- package/apps/web/upstream/src/components/startups/CustomServer.vue +495 -0
- package/apps/web/upstream/src/components/startups/KiwiBnc.vue +361 -0
- package/apps/web/upstream/src/components/startups/Personal.vue +317 -0
- package/apps/web/upstream/src/components/startups/Welcome.vue +544 -0
- package/apps/web/upstream/src/components/startups/ZncLogin.vue +229 -0
- package/apps/web/upstream/src/components/utils/InputConfirm.vue +68 -0
- package/apps/web/upstream/src/components/utils/InputPrompt.vue +118 -0
- package/apps/web/upstream/src/components/utils/InputText.vue +172 -0
- package/apps/web/upstream/src/components/utils/IrcInput.vue +579 -0
- package/apps/web/upstream/src/components/utils/PluginWrapper.vue +26 -0
- package/apps/web/upstream/src/components/utils/TabbedView.vue +149 -0
- package/apps/web/upstream/src/components/utils/TransitionExpand.vue +97 -0
- package/apps/web/upstream/src/helpers/Colours.js +128 -0
- package/apps/web/upstream/src/helpers/IrcdDiffs.js +26 -0
- package/apps/web/upstream/src/helpers/Md5.js +193 -0
- package/apps/web/upstream/src/helpers/Misc.js +401 -0
- package/apps/web/upstream/src/helpers/TextFormatting.js +237 -0
- package/apps/web/upstream/src/libs/AliasRewriter.js +157 -0
- package/apps/web/upstream/src/libs/AudioManager.js +60 -0
- package/apps/web/upstream/src/libs/BouncerMiddleware.js +247 -0
- package/apps/web/upstream/src/libs/BouncerProvider.js +551 -0
- package/apps/web/upstream/src/libs/ChathistoryMiddleware.js +153 -0
- package/apps/web/upstream/src/libs/ConfigLoader.js +94 -0
- package/apps/web/upstream/src/libs/EmojiProvider.js +46 -0
- package/apps/web/upstream/src/libs/GlobalApi.js +346 -0
- package/apps/web/upstream/src/libs/IPC.js +51 -0
- package/apps/web/upstream/src/libs/InputHandler.js +921 -0
- package/apps/web/upstream/src/libs/IrcClient.js +1506 -0
- package/apps/web/upstream/src/libs/Logger.js +71 -0
- package/apps/web/upstream/src/libs/Message.js +169 -0
- package/apps/web/upstream/src/libs/MessageFormatter.js +364 -0
- package/apps/web/upstream/src/libs/MessageParser.js +249 -0
- package/apps/web/upstream/src/libs/Notifications.js +78 -0
- package/apps/web/upstream/src/libs/ServerConnection.js +270 -0
- package/apps/web/upstream/src/libs/ServerSession.js +102 -0
- package/apps/web/upstream/src/libs/SoundBleep.js +20 -0
- package/apps/web/upstream/src/libs/StatePersistence.js +90 -0
- package/apps/web/upstream/src/libs/ThemeManager.js +128 -0
- package/apps/web/upstream/src/libs/TypingMiddleware.js +105 -0
- package/apps/web/upstream/src/libs/WindowTitle.js +63 -0
- package/apps/web/upstream/src/libs/batchedAdd.js +74 -0
- package/apps/web/upstream/src/libs/bufferTools.js +177 -0
- package/apps/web/upstream/src/libs/polyfill/Element.closest.js +18 -0
- package/apps/web/upstream/src/libs/renderers/Html.js +113 -0
- package/apps/web/upstream/src/libs/settingTools.js +31 -0
- package/apps/web/upstream/src/libs/state/BufferState.js +703 -0
- package/apps/web/upstream/src/libs/state/NetworkState.js +158 -0
- package/apps/web/upstream/src/libs/state/UserState.js +89 -0
- package/apps/web/upstream/src/libs/state/common.js +26 -0
- package/apps/web/upstream/src/libs/state.js +961 -0
- package/apps/web/upstream/src/libs/storage/Local.js +51 -0
- package/apps/web/upstream/src/main.js +566 -0
- package/apps/web/upstream/src/res/autocompleteCommands.js +31 -0
- package/apps/web/upstream/src/res/configTemplates.js +373 -0
- package/apps/web/upstream/src/res/globalStyle.css +277 -0
- package/apps/web/upstream/src/res/kiwiLoadingLogo.png +0 -0
- package/apps/web/upstream/src/res/locales/app.af-ZA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ar-SA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.bg-BG.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.bs-BA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ca-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.cs-CZ.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.da-DK.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.de-DE.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.dev.po +1246 -0
- package/apps/web/upstream/src/res/locales/app.el-GR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.en-US.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-419.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-AR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-EM.po +536 -0
- package/apps/web/upstream/src/res/locales/app.es-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-US.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.eu-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.fi-FI.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.fr-FR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.gl-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.he-IL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.hi-IN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.hu-HU.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.id-ID.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.it-IT.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ja-JP.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ko-KR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.nl-NL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.no-NO.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pl-PL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pt-BR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pt-PT.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ro-RO.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ru-RU.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sl-SI.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sq-AL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sr-SP.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sv-SE.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.tr-TR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.uk-UA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.vi-VN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.zh-CN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.zh-TW.po +1145 -0
- package/apps/web/upstream/src/res/localesList.json +27 -0
- package/apps/web/upstream/src/res/logo.png +0 -0
- package/apps/web/upstream/src/thirdparty/about.html +43 -0
- package/apps/web/upstream/src/thirdparty/index.js +5 -0
- package/apps/web/upstream/src/thirdparty/kiwiirccom.vue +185 -0
- package/apps/web/upstream/static/config.json +28 -0
- package/apps/web/upstream/static/emoticons/smile.png +0 -0
- package/apps/web/upstream/static/favicon.png +0 -0
- package/apps/web/upstream/static/highlight.mp3 +0 -0
- package/apps/web/upstream/static/highlight.ogg +0 -0
- package/apps/web/upstream/static/locales/.gitignore +2 -0
- package/apps/web/upstream/static/plugins/customise.html.example +10 -0
- package/apps/web/upstream/static/themes/coffee/theme.css +145 -0
- package/apps/web/upstream/static/themes/common/base.css +1064 -0
- package/apps/web/upstream/static/themes/dark/theme.css +196 -0
- package/apps/web/upstream/static/themes/default/theme.css +92 -0
- package/apps/web/upstream/static/themes/elite/theme.css +248 -0
- package/apps/web/upstream/static/themes/grayfox/theme.css +143 -0
- package/apps/web/upstream/static/themes/nightswatch/theme.css +367 -0
- package/apps/web/upstream/static/themes/osprey/theme.css +115 -0
- package/apps/web/upstream/static/themes/radioactive/theme.css +1211 -0
- package/apps/web/upstream/static/themes/sky/theme.css +107 -0
- package/apps/web/upstream/tests/unit/BatchAdd.spec.js +174 -0
- package/apps/web/upstream/tests/unit/MessageParser.spec.js +125 -0
- package/apps/web/upstream/tests/unit/Misc.spec.js +24 -0
- package/apps/web/upstream/tests/unit/NetworkState.spec.js +58 -0
- package/apps/web/upstream/tests/unit/StartupError.spec.js +20 -0
- package/apps/web/upstream/vue.config.js +150 -0
- package/apps/web/upstream/yarn.lock +9767 -0
- package/apps/web/vitest.config.ts +13 -0
- package/biome.json +2 -0
- package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
- package/docs/ADR-002-location-of-authority.md +82 -0
- package/docs/ADR-003-durable-object-sharding.md +110 -0
- package/docs/ADR-004-dynamodb-schema.md +96 -0
- package/docs/ADR-005-wss-only-transport-v1.md +83 -0
- package/docs/ADR-006-sasl-mechanism-scope.md +96 -0
- package/docs/ADR-007-deterministic-ports.md +82 -0
- package/docs/ADR-008-monorepo-tooling.md +60 -0
- package/docs/ADR-009-dual-transport-wss-and-irc-tls.md +138 -0
- package/docs/ADR-Index.md +15 -0
- package/docs/AWS-Adapter-Architecture.md +496 -0
- package/docs/AWS-Deployment.md +1275 -0
- package/docs/AWS-TCP-Deployment.md +410 -0
- package/docs/Cloudflare-Deployment-Guide.md +719 -0
- package/docs/Cloudflare-TCP-Deployment.md +510 -0
- package/docs/Home.md +16 -0
- package/docs/Observability.md +87 -0
- package/docs/PlanExtensions.md +812 -0
- package/docs/PlanIRCv3Websocket.md +503 -0
- package/docs/PlanWebClient.md +451 -0
- package/docs/Release-Process.md +443 -0
- package/docs/WebClientGuide.md +535 -0
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/handlers/default.ts +3 -0
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +9 -0
- package/packages/aws-adapter/src/serialize.ts +29 -1
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +49 -8
- package/packages/aws-adapter/tests/account-store.test.ts +4 -7
- package/packages/aws-adapter/tests/aws-harness.ts +5 -2
- package/packages/aws-adapter/tests/aws-integration.test.ts +8 -4
- package/packages/aws-adapter/tests/aws-runtime.test.ts +79 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +11 -2
- package/packages/aws-adapter/tests/connect.test.ts +9 -9
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +3 -4
- package/packages/aws-adapter/tests/global-setup.ts +55 -30
- package/packages/aws-adapter/tests/gone-exception.test.ts +4 -9
- package/packages/aws-adapter/tests/handlers.test.ts +90 -4
- package/packages/aws-adapter/tests/message-store.test.ts +6 -5
- package/packages/aws-adapter/tests/nlb-stream.test.ts +60 -8
- package/packages/cf-adapter/package.json +2 -1
- package/packages/cf-adapter/src/connection-do.ts +3 -0
- package/packages/cf-adapter/src/serialize.ts +25 -4
- package/packages/cf-adapter/tests/cf-harness.ts +12 -1
- package/packages/cf-adapter/tests/cf-runtime.test.ts +269 -0
- package/packages/cf-adapter/tests/channel-do.test.ts +117 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +49 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +91 -0
- package/packages/cf-adapter/tests/d1-account-store.test.ts +48 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +104 -0
- package/packages/cf-adapter/tests/serialize.test.ts +308 -0
- package/packages/cf-adapter/vitest.config.ts +1 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/reports/mutation/mutation.html +342 -0
- package/packages/irc-core/src/caps/capabilities.ts +37 -3
- package/packages/irc-core/src/caps/index.ts +1 -0
- package/packages/irc-core/src/caps/sts.ts +84 -0
- package/packages/irc-core/src/commands/account-notify.ts +53 -0
- package/packages/irc-core/src/commands/away.ts +9 -3
- package/packages/irc-core/src/commands/cap.ts +23 -5
- package/packages/irc-core/src/commands/chathistory.ts +40 -11
- package/packages/irc-core/src/commands/index.ts +34 -0
- package/packages/irc-core/src/commands/isupport.ts +53 -0
- package/packages/irc-core/src/commands/mode.ts +8 -1
- package/packages/irc-core/src/commands/monitor.ts +327 -0
- package/packages/irc-core/src/commands/multiline.ts +256 -0
- package/packages/irc-core/src/commands/pre-away.ts +112 -0
- package/packages/irc-core/src/commands/privmsg.ts +19 -12
- package/packages/irc-core/src/commands/read-marker.ts +108 -0
- package/packages/irc-core/src/commands/registration.ts +8 -0
- package/packages/irc-core/src/commands/sasl.ts +24 -1
- package/packages/irc-core/src/commands/tagmsg.ts +71 -1
- package/packages/irc-core/src/commands/whois.ts +5 -0
- package/packages/irc-core/src/config.ts +65 -10
- package/packages/irc-core/src/effects.ts +14 -0
- package/packages/irc-core/src/ports.ts +389 -0
- package/packages/irc-core/src/protocol/index.ts +14 -0
- package/packages/irc-core/src/protocol/labeled-response.ts +133 -0
- package/packages/irc-core/src/protocol/numerics.ts +15 -0
- package/packages/irc-core/src/protocol/outbound.ts +16 -1
- package/packages/irc-core/src/protocol/standard-replies.ts +277 -0
- package/packages/irc-core/src/state/connection.ts +32 -1
- package/packages/irc-core/src/types.ts +112 -0
- package/packages/irc-core/stryker.commands.conf.json +1 -2
- package/packages/irc-core/tests/away-store.test.ts +73 -0
- package/packages/irc-core/tests/batch.test.ts +15 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +119 -2
- package/packages/irc-core/tests/caps/sts.test.ts +123 -0
- package/packages/irc-core/tests/commands/account-notify.test.ts +171 -0
- package/packages/irc-core/tests/commands/away.test.ts +85 -2
- package/packages/irc-core/tests/commands/cap.test.ts +165 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +148 -1
- package/packages/irc-core/tests/commands/echo-message.test.ts +9 -3
- package/packages/irc-core/tests/commands/isupport.test.ts +80 -0
- package/packages/irc-core/tests/commands/mode.test.ts +72 -0
- package/packages/irc-core/tests/commands/monitor.test.ts +411 -0
- package/packages/irc-core/tests/commands/multiline.test.ts +428 -0
- package/packages/irc-core/tests/commands/pre-away.test.ts +241 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +154 -3
- package/packages/irc-core/tests/commands/read-marker.test.ts +211 -0
- package/packages/irc-core/tests/commands/registration.test.ts +64 -0
- package/packages/irc-core/tests/commands/sasl.test.ts +116 -2
- package/packages/irc-core/tests/commands/tagmsg.test.ts +357 -0
- package/packages/irc-core/tests/commands/whois.test.ts +34 -0
- package/packages/irc-core/tests/config.test.ts +103 -13
- package/packages/irc-core/tests/labeled-response.test.ts +181 -0
- package/packages/irc-core/tests/message-tags.test.ts +114 -0
- package/packages/irc-core/tests/monitor-store.test.ts +200 -0
- package/packages/irc-core/tests/numerics.test.ts +13 -0
- package/packages/irc-core/tests/parser.test.ts +109 -0
- package/packages/irc-core/tests/read-marker-store.test.ts +108 -0
- package/packages/irc-core/tests/standard-replies.test.ts +412 -0
- package/packages/irc-core/tests/types.test.ts +27 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +450 -3
- package/packages/irc-server/src/dispatch.ts +108 -16
- package/packages/irc-server/tests/actor.test.ts +1432 -39
- package/packages/irc-server/tests/dispatch.test.ts +263 -2
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/index.ts +6 -0
- package/packages/irc-test-support/src/scenarios.ts +24 -2
- package/packages/irc-test-support/src/test-config.ts +54 -0
- package/packages/irc-test-support/tests/test-config.test.ts +51 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/ci-hardening/tests/ddb-local.test.ts +82 -0
- package/tools/load-test/package.json +33 -0
- package/tools/load-test/src/client.ts +351 -0
- package/tools/load-test/src/config.ts +313 -0
- package/tools/load-test/src/harness.ts +116 -0
- package/tools/load-test/src/main.ts +120 -0
- package/tools/load-test/src/metrics.ts +168 -0
- package/tools/load-test/src/report.ts +106 -0
- package/tools/load-test/tests/client.test.ts +212 -0
- package/tools/load-test/tests/config.test.ts +152 -0
- package/tools/load-test/tests/framing.test.ts +37 -0
- package/tools/load-test/tests/harness.test.ts +165 -0
- package/tools/load-test/tests/metrics.test.ts +174 -0
- package/tools/load-test/tests/report.test.ts +161 -0
- package/tools/load-test/tests/smoke.test.ts +67 -0
- package/tools/load-test/tsconfig.build.json +12 -0
- package/tools/load-test/tsconfig.test.json +10 -0
- package/tools/load-test/vitest.config.ts +29 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
|
@@ -119,7 +119,13 @@ describe('privmsgChannelReducer — success', () => {
|
|
|
119
119
|
);
|
|
120
120
|
|
|
121
121
|
expect(out.effects).toEqual<EffectType[]>([
|
|
122
|
-
Effect.broadcast(
|
|
122
|
+
Effect.broadcast(
|
|
123
|
+
'#foo',
|
|
124
|
+
[L(':alice!alice@example.com PRIVMSG #foo :hello world')],
|
|
125
|
+
'c1',
|
|
126
|
+
'msgid',
|
|
127
|
+
[L('@msgid=nonce-0 :alice!alice@example.com PRIVMSG #foo :hello world')],
|
|
128
|
+
),
|
|
123
129
|
]);
|
|
124
130
|
});
|
|
125
131
|
|
|
@@ -140,6 +146,8 @@ describe('privmsgChannelReducer — success', () => {
|
|
|
140
146
|
'#foo',
|
|
141
147
|
[L(':alice!alice@example.com PRIVMSG #foo :hi there everyone')],
|
|
142
148
|
'c1',
|
|
149
|
+
'msgid',
|
|
150
|
+
[L('@msgid=nonce-0 :alice!alice@example.com PRIVMSG #foo :hi there everyone')],
|
|
143
151
|
),
|
|
144
152
|
);
|
|
145
153
|
});
|
|
@@ -415,7 +423,9 @@ describe('privmsgChannelReducer — rejections', () => {
|
|
|
415
423
|
);
|
|
416
424
|
|
|
417
425
|
expect(out.effects).toEqual<EffectType[]>([
|
|
418
|
-
Effect.broadcast('#foo', [L(':? PRIVMSG #foo :hi')], 'c1'
|
|
426
|
+
Effect.broadcast('#foo', [L(':? PRIVMSG #foo :hi')], 'c1', 'msgid', [
|
|
427
|
+
L('@msgid=nonce-0 :? PRIVMSG #foo :hi'),
|
|
428
|
+
]),
|
|
419
429
|
]);
|
|
420
430
|
});
|
|
421
431
|
});
|
|
@@ -439,7 +449,9 @@ describe('noticeChannelReducer', () => {
|
|
|
439
449
|
);
|
|
440
450
|
|
|
441
451
|
expect(out.effects).toEqual<EffectType[]>([
|
|
442
|
-
Effect.broadcast('#foo', [L(':alice!alice@example.com NOTICE #foo :psst')], 'c1'
|
|
452
|
+
Effect.broadcast('#foo', [L(':alice!alice@example.com NOTICE #foo :psst')], 'c1', 'msgid', [
|
|
453
|
+
L('@msgid=nonce-0 :alice!alice@example.com NOTICE #foo :psst'),
|
|
454
|
+
]),
|
|
443
455
|
]);
|
|
444
456
|
});
|
|
445
457
|
|
|
@@ -676,6 +688,145 @@ describe('noticeUserReducer', () => {
|
|
|
676
688
|
});
|
|
677
689
|
});
|
|
678
690
|
|
|
691
|
+
// ============================================================================
|
|
692
|
+
// privmsgChannelReducer — IRCv3 msgid tag (emit @msgid=<id> on the live
|
|
693
|
+
// fanout for cap-enabled peers; share the nonce with the chathistory record).
|
|
694
|
+
// ============================================================================
|
|
695
|
+
|
|
696
|
+
describe('privmsgChannelReducer — msgid tag', () => {
|
|
697
|
+
it('carries @msgid=<id> on the capLines delivered to msgid-cap peers', () => {
|
|
698
|
+
const chan = makeChan('#foo');
|
|
699
|
+
addMember(chan, 'c1', 'alice');
|
|
700
|
+
const conn = makeConn();
|
|
701
|
+
const ctx = makeCtx(conn);
|
|
702
|
+
|
|
703
|
+
const out = privmsgChannelReducer(
|
|
704
|
+
chan,
|
|
705
|
+
{ command: 'PRIVMSG', params: ['#foo', 'hi'], tags: {} },
|
|
706
|
+
ctx,
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
const bcast = out.effects[0];
|
|
710
|
+
expect(bcast?.tag).toBe('Broadcast');
|
|
711
|
+
if (bcast?.tag === 'Broadcast') {
|
|
712
|
+
expect(bcast.cap).toBe('msgid');
|
|
713
|
+
expect(bcast.capLines?.[0]?.text).toBe(
|
|
714
|
+
'@msgid=nonce-0 :alice!alice@example.com PRIVMSG #foo :hi',
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it('leaves the legacy lines bare so non-cap peers see no msgid tag', () => {
|
|
720
|
+
const chan = makeChan('#foo');
|
|
721
|
+
addMember(chan, 'c1', 'alice');
|
|
722
|
+
const conn = makeConn();
|
|
723
|
+
const ctx = makeCtx(conn);
|
|
724
|
+
|
|
725
|
+
const out = privmsgChannelReducer(
|
|
726
|
+
chan,
|
|
727
|
+
{ command: 'PRIVMSG', params: ['#foo', 'hi'], tags: {} },
|
|
728
|
+
ctx,
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
const bcast = out.effects[0];
|
|
732
|
+
if (bcast?.tag === 'Broadcast') {
|
|
733
|
+
expect(bcast.lines[0]?.text).toBe(':alice!alice@example.com PRIVMSG #foo :hi');
|
|
734
|
+
expect(bcast.lines[0]?.text).not.toContain('msgid');
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it('uses the same msgid on the live capLine and the chathistory record', () => {
|
|
739
|
+
const chan = makeChan('#foo');
|
|
740
|
+
addMember(chan, 'c1', 'alice');
|
|
741
|
+
const store = new InMemoryMessageStore();
|
|
742
|
+
const conn = makeConn();
|
|
743
|
+
const ctx = makeCtx(conn, new FakeClock(1_000), store);
|
|
744
|
+
|
|
745
|
+
const out = privmsgChannelReducer(
|
|
746
|
+
chan,
|
|
747
|
+
{ command: 'PRIVMSG', params: ['#foo', 'hi'], tags: {} },
|
|
748
|
+
ctx,
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
const recorded = store.query({ chan: '#foo', direction: 'latest', limit: 10 });
|
|
752
|
+
expect(recorded).toHaveLength(1);
|
|
753
|
+
const recordedMsgid = (recorded[0] as StoredMessage).msgid;
|
|
754
|
+
|
|
755
|
+
const bcast = out.effects[0];
|
|
756
|
+
if (bcast?.tag === 'Broadcast') {
|
|
757
|
+
const liveText = bcast.capLines?.[0]?.text ?? '';
|
|
758
|
+
expect(liveText).toContain(`@msgid=${recordedMsgid}`);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('still emits the msgid tag when chathistory is disabled (no MessageStore)', () => {
|
|
763
|
+
const chan = makeChan('#foo');
|
|
764
|
+
addMember(chan, 'c1', 'alice');
|
|
765
|
+
const conn = makeConn();
|
|
766
|
+
const ctx = makeCtx(conn); // no store
|
|
767
|
+
|
|
768
|
+
const out = privmsgChannelReducer(
|
|
769
|
+
chan,
|
|
770
|
+
{ command: 'PRIVMSG', params: ['#foo', 'hi'], tags: {} },
|
|
771
|
+
ctx,
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
const bcast = out.effects[0];
|
|
775
|
+
if (bcast?.tag === 'Broadcast') {
|
|
776
|
+
expect(bcast.cap).toBe('msgid');
|
|
777
|
+
expect(bcast.capLines?.[0]?.text).toContain('@msgid=');
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it('echoes the msgid-decorated line to a sender with both echo-message and msgid', () => {
|
|
782
|
+
const chan = makeChan('#foo');
|
|
783
|
+
addMember(chan, 'c1', 'alice');
|
|
784
|
+
const conn = makeConn();
|
|
785
|
+
conn.caps.add('echo-message');
|
|
786
|
+
conn.caps.add('msgid');
|
|
787
|
+
const ctx = makeCtx(conn);
|
|
788
|
+
|
|
789
|
+
const out = privmsgChannelReducer(
|
|
790
|
+
chan,
|
|
791
|
+
{ command: 'PRIVMSG', params: ['#foo', 'hi'], tags: {} },
|
|
792
|
+
ctx,
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
const echo = out.effects[1];
|
|
796
|
+
expect(echo?.tag).toBe('Send');
|
|
797
|
+
if (echo?.tag === 'Send') {
|
|
798
|
+
expect(echo.lines[0]?.text).toBe('@msgid=nonce-0 :alice!alice@example.com PRIVMSG #foo :hi');
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
// ============================================================================
|
|
804
|
+
// noticeChannelReducer — IRCv3 msgid tag
|
|
805
|
+
// ============================================================================
|
|
806
|
+
|
|
807
|
+
describe('noticeChannelReducer — msgid tag', () => {
|
|
808
|
+
it('carries @msgid=<id> on the capLines delivered to msgid-cap peers', () => {
|
|
809
|
+
const chan = makeChan('#foo');
|
|
810
|
+
addMember(chan, 'c1', 'alice');
|
|
811
|
+
const conn = makeConn();
|
|
812
|
+
const ctx = makeCtx(conn);
|
|
813
|
+
|
|
814
|
+
const out = noticeChannelReducer(
|
|
815
|
+
chan,
|
|
816
|
+
{ command: 'NOTICE', params: ['#foo', 'note'], tags: {} },
|
|
817
|
+
ctx,
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
const bcast = out.effects[0];
|
|
821
|
+
if (bcast?.tag === 'Broadcast') {
|
|
822
|
+
expect(bcast.cap).toBe('msgid');
|
|
823
|
+
expect(bcast.capLines?.[0]?.text).toBe(
|
|
824
|
+
'@msgid=nonce-0 :alice!alice@example.com NOTICE #foo :note',
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
|
|
679
830
|
// ============================================================================
|
|
680
831
|
// chathistory recording — MessageStore.record is driven from the channel
|
|
681
832
|
// PRIVMSG/NOTICE reducer for every accepted message.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
READ_MARKER_CAP,
|
|
4
|
+
READ_MARKER_TAG,
|
|
5
|
+
isReadMarkerTagmsg,
|
|
6
|
+
persistReadMarker,
|
|
7
|
+
readMarkerMsgid,
|
|
8
|
+
seedReadMarkers,
|
|
9
|
+
} from '../../src/commands/read-marker';
|
|
10
|
+
import {
|
|
11
|
+
EmptyMotdProvider,
|
|
12
|
+
FakeClock,
|
|
13
|
+
InMemoryReadMarkerStore,
|
|
14
|
+
SequentialIdFactory,
|
|
15
|
+
} from '../../src/ports';
|
|
16
|
+
import { type ConnectionState, createConnection } from '../../src/state/connection';
|
|
17
|
+
import { type Ctx, type ServerConfig, buildCtx } from '../../src/types';
|
|
18
|
+
|
|
19
|
+
const serverConfig: ServerConfig = {
|
|
20
|
+
serverName: 'irc.example.com',
|
|
21
|
+
networkName: 'ExampleNet',
|
|
22
|
+
maxChannelsPerUser: 30,
|
|
23
|
+
maxTargetsPerCommand: 10,
|
|
24
|
+
maxListEntries: 50,
|
|
25
|
+
nickLen: 30,
|
|
26
|
+
channelLen: 50,
|
|
27
|
+
topicLen: 390,
|
|
28
|
+
quitMessage: 'Client Quit',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function makeConn(id = 'c1', account?: string): ConnectionState {
|
|
32
|
+
const s = createConnection({ id, connectedSince: 0 });
|
|
33
|
+
s.nick = 'alice';
|
|
34
|
+
s.user = 'alice';
|
|
35
|
+
s.host = 'example.com';
|
|
36
|
+
s.registration = 'registered';
|
|
37
|
+
if (account !== undefined) s.account = account;
|
|
38
|
+
return s;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeCtx(conn: ConnectionState, readMarkers?: InMemoryReadMarkerStore): Ctx {
|
|
42
|
+
return buildCtx({
|
|
43
|
+
serverConfig,
|
|
44
|
+
clock: new FakeClock(1_000),
|
|
45
|
+
ids: new SequentialIdFactory(),
|
|
46
|
+
motd: EmptyMotdProvider,
|
|
47
|
+
connection: conn,
|
|
48
|
+
...(readMarkers !== undefined ? { readMarkers } : {}),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// constants
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
describe('read-marker constants', () => {
|
|
57
|
+
it('exposes the draft/read-marker cap name', () => {
|
|
58
|
+
expect(READ_MARKER_CAP).toBe('draft/read-marker');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('exposes the +draft/read-marker client-tag key', () => {
|
|
62
|
+
expect(READ_MARKER_TAG).toBe('+draft/read-marker');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// isReadMarkerTagmsg
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
describe('isReadMarkerTagmsg', () => {
|
|
71
|
+
it('returns true when the +draft/read-marker tag is present', () => {
|
|
72
|
+
expect(isReadMarkerTagmsg({ '+draft/read-marker': 'm1' })).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('returns true when the tag is valueless', () => {
|
|
76
|
+
expect(isReadMarkerTagmsg({ '+draft/read-marker': '' })).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('returns false when the tag is absent', () => {
|
|
80
|
+
expect(isReadMarkerTagmsg({ '+draft/typing': 'active' })).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns false for an empty tag bag', () => {
|
|
84
|
+
expect(isReadMarkerTagmsg({})).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// readMarkerMsgid
|
|
90
|
+
// ============================================================================
|
|
91
|
+
|
|
92
|
+
describe('readMarkerMsgid', () => {
|
|
93
|
+
it('returns the value of the +draft/read-marker tag', () => {
|
|
94
|
+
expect(readMarkerMsgid({ '+draft/read-marker': 'm42' })).toBe('m42');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns undefined when the tag is valueless', () => {
|
|
98
|
+
expect(readMarkerMsgid({ '+draft/read-marker': '' })).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('returns undefined when the tag is absent', () => {
|
|
102
|
+
expect(readMarkerMsgid({ '+draft/typing': 'active' })).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// persistReadMarker
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
describe('persistReadMarker', () => {
|
|
111
|
+
it('writes the marker to the store keyed by (account, channel)', () => {
|
|
112
|
+
const store = new InMemoryReadMarkerStore();
|
|
113
|
+
const ctx = makeCtx(makeConn('c1', 'alice'), store);
|
|
114
|
+
|
|
115
|
+
persistReadMarker(ctx, '#foo', 'm7');
|
|
116
|
+
|
|
117
|
+
expect(store.get('alice', '#foo')).toBe('m7');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('also advances the connection in-memory lastReadMarkers', () => {
|
|
121
|
+
const store = new InMemoryReadMarkerStore();
|
|
122
|
+
const conn = makeConn('c1', 'alice');
|
|
123
|
+
const ctx = makeCtx(conn, store);
|
|
124
|
+
|
|
125
|
+
persistReadMarker(ctx, '#foo', 'm7');
|
|
126
|
+
|
|
127
|
+
expect(conn.lastReadMarkers?.get('#foo')).toBe('m7');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('overwrites a previously stored marker for the same pair', () => {
|
|
131
|
+
const store = new InMemoryReadMarkerStore();
|
|
132
|
+
const ctx = makeCtx(makeConn('c1', 'alice'), store);
|
|
133
|
+
|
|
134
|
+
persistReadMarker(ctx, '#foo', 'm1');
|
|
135
|
+
persistReadMarker(ctx, '#foo', 'm2');
|
|
136
|
+
|
|
137
|
+
expect(store.get('alice', '#foo')).toBe('m2');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('is a no-op when no ReadMarkerStore is bound', () => {
|
|
141
|
+
const conn = makeConn('c1', 'alice');
|
|
142
|
+
const ctx = makeCtx(conn); // no store
|
|
143
|
+
|
|
144
|
+
persistReadMarker(ctx, '#foo', 'm1');
|
|
145
|
+
|
|
146
|
+
// In-memory marker still advances so the connection's own session tracks it.
|
|
147
|
+
expect(conn.lastReadMarkers?.get('#foo')).toBe('m1');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('is a no-op on the store when the connection has no account', () => {
|
|
151
|
+
const store = new InMemoryReadMarkerStore();
|
|
152
|
+
const conn = makeConn('c1'); // no account
|
|
153
|
+
const ctx = makeCtx(conn, store);
|
|
154
|
+
|
|
155
|
+
persistReadMarker(ctx, '#foo', 'm1');
|
|
156
|
+
|
|
157
|
+
expect(store.get('alice', '#foo')).toBeUndefined();
|
|
158
|
+
expect(store.forAccount('alice')).toEqual([]);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// seedReadMarkers
|
|
164
|
+
// ============================================================================
|
|
165
|
+
|
|
166
|
+
describe('seedReadMarkers', () => {
|
|
167
|
+
it('populates lastReadMarkers from every persisted entry for the account', () => {
|
|
168
|
+
const store = new InMemoryReadMarkerStore();
|
|
169
|
+
store.set('alice', '#foo', 'm1');
|
|
170
|
+
store.set('alice', '#bar', 'm2');
|
|
171
|
+
const conn = makeConn();
|
|
172
|
+
|
|
173
|
+
seedReadMarkers(conn, store, 'alice');
|
|
174
|
+
|
|
175
|
+
expect(conn.lastReadMarkers?.get('#foo')).toBe('m1');
|
|
176
|
+
expect(conn.lastReadMarkers?.get('#bar')).toBe('m2');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('leaves existing in-memory markers untouched when the store is empty', () => {
|
|
180
|
+
const store = new InMemoryReadMarkerStore();
|
|
181
|
+
const conn = makeConn();
|
|
182
|
+
conn.lastReadMarkers = new Map<string, string>([['#foo', 'm9']]);
|
|
183
|
+
|
|
184
|
+
seedReadMarkers(conn, store, 'alice');
|
|
185
|
+
|
|
186
|
+
expect(conn.lastReadMarkers?.get('#foo')).toBe('m9');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('seeds markers for an account regardless of case spelling', () => {
|
|
190
|
+
const store = new InMemoryReadMarkerStore();
|
|
191
|
+
store.set('Alice', '#foo', 'm1');
|
|
192
|
+
const conn = makeConn();
|
|
193
|
+
|
|
194
|
+
seedReadMarkers(conn, store, 'alice');
|
|
195
|
+
|
|
196
|
+
expect(conn.lastReadMarkers?.get('#foo')).toBe('m1');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('does not overwrite an in-memory marker already present for a channel', () => {
|
|
200
|
+
const store = new InMemoryReadMarkerStore();
|
|
201
|
+
store.set('alice', '#foo', 'stored');
|
|
202
|
+
const conn = makeConn();
|
|
203
|
+
conn.lastReadMarkers = new Map<string, string>([['#foo', 'in-memory']]);
|
|
204
|
+
|
|
205
|
+
seedReadMarkers(conn, store, 'alice');
|
|
206
|
+
|
|
207
|
+
// The in-memory (session) marker wins: the connection may have already
|
|
208
|
+
// advanced past the persisted value this session.
|
|
209
|
+
expect(conn.lastReadMarkers?.get('#foo')).toBe('in-memory');
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -1102,3 +1102,67 @@ function expectedWelcomeWithHost(nick: string, hostmask: string): RawLine[] {
|
|
|
1102
1102
|
L(`${prefix} 422 ${nick} :MOTD File is missing`),
|
|
1103
1103
|
];
|
|
1104
1104
|
}
|
|
1105
|
+
|
|
1106
|
+
// ============================================================================
|
|
1107
|
+
// emitWelcomeIfReady — user mode `S` (TLS connected)
|
|
1108
|
+
// ============================================================================
|
|
1109
|
+
|
|
1110
|
+
describe('emitWelcomeIfReady — user mode `S` (TLS connected)', () => {
|
|
1111
|
+
it('sets user mode `S` (tls) at registration when the transport is secure', () => {
|
|
1112
|
+
const state = makeState();
|
|
1113
|
+
state.nick = 'alice';
|
|
1114
|
+
state.user = 'alice';
|
|
1115
|
+
state.host = 'example.com';
|
|
1116
|
+
state.realname = 'Alice';
|
|
1117
|
+
state.secure = true;
|
|
1118
|
+
const ctx = makeCtx(state);
|
|
1119
|
+
|
|
1120
|
+
emitWelcomeIfReady(state, ctx);
|
|
1121
|
+
|
|
1122
|
+
expect(state.registration).toBe('registered');
|
|
1123
|
+
expect(state.userModes.tls).toBe(true);
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
it('does not set user mode `S` when the transport is not secure', () => {
|
|
1127
|
+
const state = makeState();
|
|
1128
|
+
state.nick = 'alice';
|
|
1129
|
+
state.user = 'alice';
|
|
1130
|
+
state.host = 'example.com';
|
|
1131
|
+
state.realname = 'Alice';
|
|
1132
|
+
state.secure = false;
|
|
1133
|
+
const ctx = makeCtx(state);
|
|
1134
|
+
|
|
1135
|
+
emitWelcomeIfReady(state, ctx);
|
|
1136
|
+
|
|
1137
|
+
expect(state.registration).toBe('registered');
|
|
1138
|
+
expect(state.userModes.tls).toBe(false);
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
it('does not set user mode `S` when secure is absent (default plain transport)', () => {
|
|
1142
|
+
const state = makeState();
|
|
1143
|
+
state.nick = 'alice';
|
|
1144
|
+
state.user = 'alice';
|
|
1145
|
+
state.host = 'example.com';
|
|
1146
|
+
state.realname = 'Alice';
|
|
1147
|
+
const ctx = makeCtx(state);
|
|
1148
|
+
|
|
1149
|
+
emitWelcomeIfReady(state, ctx);
|
|
1150
|
+
|
|
1151
|
+
expect(state.registration).toBe('registered');
|
|
1152
|
+
expect(state.userModes.tls).toBe(false);
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
it('completes registration over TLS via the NICK reducer end-to-end', () => {
|
|
1156
|
+
const state = makeState();
|
|
1157
|
+
state.user = 'alice';
|
|
1158
|
+
state.realname = 'Alice';
|
|
1159
|
+
state.registration = 'registering';
|
|
1160
|
+
state.secure = true;
|
|
1161
|
+
const ctx = makeCtx(state);
|
|
1162
|
+
|
|
1163
|
+
const out = nickReducer(state, { command: 'NICK', params: ['alice'], tags: {} }, ctx);
|
|
1164
|
+
|
|
1165
|
+
expect(out.state.registration).toBe('registered');
|
|
1166
|
+
expect(out.state.userModes.tls).toBe(true);
|
|
1167
|
+
});
|
|
1168
|
+
});
|
|
@@ -4,8 +4,21 @@ import { capReducer } from '../../src/commands/cap';
|
|
|
4
4
|
import { authenticateReducer } from '../../src/commands/sasl';
|
|
5
5
|
import { Effect } from '../../src/effects';
|
|
6
6
|
import type { Effect as EffectType, RawLine } from '../../src/effects';
|
|
7
|
-
import type {
|
|
8
|
-
|
|
7
|
+
import type {
|
|
8
|
+
AccountStore,
|
|
9
|
+
AwayStore,
|
|
10
|
+
MtlsIdentityProvider,
|
|
11
|
+
ReadMarkerStore,
|
|
12
|
+
SaslPayload,
|
|
13
|
+
SaslResult,
|
|
14
|
+
} from '../../src/ports';
|
|
15
|
+
import {
|
|
16
|
+
EmptyMotdProvider,
|
|
17
|
+
FakeClock,
|
|
18
|
+
InMemoryAwayStore,
|
|
19
|
+
InMemoryReadMarkerStore,
|
|
20
|
+
SequentialIdFactory,
|
|
21
|
+
} from '../../src/ports';
|
|
9
22
|
import { encodeBase64 } from '../../src/protocol/base64';
|
|
10
23
|
import type { IrcMessage } from '../../src/protocol/messages';
|
|
11
24
|
import { type ConnectionState, createConnection } from '../../src/state/connection';
|
|
@@ -44,6 +57,8 @@ function makeCtx(
|
|
|
44
57
|
state: ConnectionState,
|
|
45
58
|
accounts?: AccountStore,
|
|
46
59
|
mtlsIdentity?: MtlsIdentityProvider,
|
|
60
|
+
readMarkers?: ReadMarkerStore,
|
|
61
|
+
away?: AwayStore,
|
|
47
62
|
): Ctx {
|
|
48
63
|
return buildCtx({
|
|
49
64
|
serverConfig,
|
|
@@ -53,6 +68,8 @@ function makeCtx(
|
|
|
53
68
|
connection: state,
|
|
54
69
|
...(accounts !== undefined ? { accounts } : {}),
|
|
55
70
|
...(mtlsIdentity !== undefined ? { mtlsIdentity } : {}),
|
|
71
|
+
...(readMarkers !== undefined ? { readMarkers } : {}),
|
|
72
|
+
...(away !== undefined ? { away } : {}),
|
|
56
73
|
});
|
|
57
74
|
}
|
|
58
75
|
|
|
@@ -144,6 +161,103 @@ describe('authenticateReducer — PLAIN success', () => {
|
|
|
144
161
|
expect(state.account).toBe('alice');
|
|
145
162
|
});
|
|
146
163
|
|
|
164
|
+
it('seeds lastReadMarkers from the ReadMarkerStore at identify', () => {
|
|
165
|
+
const state = saslReadyState();
|
|
166
|
+
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|
|
167
|
+
const readMarkers = new InMemoryReadMarkerStore();
|
|
168
|
+
readMarkers.set('alice', '#foo', 'm1');
|
|
169
|
+
readMarkers.set('alice', '#bar', 'm2');
|
|
170
|
+
const ctx = makeCtx(state, accounts, undefined, readMarkers);
|
|
171
|
+
|
|
172
|
+
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
173
|
+
authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
174
|
+
|
|
175
|
+
expect(state.lastReadMarkers?.get('#foo')).toBe('m1');
|
|
176
|
+
expect(state.lastReadMarkers?.get('#bar')).toBe('m2');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('does not seed markers for a different account', () => {
|
|
180
|
+
const state = saslReadyState();
|
|
181
|
+
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|
|
182
|
+
const readMarkers = new InMemoryReadMarkerStore();
|
|
183
|
+
readMarkers.set('bob', '#foo', 'm9');
|
|
184
|
+
const ctx = makeCtx(state, accounts, undefined, readMarkers);
|
|
185
|
+
|
|
186
|
+
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
187
|
+
authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
188
|
+
|
|
189
|
+
expect(state.lastReadMarkers?.get('#foo')).toBeUndefined();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('replays the persisted away reason on identify (draft/pre-away)', () => {
|
|
193
|
+
const state = saslReadyState();
|
|
194
|
+
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|
|
195
|
+
const away = new InMemoryAwayStore();
|
|
196
|
+
away.set('alice', 'brb');
|
|
197
|
+
const ctx = makeCtx(state, accounts, undefined, undefined, away);
|
|
198
|
+
|
|
199
|
+
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
200
|
+
authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
201
|
+
|
|
202
|
+
expect(state.away).toBe('brb');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('emits 306 RPL_NOWAWAY after identifying when an away reason is replayed', () => {
|
|
206
|
+
const state = saslReadyState();
|
|
207
|
+
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|
|
208
|
+
const away = new InMemoryAwayStore();
|
|
209
|
+
away.set('alice', 'brb');
|
|
210
|
+
const ctx = makeCtx(state, accounts, undefined, undefined, away);
|
|
211
|
+
|
|
212
|
+
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
213
|
+
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
214
|
+
|
|
215
|
+
const texts = out.effects.flatMap((e) => (e.tag === 'Send' ? e.lines : []));
|
|
216
|
+
expect(texts.map((l) => l.text)).toContain(
|
|
217
|
+
':irc.example.com 306 alice :You have been marked as being away',
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('does not replay an away reason when none is persisted', () => {
|
|
222
|
+
const state = saslReadyState();
|
|
223
|
+
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|
|
224
|
+
const away = new InMemoryAwayStore();
|
|
225
|
+
const ctx = makeCtx(state, accounts, undefined, undefined, away);
|
|
226
|
+
|
|
227
|
+
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
228
|
+
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
229
|
+
|
|
230
|
+
expect(state.away).toBeUndefined();
|
|
231
|
+
const texts = out.effects.flatMap((e) => (e.tag === 'Send' ? e.lines : []));
|
|
232
|
+
expect(texts.some((l) => l.text.includes('306'))).toBe(false);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('does not replay an away reason when no AwayStore is bound', () => {
|
|
236
|
+
const state = saslReadyState();
|
|
237
|
+
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|
|
238
|
+
const ctx = makeCtx(state, accounts); // no away store
|
|
239
|
+
|
|
240
|
+
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
241
|
+
const out = authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
242
|
+
|
|
243
|
+
expect(state.away).toBeUndefined();
|
|
244
|
+
const texts = out.effects.flatMap((e) => (e.tag === 'Send' ? e.lines : []));
|
|
245
|
+
expect(texts.some((l) => l.text.includes('306'))).toBe(false);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('looks up the persisted away reason case-insensitively', () => {
|
|
249
|
+
const state = saslReadyState();
|
|
250
|
+
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|
|
251
|
+
const away = new InMemoryAwayStore();
|
|
252
|
+
away.set('ALICE', 'brb');
|
|
253
|
+
const ctx = makeCtx(state, accounts, undefined, undefined, away);
|
|
254
|
+
|
|
255
|
+
authenticateReducer(state, authenticate('PLAIN'), ctx);
|
|
256
|
+
authenticateReducer(state, authenticate(plainPayload('alice', 'secret')), ctx);
|
|
257
|
+
|
|
258
|
+
expect(state.away).toBe('brb');
|
|
259
|
+
});
|
|
260
|
+
|
|
147
261
|
it('clears the in-progress mechanism after a successful authentication', () => {
|
|
148
262
|
const state = saslReadyState();
|
|
149
263
|
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|