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
|
@@ -18,13 +18,14 @@
|
|
|
18
18
|
import { caseFold } from '../case-fold.js';
|
|
19
19
|
import { Effect } from '../effects.js';
|
|
20
20
|
import type { Effect as EffectType, RawLine } from '../effects.js';
|
|
21
|
-
import type { StoredMessage } from '../ports.js';
|
|
21
|
+
import type { PersistedTopic, StoredMessage } from '../ports.js';
|
|
22
22
|
import { wrapBatch } from '../protocol/batch.js';
|
|
23
23
|
import { Numerics } from '../protocol/numerics.js';
|
|
24
|
-
import { type ChanName, type ChannelState, prefixOf } from '../state/channel.js';
|
|
24
|
+
import { type ChanName, type ChannelState, type ChannelTopic, prefixOf } from '../state/channel.js';
|
|
25
25
|
import { hostmaskOf } from '../state/connection.js';
|
|
26
26
|
import type { ConnectionState } from '../state/connection.js';
|
|
27
27
|
import type { Ctx, Reducer } from '../types.js';
|
|
28
|
+
import { CHANSERV_HOSTMASK, DEFAULT_CHANNEL_LEVELS } from './chanserv.js';
|
|
28
29
|
import { CHATHISTORY_CAP, buildChathistoryBatch } from './chathistory.js';
|
|
29
30
|
|
|
30
31
|
/**
|
|
@@ -226,6 +227,21 @@ export const joinReducer: Reducer<ChannelState> = (state, msg, ctx) => {
|
|
|
226
227
|
return { state, effects };
|
|
227
228
|
}
|
|
228
229
|
|
|
230
|
+
// ChanServ +R check: unidentified clients may not join a +R channel.
|
|
231
|
+
if (state.modes.blockUnidentified && ctx.connection.account === undefined) {
|
|
232
|
+
effects.push(
|
|
233
|
+
Effect.send(ctx.connId, [
|
|
234
|
+
numericErr(
|
|
235
|
+
ctx,
|
|
236
|
+
Numerics.ERR_ALLMUSTREGISTER,
|
|
237
|
+
'You must register with NickServ to join this channel',
|
|
238
|
+
rawName,
|
|
239
|
+
),
|
|
240
|
+
]),
|
|
241
|
+
);
|
|
242
|
+
return { state, effects };
|
|
243
|
+
}
|
|
244
|
+
|
|
229
245
|
// Limit check.
|
|
230
246
|
if (state.modes.limit !== undefined && state.members.size >= state.modes.limit) {
|
|
231
247
|
effects.push(
|
|
@@ -250,14 +266,38 @@ export const joinReducer: Reducer<ChannelState> = (state, msg, ctx) => {
|
|
|
250
266
|
}
|
|
251
267
|
|
|
252
268
|
// All checks passed: add to roster. First joiner gets op per RFC 2812.
|
|
253
|
-
const
|
|
269
|
+
const wasEmpty = state.members.size === 0;
|
|
270
|
+
// ChanServ ACCESS auto-prefix: when services are bound, the channel is
|
|
271
|
+
// registered, and this is not the RFC 2812 first-joiner case, look up
|
|
272
|
+
// the joiner's access level and apply the matching channel prefix.
|
|
273
|
+
// `chanservPrefix` is non-null only when ChanServ (not the RFC 2812
|
|
274
|
+
// rule) granted the prefix, so the JOIN hook knows to emit a ChanServ
|
|
275
|
+
// MODE broadcast alongside the roster mutation.
|
|
276
|
+
const autoPrefix = wasEmpty ? null : computeChanservAutoPrefix(ctx, state.nameLower);
|
|
277
|
+
const grantOp = wasEmpty || (autoPrefix?.op ?? false);
|
|
278
|
+
const grantVoice = autoPrefix?.voice ?? false;
|
|
279
|
+
const joinerNick = ctx.connection.nick ?? '?';
|
|
254
280
|
state.members.set(ctx.connId, {
|
|
255
281
|
conn: ctx.connId,
|
|
256
|
-
nick:
|
|
257
|
-
op:
|
|
258
|
-
voice:
|
|
282
|
+
nick: joinerNick,
|
|
283
|
+
op: grantOp,
|
|
284
|
+
voice: grantVoice,
|
|
259
285
|
});
|
|
260
286
|
|
|
287
|
+
// ChanServ KEEPTOPIC restore: when the channel was empty immediately
|
|
288
|
+
// before this join and services have a persisted topic snapshot for a
|
|
289
|
+
// keep-topic-enabled registration, restore the topic onto the
|
|
290
|
+
// authoritative ChannelState. The 332/333 numerics are emitted to the
|
|
291
|
+
// joiner below alongside the NAMES list.
|
|
292
|
+
let restoredTopic: ChannelTopic | undefined;
|
|
293
|
+
if (wasEmpty) {
|
|
294
|
+
restoredTopic = restoreKeepTopic(ctx, state.nameLower);
|
|
295
|
+
if (restoredTopic !== undefined) {
|
|
296
|
+
state.topic = restoredTopic;
|
|
297
|
+
effects.push(Effect.applyChannelDelta(state.name, { topic: restoredTopic }));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
261
301
|
// Pending invite is consumed on a successful join.
|
|
262
302
|
if (myNick !== undefined) state.pendingInvites.delete(caseFold('rfc1459', myNick));
|
|
263
303
|
|
|
@@ -275,13 +315,24 @@ export const joinReducer: Reducer<ChannelState> = (state, msg, ctx) => {
|
|
|
275
315
|
{
|
|
276
316
|
type: 'add',
|
|
277
317
|
conn: ctx.connId,
|
|
278
|
-
nick:
|
|
279
|
-
op:
|
|
318
|
+
nick: joinerNick,
|
|
319
|
+
op: grantOp,
|
|
320
|
+
voice: grantVoice,
|
|
280
321
|
},
|
|
281
322
|
],
|
|
282
323
|
}),
|
|
283
324
|
);
|
|
284
325
|
|
|
326
|
+
// ChanServ auto-prefix broadcast: when ChanServ granted op or voice
|
|
327
|
+
// (i.e. NOT the RFC 2812 first-joiner case), emit a
|
|
328
|
+
// `:ChanServ!ChanServ@services MODE #chan +o nick` broadcast so every
|
|
329
|
+
// client in the channel updates its prefix display. Supersedes the
|
|
330
|
+
// implicit voice when op is granted (the channel-prefix grammar
|
|
331
|
+
// already renders `@` for ops).
|
|
332
|
+
if (autoPrefix !== null && (autoPrefix.op || autoPrefix.voice)) {
|
|
333
|
+
effects.push(buildChanservPrefixBroadcast(state.name, joinerNick, autoPrefix));
|
|
334
|
+
}
|
|
335
|
+
|
|
285
336
|
// IRCv3 draft/chathistory auto-playback: a cap-enabled joiner with a
|
|
286
337
|
// bound MessageStore receives the recent backlog as a BATCH *before*
|
|
287
338
|
// the JOIN broadcast, then the per-(connection, channel) "last read"
|
|
@@ -328,6 +379,20 @@ export const joinReducer: Reducer<ChannelState> = (state, msg, ctx) => {
|
|
|
328
379
|
|
|
329
380
|
effects.push(Effect.send(ctx.connId, lines));
|
|
330
381
|
|
|
382
|
+
// When ChanServ KEEPTOPIC restored a topic this JOIN, emit the 332/333
|
|
383
|
+
// topic numerics to the joiner so their client renders the restored
|
|
384
|
+
// topic (mirroring the TOPIC read-form reply shape).
|
|
385
|
+
if (restoredTopic !== undefined) {
|
|
386
|
+
effects.push(
|
|
387
|
+
Effect.send(ctx.connId, [
|
|
388
|
+
{ text: `:${ctx.serverName} 332 ${nick} ${chanName} :${restoredTopic.text}` },
|
|
389
|
+
{
|
|
390
|
+
text: `:${ctx.serverName} 333 ${nick} ${chanName} ${restoredTopic.setter} ${restoredTopic.setAt}`,
|
|
391
|
+
},
|
|
392
|
+
]),
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
331
396
|
return { state, effects };
|
|
332
397
|
};
|
|
333
398
|
|
|
@@ -360,3 +425,94 @@ export function handleJoinZero(ctx: Ctx): EffectType[] {
|
|
|
360
425
|
ctx.connection.joinedChannels.clear();
|
|
361
426
|
return effects;
|
|
362
427
|
}
|
|
428
|
+
|
|
429
|
+
// ============================================================================
|
|
430
|
+
// ChanServ KEEPTOPIC restore
|
|
431
|
+
// ============================================================================
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Returns the persisted topic snapshot to restore on a JOIN that re-creates
|
|
435
|
+
* an empty channel, or `undefined` when:
|
|
436
|
+
* - no services store is bound,
|
|
437
|
+
* - the channel is not registered,
|
|
438
|
+
* - KEEPTOPIC is off, or
|
|
439
|
+
* - no topic snapshot is stored.
|
|
440
|
+
*
|
|
441
|
+
* Reads the per-channel lowercased name from the joiner's `nameLower`
|
|
442
|
+
* field; services-store keys are rfc1459-folded so the lookup is
|
|
443
|
+
* case-insensitive.
|
|
444
|
+
*/
|
|
445
|
+
function restoreKeepTopic(ctx: Ctx, nameLower: string): ChannelTopic | undefined {
|
|
446
|
+
const services = ctx.services;
|
|
447
|
+
if (services === undefined) return undefined;
|
|
448
|
+
const rec = services.getChannel(nameLower);
|
|
449
|
+
if (rec === undefined || !rec.keepTopic) return undefined;
|
|
450
|
+
const persisted: PersistedTopic | undefined = services.getChannelTopic(nameLower);
|
|
451
|
+
if (persisted === undefined) return undefined;
|
|
452
|
+
return { text: persisted.text, setter: persisted.setter, setAt: persisted.setAt };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ============================================================================
|
|
456
|
+
// ChanServ ACCESS auto-prefix
|
|
457
|
+
// ============================================================================
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Decides whether ChanServ should auto-grant a channel prefix to the
|
|
461
|
+
* joining connection on a non-empty registered channel.
|
|
462
|
+
*
|
|
463
|
+
* Returns `{ op: true, voice: false }` for the channel founder or any
|
|
464
|
+
* account whose access level meets the founder-configured `AUTOOP`
|
|
465
|
+
* threshold; `{ op: false, voice: true }` for accounts that meet the
|
|
466
|
+
* `AUTOVOICE` threshold; `{ op: false, voice: false }` otherwise.
|
|
467
|
+
*
|
|
468
|
+
* Returns `null` when no services store is bound, the channel is not
|
|
469
|
+
* registered, or the joiner is unidentified — the caller treats all of
|
|
470
|
+
* those as "no ChanServ-derived prefix" and falls back to the RFC 2812
|
|
471
|
+
* first-joiner rule or the plain-roster defaults.
|
|
472
|
+
*
|
|
473
|
+
* Threshold lookup consults the founder-redefined overrides via
|
|
474
|
+
* {@link ServicesStore.getChannelLevel} and falls back to
|
|
475
|
+
* {@link DEFAULT_CHANNEL_LEVELS} for ops the founder has not overridden.
|
|
476
|
+
*/
|
|
477
|
+
function computeChanservAutoPrefix(
|
|
478
|
+
ctx: Ctx,
|
|
479
|
+
nameLower: string,
|
|
480
|
+
): { op: boolean; voice: boolean } | null {
|
|
481
|
+
const services = ctx.services;
|
|
482
|
+
if (services === undefined) return null;
|
|
483
|
+
const rec = services.getChannel(nameLower);
|
|
484
|
+
if (rec === undefined) return null;
|
|
485
|
+
const account = ctx.connection.account;
|
|
486
|
+
if (account === undefined) return null;
|
|
487
|
+
if (caseFold('rfc1459', rec.founder) === caseFold('rfc1459', account)) {
|
|
488
|
+
return { op: true, voice: false };
|
|
489
|
+
}
|
|
490
|
+
const level = services.getChannelAccess(nameLower, account);
|
|
491
|
+
const autoOp = services.getChannelLevel(nameLower, 'AUTOOP') ?? DEFAULT_CHANNEL_LEVELS.AUTOOP;
|
|
492
|
+
const autoVoice =
|
|
493
|
+
services.getChannelLevel(nameLower, 'AUTOVOICE') ?? DEFAULT_CHANNEL_LEVELS.AUTOVOICE;
|
|
494
|
+
if (level >= autoOp) return { op: true, voice: false };
|
|
495
|
+
if (level >= autoVoice) return { op: false, voice: true };
|
|
496
|
+
return { op: false, voice: false };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Builds the `Broadcast` effect for a ChanServ-emitted channel prefix
|
|
501
|
+
* grant. The line shape mirrors the MODE reducer's ChanServ-MLOCK
|
|
502
|
+
* reassert broadcast so existing clients already special-case
|
|
503
|
+
* `:ChanServ!ChanServ@services MODE …` correctly.
|
|
504
|
+
*
|
|
505
|
+
* Op grants emit `+o` only (the channel-prefix grammar renders `@` for
|
|
506
|
+
* ops, so a redundant `+v` would only confuse client UIs that show both
|
|
507
|
+
* prefixes side-by-side).
|
|
508
|
+
*/
|
|
509
|
+
function buildChanservPrefixBroadcast(
|
|
510
|
+
chanName: ChanName,
|
|
511
|
+
nick: string,
|
|
512
|
+
prefix: { op: boolean; voice: boolean },
|
|
513
|
+
): EffectType {
|
|
514
|
+
const mode = prefix.op ? '+o' : '+v';
|
|
515
|
+
return Effect.broadcast(chanName, [
|
|
516
|
+
{ text: `:${CHANSERV_HOSTMASK} MODE ${chanName} ${mode} ${nick}` },
|
|
517
|
+
]);
|
|
518
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure reducer for the IRCv3 `draft/read-marker` `MARKREAD` command.
|
|
3
|
+
*
|
|
4
|
+
* Spec (work-in-progress): https://ircv3.net/specs/extensions/read-marker
|
|
5
|
+
*
|
|
6
|
+
* The serverless-ircd deployment stores read markers **by msgid** (see
|
|
7
|
+
* {@link ../commands/read-marker.ts}) so they compose with the chathistory
|
|
8
|
+
* last-read plumbing. The draft `MARKREAD` verb, by contrast, is
|
|
9
|
+
* timestamp-based: a client sends `MARKREAD <target> <timestamp>` where the
|
|
10
|
+
* timestamp is the `time` tag of the last read message. This reducer is the
|
|
11
|
+
* translation layer — it resolves the client timestamp to the most recent
|
|
12
|
+
* retained message at or before it ({@link MessageStore.findAtOrBefore}),
|
|
13
|
+
* advances the msgid-based marker via {@link persistReadMarker}, and replies
|
|
14
|
+
* with the resolved message's timestamp so the client observes a value that
|
|
15
|
+
* corresponds to a real `time` tag.
|
|
16
|
+
*
|
|
17
|
+
* Wire forms handled:
|
|
18
|
+
* - `MARKREAD <target>` (GET) — replies with the stored marker's timestamp
|
|
19
|
+
* (or `*` when none / unresolvable).
|
|
20
|
+
* - `MARKREAD <target> <timestamp>` (SET) — advances the marker subject to
|
|
21
|
+
* monotonicity (the marker never moves backwards) and replies with the
|
|
22
|
+
* resolved timestamp, or with the stored value when the client timestamp
|
|
23
|
+
* is older-or-equal.
|
|
24
|
+
*
|
|
25
|
+
* Errors use the IRCv3 `standard-replies` `FAIL` form (the draft specifies no
|
|
26
|
+
* legacy numerics for `MARKREAD`):
|
|
27
|
+
* - `FAIL MARKREAD NEED_MORE_PARAMS :Missing parameters`
|
|
28
|
+
* - `FAIL MARKREAD INVALID_PARAMS :Invalid parameters`
|
|
29
|
+
*
|
|
30
|
+
* The command is gated behind the `draft/read-marker` capability: a client
|
|
31
|
+
* that did not negotiate it sees `421 ERR_UNKNOWNCOMMAND` (the spec's
|
|
32
|
+
* permitted "unknown command" fallback, matching the `TAGMSG` precedent).
|
|
33
|
+
*
|
|
34
|
+
* Membership mirrors the `TAGMSG` read-marker path: a non-member on a `+n`
|
|
35
|
+
* channel is ignored silently (the user has no marker to set or get).
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { Effect } from '../effects.js';
|
|
39
|
+
import type { RawLine } from '../effects.js';
|
|
40
|
+
import { Numerics } from '../protocol/numerics.js';
|
|
41
|
+
import { formatServerTime } from '../protocol/outbound.js';
|
|
42
|
+
import type { ChannelState } from '../state/channel.js';
|
|
43
|
+
import type { Ctx, Reducer } from '../types.js';
|
|
44
|
+
import { READ_MARKER_CAP, persistReadMarker } from './read-marker.js';
|
|
45
|
+
|
|
46
|
+
/** Prefix that introduces the server-time timestamp in a SET param. */
|
|
47
|
+
const TIMESTAMP_PREFIX = 'timestamp=';
|
|
48
|
+
|
|
49
|
+
/** Literal timestamp meaning "no known read marker" (server replies only). */
|
|
50
|
+
const NO_MARKER = '*';
|
|
51
|
+
|
|
52
|
+
/** Formats a numeric error line addressed to the connection's nick (or `*`). */
|
|
53
|
+
function numericErr(ctx: Ctx, code: number, trailing: string, middle: string): RawLine {
|
|
54
|
+
const nick = ctx.connection.nick ?? '*';
|
|
55
|
+
const codeStr = code.toString().padStart(3, '0');
|
|
56
|
+
return { text: [`:${ctx.serverName}`, codeStr, nick, middle, `:${trailing}`].join(' ') };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Formats a `FAIL MARKREAD <code> :<desc>` standard-reply line. */
|
|
60
|
+
function failReply(ctx: Ctx, code: string, desc: string): RawLine {
|
|
61
|
+
return { text: `:${ctx.serverName} FAIL MARKREAD ${code} :${desc}` };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Formats the `:<server> MARKREAD <target> <timestamp>` server reply line. */
|
|
65
|
+
function markreadReply(ctx: Ctx, target: string, timestamp: string): RawLine {
|
|
66
|
+
return { text: `:${ctx.serverName} MARKREAD ${target} ${timestamp}` };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Formats `ms` as the wire timestamp token carried by every server
|
|
71
|
+
* `MARKREAD` reply: `timestamp=<server-time>` (matching the client SET form
|
|
72
|
+
* and the spec examples). `NO_MARKER` (`*`) is returned verbatim by callers
|
|
73
|
+
* and never passes through here.
|
|
74
|
+
*/
|
|
75
|
+
function timestampTag(ms: number): string {
|
|
76
|
+
return `${TIMESTAMP_PREFIX}${formatServerTime(ms)}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Parses a SET timestamp param (`timestamp=<iso>`) into epoch ms, or
|
|
81
|
+
* `undefined` when the param is malformed (missing prefix or an unparseable
|
|
82
|
+
* date). The literal `*` is rejected upstream (it is invalid for SET) before
|
|
83
|
+
* this helper runs.
|
|
84
|
+
*/
|
|
85
|
+
function parseTimestampParam(raw: string): number | undefined {
|
|
86
|
+
if (!raw.startsWith(TIMESTAMP_PREFIX)) return undefined;
|
|
87
|
+
const value = raw.slice(TIMESTAMP_PREFIX.length);
|
|
88
|
+
if (value.length === 0) return undefined;
|
|
89
|
+
const ms = Date.parse(value);
|
|
90
|
+
return Number.isNaN(ms) ? undefined : ms;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolves the connection's current marker for `chanLower` to its message's
|
|
95
|
+
* epoch-ms time, or `undefined` when there is no marker, no bound message
|
|
96
|
+
* store, or the marker's message is no longer retained. The epoch-ms form
|
|
97
|
+
* drives the monotonicity comparison; {@link markerTimestampTag} formats the
|
|
98
|
+
* wire form for replies.
|
|
99
|
+
*/
|
|
100
|
+
function markerTime(
|
|
101
|
+
ctx: Ctx,
|
|
102
|
+
chanLower: string,
|
|
103
|
+
messages: MessageStoreLike | undefined,
|
|
104
|
+
): number | undefined {
|
|
105
|
+
const msgid = ctx.connection.lastReadMarkers?.get(chanLower);
|
|
106
|
+
if (msgid === undefined || messages === undefined) return undefined;
|
|
107
|
+
return messages.getMsg(chanLower, msgid)?.time;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface MessageStoreLike {
|
|
111
|
+
getMsg(chan: string, msgid: string): { time: number } | undefined;
|
|
112
|
+
findAtOrBefore(chan: string, ts: number): { msgid: string; time: number } | undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Reducer for `MARKREAD <channel>` (GET) and `MARKREAD <channel> <ts>` (SET). */
|
|
116
|
+
export const markreadChannelReducer: Reducer<ChannelState> = (state, msg, ctx) => {
|
|
117
|
+
ctx.connection.lastSeen = ctx.clock.now();
|
|
118
|
+
|
|
119
|
+
// Cap gate: clients that did not negotiate draft/read-marker see RFC 2812
|
|
120
|
+
// only (the spec's permitted "unknown command" fallback).
|
|
121
|
+
if (!ctx.connection.caps.has(READ_MARKER_CAP)) {
|
|
122
|
+
return {
|
|
123
|
+
state,
|
|
124
|
+
effects: [
|
|
125
|
+
Effect.send(ctx.connId, [
|
|
126
|
+
numericErr(ctx, Numerics.ERR_UNKNOWNCOMMAND, 'Unknown command', 'MARKREAD'),
|
|
127
|
+
]),
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const target = msg.params[0];
|
|
133
|
+
if (target === undefined || target.length === 0) {
|
|
134
|
+
return {
|
|
135
|
+
state,
|
|
136
|
+
effects: [
|
|
137
|
+
Effect.send(ctx.connId, [failReply(ctx, 'NEED_MORE_PARAMS', 'Missing parameters')]),
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Membership: a non-member on a +n channel has no marker to set or get.
|
|
143
|
+
// Silent, mirroring the TAGMSG read-marker path.
|
|
144
|
+
const senderEntry = state.members.get(ctx.connId);
|
|
145
|
+
if (state.modes.noExternal && senderEntry === undefined) {
|
|
146
|
+
return { state, effects: [] };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const messages = ctx.messages as MessageStoreLike | undefined;
|
|
150
|
+
|
|
151
|
+
// GET command: reply with the stored marker timestamp (or *).
|
|
152
|
+
if (msg.params.length < 2) {
|
|
153
|
+
const stored = markerTime(ctx, state.nameLower, messages);
|
|
154
|
+
const tag = stored === undefined ? NO_MARKER : timestampTag(stored);
|
|
155
|
+
return { state, effects: [Effect.send(ctx.connId, [markreadReply(ctx, target, tag)])] };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// SET command.
|
|
159
|
+
const raw = msg.params[1] as string;
|
|
160
|
+
if (raw === NO_MARKER) {
|
|
161
|
+
// The literal * is reserved for server replies; a client SET MUST NOT use it.
|
|
162
|
+
return {
|
|
163
|
+
state,
|
|
164
|
+
effects: [Effect.send(ctx.connId, [failReply(ctx, 'INVALID_PARAMS', 'Invalid parameters')])],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const clientMs = parseTimestampParam(raw);
|
|
168
|
+
if (clientMs === undefined) {
|
|
169
|
+
return {
|
|
170
|
+
state,
|
|
171
|
+
effects: [Effect.send(ctx.connId, [failReply(ctx, 'INVALID_PARAMS', 'Invalid parameters')])],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Translate the client timestamp to the most recent retained message at or
|
|
176
|
+
// before it. Without a bound store, or when nothing matches (e.g. the
|
|
177
|
+
// timestamp predates every retained message), there is no msgid to advance
|
|
178
|
+
// to: reply with the current marker and do not mutate state.
|
|
179
|
+
const resolved = messages?.findAtOrBefore(state.nameLower, clientMs);
|
|
180
|
+
if (resolved === undefined) {
|
|
181
|
+
const stored = markerTime(ctx, state.nameLower, messages);
|
|
182
|
+
const tag = stored === undefined ? NO_MARKER : timestampTag(stored);
|
|
183
|
+
return { state, effects: [Effect.send(ctx.connId, [markreadReply(ctx, target, tag)])] };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Monotonicity: the marker only ever advances. A client timestamp that
|
|
187
|
+
// resolves to a message at or before the stored marker is rejected — the
|
|
188
|
+
// server replies with the stored (newer) value and ignores the client value.
|
|
189
|
+
const storedMs = markerTime(ctx, state.nameLower, messages);
|
|
190
|
+
if (storedMs !== undefined && resolved.time <= storedMs) {
|
|
191
|
+
return {
|
|
192
|
+
state,
|
|
193
|
+
effects: [Effect.send(ctx.connId, [markreadReply(ctx, target, timestampTag(storedMs))])],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
persistReadMarker(ctx, state.nameLower, resolved.msgid);
|
|
198
|
+
return {
|
|
199
|
+
state,
|
|
200
|
+
effects: [Effect.send(ctx.connId, [markreadReply(ctx, target, timestampTag(resolved.time))])],
|
|
201
|
+
};
|
|
202
|
+
};
|