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
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IRCv3 `standard-replies` — `FAIL` / `WARN` / `NOTE` dispatch helper.
|
|
3
|
+
*
|
|
4
|
+
* Spec: https://ircv3.net/specs/extensions/standard-replies-3.0
|
|
5
|
+
*
|
|
6
|
+
* Modern IRC clients that negotiated the `standard-replies` cap accept the
|
|
7
|
+
* structured reply form
|
|
8
|
+
*
|
|
9
|
+
* :<server> FAIL <command> <code> [<context>...] :<description>
|
|
10
|
+
* :<server> WARN <command> <code> [<context>...] :<description>
|
|
11
|
+
* :<server> NOTE <command> <code> [<context>...] :<description>
|
|
12
|
+
*
|
|
13
|
+
* in place of the legacy `:server NNN nick ... :trailing` numerics for a
|
|
14
|
+
* curated subset of errors. Numeric-only replies (MOTD lines, NAMES,
|
|
15
|
+
* welcome block, …) stay on the numeric form — only the curated error
|
|
16
|
+
* numerics are translated.
|
|
17
|
+
*
|
|
18
|
+
* This module is the data-driven mapping plus a single pure dispatch
|
|
19
|
+
* helper ({@link toStandardReply}) that the dispatch / outbound layer
|
|
20
|
+
* applies per recipient. Translating at the outbound layer (rather than
|
|
21
|
+
* scattering `if (caps.has('standard-replies'))` through every reducer)
|
|
22
|
+
* keeps the reducer code cap-agnostic, matching the model already used
|
|
23
|
+
* for `server-time` and `message-tags`.
|
|
24
|
+
*
|
|
25
|
+
* Reducers keep emitting the canonical numeric line; the dispatch layer
|
|
26
|
+
* runs this helper to translate the curated numerics for cap-enabled
|
|
27
|
+
* recipients and passes everything else through verbatim.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import type { RawLine } from '../effects.js';
|
|
31
|
+
import { Numerics } from './numerics.js';
|
|
32
|
+
|
|
33
|
+
/** One of the IRCv3 standard-reply verbs. */
|
|
34
|
+
export type ReplyKind = 'FAIL' | 'WARN' | 'NOTE';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* One row of the {@link numericToStandardReply} table, as a discriminated
|
|
38
|
+
* union over where the COMMAND token comes from on the wire.
|
|
39
|
+
*
|
|
40
|
+
* - When {@link CommandFromMiddle.commandFromMiddle} is `true`, the
|
|
41
|
+
* numeric's middle parameter IS the failed command name (true for
|
|
42
|
+
* `461 ERR_NEEDMOREPARAMS`, whose middle is the command the client
|
|
43
|
+
* undersupplied).
|
|
44
|
+
* - When {@link CommandFixed.commandFromMiddle} is `false`, a fixed
|
|
45
|
+
* {@link CommandFixed.command} is emitted (true for `432`/`433`,
|
|
46
|
+
* always `NICK`, and for SASL failures, always `AUTHENTICATE`).
|
|
47
|
+
*
|
|
48
|
+
* The discriminated union lets `pickCommand` return `mapping.command`
|
|
49
|
+
* directly in the fixed branch with no defensive `?? null` — the type
|
|
50
|
+
* system proves `command` is always set there.
|
|
51
|
+
*/
|
|
52
|
+
export type StandardReplyMapping = CommandFromMiddle | CommandFixed;
|
|
53
|
+
|
|
54
|
+
/** A row whose COMMAND token comes from the numeric's middle parameter. */
|
|
55
|
+
export interface CommandFromMiddle {
|
|
56
|
+
/** Discriminant: true → read COMMAND from `parsed.middle[0]`. */
|
|
57
|
+
commandFromMiddle: true;
|
|
58
|
+
/** Which of `FAIL`/`WARN`/`NOTE` to emit. */
|
|
59
|
+
kind: ReplyKind;
|
|
60
|
+
/** Stable wire code (`INVALID_PARAMS`, …). */
|
|
61
|
+
code: string;
|
|
62
|
+
/** Always false here — the middle is consumed as the command, not context. */
|
|
63
|
+
contextFromMiddle: false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A row whose COMMAND token is a fixed string. */
|
|
67
|
+
export interface CommandFixed {
|
|
68
|
+
/** Discriminant: false → read COMMAND from {@link command}. */
|
|
69
|
+
commandFromMiddle: false;
|
|
70
|
+
/** Which of `FAIL`/`WARN`/`NOTE` to emit. */
|
|
71
|
+
kind: ReplyKind;
|
|
72
|
+
/** Stable wire code (`NO_SUCH_CHANNEL`, `INVALID_NICK`, …). */
|
|
73
|
+
code: string;
|
|
74
|
+
/** Fixed command token emitted after the FAIL/WARN/NOTE verb. */
|
|
75
|
+
command: string;
|
|
76
|
+
/**
|
|
77
|
+
* True iff the numeric's middle parameter carries a context token
|
|
78
|
+
* (the bad nick, the bad channel) that should follow the code.
|
|
79
|
+
*/
|
|
80
|
+
contextFromMiddle: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The curated numeric → standard-reply mapping.
|
|
85
|
+
*
|
|
86
|
+
* Only a deliberately small subset of error numerics is translated —
|
|
87
|
+
* the ones that carry enough context for the structured form to be a
|
|
88
|
+
* strict improvement over the bare numeric. Numeric-only families
|
|
89
|
+
* (MOTD, NAMES, the welcome block, mode replies, …) stay on the
|
|
90
|
+
* numeric form per the spec.
|
|
91
|
+
*/
|
|
92
|
+
export const numericToStandardReply: ReadonlyMap<number, StandardReplyMapping> = new Map<
|
|
93
|
+
number,
|
|
94
|
+
StandardReplyMapping
|
|
95
|
+
>([
|
|
96
|
+
[
|
|
97
|
+
Numerics.ERR_NEEDMOREPARAMS,
|
|
98
|
+
{
|
|
99
|
+
kind: 'FAIL',
|
|
100
|
+
code: 'INVALID_PARAMS',
|
|
101
|
+
commandFromMiddle: true,
|
|
102
|
+
contextFromMiddle: false,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
[
|
|
106
|
+
Numerics.ERR_NOSUCHCHANNEL,
|
|
107
|
+
{
|
|
108
|
+
kind: 'FAIL',
|
|
109
|
+
code: 'NO_SUCH_CHANNEL',
|
|
110
|
+
commandFromMiddle: false,
|
|
111
|
+
command: 'JOIN',
|
|
112
|
+
contextFromMiddle: true,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
[
|
|
116
|
+
Numerics.ERR_ERRONEUSNICKNAME,
|
|
117
|
+
{
|
|
118
|
+
kind: 'FAIL',
|
|
119
|
+
code: 'INVALID_NICK',
|
|
120
|
+
commandFromMiddle: false,
|
|
121
|
+
command: 'NICK',
|
|
122
|
+
contextFromMiddle: true,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
[
|
|
126
|
+
Numerics.ERR_NICKNAMEINUSE,
|
|
127
|
+
{
|
|
128
|
+
kind: 'FAIL',
|
|
129
|
+
code: 'NICKNAME_IN_USE',
|
|
130
|
+
commandFromMiddle: false,
|
|
131
|
+
command: 'NICK',
|
|
132
|
+
contextFromMiddle: true,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
[
|
|
136
|
+
Numerics.ERR_SASLFAIL,
|
|
137
|
+
{
|
|
138
|
+
kind: 'FAIL',
|
|
139
|
+
code: 'SASL_FAILED',
|
|
140
|
+
commandFromMiddle: false,
|
|
141
|
+
command: 'AUTHENTICATE',
|
|
142
|
+
contextFromMiddle: false,
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Translates a single outbound {@link RawLine} for a recipient whose
|
|
149
|
+
* negotiated caps are `caps`.
|
|
150
|
+
*
|
|
151
|
+
* - If the recipient lacks the `standard-replies` cap, the line is
|
|
152
|
+
* returned unchanged (the legacy numeric still ships).
|
|
153
|
+
* - If the line is not a `:server NNN …` numeric, it is returned
|
|
154
|
+
* unchanged (PRIVMSG, BATCH, FAIL/WARN/NOTE already emitted by
|
|
155
|
+
* reducers, etc.).
|
|
156
|
+
* - If the numeric is not in {@link numericToStandardReply}, the line
|
|
157
|
+
* is returned unchanged.
|
|
158
|
+
* - Otherwise the line is rewritten into the structured
|
|
159
|
+
* `FAIL`/`WARN`/`NOTE` form using the table.
|
|
160
|
+
*
|
|
161
|
+
* The input is never mutated; the original object is returned whenever
|
|
162
|
+
* no translation occurred so callers can cheaply detect "no change".
|
|
163
|
+
*/
|
|
164
|
+
export function toStandardReply(line: RawLine, caps: ReadonlySet<string>): RawLine {
|
|
165
|
+
if (!caps.has('standard-replies')) return line;
|
|
166
|
+
if (!line.text.startsWith(':')) return line;
|
|
167
|
+
|
|
168
|
+
const parsed = parseNumericLine(line.text);
|
|
169
|
+
if (parsed === null) return line;
|
|
170
|
+
|
|
171
|
+
const mapping = numericToStandardReply.get(parsed.code);
|
|
172
|
+
if (mapping === undefined) return line;
|
|
173
|
+
|
|
174
|
+
const command = pickCommand(parsed, mapping);
|
|
175
|
+
if (command === null) return line;
|
|
176
|
+
|
|
177
|
+
// The structured form requires a `:description` token. A curated numeric
|
|
178
|
+
// missing its trailing parameter (malformed input that never ships from a
|
|
179
|
+
// reducer) is left on the bare numeric so we never emit an empty FAIL.
|
|
180
|
+
if (parsed.trailing === undefined) return line;
|
|
181
|
+
|
|
182
|
+
const context = pickContext(parsed, mapping);
|
|
183
|
+
const trailing = parsed.trailing;
|
|
184
|
+
|
|
185
|
+
const head = `:${parsed.server} ${mapping.kind} ${command} ${mapping.code}`;
|
|
186
|
+
const withContext = context !== null ? `${head} ${context}` : head;
|
|
187
|
+
return { text: `${withContext} :${trailing}` };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Internal: the parsed shape of a `:server NNN nick [middle] [:trailing]` line. */
|
|
191
|
+
interface ParsedNumeric {
|
|
192
|
+
server: string;
|
|
193
|
+
code: number;
|
|
194
|
+
/** Every token between the nick and the trailing `:`-parameter. May be empty. */
|
|
195
|
+
middle: string[];
|
|
196
|
+
/** The trailing `:`-prefixed parameter, with the leading `:` stripped. */
|
|
197
|
+
trailing: string | undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Parses a single `:server NNN ...` line into its components.
|
|
202
|
+
*
|
|
203
|
+
* Returns `null` for any shape this server does not emit for numerics:
|
|
204
|
+
* lines without a server prefix, lines whose second token is not a
|
|
205
|
+
* three-digit numeric, and lines with no nick token. The trailing
|
|
206
|
+
* `:`-parameter is optional (a handful of numerics — currently none in
|
|
207
|
+
* the curated set — ship without one).
|
|
208
|
+
*/
|
|
209
|
+
function parseNumericLine(text: string): ParsedNumeric | null {
|
|
210
|
+
// Strip the leading `:server ` prefix.
|
|
211
|
+
const firstSpace = text.indexOf(' ');
|
|
212
|
+
if (firstSpace === -1) return null;
|
|
213
|
+
const server = text.slice(1, firstSpace);
|
|
214
|
+
|
|
215
|
+
const rest = text.slice(firstSpace + 1);
|
|
216
|
+
const secondSpace = rest.indexOf(' ');
|
|
217
|
+
if (secondSpace === -1) return null;
|
|
218
|
+
const codeTok = rest.slice(0, secondSpace);
|
|
219
|
+
if (!/^\d{3}$/.test(codeTok)) return null;
|
|
220
|
+
const code = Number.parseInt(codeTok, 10);
|
|
221
|
+
|
|
222
|
+
const afterCode = rest.slice(secondSpace + 1);
|
|
223
|
+
|
|
224
|
+
// The nick token is mandatory between the code and the optional trailing.
|
|
225
|
+
// Empty after-code or an `:`-prefixed after-code (the trailing param
|
|
226
|
+
// immediately following the code) is malformed for a numeric this server
|
|
227
|
+
// emits; bail so the caller passes the line through verbatim.
|
|
228
|
+
if (afterCode.length === 0) return null;
|
|
229
|
+
if (afterCode[0] === ':') return null;
|
|
230
|
+
|
|
231
|
+
// Split off the trailing `:`-parameter if present.
|
|
232
|
+
const trailingIdx = findTrailingColon(afterCode);
|
|
233
|
+
let head = afterCode;
|
|
234
|
+
let trailing: string | undefined;
|
|
235
|
+
if (trailingIdx !== -1) {
|
|
236
|
+
head = afterCode.slice(0, trailingIdx).trimEnd();
|
|
237
|
+
trailing = afterCode.slice(trailingIdx + 1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// head starts with the nick and optional middle params.
|
|
241
|
+
const tokens = head.length === 0 ? [] : head.split(' ');
|
|
242
|
+
if (tokens.length === 0) return null;
|
|
243
|
+
// Drop the nick — it is not used in the structured form (the reply is
|
|
244
|
+
// routed to a specific recipient, not addressed by nick).
|
|
245
|
+
const middle = tokens.slice(1);
|
|
246
|
+
|
|
247
|
+
return { server, code, middle, trailing };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Returns the index of the ` :` that introduces the trailing parameter,
|
|
252
|
+
* or `-1` when the line has no trailing. The leading space is required
|
|
253
|
+
* so a `:` inside a middle token (e.g. a malformed channel name) is not
|
|
254
|
+
* misread.
|
|
255
|
+
*/
|
|
256
|
+
function findTrailingColon(s: string): number {
|
|
257
|
+
for (let i = 0; i < s.length; i++) {
|
|
258
|
+
if (s[i] === ' ' && s[i + 1] === ':') return i + 1;
|
|
259
|
+
}
|
|
260
|
+
return -1;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Resolves the COMMAND token for the structured reply per the mapping. */
|
|
264
|
+
function pickCommand(parsed: ParsedNumeric, mapping: StandardReplyMapping): string | null {
|
|
265
|
+
if (mapping.commandFromMiddle) {
|
|
266
|
+
const cmd = parsed.middle[0];
|
|
267
|
+
return cmd === undefined ? null : cmd;
|
|
268
|
+
}
|
|
269
|
+
return mapping.command;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Resolves the optional CONTEXT token for the structured reply per the mapping. */
|
|
273
|
+
function pickContext(parsed: ParsedNumeric, mapping: StandardReplyMapping): string | null {
|
|
274
|
+
if (!mapping.contextFromMiddle) return null;
|
|
275
|
+
const ctx = parsed.middle[0];
|
|
276
|
+
return ctx ?? null;
|
|
277
|
+
}
|
|
@@ -20,13 +20,21 @@ export type RegistrationState = 'pre-registration' | 'registering' | 'registered
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* User modes applied to a connection. The IRC user-mode letters are
|
|
23
|
-
* `i` (invisible), `o` (oper), `w` (wallops), `s` (server notices)
|
|
23
|
+
* `i` (invisible), `o` (oper), `w` (wallops), `s` (server notices),
|
|
24
|
+
* `S` (TLS connected — read-only, set by the transport at registration).
|
|
24
25
|
*/
|
|
25
26
|
export interface UserModes {
|
|
26
27
|
invisible: boolean;
|
|
27
28
|
oper: boolean;
|
|
28
29
|
wallops: boolean;
|
|
29
30
|
serverNotices: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* User mode `S` — the connection arrived over TLS (wss or irc+tls).
|
|
33
|
+
* Read-only: set once by the registration reducer from
|
|
34
|
+
* {@link ConnectionState.secure} and never mutated afterwards. Not
|
|
35
|
+
* settable via `MODE` (the parser rejects `+S`/`-S` with `501`).
|
|
36
|
+
*/
|
|
37
|
+
tls: boolean;
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
/**
|
|
@@ -84,6 +92,15 @@ export interface ConnectionState {
|
|
|
84
92
|
* `CAP END` arrives (per IRCv3 capability-negotiation spec).
|
|
85
93
|
*/
|
|
86
94
|
capNegotiating: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Transport-level fact: true when the connection arrived over TLS
|
|
97
|
+
* (`wss://` WebSocket or `irc+tls://` raw TCP). Set once by the adapter
|
|
98
|
+
* at construction from the platform's transport metadata and read by the
|
|
99
|
+
* registration reducer to surface user mode `S` ({@link UserModes.tls}).
|
|
100
|
+
* Defaults to `false` so existing callers (plain ws test harnesses) keep
|
|
101
|
+
* the pre-TLS-mode behaviour.
|
|
102
|
+
*/
|
|
103
|
+
secure: boolean;
|
|
87
104
|
/** IRCv3 capabilities negotiated via `CAP`. */
|
|
88
105
|
caps: Set<string>;
|
|
89
106
|
/** Away reason; absent when not away. */
|
|
@@ -109,6 +126,18 @@ export interface ConnectionState {
|
|
|
109
126
|
* PART/re-JOIN cycle with no new activity replays nothing.
|
|
110
127
|
*/
|
|
111
128
|
lastReadMarkers?: Map<string, string>;
|
|
129
|
+
/**
|
|
130
|
+
* Per-connection MONITOR watchlist (IRCv3 `MONITOR`). Each entry is the
|
|
131
|
+
* case-folded (rfc1459) spelling of a watched nick; the display spelling
|
|
132
|
+
* is resolved at emission time via the nick registry. Lazily allocated
|
|
133
|
+
* on first `MONITOR +`; persisted verbatim so a reconnect can restore
|
|
134
|
+
* the watchlist without re-issuing every `MONITOR` line.
|
|
135
|
+
*
|
|
136
|
+
* Capped at the deployment's `MONITOR=<n>` ISUPPORT value (default 30);
|
|
137
|
+
* the {@link monitorReducer} enforces the cap and emits `734
|
|
138
|
+
* ERR_MONLISTFULL` for rejected nicks.
|
|
139
|
+
*/
|
|
140
|
+
monitorList?: Set<string>;
|
|
112
141
|
}
|
|
113
142
|
|
|
114
143
|
/**
|
|
@@ -147,6 +176,7 @@ export function createConnection(opts: CreateConnectionOptions): ConnectionState
|
|
|
147
176
|
id: opts.id,
|
|
148
177
|
registration: 'pre-registration',
|
|
149
178
|
capNegotiating: false,
|
|
179
|
+
secure: false,
|
|
150
180
|
caps: new Set<string>(),
|
|
151
181
|
joinedChannels: new Set<ChanName>(),
|
|
152
182
|
userModes: {
|
|
@@ -154,6 +184,7 @@ export function createConnection(opts: CreateConnectionOptions): ConnectionState
|
|
|
154
184
|
oper: false,
|
|
155
185
|
wallops: false,
|
|
156
186
|
serverNotices: false,
|
|
187
|
+
tls: false,
|
|
157
188
|
},
|
|
158
189
|
lastSeen: opts.lastSeen ?? opts.connectedSince,
|
|
159
190
|
connectedSince: opts.connectedSince,
|
|
@@ -10,12 +10,14 @@ import type { Effect } from './effects.js';
|
|
|
10
10
|
import type { FloodControlConfig } from './flood-control.js';
|
|
11
11
|
import type {
|
|
12
12
|
AccountStore,
|
|
13
|
+
AwayStore,
|
|
13
14
|
Clock,
|
|
14
15
|
IdFactory,
|
|
15
16
|
MessageStore,
|
|
16
17
|
MotdProvider,
|
|
17
18
|
MtlsIdentityProvider,
|
|
18
19
|
NickHistoryStore,
|
|
20
|
+
ReadMarkerStore,
|
|
19
21
|
} from './ports.js';
|
|
20
22
|
import type { IrcMessage } from './protocol/messages.js';
|
|
21
23
|
import type { ConnectionState } from './state/connection.js';
|
|
@@ -33,6 +35,23 @@ export interface CloakingConfig {
|
|
|
33
35
|
readonly cloakedSuffix?: string | undefined;
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
/**
|
|
39
|
+
* IRCv3 `sts` (Strict Transport Security) policy as seen by reducers.
|
|
40
|
+
* Mirrors the {@link StsConfigSchema} shape; the cap builder reads it to
|
|
41
|
+
* construct the `sts=…` advertisement per connection.
|
|
42
|
+
*
|
|
43
|
+
* Optional fields are widened to `T | undefined` to mirror Zod's
|
|
44
|
+
* `.optional()` output under `exactOptionalPropertyTypes: true`.
|
|
45
|
+
*/
|
|
46
|
+
export interface StsConfig {
|
|
47
|
+
/** Pin duration in seconds. */
|
|
48
|
+
readonly duration: number;
|
|
49
|
+
/** TLS listener port (plaintext advertisements only). */
|
|
50
|
+
readonly port: number;
|
|
51
|
+
/** Optional preload directive (plaintext advertisements only). */
|
|
52
|
+
readonly preload?: boolean | undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
36
55
|
/**
|
|
37
56
|
* A single IRC operator credential. The {@link operReducer} consults the
|
|
38
57
|
* configured list to authenticate `OPER <name> <password>`. Both fields are
|
|
@@ -127,6 +146,55 @@ export interface ServerConfig {
|
|
|
127
146
|
* `exactOptionalPropertyTypes: true`.
|
|
128
147
|
*/
|
|
129
148
|
chatHistoryPlaybackLimit?: number | undefined;
|
|
149
|
+
/**
|
|
150
|
+
* IRCv3 `sts` (Strict Transport Security) policy. `undefined` disables
|
|
151
|
+
* STS advertisement; an object supplies the policy the cap builder
|
|
152
|
+
* serializes per connection (plaintext vs TLS).
|
|
153
|
+
*
|
|
154
|
+
* Typed `StsConfig | undefined` to mirror Zod's optional output shape
|
|
155
|
+
* under `exactOptionalPropertyTypes: true`.
|
|
156
|
+
*/
|
|
157
|
+
readonly sts?: StsConfig | undefined;
|
|
158
|
+
/**
|
|
159
|
+
* IRCv3 `draft/multiline` byte budget. Advertised as
|
|
160
|
+
* `draft/multiline=<n>` in `CAP LS` and enforced on every finalized
|
|
161
|
+
* multi-line batch. Defaults to {@link DEFAULT_MULTILINE_MAX_BYTES}
|
|
162
|
+
* (4096) when omitted; the parsed config always supplies a value.
|
|
163
|
+
*
|
|
164
|
+
* Typed `number | undefined` to mirror Zod's optional output shape
|
|
165
|
+
* under `exactOptionalPropertyTypes: true` so inline `ServerConfig`
|
|
166
|
+
* literals in tests may omit it.
|
|
167
|
+
*/
|
|
168
|
+
readonly multilineMaxBytes?: number | undefined;
|
|
169
|
+
/**
|
|
170
|
+
* Per-connection MONITOR watchlist ceiling, advertised as `MONITOR=<n>`
|
|
171
|
+
* in `005 RPL_ISUPPORT`. Omitted → the reducer falls back to
|
|
172
|
+
* {@link DEFAULT_MONITOR_LIMIT} (the monitor cap's built-in ceiling).
|
|
173
|
+
*
|
|
174
|
+
* Typed `number | undefined` to mirror Zod's optional output shape under
|
|
175
|
+
* `exactOptionalPropertyTypes: true`.
|
|
176
|
+
*/
|
|
177
|
+
readonly monitorLimit?: number | undefined;
|
|
178
|
+
/**
|
|
179
|
+
* Extended-ban prefix sigil advertised as `EXTBAN=<prefix>,…` in
|
|
180
|
+
* `005 RPL_ISUPPORT`. Omitted → defaults to `~` (the IRC-standard
|
|
181
|
+
* extban prefix).
|
|
182
|
+
*
|
|
183
|
+
* Typed `string | undefined` to mirror Zod's optional output shape under
|
|
184
|
+
* `exactOptionalPropertyTypes: true`.
|
|
185
|
+
*/
|
|
186
|
+
readonly extbanPrefix?: string | undefined;
|
|
187
|
+
/**
|
|
188
|
+
* Whether IRC services (NickServ/ChanServ) are wired up for this
|
|
189
|
+
* deployment. When true, `005 RPL_ISUPPORT` advertises `ACCOUNTEXTBAN=a`
|
|
190
|
+
* (account-name extended bans), which are only meaningful once nicks are
|
|
191
|
+
* registered. Omitted/false → the token is suppressed until services
|
|
192
|
+
* ship.
|
|
193
|
+
*
|
|
194
|
+
* Typed `boolean | undefined` to mirror Zod's optional output shape under
|
|
195
|
+
* `exactOptionalPropertyTypes: true`.
|
|
196
|
+
*/
|
|
197
|
+
readonly servicesEnabled?: boolean | undefined;
|
|
130
198
|
}
|
|
131
199
|
|
|
132
200
|
/** Per-invocation context handed to every reducer. */
|
|
@@ -162,10 +230,43 @@ export interface Ctx {
|
|
|
162
230
|
* entries and the WHOWAS reducer replays them.
|
|
163
231
|
*/
|
|
164
232
|
readonly history?: NickHistoryStore;
|
|
233
|
+
/**
|
|
234
|
+
* Read-marker persistence source for IRCv3 `draft/read-marker` (absent
|
|
235
|
+
* when the deployment does not persist read markers). When present, a
|
|
236
|
+
* `+draft/read-marker=<msgid>` TAGMSG from an identified, cap-enabled
|
|
237
|
+
* client updates the per-`(account, channel)` marker, and SASL identify
|
|
238
|
+
* seeds the connection's `lastReadMarkers` from the stored values so a
|
|
239
|
+
* reconnect restores the user's read position.
|
|
240
|
+
*/
|
|
241
|
+
readonly readMarkers?: ReadMarkerStore;
|
|
242
|
+
/**
|
|
243
|
+
* Away-status persistence source for IRCv3 `draft/pre-away` (absent
|
|
244
|
+
* when the deployment does not persist away reasons). When present, a
|
|
245
|
+
* set/clear of `AWAY` from an identified connection mirrors the change
|
|
246
|
+
* into the per-`account` record, and SASL identify replays the stored
|
|
247
|
+
* reason onto the connection (emitting `306 RPL_NOWAWAY`) so a
|
|
248
|
+
* reconnect restores the user's away state. Omitted (→ `ctx.away`
|
|
249
|
+
* undefined) preserves the session-only behaviour: the connection
|
|
250
|
+
* still tracks its own `AWAY` but the reason is not shared across
|
|
251
|
+
* connections or restored on reconnect.
|
|
252
|
+
*/
|
|
253
|
+
readonly away?: AwayStore;
|
|
165
254
|
/** The connection invoking the command. Reducers mutate this freely. */
|
|
166
255
|
readonly connection: ConnectionState;
|
|
167
256
|
/** Convenience: `connection.id`. */
|
|
168
257
|
readonly connId: string;
|
|
258
|
+
/**
|
|
259
|
+
* IRCv3 `labeled-response` label carried by the inbound message (the value
|
|
260
|
+
* of the `+label` client tag), or `undefined` when the request was
|
|
261
|
+
* unlabeled. Plumbed through ctx so every reducer's emitted effects are
|
|
262
|
+
* uniformly attributable to the labeled request; the actor layer wraps the
|
|
263
|
+
* requester's replies in one `BATCH +label labeled-response … -label` frame
|
|
264
|
+
* (or emits an empty batch when there is no reply).
|
|
265
|
+
*
|
|
266
|
+
* Typed `string | undefined` to mirror the optional-output shape under
|
|
267
|
+
* `exactOptionalPropertyTypes: true`.
|
|
268
|
+
*/
|
|
269
|
+
readonly label?: string | undefined;
|
|
169
270
|
}
|
|
170
271
|
|
|
171
272
|
/** Result of a single reducer invocation. */
|
|
@@ -190,7 +291,15 @@ export interface BuildCtxOptions {
|
|
|
190
291
|
mtlsIdentity?: MtlsIdentityProvider;
|
|
191
292
|
messages?: MessageStore;
|
|
192
293
|
history?: NickHistoryStore;
|
|
294
|
+
readMarkers?: ReadMarkerStore;
|
|
295
|
+
away?: AwayStore;
|
|
193
296
|
connection: ConnectionState;
|
|
297
|
+
/**
|
|
298
|
+
* IRCv3 `labeled-response` label extracted from the inbound `+label` tag.
|
|
299
|
+
* Omitted (→ `ctx.label` undefined) when the request was unlabeled. See
|
|
300
|
+
* {@link Ctx.label}.
|
|
301
|
+
*/
|
|
302
|
+
readonly label?: string | undefined;
|
|
194
303
|
}
|
|
195
304
|
|
|
196
305
|
/** Constructs a {@link Ctx} from the required parts. */
|
|
@@ -208,6 +317,9 @@ export function buildCtx(opts: BuildCtxOptions): Ctx {
|
|
|
208
317
|
...(opts.mtlsIdentity !== undefined ? { mtlsIdentity: opts.mtlsIdentity } : {}),
|
|
209
318
|
...(opts.messages !== undefined ? { messages: opts.messages } : {}),
|
|
210
319
|
...(opts.history !== undefined ? { history: opts.history } : {}),
|
|
320
|
+
...(opts.readMarkers !== undefined ? { readMarkers: opts.readMarkers } : {}),
|
|
321
|
+
...(opts.away !== undefined ? { away: opts.away } : {}),
|
|
322
|
+
...(opts.label !== undefined ? { label: opts.label } : {}),
|
|
211
323
|
};
|
|
212
324
|
return ctx;
|
|
213
325
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { type AwayStore, InMemoryAwayStore } from '../src/ports';
|
|
3
|
+
|
|
4
|
+
describe('InMemoryAwayStore — port conformance', () => {
|
|
5
|
+
it('implements the AwayStore port', () => {
|
|
6
|
+
const store: AwayStore = new InMemoryAwayStore();
|
|
7
|
+
expect(store).toBeDefined();
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('InMemoryAwayStore — get / set / clear', () => {
|
|
12
|
+
it('returns undefined for an unknown account', () => {
|
|
13
|
+
const store = new InMemoryAwayStore();
|
|
14
|
+
expect(store.get('alice')).toBeUndefined();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('stores and returns the away reason for a known account', () => {
|
|
18
|
+
const store = new InMemoryAwayStore();
|
|
19
|
+
store.set('alice', 'brb');
|
|
20
|
+
expect(store.get('alice')).toBe('brb');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('overwrites the reason on a subsequent set for the same account', () => {
|
|
24
|
+
const store = new InMemoryAwayStore();
|
|
25
|
+
store.set('alice', 'brb');
|
|
26
|
+
store.set('alice', 'lunch');
|
|
27
|
+
expect(store.get('alice')).toBe('lunch');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('keys accounts case-insensitively (rfc1459 fold)', () => {
|
|
31
|
+
const store = new InMemoryAwayStore();
|
|
32
|
+
store.set('Alice', 'brb');
|
|
33
|
+
expect(store.get('alice')).toBe('brb');
|
|
34
|
+
expect(store.get('ALICE')).toBe('brb');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('keeps reasons for distinct accounts separate', () => {
|
|
38
|
+
const store = new InMemoryAwayStore();
|
|
39
|
+
store.set('alice', 'brb');
|
|
40
|
+
store.set('bob', 'afk');
|
|
41
|
+
expect(store.get('alice')).toBe('brb');
|
|
42
|
+
expect(store.get('bob')).toBe('afk');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('clears the reason for an account on clear', () => {
|
|
46
|
+
const store = new InMemoryAwayStore();
|
|
47
|
+
store.set('alice', 'brb');
|
|
48
|
+
store.clear('alice');
|
|
49
|
+
expect(store.get('alice')).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('is a no-op to clear an account that has no stored reason', () => {
|
|
53
|
+
const store = new InMemoryAwayStore();
|
|
54
|
+
store.clear('alice');
|
|
55
|
+
expect(store.get('alice')).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('clears only the named account, leaving others intact', () => {
|
|
59
|
+
const store = new InMemoryAwayStore();
|
|
60
|
+
store.set('alice', 'brb');
|
|
61
|
+
store.set('bob', 'afk');
|
|
62
|
+
store.clear('alice');
|
|
63
|
+
expect(store.get('alice')).toBeUndefined();
|
|
64
|
+
expect(store.get('bob')).toBe('afk');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('folds the account name case-insensitively on clear', () => {
|
|
68
|
+
const store = new InMemoryAwayStore();
|
|
69
|
+
store.set('Alice', 'brb');
|
|
70
|
+
store.clear('ALICE');
|
|
71
|
+
expect(store.get('alice')).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -34,6 +34,21 @@ describe('wrapBatch — framing', () => {
|
|
|
34
34
|
expect(out).toEqual<RawLine[]>([]);
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
it('returns an empty BatchFrame carrying the framing metadata when lines is empty', () => {
|
|
38
|
+
// Empty batches are spec-forbidden and silently elided, but the returned
|
|
39
|
+
// array still advertises the id/type/start/body/end surface so callers
|
|
40
|
+
// can read the framing shape without a special-case branch.
|
|
41
|
+
const empty = wrapBatch([], 'ref7', 'chathistory', '#foo');
|
|
42
|
+
expect(empty).toHaveLength(0);
|
|
43
|
+
expect(empty.id).toBe('ref7');
|
|
44
|
+
expect(empty.type).toBe('chathistory');
|
|
45
|
+
expect(empty.start).toEqual({ text: '' });
|
|
46
|
+
expect(empty.body).toEqual([]);
|
|
47
|
+
expect(empty.end).toEqual({ text: '' });
|
|
48
|
+
// Non-enumerable props must not leak into JSON serialization.
|
|
49
|
+
expect(JSON.stringify(empty)).toBe('[]');
|
|
50
|
+
});
|
|
51
|
+
|
|
37
52
|
it('preserves the order of the inner lines', () => {
|
|
38
53
|
const lines = [L('one'), L('two'), L('three'), L('four')];
|
|
39
54
|
const out = wrapBatch(lines, 'x', 'test');
|