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
|
@@ -340,6 +340,13 @@ export type StoredCommand = 'PRIVMSG' | 'NOTICE' | 'TAGMSG';
|
|
|
340
340
|
* is reconstructed by the reducer from the authoritative {@link ChannelState}
|
|
341
341
|
* when building replay lines. `time` is epoch milliseconds from the injected
|
|
342
342
|
* {@link Clock}; `msgid` is a fresh nonce from {@link IdFactory}.
|
|
343
|
+
*
|
|
344
|
+
* `clientTags` carries optional IRCv3 message-tags client-only entries
|
|
345
|
+
* (keys prefixed with `+`) that the recording reducer wants preserved on
|
|
346
|
+
* replay — e.g. `+draft/multiline` so a chathistory client can recognise
|
|
347
|
+
* that a stored PRIVMSG originated from a multi-line batch. Omitted (or
|
|
348
|
+
* empty) for normal PRIVMSG/NOTICE/TAGMSG. The chathistory replay weaves
|
|
349
|
+
* these into the `@time=…;msgid=…` tag section verbatim.
|
|
343
350
|
*/
|
|
344
351
|
export interface StoredMessage {
|
|
345
352
|
msgid: string;
|
|
@@ -350,6 +357,7 @@ export interface StoredMessage {
|
|
|
350
357
|
user?: string;
|
|
351
358
|
host?: string;
|
|
352
359
|
text: string;
|
|
360
|
+
readonly clientTags?: readonly string[];
|
|
353
361
|
}
|
|
354
362
|
|
|
355
363
|
/** Subcommands of `CHATHISTORY` that select a slice of the backlog. */
|
|
@@ -570,6 +578,180 @@ function tail<T>(arr: readonly T[], n: number): T[] {
|
|
|
570
578
|
return arr.slice(start);
|
|
571
579
|
}
|
|
572
580
|
|
|
581
|
+
// ============================================================================
|
|
582
|
+
// Read-marker persistence (IRCv3 draft/read-marker)
|
|
583
|
+
// ============================================================================
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* One persisted last-read marker, keyed per `(account, channel)`.
|
|
587
|
+
*
|
|
588
|
+
* `channel` is the lowercased channel name (the storage key); `msgid` is the
|
|
589
|
+
* msgid of the most recent message the account has read in that channel.
|
|
590
|
+
* Returned by {@link ReadMarkerStore.forAccount} when seeding a fresh
|
|
591
|
+
* connection at identify time.
|
|
592
|
+
*/
|
|
593
|
+
export interface ReadMarkerEntry {
|
|
594
|
+
/** Canonical account name (as recorded by SASL). */
|
|
595
|
+
account: string;
|
|
596
|
+
/** Lowercased channel name (rfc1459-folded storage key). */
|
|
597
|
+
channel: string;
|
|
598
|
+
/** Msgid of the most recent read message in this channel. */
|
|
599
|
+
msgid: string;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Persistence port for IRCv3 `draft/read-marker`.
|
|
604
|
+
*
|
|
605
|
+
* The chathistory command keeps a per-`(connection, channel)`
|
|
606
|
+
* last-read marker in {@link ConnectionState.lastReadMarkers}; this port
|
|
607
|
+
* promotes that marker to a per-`(account, channel)` record so it survives
|
|
608
|
+
* reconnects and is shared across an account's connections.
|
|
609
|
+
*
|
|
610
|
+
* Mirrors the {@link MessageStore} / {@link AccountStore} pattern: the port
|
|
611
|
+
* is **synchronous** (reducers must stay pure and free of IO) and injected
|
|
612
|
+
* via {@link Ctx}. Adapters that need async storage (D1, DynamoDB) pre-load
|
|
613
|
+
* the relevant slice into a synchronously-readable store; the in-memory
|
|
614
|
+
* reference implementation ({@link InMemoryReadMarkerStore}) ships here.
|
|
615
|
+
*
|
|
616
|
+
* Account and channel keys are both case-folded (rfc1459) so lookups are
|
|
617
|
+
* case-insensitive, matching the rest of the server's comparison semantics.
|
|
618
|
+
*/
|
|
619
|
+
export interface ReadMarkerStore {
|
|
620
|
+
/**
|
|
621
|
+
* Returns the stored last-read msgid for `(account, channel)`, or
|
|
622
|
+
* `undefined` when no marker has been recorded.
|
|
623
|
+
*/
|
|
624
|
+
get(account: string, channel: string): string | undefined;
|
|
625
|
+
/**
|
|
626
|
+
* Records (or replaces) the last-read msgid for `(account, channel)`.
|
|
627
|
+
* Callers MUST only move the marker forward (towards newer messages);
|
|
628
|
+
* the store itself does not order msgids, it stores the latest value set.
|
|
629
|
+
*/
|
|
630
|
+
set(account: string, channel: string, msgid: string): void;
|
|
631
|
+
/**
|
|
632
|
+
* Enumerates every recorded marker for `account`. Used at identify time to
|
|
633
|
+
* seed a fresh connection's {@link ConnectionState.lastReadMarkers} so a
|
|
634
|
+
* reconnect restores the user's read position across all their channels.
|
|
635
|
+
*/
|
|
636
|
+
forAccount(account: string): ReadonlyArray<ReadMarkerEntry>;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Reference in-memory {@link ReadMarkerStore} backed by a
|
|
641
|
+
* `foldedAccount → (foldedChannel → msgid)` nested map.
|
|
642
|
+
*
|
|
643
|
+
* Used by the local CLI, integration tests, and any deployment that has not
|
|
644
|
+
* bound a distributed backend. Cloud adapters replace this with a D1 /
|
|
645
|
+
* DynamoDB-backed implementation against the same surface.
|
|
646
|
+
*
|
|
647
|
+
* Both keys are folded via {@link caseFold} (`rfc1459`) so lookups are
|
|
648
|
+
* case-insensitive; the returned {@link ReadMarkerEntry.channel} carries the
|
|
649
|
+
* folded spelling and {@link ReadMarkerEntry.account} the canonical name
|
|
650
|
+
* passed to {@link ReadMarkerStore.forAccount}.
|
|
651
|
+
*/
|
|
652
|
+
export class InMemoryReadMarkerStore implements ReadMarkerStore {
|
|
653
|
+
private readonly byAccount = new Map<string, Map<string, string>>();
|
|
654
|
+
|
|
655
|
+
get(account: string, channel: string): string | undefined {
|
|
656
|
+
return this.byAccount.get(folded(account))?.get(folded(channel));
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
set(account: string, channel: string, msgid: string): void {
|
|
660
|
+
const acct = folded(account);
|
|
661
|
+
let chanMap = this.byAccount.get(acct);
|
|
662
|
+
if (chanMap === undefined) {
|
|
663
|
+
chanMap = new Map<string, string>();
|
|
664
|
+
this.byAccount.set(acct, chanMap);
|
|
665
|
+
}
|
|
666
|
+
chanMap.set(folded(channel), msgid);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
forAccount(account: string): ReadonlyArray<ReadMarkerEntry> {
|
|
670
|
+
const chanMap = this.byAccount.get(folded(account));
|
|
671
|
+
if (chanMap === undefined) return [];
|
|
672
|
+
const out: ReadMarkerEntry[] = [];
|
|
673
|
+
for (const [channel, msgid] of chanMap) {
|
|
674
|
+
out.push({ account, channel, msgid });
|
|
675
|
+
}
|
|
676
|
+
return out;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** rfc1459 case-fold used for both account and channel keys. */
|
|
681
|
+
function folded(value: string): string {
|
|
682
|
+
return caseFold('rfc1459', value);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ============================================================================
|
|
686
|
+
// Away-status persistence (IRCv3 draft/pre-away)
|
|
687
|
+
// ============================================================================
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Persistence port for IRCv3 `draft/pre-away`.
|
|
691
|
+
*
|
|
692
|
+
* The base `AWAY` reducer keeps the away reason in
|
|
693
|
+
* {@link ConnectionState.away} for the lifetime of a single connection; this
|
|
694
|
+
* port promotes that reason to a per-`account` record so it survives
|
|
695
|
+
* reconnects and is shared across an account's connections.
|
|
696
|
+
*
|
|
697
|
+
* Mirrors the {@link ReadMarkerStore} pattern: the port is
|
|
698
|
+
* **synchronous** (reducers must stay pure and free of IO) and injected
|
|
699
|
+
* via {@link Ctx}. Adapters that need async storage (D1, DynamoDB) pre-load
|
|
700
|
+
* the relevant slice into a synchronously-readable store; the in-memory
|
|
701
|
+
* reference implementation ({@link InMemoryAwayStore}) ships here.
|
|
702
|
+
*
|
|
703
|
+
* The account key is case-folded (rfc1459) so lookups are
|
|
704
|
+
* case-insensitive, matching the rest of the server's comparison
|
|
705
|
+
* semantics. The unified services `UserStore` should fold this store in
|
|
706
|
+
* when it lands.
|
|
707
|
+
*/
|
|
708
|
+
export interface AwayStore {
|
|
709
|
+
/**
|
|
710
|
+
* Returns the stored away reason for `account`, or `undefined` when no
|
|
711
|
+
* reason has been recorded.
|
|
712
|
+
*/
|
|
713
|
+
get(account: string): string | undefined;
|
|
714
|
+
/**
|
|
715
|
+
* Records (or replaces) the away reason for `account`. An empty string
|
|
716
|
+
* is permitted at the store layer (callers avoid it via
|
|
717
|
+
* {@link clearAway}).
|
|
718
|
+
*/
|
|
719
|
+
set(account: string, reason: string): void;
|
|
720
|
+
/**
|
|
721
|
+
* Removes the stored away reason for `account`. Used when the user
|
|
722
|
+
* unsets `AWAY` so the next reconnect does not auto-mark them away.
|
|
723
|
+
*/
|
|
724
|
+
clear(account: string): void;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Reference in-memory {@link AwayStore} backed by a
|
|
729
|
+
* `foldedAccount → reason` map.
|
|
730
|
+
*
|
|
731
|
+
* Used by the local CLI, integration tests, and any deployment that has
|
|
732
|
+
* not bound a distributed backend. Cloud adapters replace this with a D1
|
|
733
|
+
* / DynamoDB-backed implementation against the same surface.
|
|
734
|
+
*
|
|
735
|
+
* The account key is folded via {@link caseFold} (`rfc1459`) so lookups
|
|
736
|
+
* are case-insensitive, matching the IRC nick-comparison semantics
|
|
737
|
+
* advertised via `005 RPL_ISUPPORT CASEMAPPING=rfc1459`.
|
|
738
|
+
*/
|
|
739
|
+
export class InMemoryAwayStore implements AwayStore {
|
|
740
|
+
private readonly byAccount = new Map<string, string>();
|
|
741
|
+
|
|
742
|
+
get(account: string): string | undefined {
|
|
743
|
+
return this.byAccount.get(folded(account));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
set(account: string, reason: string): void {
|
|
747
|
+
this.byAccount.set(folded(account), reason);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
clear(account: string): void {
|
|
751
|
+
this.byAccount.delete(folded(account));
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
573
755
|
// ============================================================================
|
|
574
756
|
// Nick history (WHOWAS)
|
|
575
757
|
// ============================================================================
|
|
@@ -672,6 +854,213 @@ export class InMemoryNickHistoryStore implements NickHistoryStore {
|
|
|
672
854
|
}
|
|
673
855
|
}
|
|
674
856
|
|
|
857
|
+
// ============================================================================
|
|
858
|
+
// MONITOR subscription registry (IRCv3 MONITOR)
|
|
859
|
+
// ============================================================================
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* One async push notification the {@link MonitorStore} requests from the
|
|
863
|
+
* actor layer. The actor turns each push into one or more `Send` effects
|
|
864
|
+
* addressed to the watcher's connection.
|
|
865
|
+
*
|
|
866
|
+
* - `online` — the nick came online; the watcher should receive a
|
|
867
|
+
* `730 RPL_MONONLINE` line naming `display`.
|
|
868
|
+
* - `offline` — the nick went offline; the watcher should receive a
|
|
869
|
+
* `731 RPL_MONOFFLINE` line naming `display`.
|
|
870
|
+
*/
|
|
871
|
+
export interface MonitorPush {
|
|
872
|
+
/** Watcher's connection id. */
|
|
873
|
+
connId: string;
|
|
874
|
+
/** Display spelling of the nick that changed state. */
|
|
875
|
+
display: string;
|
|
876
|
+
/** New state. */
|
|
877
|
+
status: 'online' | 'offline';
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Subscription registry for the IRCv3 `MONITOR` command.
|
|
882
|
+
*
|
|
883
|
+
* The watchlist itself lives on {@link ConnectionState.monitorList} (so it
|
|
884
|
+
* persists across actor re-bindings); this port owns the **reverse index**
|
|
885
|
+
* (nick → set of watcher connIds) and the **online set** the actor consults
|
|
886
|
+
* when synchronously resolving `MONITOR +` / `MONITOR S`.
|
|
887
|
+
*
|
|
888
|
+
* Mirrors the {@link MessageStore} / {@link AccountStore} pattern: the port
|
|
889
|
+
* is **synchronous** (reducers must stay pure and free of IO). The actor
|
|
890
|
+
* layer drains pending pushes after each command via {@link drainPushes};
|
|
891
|
+
* adapters that need to fan notifications out across processes (CF
|
|
892
|
+
* ConnectionDO sharding, AWS WebSocket fanout) replace
|
|
893
|
+
* {@link InMemoryMonitorStore} with their own implementation against the
|
|
894
|
+
* same surface.
|
|
895
|
+
*
|
|
896
|
+
* Lifecycle:
|
|
897
|
+
* - On `MONITOR +`, the actor calls {@link sync} with the before/after
|
|
898
|
+
* watchlist snapshots; the store records each newly-added nick →
|
|
899
|
+
* watcher mapping.
|
|
900
|
+
* - On `MONITOR -` / `C`, {@link sync} drops the removed mappings.
|
|
901
|
+
* - On `NICK` (reserve/change) the actor calls {@link onNickOnline} for
|
|
902
|
+
* each newly-reserved nick; the store appends a {@link MonitorPush}
|
|
903
|
+
* for every watcher of that nick.
|
|
904
|
+
* - On `QUIT` (or any release) the actor calls {@link onNickOffline};
|
|
905
|
+
* same flow with a `731` push.
|
|
906
|
+
* - On connection teardown, the actor calls {@link unsubscribe} so the
|
|
907
|
+
* reverse index does not leak.
|
|
908
|
+
*/
|
|
909
|
+
export interface MonitorStore {
|
|
910
|
+
/**
|
|
911
|
+
* Reconciles the reverse index for `connId` against a watchlist delta.
|
|
912
|
+
* `before` is the watchlist at the start of the command (folded nicks);
|
|
913
|
+
* `after` is the watchlist after the reducer ran. The store adds
|
|
914
|
+
* mappings for entries in `after` not in `before`, and removes mappings
|
|
915
|
+
* for entries in `before` not in `after`.
|
|
916
|
+
*/
|
|
917
|
+
sync(connId: string, before: ReadonlySet<string>, after: ReadonlySet<string>): void;
|
|
918
|
+
/**
|
|
919
|
+
* Removes every subscription for `connId`. Safe to call when the
|
|
920
|
+
* connection had no subscriptions. Idempotent.
|
|
921
|
+
*/
|
|
922
|
+
unsubscribe(connId: string): void;
|
|
923
|
+
/**
|
|
924
|
+
* Records `nick` as online and enqueues a `730 RPL_MONONLINE` push to
|
|
925
|
+
* every watcher currently subscribed to that nick. `display` is the
|
|
926
|
+
* registered spelling the wire line should carry.
|
|
927
|
+
*
|
|
928
|
+
* Idempotent: a no-op when `nick` was already online (the second call
|
|
929
|
+
* enqueues nothing — prevents double-pushes on a `ReserveNick` for an
|
|
930
|
+
* already-reserved nick the runtime allowed through as a same-conn
|
|
931
|
+
* re-reserve).
|
|
932
|
+
*/
|
|
933
|
+
onNickOnline(display: string): void;
|
|
934
|
+
/**
|
|
935
|
+
* Records `nick` as offline and enqueues a `731 RPL_MONOFFLINE` push to
|
|
936
|
+
* every watcher currently subscribed to that nick. Idempotent in the
|
|
937
|
+
* same way {@link onNickOnline} is.
|
|
938
|
+
*/
|
|
939
|
+
onNickOffline(display: string): void;
|
|
940
|
+
/**
|
|
941
|
+
* Returns every display spelling currently marked online, in insertion
|
|
942
|
+
* order. Used by the actor's synchronous `MONITOR +` / `MONITOR S`
|
|
943
|
+
* resolution so the reducer can emit `730`/`731` lines without an
|
|
944
|
+
* O(N) `lookupNick` per requested nick.
|
|
945
|
+
*/
|
|
946
|
+
onlineNicks(): string[];
|
|
947
|
+
/**
|
|
948
|
+
* Drains the queue of pending {@link MonitorPush}es the store has
|
|
949
|
+
* enqueued since the last drain. The actor calls this once per
|
|
950
|
+
* dispatched batch (after `NICK`/`QUIT` reducers release/reserve
|
|
951
|
+
* nicks) and turns each push into a `Send` effect for the watcher.
|
|
952
|
+
*/
|
|
953
|
+
drainPushes(): MonitorPush[];
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Reference in-memory {@link MonitorStore} used by the local CLI, the
|
|
958
|
+
* integration tests, and any deployment that has not bound a distributed
|
|
959
|
+
* backend.
|
|
960
|
+
*
|
|
961
|
+
* Backed by three plain `Map`s:
|
|
962
|
+
* - `watchers` connId → set of folded watched nicks.
|
|
963
|
+
* - `reverse` folded nick → set of watcher connIds.
|
|
964
|
+
* - `online` folded nick → display spelling (insertion-ordered).
|
|
965
|
+
*
|
|
966
|
+
* Plus a `pending` array of {@link MonitorPush} entries drained by the
|
|
967
|
+
* actor after each dispatched batch.
|
|
968
|
+
*/
|
|
969
|
+
export class InMemoryMonitorStore implements MonitorStore {
|
|
970
|
+
private readonly watchers = new Map<string, Set<string>>();
|
|
971
|
+
private readonly reverse = new Map<string, Set<string>>();
|
|
972
|
+
private readonly online = new Map<string, string>();
|
|
973
|
+
private readonly pending: MonitorPush[] = [];
|
|
974
|
+
|
|
975
|
+
sync(connId: string, before: ReadonlySet<string>, after: ReadonlySet<string>): void {
|
|
976
|
+
// Compute the delta without mutating `before`/`after` (callers pass
|
|
977
|
+
// references to their own state).
|
|
978
|
+
const added: string[] = [];
|
|
979
|
+
const removed: string[] = [];
|
|
980
|
+
for (const n of after) if (!before.has(n)) added.push(n);
|
|
981
|
+
for (const n of before) if (!after.has(n)) removed.push(n);
|
|
982
|
+
|
|
983
|
+
if (added.length === 0 && removed.length === 0) return;
|
|
984
|
+
|
|
985
|
+
// Look up (or allocate) the watcher's per-connection set; cleared from
|
|
986
|
+
// `watchers` when it ends up empty so unsubscribed connections do not
|
|
987
|
+
// leak in the forward map.
|
|
988
|
+
let wSet = this.watchers.get(connId);
|
|
989
|
+
if (wSet === undefined) {
|
|
990
|
+
wSet = new Set<string>();
|
|
991
|
+
this.watchers.set(connId, wSet);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
for (const folded of added) {
|
|
995
|
+
wSet.add(folded);
|
|
996
|
+
let watchers = this.reverse.get(folded);
|
|
997
|
+
if (watchers === undefined) {
|
|
998
|
+
watchers = new Set<string>();
|
|
999
|
+
this.reverse.set(folded, watchers);
|
|
1000
|
+
}
|
|
1001
|
+
watchers.add(connId);
|
|
1002
|
+
}
|
|
1003
|
+
for (const folded of removed) {
|
|
1004
|
+
wSet.delete(folded);
|
|
1005
|
+
// The forward (`watchers`) and reverse indexes are kept in lockstep
|
|
1006
|
+
// by sync, so any folded nick present in `before` has a
|
|
1007
|
+
// corresponding reverse entry. The non-null assertion encodes that
|
|
1008
|
+
// invariant; the alternative `if (watchers === undefined) return`
|
|
1009
|
+
// would introduce an unreachable branch that violates the project's
|
|
1010
|
+
// 100% branch-coverage threshold.
|
|
1011
|
+
// biome-ignore lint/style/noNonNullAssertion: invariant — see above.
|
|
1012
|
+
const watchers = this.reverse.get(folded)!;
|
|
1013
|
+
watchers.delete(connId);
|
|
1014
|
+
if (watchers.size === 0) this.reverse.delete(folded);
|
|
1015
|
+
}
|
|
1016
|
+
if (wSet.size === 0) this.watchers.delete(connId);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
unsubscribe(connId: string): void {
|
|
1020
|
+
const wSet = this.watchers.get(connId);
|
|
1021
|
+
if (wSet === undefined) return;
|
|
1022
|
+
for (const folded of wSet) {
|
|
1023
|
+
// Same invariant as in `sync`: the forward/reverse indexes are kept
|
|
1024
|
+
// in lockstep, so every folded nick in `wSet` has a reverse entry.
|
|
1025
|
+
// biome-ignore lint/style/noNonNullAssertion: invariant — see above.
|
|
1026
|
+
const watchers = this.reverse.get(folded)!;
|
|
1027
|
+
watchers.delete(connId);
|
|
1028
|
+
if (watchers.size === 0) this.reverse.delete(folded);
|
|
1029
|
+
}
|
|
1030
|
+
this.watchers.delete(connId);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
onNickOnline(display: string): void {
|
|
1034
|
+
const folded = display.toLowerCase();
|
|
1035
|
+
if (this.online.has(folded)) return;
|
|
1036
|
+
this.online.set(folded, display);
|
|
1037
|
+
const watchers = this.reverse.get(folded);
|
|
1038
|
+
if (watchers === undefined) return;
|
|
1039
|
+
for (const connId of watchers) {
|
|
1040
|
+
this.pending.push({ connId, display, status: 'online' });
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
onNickOffline(display: string): void {
|
|
1045
|
+
const folded = display.toLowerCase();
|
|
1046
|
+
if (!this.online.has(folded)) return;
|
|
1047
|
+
this.online.delete(folded);
|
|
1048
|
+
const watchers = this.reverse.get(folded);
|
|
1049
|
+
if (watchers === undefined) return;
|
|
1050
|
+
for (const connId of watchers) {
|
|
1051
|
+
this.pending.push({ connId, display, status: 'offline' });
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
onlineNicks(): string[] {
|
|
1056
|
+
return Array.from(this.online.values());
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
drainPushes(): MonitorPush[] {
|
|
1060
|
+
return this.pending.splice(0);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
675
1064
|
// ============================================================================
|
|
676
1065
|
// Network statistics (LUSERS / STATS)
|
|
677
1066
|
// ============================================================================
|
|
@@ -9,6 +9,14 @@ export {
|
|
|
9
9
|
formatServerTime,
|
|
10
10
|
} from './outbound.js';
|
|
11
11
|
export { wrapBatch, type BatchFrame } from './batch.js';
|
|
12
|
+
export {
|
|
13
|
+
LABELED_RESPONSE_CAP_NAME,
|
|
14
|
+
LABEL_TAG,
|
|
15
|
+
applyLabelToEffects,
|
|
16
|
+
type LabelApplyOptions,
|
|
17
|
+
extractLabel,
|
|
18
|
+
wrapLabeledResponse,
|
|
19
|
+
} from './labeled-response.js';
|
|
12
20
|
export { decodeBase64, encodeBase64 } from './base64.js';
|
|
13
21
|
export {
|
|
14
22
|
formatNumeric,
|
|
@@ -17,3 +25,9 @@ export {
|
|
|
17
25
|
numericToName,
|
|
18
26
|
Numerics,
|
|
19
27
|
} from './numerics.js';
|
|
28
|
+
export {
|
|
29
|
+
type ReplyKind,
|
|
30
|
+
type StandardReplyMapping,
|
|
31
|
+
numericToStandardReply,
|
|
32
|
+
toStandardReply,
|
|
33
|
+
} from './standard-replies.js';
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IRCv3 `labeled-response` — correlate a request with its replies.
|
|
3
|
+
*
|
|
4
|
+
* Spec: https://ircv3.net/specs/extensions/labeled-response
|
|
5
|
+
*
|
|
6
|
+
* A client tags an inbound message with the `+label=<id>` client tag; the
|
|
7
|
+
* server then wraps every reply caused by that message (addressed back to
|
|
8
|
+
* the requester) in a single `BATCH +id labeled-response … BATCH -id`
|
|
9
|
+
* frame. If the request produces no reply for the requester, the server
|
|
10
|
+
* emits an empty batch so the client can still correlate the label with a
|
|
11
|
+
* definitive "no replies" answer.
|
|
12
|
+
*
|
|
13
|
+
* Unlike the generic {@link wrapBatch} helper — which elides empty batches
|
|
14
|
+
* because the IRCv3 `batch` spec forbids them — {@link wrapLabeledResponse}
|
|
15
|
+
* ALWAYS emits the start/end markers. The empty-batch case is the whole
|
|
16
|
+
* point of labeled-response: it is the correlation signal when there is
|
|
17
|
+
* nothing else to send.
|
|
18
|
+
*
|
|
19
|
+
* This module is pure data transformation. The actor layer:
|
|
20
|
+
* 1. extracts the label from the parsed message tags,
|
|
21
|
+
* 2. plumbs it onto {@link Ctx.label} so every reducer's emitted effects
|
|
22
|
+
* are uniformly attributable to the labeled request, and
|
|
23
|
+
* 3. runs {@link applyLabelToEffects} over the reducer's `Effect[]` to
|
|
24
|
+
* gather every `Send`-to-self and wrap them (or emit an empty batch).
|
|
25
|
+
*
|
|
26
|
+
* Only `Send` effects addressed to the originating connection are wrapped.
|
|
27
|
+
* Broadcasts / `SendToNick` / `BroadcastWallops` reach OTHER connections
|
|
28
|
+
* and are left in place; the label correlation applies solely to the
|
|
29
|
+
* requester's own reply stream.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { Effect } from '../effects.js';
|
|
33
|
+
import type { Effect as EffectType, RawLine } from '../effects.js';
|
|
34
|
+
import type { ConnId } from '../state/connection.js';
|
|
35
|
+
|
|
36
|
+
/** Wire name of the `labeled-response` capability. */
|
|
37
|
+
export const LABELED_RESPONSE_CAP_NAME = 'labeled-response';
|
|
38
|
+
|
|
39
|
+
/** Wire name of the `+label` client tag carried on the inbound message. */
|
|
40
|
+
export const LABEL_TAG = '+label';
|
|
41
|
+
|
|
42
|
+
/** Batch type token used for every labeled-response frame. */
|
|
43
|
+
const LABELED_RESPONSE_BATCH_TYPE = 'labeled-response';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Reads the `+label` client tag from a parsed message's tag map.
|
|
47
|
+
*
|
|
48
|
+
* Returns `undefined` when the tag is absent OR present-but-empty, so callers
|
|
49
|
+
* can treat a missing label and a bare `+label` (no value) identically: both
|
|
50
|
+
* mean "this request is unlabeled".
|
|
51
|
+
*/
|
|
52
|
+
export function extractLabel(tags: Readonly<Record<string, string>>): string | undefined {
|
|
53
|
+
const value = tags[LABEL_TAG];
|
|
54
|
+
if (value === undefined || value.length === 0) return undefined;
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Wraps `lines` in IRCv3 `BATCH +label labeled-response … BATCH -label`
|
|
60
|
+
* framing. ALWAYS emits the start and end markers — even when `lines` is
|
|
61
|
+
* empty — because the labeled-response spec requires an empty batch when a
|
|
62
|
+
* labeled request produces no reply (it is the correlation signal).
|
|
63
|
+
*/
|
|
64
|
+
export function wrapLabeledResponse(label: string, lines: readonly RawLine[]): RawLine[] {
|
|
65
|
+
const start: RawLine = { text: `BATCH +${label} ${LABELED_RESPONSE_BATCH_TYPE}` };
|
|
66
|
+
const end: RawLine = { text: `BATCH -${label}` };
|
|
67
|
+
return [start, ...lines, end];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Options for {@link applyLabelToEffects}.
|
|
72
|
+
*
|
|
73
|
+
* `label` is `string | undefined` (NOT an optional field) so the helper can
|
|
74
|
+
* be a clean no-op under `exactOptionalPropertyTypes` when the inbound
|
|
75
|
+
* request carried no label.
|
|
76
|
+
*/
|
|
77
|
+
export interface LabelApplyOptions {
|
|
78
|
+
/** The label extracted from the inbound `+label` tag, or `undefined`. */
|
|
79
|
+
readonly label: string | undefined;
|
|
80
|
+
/** The originating connection id (replies wrapped are addressed here). */
|
|
81
|
+
readonly connId: ConnId;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Gathers every `Send` effect addressed to `connId` and replaces them with a
|
|
86
|
+
* single `Send` whose lines are wrapped in one `labeled-response` batch.
|
|
87
|
+
*
|
|
88
|
+
* Behaviour:
|
|
89
|
+
* - When `label` is `undefined` the effect list is returned unchanged
|
|
90
|
+
* (copy) — the request was not labeled.
|
|
91
|
+
* - All `Send`-to-`connId` effects, wherever they appear, are accumulated
|
|
92
|
+
* in original order into one batch.
|
|
93
|
+
* - The wrapped batch is placed at the position of the FIRST collected
|
|
94
|
+
* `Send`-to-self, so the reducer's intended ordering relative to other
|
|
95
|
+
* effects (broadcasts, etc.) is preserved.
|
|
96
|
+
* - When NO `Send`-to-self exists, an empty labeled batch is appended at
|
|
97
|
+
* the end — the requester still observes the label correlation.
|
|
98
|
+
* - Every other effect (Broadcast, SendToNick, Disconnect, …) is left in
|
|
99
|
+
* place and in order; only the requester's own reply stream is wrapped.
|
|
100
|
+
*/
|
|
101
|
+
export function applyLabelToEffects(
|
|
102
|
+
effects: readonly EffectType[],
|
|
103
|
+
opts: LabelApplyOptions,
|
|
104
|
+
): EffectType[] {
|
|
105
|
+
const { label, connId } = opts;
|
|
106
|
+
if (label === undefined) return [...effects];
|
|
107
|
+
|
|
108
|
+
const selfLines: RawLine[] = [];
|
|
109
|
+
let firstSelfIdx = -1;
|
|
110
|
+
for (const [i, effect] of effects.entries()) {
|
|
111
|
+
if (effect.tag === 'Send' && effect.to === connId) {
|
|
112
|
+
if (firstSelfIdx === -1) firstSelfIdx = i;
|
|
113
|
+
selfLines.push(...effect.lines);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const batchEffect: EffectType = Effect.send(connId, wrapLabeledResponse(label, selfLines));
|
|
118
|
+
|
|
119
|
+
// No self-send to anchor on: append the empty labeled batch at the end.
|
|
120
|
+
if (firstSelfIdx === -1) {
|
|
121
|
+
return [...effects, batchEffect];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const out: EffectType[] = [];
|
|
125
|
+
for (const [i, effect] of effects.entries()) {
|
|
126
|
+
// Emit the single wrapped batch where the first self-send lived.
|
|
127
|
+
if (i === firstSelfIdx) out.push(batchEffect);
|
|
128
|
+
// Drop every original self-send (they are now inside the batch).
|
|
129
|
+
if (effect.tag === 'Send' && effect.to === connId) continue;
|
|
130
|
+
out.push(effect);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
@@ -53,6 +53,15 @@ export const Numerics = {
|
|
|
53
53
|
RPL_UNAWAY: 305,
|
|
54
54
|
RPL_NOWAWAY: 306,
|
|
55
55
|
|
|
56
|
+
// IRCv3 MONITOR (modern ISON replacement). The watchlist lives on
|
|
57
|
+
// {@link ConnectionState.monitorList}; transitions push 730/731
|
|
58
|
+
// asynchronously via the actor's nick-registry hooks.
|
|
59
|
+
RPL_MONONLINE: 730,
|
|
60
|
+
RPL_MONOFFLINE: 731,
|
|
61
|
+
RPL_MONLIST: 732,
|
|
62
|
+
RPL_ENDOFMONLIST: 733,
|
|
63
|
+
ERR_MONLISTFULL: 734,
|
|
64
|
+
|
|
56
65
|
// Version
|
|
57
66
|
RPL_VERSION: 351,
|
|
58
67
|
|
|
@@ -64,6 +73,12 @@ export const Numerics = {
|
|
|
64
73
|
RPL_ENDOFWHOIS: 318,
|
|
65
74
|
RPL_WHOISCHANNELS: 319,
|
|
66
75
|
RPL_WHOISACCOUNT: 330,
|
|
76
|
+
/**
|
|
77
|
+
* `276 RPL_WHOISSECURE` — target is connected over TLS (user mode `S`).
|
|
78
|
+
* Surfaced between the oper/account lines and the idle line so clients
|
|
79
|
+
* render it as part of the secure-connection WHOIS block.
|
|
80
|
+
*/
|
|
81
|
+
RPL_WHOISSECURE: 276,
|
|
67
82
|
RPL_WHOWASUSER: 314,
|
|
68
83
|
|
|
69
84
|
// LIST
|
|
@@ -126,6 +126,14 @@ export function enforceLineLimit(
|
|
|
126
126
|
* Server-defined tags (no `+` prefix) are left in place — each is gated by
|
|
127
127
|
* its own capability (server-time, account-tag, …) applied elsewhere.
|
|
128
128
|
*
|
|
129
|
+
* Spec exception: IRCv3 `draft/typing` carves out a whitelist for the
|
|
130
|
+
* `+draft/typing` client tag. A recipient that announced `draft/typing`
|
|
131
|
+
* (but NOT `message-tags`) still receives `+draft/typing` so typing
|
|
132
|
+
* indicators work without the broader message-tags cap. `draft/read-marker`
|
|
133
|
+
* carves out the same exception for its `+draft/read-marker` tag so a
|
|
134
|
+
* read-marker update reaches the user's other connections. Every other
|
|
135
|
+
* client tag remains stripped for such recipients.
|
|
136
|
+
*
|
|
129
137
|
* When filtering removes every tag, the leading `@... ` section is dropped
|
|
130
138
|
* entirely so legacy clients never observe a bare tag prefix.
|
|
131
139
|
*
|
|
@@ -142,10 +150,17 @@ export function filterClientTags(line: RawLine, caps: ReadonlySet<string>): RawL
|
|
|
142
150
|
const rest = line.text.slice(spaceIdx + 1);
|
|
143
151
|
const entries = tagSection.split(';');
|
|
144
152
|
|
|
153
|
+
const typingWhitelisted = caps.has('draft/typing');
|
|
154
|
+
const readMarkerWhitelisted = caps.has('draft/read-marker');
|
|
145
155
|
const hasClientTag = entries.some((entry) => clientTagKey(entry).startsWith('+'));
|
|
146
156
|
if (!hasClientTag) return line;
|
|
147
157
|
|
|
148
|
-
const kept = entries.filter((entry) =>
|
|
158
|
+
const kept = entries.filter((entry) => {
|
|
159
|
+
const key = clientTagKey(entry);
|
|
160
|
+
if (!key.startsWith('+')) return true;
|
|
161
|
+
if (typingWhitelisted && key === '+draft/typing') return true;
|
|
162
|
+
return readMarkerWhitelisted && key === '+draft/read-marker';
|
|
163
|
+
});
|
|
149
164
|
if (kept.length === 0) return { text: rest };
|
|
150
165
|
return { text: `@${kept.join(';')} ${rest}` };
|
|
151
166
|
}
|