diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..33a10b308 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,29 @@ +{ + "root": true, + "extends": ["@rocket.chat/eslint-config", "prettier", "plugin:@typescript-eslint/recommended"], + "plugins": ["prettier", "anti-trojan-source", "no-floating-promise"], + "settings": { + "import/core-modules": ["bun", "bun:test", "bun:sqlite"], + "import/resolver": { + "typescript": { + "alwaysTryTypes": true, + "project": ["./tsconfig.json", "./packages/*/tsconfig.json"] + }, + "node": { + "extensions": [".js", ".jsx", ".ts", ".tsx"] + } + } + }, + "rules": { + "new-cap": ["error", { + "capIsNewExceptionPattern": "^t\\.(Object|String|Number|Boolean|Array|Tuple|Record|Union|Intersect|Literal|Optional|Nullable|Any|Unknown|Never|Void|Undefined|Null|Date|Files|File|Numeric|Integer|Enum|TemplateLiteral|Not|Recursive|Transform|Unsafe|Function|Constructor|Promise|AsyncIterator|Iterator|RegExp|Index|KeyOf|Partial|Required|Pick|Omit|Exclude|Extract|InstanceType|Parameters|ReturnType|Awaited|BooleanString|NumericString|MaybeEmpty|ObjectString|Lowercase|Uppercase|Capitalize|Uncapitalize)$" + }], + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": "interface", + "format": null + } + ] + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59af57043..78cd44bb4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: - uses: oven-sh/setup-bun@v2 - run: bun install - run: bun run build - - run: bun lint:ci + - run: bun lint - run: bun tsc --noEmit - uses: supercharge/mongodb-github-action@1.12.0 - run: RUN_MONGO_TESTS=1 bun test packages/federation-sdk/src/services/state.service.spec.ts packages/federation-sdk/src/services/room.service.spec.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d5f3a987b..3ff44b698 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,7 +32,7 @@ jobs: - run: bun install - - run: bun run lint:ci + - run: bun run lint - run: bun run build diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..f290d4516 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,12 @@ +{ + "semi": true, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf", + "jsxSingleQuote": true, + "printWidth": 140, + "quoteProps": "consistent", + "singleQuote": true, + "trailingComma": "all", + "useTabs": true +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 16e8e6664..000000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["biomejs.biome"] -} diff --git a/bun.lock b/bun.lock index 0a6f88c81..1e86645e5 100644 --- a/bun.lock +++ b/bun.lock @@ -13,13 +13,23 @@ "tweetnacl": "^1.0.3", }, "devDependencies": { - "@biomejs/biome": "^1.9.4", + "@rocket.chat/eslint-config": "^0.7.0", "@types/bun": "latest", "@types/express": "^5.0.1", "@types/node": "^22.15.18", "@types/sinon": "^17.0.4", + "@typescript-eslint/eslint-plugin": "5.62.0", + "@typescript-eslint/parser": "5.62.0", + "eslint": "~8.45.0", + "eslint-config-prettier": "~9.1.2", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-anti-trojan-source": "^1.1.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-no-floating-promise": "~2.0.0", + "eslint-plugin-prettier": "4.2.1", "husky": "^9.1.7", "lint-staged": "^16.1.2", + "prettier": "2.8.8", "sinon": "^20.0.0", "tsconfig-paths": "^4.2.0", "turbo": "~2.5.6", @@ -54,7 +64,7 @@ }, "packages/federation-sdk": { "name": "@rocket.chat/federation-sdk", - "version": "0.3.0", + "version": "0.3.9", "dependencies": { "@rocket.chat/emitter": "^0.31.25", "@rocket.chat/federation-core": "workspace:*", @@ -105,25 +115,37 @@ "packages": { "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + + "@babel/eslint-parser": ["@babel/eslint-parser@7.23.10", "", { "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", "eslint-visitor-keys": "^2.1.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.11.0", "eslint": "^7.5.0 || ^8.0.0" } }, "sha512-3wSYDPZVnhseRnxRJH6ZVTNknBz76AEnyC+AYYhasjP3Yy23qz0ERR7Fcd2SHmYuSFJ2kY9gaaDd3vyqU09eSw=="], + + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], - "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], "@bogeychan/elysia-etag": ["@bogeychan/elysia-etag@0.0.6", "", { "peerDependencies": { "elysia": ">= 1.0.22" } }, "sha512-DPHRQJLm4mR5zkQk+DYhDEX1YFh0S5M3ZTqLy/SJ+kdT68c3wxynkyZZtL2YRNhZ0LKHuDXzqAXd77eTZeKxMg=="], @@ -137,6 +159,30 @@ "@elysiajs/swagger": ["@elysiajs/swagger@1.3.1", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-LcbLHa0zE6FJKWPWKsIC/f+62wbDv3aXydqcNPVPyqNcaUgwvCajIi+5kHEU6GO3oXUCpzKaMsb3gsjt8sLzFQ=="], + "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="], + + "@eslint/js": ["@eslint/js@8.44.0", "", {}, "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw=="], + + "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.11.14", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], @@ -145,10 +191,22 @@ "@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.3.0", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + + "@nicolo-ribaudo/eslint-scope-5-internals": ["@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1", "", { "dependencies": { "eslint-scope": "5.1.1" } }, "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg=="], + "@noble/ed25519": ["@noble/ed25519@3.0.0", "", {}, "sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@rocket.chat/emitter": ["@rocket.chat/emitter@0.31.25", "", {}, "sha512-hw5BpDlNwpYSb+K5X3DNMNUVEVXxmXugUPetGZGCWvntSVFsOjYuVEypoKW6vBBXSfqCBb0kN1npYcKEb4NFBw=="], + "@rocket.chat/eslint-config": ["@rocket.chat/eslint-config@0.7.0", "", { "dependencies": { "@babel/core": "^7.20.7", "@babel/eslint-parser": "~7.23.3", "@types/eslint": "~8.44.6", "@types/prettier": "^2.6.3", "@typescript-eslint/eslint-plugin": "~5.60.1", "@typescript-eslint/parser": "~5.60.1", "eslint": "~8.45.0", "eslint-config-prettier": "~8.8.0", "eslint-plugin-anti-trojan-source": "~1.1.1", "eslint-plugin-import": "~2.26.0", "eslint-plugin-jest": "~27.2.3", "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-prettier": "~4.2.1", "prettier": "~2.8.8" } }, "sha512-6AlE/MpJfITicLVNmgToK8hjp7doB4UtJdkDXm58JQZcDuU4Vt7co5/xUpg8tUhT7OouzlqdPr9keNuGm6Wi7A=="], + "@rocket.chat/federation-core": ["@rocket.chat/federation-core@workspace:packages/core"], "@rocket.chat/federation-crypto": ["@rocket.chat/federation-crypto@workspace:packages/crypto"], @@ -203,6 +261,8 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w=="], + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], "@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="], @@ -229,12 +289,16 @@ "@tsconfig/node16": ["@tsconfig/node16@1.0.4", "", {}, "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + "@types/eslint": ["@types/eslint@8.44.9", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-6yBxcvwnnYoYT1Uk2d+jvIfsuP4mb2EdIxFnrPABj5a/838qe5bGkNLFOiipX4ULQ7XVQvTxOh7jO+BTAiqsEw=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="], @@ -243,16 +307,28 @@ "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + "@types/minimist": ["@types/minimist@1.2.5", "", {}, "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag=="], + "@types/node": ["@types/node@22.16.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ=="], + "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], + + "@types/prettier": ["@types/prettier@2.7.3", "", {}, "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA=="], + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], + "@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="], + "@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="], "@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="], @@ -265,35 +341,147 @@ "@types/whatwg-url": ["@types/whatwg-url@11.0.5", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@5.62.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/type-utils": "5.62.0", "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", "semver": "^7.3.7", "tsutils": "^3.21.0" }, "peerDependencies": { "@typescript-eslint/parser": "^5.0.0", "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@5.62.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", "@typescript-eslint/typescript-estree": "5.62.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0" } }, "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@5.62.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "5.62.0", "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, "peerDependencies": { "eslint": "*" } }, "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@5.62.0", "", {}, "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "semver": "^7.3.7", "tsutils": "^3.21.0" } }, "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@5.62.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", "@typescript-eslint/typescript-estree": "5.62.0", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" } }, "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw=="], + "@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="], + "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], + + "@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="], + + "@unrs/resolver-binding-darwin-arm64": ["@unrs/resolver-binding-darwin-arm64@1.11.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g=="], + + "@unrs/resolver-binding-darwin-x64": ["@unrs/resolver-binding-darwin-x64@1.11.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ=="], + + "@unrs/resolver-binding-freebsd-x64": ["@unrs/resolver-binding-freebsd-x64@1.11.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw=="], + + "@unrs/resolver-binding-linux-arm-gnueabihf": ["@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw=="], + + "@unrs/resolver-binding-linux-arm-musleabihf": ["@unrs/resolver-binding-linux-arm-musleabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw=="], + + "@unrs/resolver-binding-linux-arm64-gnu": ["@unrs/resolver-binding-linux-arm64-gnu@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ=="], + + "@unrs/resolver-binding-linux-arm64-musl": ["@unrs/resolver-binding-linux-arm64-musl@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w=="], + + "@unrs/resolver-binding-linux-ppc64-gnu": ["@unrs/resolver-binding-linux-ppc64-gnu@1.11.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA=="], + + "@unrs/resolver-binding-linux-riscv64-gnu": ["@unrs/resolver-binding-linux-riscv64-gnu@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ=="], + + "@unrs/resolver-binding-linux-riscv64-musl": ["@unrs/resolver-binding-linux-riscv64-musl@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew=="], + + "@unrs/resolver-binding-linux-s390x-gnu": ["@unrs/resolver-binding-linux-s390x-gnu@1.11.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg=="], + + "@unrs/resolver-binding-linux-x64-gnu": ["@unrs/resolver-binding-linux-x64-gnu@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w=="], + + "@unrs/resolver-binding-linux-x64-musl": ["@unrs/resolver-binding-linux-x64-musl@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA=="], + + "@unrs/resolver-binding-wasm32-wasi": ["@unrs/resolver-binding-wasm32-wasi@1.11.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" }, "cpu": "none" }, "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ=="], + + "@unrs/resolver-binding-win32-arm64-msvc": ["@unrs/resolver-binding-win32-arm64-msvc@1.11.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw=="], + + "@unrs/resolver-binding-win32-ia32-msvc": ["@unrs/resolver-binding-win32-ia32-msvc@1.11.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ=="], + + "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + "ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "anti-trojan-source": ["anti-trojan-source@1.8.1", "", { "dependencies": { "globby": "^12.0.2", "meow": "^10.1.1" }, "bin": { "anti-trojan-source": "bin/anti-trojan-source.js" } }, "sha512-m3czWxpwc3XuVEYAabsEOfUwtIzq4sztTRlUDZInyjRG3g49GwWotI5+kyngv8wkuzQIAVoa7dxpgOX20wjDZA=="], + "arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], + + "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], + + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + + "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], + + "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + + "arrify": ["arrify@1.0.1", "", {}, "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA=="], + + "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], + + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "axe-core": ["axe-core@4.11.1", "", {}, "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + "bson": ["bson@6.10.4", "", {}, "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng=="], "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], "bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], - "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + + "camelcase-keys": ["camelcase-keys@7.0.2", "", { "dependencies": { "camelcase": "^6.3.0", "map-obj": "^4.1.0", "quick-lru": "^5.1.1", "type-fest": "^1.2.1" } }, "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001770", "", {}, "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], @@ -307,28 +495,118 @@ "commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], "create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], + + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], + + "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decamelize": ["decamelize@5.0.1", "", {}, "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA=="], + + "decamelize-keys": ["decamelize-keys@1.1.1", "", { "dependencies": { "decamelize": "^1.1.0", "map-obj": "^1.0.0" } }, "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], "diff": ["diff@7.0.0", "", {}, "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw=="], + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="], + "elysia": ["elysia@1.3.5", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-XVIKXlKFwUT7Sta8GY+wO5reD9I0rqAEtaz1Z71UgJb61csYt8Q3W9al8rtL5RgumuRR8e3DNdzlUN9GkC4KDw=="], - "emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], + + "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@8.45.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", "@eslint/eslintrc": "^2.1.0", "@eslint/js": "8.44.0", "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.0", "eslint-visitor-keys": "^3.4.1", "espree": "^9.6.0", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw=="], + + "eslint-config-prettier": ["eslint-config-prettier@9.1.2", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ=="], + + "eslint-import-context": ["eslint-import-context@0.1.9", "", { "dependencies": { "get-tsconfig": "^4.10.1", "stable-hash-x": "^0.2.0" }, "peerDependencies": { "unrs-resolver": "^1.0.0" }, "optionalPeers": ["unrs-resolver"] }, "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg=="], + + "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="], + + "eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@4.4.4", "", { "dependencies": { "debug": "^4.4.1", "eslint-import-context": "^0.1.8", "get-tsconfig": "^4.10.1", "is-bun-module": "^2.0.0", "stable-hash-x": "^0.2.0", "tinyglobby": "^0.2.14", "unrs-resolver": "^1.7.11" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw=="], + + "eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], + + "eslint-plugin-anti-trojan-source": ["eslint-plugin-anti-trojan-source@1.1.2", "", { "dependencies": { "anti-trojan-source": "^1.3.1" } }, "sha512-nFpOvnLedNiTM6U8x+tjP8IX8EzyGrp9niCZV1QoAWEBBn5cbK08lhkm1Anb4/40cn4JFDer8N0VkblH8laVEw=="], + + "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], + + "eslint-plugin-jest": ["eslint-plugin-jest@27.2.3", "", { "dependencies": { "@typescript-eslint/utils": "^5.10.0" }, "peerDependencies": { "@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0", "eslint": "^7.0.0 || ^8.0.0", "jest": "*" }, "optionalPeers": ["@typescript-eslint/eslint-plugin", "jest"] }, "sha512-sRLlSCpICzWuje66Gl9zvdF6mwD5X86I4u55hJyFBsxYOsBCmT5+kSUjf+fkFWVMMgpzNEupjW8WzUqi83hJAQ=="], + + "eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="], + + "eslint-plugin-no-floating-promise": ["eslint-plugin-no-floating-promise@2.0.0", "", { "dependencies": { "requireindex": "1.2.0" } }, "sha512-XVAk+a1Qq3fsfY8tLT64Ky4sdJjZSjx0BsDxjdWbyceU0yWwg1ZKHiWgaOZ0hDYNzrl3qx5MEGrRlTceGzd0ow=="], + + "eslint-plugin-prettier": ["eslint-plugin-prettier@4.2.1", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0" }, "peerDependencies": { "eslint": ">=7.28.0", "prettier": ">=2.0.0" } }, "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ=="], + + "eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], @@ -341,78 +619,264 @@ "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="], "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], + "file-type": ["file-type@21.0.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.7", "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], + + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + + "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], + + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + "global-prefix": ["global-prefix@4.0.0", "", { "dependencies": { "ini": "^4.1.3", "kind-of": "^6.0.3", "which": "^4.0.0" } }, "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA=="], + "globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "grapheme-splitter": ["grapheme-splitter@1.0.4", "", {}, "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ=="], + + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "hard-rejection": ["hard-rejection@2.1.0", "", {}, "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA=="], + + "has": ["has@1.0.4", "", {}, "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ=="], + + "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="], + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], + + "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], + + "is-bun-module": ["is-bun-module@2.0.0", "", { "dependencies": { "semver": "^7.7.1" } }, "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], + + "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - "isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], + "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + + "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], + + "is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], + + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], + + "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], + "language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="], + + "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + "lint-staged": ["lint-staged@16.1.2", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^14.0.0", "debug": "^4.4.1", "lilconfig": "^3.1.3", "listr2": "^8.3.3", "micromatch": "^4.0.8", "nano-spawn": "^1.0.2", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q=="], "listr2": ["listr2@8.3.3", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ=="], + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lodash.get": ["lodash.get@4.4.2", "", {}, "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="], + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], "make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="], + "map-obj": ["map-obj@4.3.0", "", {}, "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "memoize": ["memoize@10.1.0", "", { "dependencies": { "mimic-function": "^5.0.1" } }, "sha512-MMbFhJzh4Jlg/poq1si90XRlTZRDHVqdlz2mPyGJ6kqMpyHUyVpDd5gpFAvVehW64+RA1eKE9Yt8aSLY7w2Kgg=="], "memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="], + "meow": ["meow@10.1.5", "", { "dependencies": { "@types/minimist": "^1.2.2", "camelcase-keys": "^7.0.0", "decamelize": "^5.0.0", "decamelize-keys": "^1.1.0", "hard-rejection": "^2.1.0", "minimist-options": "4.1.0", "normalize-package-data": "^3.0.2", "read-pkg-up": "^8.0.0", "redent": "^4.0.0", "trim-newlines": "^4.0.2", "type-fest": "^1.2.2", "yargs-parser": "^20.2.9" } }, "sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "minimist-options": ["minimist-options@4.1.0", "", { "dependencies": { "arrify": "^1.0.1", "is-plain-obj": "^1.1.0", "kind-of": "^6.0.3" } }, "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A=="], + "mongodb": ["mongodb@6.17.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.1.9", "bson": "^6.10.4", "mongodb-connection-string-url": "^3.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", "snappy": "^7.2.2", "socks": "^2.7.1" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA=="], "mongodb-connection-string-url": ["mongodb-connection-string-url@3.0.2", "", { "dependencies": { "@types/whatwg-url": "^11.0.2", "whatwg-url": "^14.1.0 || ^13.0.0" } }, "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA=="], @@ -423,6 +887,28 @@ "nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], + "napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "natural-compare-lite": ["natural-compare-lite@1.4.0", "", {}, "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g=="], + + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + + "normalize-package-data": ["normalize-package-data@3.0.3", "", { "dependencies": { "hosted-git-info": "^4.0.1", "is-core-module": "^2.5.0", "semver": "^7.3.4", "validate-npm-package-license": "^3.0.1" } }, "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + + "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], + + "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], @@ -431,13 +917,33 @@ "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], @@ -449,6 +955,14 @@ "pino-std-serializers": ["pino-std-serializers@6.2.2", "", {}, "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA=="], + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + + "prettier-linter-helpers": ["prettier-linter-helpers@1.0.1", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg=="], + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], "process-warning": ["process-warning@3.0.0", "", {}, "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ=="], @@ -457,54 +971,128 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], + + "read-pkg": ["read-pkg@6.0.0", "", { "dependencies": { "@types/normalize-package-data": "^2.4.0", "normalize-package-data": "^3.0.2", "parse-json": "^5.2.0", "type-fest": "^1.0.1" } }, "sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q=="], + + "read-pkg-up": ["read-pkg-up@8.0.0", "", { "dependencies": { "find-up": "^5.0.0", "read-pkg": "^6.0.0", "type-fest": "^1.0.1" } }, "sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ=="], + "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + "redent": ["redent@4.0.0", "", { "dependencies": { "indent-string": "^5.0.0", "strip-indent": "^4.0.0" } }, "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag=="], + "reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="], + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + + "requireindex": ["requireindex@1.2.0", "", {}, "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww=="], + "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + "rollup": ["rollup@4.52.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.4", "@rollup/rollup-android-arm64": "4.52.4", "@rollup/rollup-darwin-arm64": "4.52.4", "@rollup/rollup-darwin-x64": "4.52.4", "@rollup/rollup-freebsd-arm64": "4.52.4", "@rollup/rollup-freebsd-x64": "4.52.4", "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", "@rollup/rollup-linux-arm-musleabihf": "4.52.4", "@rollup/rollup-linux-arm64-gnu": "4.52.4", "@rollup/rollup-linux-arm64-musl": "4.52.4", "@rollup/rollup-linux-loong64-gnu": "4.52.4", "@rollup/rollup-linux-ppc64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-musl": "4.52.4", "@rollup/rollup-linux-s390x-gnu": "4.52.4", "@rollup/rollup-linux-x64-gnu": "4.52.4", "@rollup/rollup-linux-x64-musl": "4.52.4", "@rollup/rollup-openharmony-arm64": "4.52.4", "@rollup/rollup-win32-arm64-msvc": "4.52.4", "@rollup/rollup-win32-ia32-msvc": "4.52.4", "@rollup/rollup-win32-x64-gnu": "4.52.4", "@rollup/rollup-win32-x64-msvc": "4.52.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ=="], "rollup-plugin-dts": ["rollup-plugin-dts@6.2.3", "", { "dependencies": { "magic-string": "^0.30.17" }, "optionalDependencies": { "@babel/code-frame": "^7.27.1" }, "peerDependencies": { "rollup": "^3.29.4 || ^4", "typescript": "^4.5 || ^5.0" } }, "sha512-UgnEsfciXSPpASuOelix7m4DrmyQgiaWBnvI0TM4GxuDh5FkqW8E5hu57bCxXB90VvR1WNfLV80yEDN18UogSA=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], "secure-json-parse": ["secure-json-parse@4.0.0", "", {}, "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA=="], - "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "sinon": ["sinon@20.0.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.1", "@sinonjs/fake-timers": "^13.0.5", "@sinonjs/samsam": "^8.0.1", "diff": "^7.0.0", "supports-color": "^7.2.0" } }, "sha512-+FXOAbdnj94AQIxH0w1v8gzNxkawVvNqE3jUzRLptR71Oykeu2RrQXXl/VQjKay+Qnh73fDt/oDfMo6xMeDQbQ=="], + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], "sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="], + "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], + + "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], + + "spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], + + "spdx-license-ids": ["spdx-license-ids@3.0.22", "", {}, "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ=="], + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "stable-hash-x": ["stable-hash-x@0.2.0", "", {}, "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ=="], + + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + "strip-indent": ["strip-indent@4.1.1", "", {}, "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA=="], + "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], "strtok3": ["strtok3@10.3.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw=="], @@ -513,14 +1101,20 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], + "thread-stream": ["thread-stream@2.7.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "token-types": ["token-types@6.0.3", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-IKJ6EzuPPWtKtEIEPpIdXv9j5j2LGJEYk0CKY2efgKoYKLBiZdh6iQkLVBow/CB3phyWAWCyk+bZeaimJn6uRQ=="], "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + "trim-newlines": ["trim-newlines@4.1.1", "", {}, "sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ=="], + "ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-script": "dist/bin-script-deprecated.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="], "ts-patch": ["ts-patch@3.3.0", "", { "dependencies": { "chalk": "^4.1.2", "global-prefix": "^4.0.0", "minimist": "^1.2.8", "resolve": "^1.22.2", "semver": "^7.6.3", "strip-ansi": "^6.0.1" }, "bin": { "ts-patch": "bin/ts-patch.js", "tspc": "bin/tspc.js" } }, "sha512-zAOzDnd5qsfEnjd9IGy1IRuvA7ygyyxxdxesbhMdutt8AHFjD8Vw8hU2rMF89HX1BKRWFYqKHrO8Q6lw0NeUZg=="], @@ -529,6 +1123,8 @@ "tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "tsutils": ["tsutils@3.21.0", "", { "dependencies": { "tslib": "^1.8.1" }, "peerDependencies": { "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA=="], + "tsyringe": ["tsyringe@4.10.0", "", { "dependencies": { "tslib": "^1.9.3" } }, "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw=="], "turbo": ["turbo@2.5.6", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.6", "turbo-darwin-arm64": "2.5.6", "turbo-linux-64": "2.5.6", "turbo-linux-arm64": "2.5.6", "turbo-windows-64": "2.5.6", "turbo-windows-arm64": "2.5.6" }, "bin": { "turbo": "bin/turbo" } }, "sha512-gxToHmi9oTBNB05UjUsrWf0OyN5ZXtD0apOarC1KIx232Vp3WimRNy3810QzeNSgyD5rsaIDXlxlbnOzlouo+w=="], @@ -547,40 +1143,128 @@ "tweetnacl": ["tweetnacl@1.0.3", "", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], - "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], + + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], + + "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], "uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="], + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="], + "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], - "which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + + "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="], + "yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + "yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], "zod": ["zod@3.25.71", "", {}, "sha512-BsBc/NPk7h8WsUWYWYL+BajcJPY8YhjelaWu2NMLuzgraKAz4Lb4/6K11g9jpuDetjMiqhZ6YaexFLOC0Ogi3Q=="], + "@babel/core/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/core/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@2.1.0", "", {}, "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw=="], + + "@babel/eslint-parser/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/template/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/traverse/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@bogeychan/elysia-logger/pino": ["pino@9.7.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg=="], + "@emnapi/core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@eslint/eslintrc/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "@humanwhocodes/config-array/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@jridgewell/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="], + "@nicolo-ribaudo/eslint-scope-5-internals/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], + + "@rocket.chat/eslint-config/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@5.60.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.60.1", "@typescript-eslint/type-utils": "5.60.1", "@typescript-eslint/utils": "5.60.1", "debug": "^4.3.4", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", "semver": "^7.3.7", "tsutils": "^3.21.0" }, "peerDependencies": { "@typescript-eslint/parser": "^5.0.0", "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-KSWsVvsJsLJv3c4e73y/Bzt7OpqMCADUO846bHcuWYSYM19bldbAeDv7dYyV0jwkbMfJ2XdlzwjhXtuD7OY6bw=="], + + "@rocket.chat/eslint-config/@typescript-eslint/parser": ["@typescript-eslint/parser@5.60.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "5.60.1", "@typescript-eslint/types": "5.60.1", "@typescript-eslint/typescript-estree": "5.60.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-pHWlc3alg2oSMGwsU/Is8hbm3XFbcrb6P5wIxcQW9NsYBfnrubl/GhVVD/Jm/t8HXhA2WncoIRfBtnCgRGV96Q=="], + + "@rocket.chat/eslint-config/eslint-config-prettier": ["eslint-config-prettier@8.8.0", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA=="], + + "@rocket.chat/eslint-config/eslint-plugin-import": ["eslint-plugin-import@2.26.0", "", { "dependencies": { "array-includes": "^3.1.4", "array.prototype.flat": "^1.2.5", "debug": "^2.6.9", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.6", "eslint-module-utils": "^2.7.3", "has": "^1.0.3", "is-core-module": "^2.8.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.values": "^1.1.5", "resolve": "^1.22.0", "tsconfig-paths": "^3.14.1" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" } }, "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA=="], + + "@rocket.chat/eslint-config/eslint-plugin-prettier": ["eslint-plugin-prettier@4.2.5", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0" }, "peerDependencies": { "eslint": ">=7.28.0", "prettier": ">=2.0.0" } }, "sha512-9Ni+xgemM2IWLq6aXEpP2+V/V30GeA/46Ar629vcMqVPodFFWC9skHu/D1phvuqtS8bJCFnNf01/qcmqYEwNfg=="], + "@rocket.chat/federation-crypto/bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], "@rocket.chat/federation-room/bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], @@ -591,44 +1275,180 @@ "@sinonjs/samsam/type-detect": ["type-detect@4.1.0", "", {}, "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw=="], + "@tokenizer/inflate/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "@tybys/wasm-util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@types/bun/bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], + "@typescript-eslint/typescript-estree/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@typescript-eslint/utils/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], + + "@typescript-eslint/utils/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "anti-trojan-source/globby": ["globby@12.2.0", "", { "dependencies": { "array-union": "^3.0.1", "dir-glob": "^3.0.1", "fast-glob": "^3.2.7", "ignore": "^5.1.9", "merge2": "^1.4.1", "slash": "^4.0.0" } }, "sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA=="], + + "camelcase-keys/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], + + "decamelize-keys/decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + + "decamelize-keys/map-obj": ["map-obj@1.0.1", "", {}, "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg=="], + + "eslint/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + + "eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "eslint-plugin-import/tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "global-prefix/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], + + "hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "lint-staged/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + + "lint-staged/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "log-update/slice-ansi": ["slice-ansi@7.1.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="], "log-update/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "meow/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "normalize-package-data/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "parse-json/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + "pino/pino-abstract-transport": ["pino-abstract-transport@1.2.0", "", { "dependencies": { "readable-stream": "^4.0.0", "split2": "^4.0.0" } }, "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q=="], "pino/sonic-boom": ["sonic-boom@3.8.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg=="], + "read-pkg/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], + + "read-pkg-up/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], + "slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + "string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + "string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], "ts-node/diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="], - "ts-patch/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "ts-patch/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], "wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "@babel/core/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/template/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@bogeychan/elysia-logger/pino/pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="], "@bogeychan/elysia-logger/pino/process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], "@bogeychan/elysia-logger/pino/thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], + "@nicolo-ribaudo/eslint-scope-5-internals/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + + "@rocket.chat/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@5.60.1", "", { "dependencies": { "@typescript-eslint/types": "5.60.1", "@typescript-eslint/visitor-keys": "5.60.1" } }, "sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ=="], + + "@rocket.chat/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@5.60.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "5.60.1", "@typescript-eslint/utils": "5.60.1", "debug": "^4.3.4", "tsutils": "^3.21.0" }, "peerDependencies": { "eslint": "*" } }, "sha512-vN6UztYqIu05nu7JqwQGzQKUJctzs3/Hg7E2Yx8rz9J+4LgtIDFWjjl1gm3pycH0P3mHAcEUBd23LVgfrsTR8A=="], + + "@rocket.chat/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@5.60.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", "@typescript-eslint/scope-manager": "5.60.1", "@typescript-eslint/types": "5.60.1", "@typescript-eslint/typescript-estree": "5.60.1", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ=="], + + "@rocket.chat/eslint-config/@typescript-eslint/eslint-plugin/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "@rocket.chat/eslint-config/@typescript-eslint/eslint-plugin/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@rocket.chat/eslint-config/@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@5.60.1", "", { "dependencies": { "@typescript-eslint/types": "5.60.1", "@typescript-eslint/visitor-keys": "5.60.1" } }, "sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ=="], + + "@rocket.chat/eslint-config/@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@5.60.1", "", {}, "sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg=="], + + "@rocket.chat/eslint-config/@typescript-eslint/parser/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@5.60.1", "", { "dependencies": { "@typescript-eslint/types": "5.60.1", "@typescript-eslint/visitor-keys": "5.60.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "semver": "^7.3.7", "tsutils": "^3.21.0" } }, "sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw=="], + + "@rocket.chat/eslint-config/@typescript-eslint/parser/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "@rocket.chat/eslint-config/eslint-plugin-import/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "@rocket.chat/eslint-config/eslint-plugin-import/doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + + "@rocket.chat/eslint-config/eslint-plugin-import/tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], + "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="], + "@scalar/themes/@scalar/types/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + + "@typescript-eslint/utils/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + + "anti-trojan-source/globby/array-union": ["array-union@3.0.1", "", {}, "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw=="], + + "anti-trojan-source/globby/slash": ["slash@4.0.0", "", {}, "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew=="], + + "eslint-plugin-import/tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + + "global-prefix/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], + + "hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.0.0", "", { "dependencies": { "get-east-asian-width": "^1.0.0" } }, "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA=="], "log-update/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "parse-json/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "@rocket.chat/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@5.60.1", "", {}, "sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg=="], + + "@rocket.chat/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@5.60.1", "", { "dependencies": { "@typescript-eslint/types": "5.60.1", "eslint-visitor-keys": "^3.3.0" } }, "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw=="], + + "@rocket.chat/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@5.60.1", "", { "dependencies": { "@typescript-eslint/types": "5.60.1", "@typescript-eslint/visitor-keys": "5.60.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "semver": "^7.3.7", "tsutils": "^3.21.0" } }, "sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw=="], + + "@rocket.chat/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@5.60.1", "", {}, "sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg=="], + + "@rocket.chat/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@5.60.1", "", { "dependencies": { "@typescript-eslint/types": "5.60.1", "@typescript-eslint/visitor-keys": "5.60.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "semver": "^7.3.7", "tsutils": "^3.21.0" } }, "sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw=="], + + "@rocket.chat/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], + + "@rocket.chat/eslint-config/@typescript-eslint/parser/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@5.60.1", "", { "dependencies": { "@typescript-eslint/types": "5.60.1", "eslint-visitor-keys": "^3.3.0" } }, "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw=="], + + "@rocket.chat/eslint-config/@typescript-eslint/parser/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@5.60.1", "", { "dependencies": { "@typescript-eslint/types": "5.60.1", "eslint-visitor-keys": "^3.3.0" } }, "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw=="], + + "@rocket.chat/eslint-config/@typescript-eslint/parser/@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "@rocket.chat/eslint-config/eslint-plugin-import/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "@rocket.chat/eslint-config/eslint-plugin-import/tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + + "@rocket.chat/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@5.60.1", "", {}, "sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg=="], + + "@rocket.chat/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@5.60.1", "", { "dependencies": { "@typescript-eslint/types": "5.60.1", "eslint-visitor-keys": "^3.3.0" } }, "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw=="], + + "@rocket.chat/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@5.60.1", "", { "dependencies": { "@typescript-eslint/types": "5.60.1", "eslint-visitor-keys": "^3.3.0" } }, "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw=="], + + "@rocket.chat/eslint-config/@typescript-eslint/eslint-plugin/@typescript-eslint/utils/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], } } diff --git a/bundle.ts b/bundle.ts index 3eff95e88..5871963ca 100644 --- a/bundle.ts +++ b/bundle.ts @@ -6,24 +6,20 @@ const outputDir = './federation-bundle'; function getLocalPackages(packages: string[]) { const localPackages = new Set(); for (const pkg of packages) { + // eslint-disable-next-line import/no-dynamic-require, @typescript-eslint/no-var-requires const packageJson = require(`./packages/${pkg}/package.json`); localPackages.add(packageJson.name); } return localPackages; } -const getDependencies = ( - pkg: string, - type: 'dependencies' | 'devDependencies' | 'peerDependencies', -) => { +const getDependencies = (pkg: string, type: 'dependencies' | 'devDependencies' | 'peerDependencies') => { + // eslint-disable-next-line import/no-dynamic-require, @typescript-eslint/no-var-requires const packageJson = require(`./packages/${pkg}/package.json`); return packageJson[type] ?? {}; }; -const getDependenciesFromPackages = ( - packages: string[], - type: 'dependencies' | 'devDependencies' | 'peerDependencies', -) => { +const getDependenciesFromPackages = (packages: string[], type: 'dependencies' | 'devDependencies' | 'peerDependencies') => { return packages.reduce((acc, name) => { // biome-ignore lint/performance/noAccumulatingSpread: return { ...acc, ...getDependencies(name, type) }; @@ -31,21 +27,14 @@ const getDependenciesFromPackages = ( }; const filterWorkspace = (deps: Record) => - Object.fromEntries( - Object.entries(deps || {}).filter( - ([, value]) => - typeof value === 'string' && !value.startsWith('workspace:'), - ), - ); + Object.fromEntries(Object.entries(deps || {}).filter(([, value]) => typeof value === 'string' && !value.startsWith('workspace:'))); // TODO get list of packages programmatically const packages = ['core', 'crypto', 'federation-sdk', 'room']; const localPackagesNames = getLocalPackages(packages); -const packageJson = JSON.parse( - await Bun.file(`${inputDir}/package.json`).text(), -); +const packageJson = JSON.parse(await Bun.file(`${inputDir}/package.json`).text()); async function main() { await $`rm -rf ${outputDir}/dist`; @@ -54,23 +43,15 @@ async function main() { await $`touch ${outputDir}/yarn.lock`; const dependencies = getDependenciesFromPackages(packages, 'dependencies'); - const devDependencies = getDependenciesFromPackages( - packages, - 'devDependencies', - ); - const peerDependencies = getDependenciesFromPackages( - packages, - 'peerDependencies', - ); + const devDependencies = getDependenciesFromPackages(packages, 'devDependencies'); + const peerDependencies = getDependenciesFromPackages(packages, 'peerDependencies'); await Bun.build({ entrypoints: [`${inputDir}/src/index.ts`], outdir: `${outputDir}/dist`, target: 'node', format: 'cjs', - external: Object.keys(dependencies).filter( - (dep) => !localPackagesNames.has(dep), - ), + external: Object.keys(dependencies).filter((dep) => !localPackagesNames.has(dep)), env: 'disable', define: { 'process.env.NODE_ENV': '"production"', @@ -92,9 +73,7 @@ async function main() { ...peerDependencies, }); - await Bun.file(`${outputDir}/package.json`).write( - `${JSON.stringify(packageJson, null, 2)}\n`, - ); + await Bun.file(`${outputDir}/package.json`).write(`${JSON.stringify(packageJson, null, 2)}\n`); await $`bun run rollup -c`; diff --git a/package.json b/package.json index 5e5ec4a37..679ac32d7 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,23 @@ "private": true, "type": "module", "devDependencies": { - "@biomejs/biome": "^1.9.4", + "@rocket.chat/eslint-config": "^0.7.0", "@types/bun": "latest", "@types/express": "^5.0.1", "@types/node": "^22.15.18", "@types/sinon": "^17.0.4", + "@typescript-eslint/eslint-plugin": "5.62.0", + "@typescript-eslint/parser": "5.62.0", + "eslint": "~8.45.0", + "eslint-config-prettier": "~9.1.2", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-anti-trojan-source": "^1.1.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-no-floating-promise": "~2.0.0", + "eslint-plugin-prettier": "4.2.1", "husky": "^9.1.7", "lint-staged": "^16.1.2", + "prettier": "2.8.8", "sinon": "^20.0.0", "tsconfig-paths": "^4.2.0", "turbo": "~2.5.6", @@ -31,27 +41,15 @@ "pre-commit": "bun test" } }, - "packageManager": "bun@1.1.10", - "lint-staged": { - "**.{js|ts|cjs|mjs|d.cts|d.mts|jsx|tsx|json|jsonc}": [ - "biome check --files-ignore-unknown=true --diagnostic-level=error", - "biome check --write --no-errors-on-unmatched --diagnostic-level=error", - "biome check --write --organize-imports-enabled=false --no-errors-on-unmatched --diagnostic-level=error", - "biome check --write --unsafe --no-errors-on-unmatched --diagnostic-level=error", - "biome format --write --no-errors-on-unmatched --diagnostic-level=error", - "biome lint --write --no-errors-on-unmatched --diagnostic-level=error" - ], - "*": ["biome check --no-errors-on-unmatched --files-ignore-unknown=true"] - }, + "packageManager": "bun@1.3.0", + "lint-staged": {}, "scripts": { "prepare": "husky", "start": "bun run index.ts", "test": "bun test", + "lint": "eslint . --ext .ts,.js,.tsx,.jsx --ignore-path .gitignore", "build": "turbo build", "test:coverage": "bun test --coverage", - "lint": "bunx @biomejs/biome lint --diagnostic-level=error", - "lint:ci": "bunx @biomejs/biome ci --diagnostic-level=error", - "lint:fix": "bunx @biomejs/biome lint --fix", "tsc": "tsc --noEmit", "bundle:sdk": "bun run build && bun run bundle.ts" } diff --git a/packages/core/.eslintrc.json b/packages/core/.eslintrc.json new file mode 100644 index 000000000..be97c53fb --- /dev/null +++ b/packages/core/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.eslintrc.json" +} diff --git a/packages/core/src/AsyncDispatcher.ts b/packages/core/src/AsyncDispatcher.ts index 487a34c5f..26d43895d 100644 --- a/packages/core/src/AsyncDispatcher.ts +++ b/packages/core/src/AsyncDispatcher.ts @@ -5,8 +5,7 @@ export type DefaultEventMap = Record; export type AnyEventTypeOf = keyof EventMap; /** @public */ -export type AnyEventOf = - EventMap[keyof EventMap]; +export type AnyEventOf = EventMap[keyof EventMap]; /** @public */ export type AnyEventHandlerOf = { @@ -16,26 +15,17 @@ export type AnyEventHandlerOf = { }[keyof EventMap]; /** @public */ -export type EventTypeOf< - EventMap extends DefaultEventMap, - EventValue extends EventMap[keyof EventMap], -> = { - [EventType in keyof EventMap]: EventMap[EventType] extends EventValue - ? EventType - : never; +export type EventTypeOf = { + [EventType in keyof EventMap]: EventMap[EventType] extends EventValue ? EventType : never; }[keyof EventMap]; /** @public */ -export type EventOf< - EventMap extends DefaultEventMap, - EventType extends AnyEventTypeOf, -> = EventMap[EventType] extends void ? never : EventMap[EventType]; +export type EventOf> = EventMap[EventType] extends void + ? never + : EventMap[EventType]; /** @public */ -export type EventHandlerOf< - EventMap extends DefaultEventMap, - EventType extends AnyEventTypeOf, -> = EventMap[EventType] extends void +export type EventHandlerOf> = EventMap[EventType] extends void ? () => unknown | Promise : (event: EventMap[EventType]) => unknown | Promise; @@ -43,39 +33,26 @@ export type EventHandlerOf< export type OffCallbackHandler = () => void; /** @public */ -export interface IAsyncDispatcher< - EventMap extends DefaultEventMap = DefaultEventMap, -> { - on< - T extends AnyEventOf, - EventType extends AnyEventTypeOf = EventTypeOf, - >( +export interface IAsyncDispatcher { + on, EventType extends AnyEventTypeOf = EventTypeOf>( type: EventType, handler: EventHandlerOf, ): OffCallbackHandler; - once< - T extends AnyEventOf, - EventType extends AnyEventTypeOf = EventTypeOf, - >( + once, EventType extends AnyEventTypeOf = EventTypeOf>( type: EventType, handler: EventHandlerOf, ): OffCallbackHandler; - off< - T extends AnyEventOf, - EventType extends AnyEventTypeOf = EventTypeOf, - >(type: EventType, handler: EventHandlerOf): void; + off, EventType extends AnyEventTypeOf = EventTypeOf>( + type: EventType, + handler: EventHandlerOf, + ): void; has(key: AnyEventTypeOf): boolean; events(): AnyEventTypeOf[]; - emit< - T extends AnyEventOf, - EventType extends AnyEventTypeOf = EventTypeOf, - >( + emit, EventType extends AnyEventTypeOf = EventTypeOf>( type: EventType, - ...[event]: EventOf extends void - ? [undefined?] - : [EventOf] + ...[event]: EventOf extends void ? [undefined?] : [EventOf] ): Promise; } @@ -89,13 +66,8 @@ const kEvents = Symbol('events'); * * @public */ -export class AsyncDispatcher - implements IAsyncDispatcher -{ - private [kEvents] = new Map< - AnyEventTypeOf, - AnyEventHandlerOf[] - >(); +export class AsyncDispatcher implements IAsyncDispatcher { + private [kEvents] = new Map, AnyEventHandlerOf[]>(); private [kOnce] = new WeakMap, number>(); @@ -142,7 +114,7 @@ export class AsyncDispatcher async emit(type: keyof EventMap, ...[event]: any[]): Promise { const list = [...(this[kEvents].get(type) ?? [])]; - for (const handler of list) { + for await (const handler of list) { await handler(event); if (this[kOnce].get(handler)) { diff --git a/packages/core/src/errors.ts b/packages/core/src/errors.ts index bd558f185..e73e3d30b 100644 --- a/packages/core/src/errors.ts +++ b/packages/core/src/errors.ts @@ -1,10 +1,7 @@ export class MatrixError extends Error { public readonly status: number = 400; - public constructor( - public readonly errcode: TCode, - message: string, - ) { + public constructor(public readonly errcode: TCode, message: string) { super(message); } @@ -33,10 +30,7 @@ export class UnknownTokenError extends MatrixError<'M_UNKNOWN_TOKEN'> { public softLogout?: boolean; - public constructor( - message: string, - { softLogout }: { softLogout?: boolean } = {}, - ) { + public constructor(message: string, { softLogout }: { softLogout?: boolean } = {}) { super('M_UNKNOWN_TOKEN', message); this.softLogout = softLogout; } @@ -97,10 +91,7 @@ export class LimitExceededError extends MatrixError<'M_LIMIT_EXCEEDED'> { * This is expected to be returned with a 404 HTTP status code if the endpoint is not implemented or a 405 HTTP status code if the endpoint is implemented, but the incorrect HTTP method is used. */ export class UnrecognizedError extends MatrixError<'M_UNRECOGNIZED'> { - private constructor( - message: string, - public readonly status: number, - ) { + private constructor(message: string, public readonly status: number) { super('M_UNRECOGNIZED', message); } @@ -222,10 +213,7 @@ export class UnsupportedRoomVersionError extends MatrixError<'M_UNSUPPORTED_ROOM export class IncompatibleRoomVersionError extends MatrixError<'M_INCOMPATIBLE_ROOM_VERSION'> { public roomVersion: string; - public constructor( - message: string, - { roomVersion }: { roomVersion: string }, - ) { + public constructor(message: string, { roomVersion }: { roomVersion: string }) { super('M_INCOMPATIBLE_ROOM_VERSION', message); this.roomVersion = roomVersion; } @@ -308,10 +296,7 @@ export class ExclusiveError extends MatrixError<'M_EXCLUSIVE'> { * Typically, this error will appear on routes which attempt to modify state (e.g.: sending messages, account data, etc) and not routes which only read state (e.g.: /sync, get account data, etc). */ export class ResourceLimitExceededError extends MatrixError<'M_RESOURCE_LIMIT_EXCEEDED'> { - public constructor( - message: string, - public readonly adminContact: string, - ) { + public constructor(message: string, public readonly adminContact: string) { super('M_RESOURCE_LIMIT_EXCEEDED', message); } @@ -343,6 +328,7 @@ export enum HttpStatus { export class HttpException extends Error { public readonly status: number; + public readonly response: string | Record; constructor(response: string | Record, status: number) { diff --git a/packages/core/src/events/edu/base.spec.ts b/packages/core/src/events/edu/base.spec.ts index 3b6b63b8a..b8c3c95d0 100644 --- a/packages/core/src/events/edu/base.spec.ts +++ b/packages/core/src/events/edu/base.spec.ts @@ -1,12 +1,6 @@ import { describe, expect, test } from 'bun:test'; -import { - type BaseEDU, - type FederationEDUResponse, - type MatrixEDU, - isFederationEDUResponse, - isMatrixEDU, -} from './base'; +import { type BaseEDU, type FederationEDUResponse, type MatrixEDU, isFederationEDUResponse, isMatrixEDU } from './base'; describe('BaseEDU', () => { describe('isMatrixEDU', () => { diff --git a/packages/core/src/events/edu/base.ts b/packages/core/src/events/edu/base.ts index 45a05625d..755d620a0 100644 --- a/packages/core/src/events/edu/base.ts +++ b/packages/core/src/events/edu/base.ts @@ -30,16 +30,10 @@ export const isMatrixEDU = (obj: unknown): obj is MatrixEDU => { } const edu = obj as Record; - return ( - typeof edu.edu_type === 'string' && - typeof edu.content === 'object' && - edu.content !== null - ); + return typeof edu.edu_type === 'string' && typeof edu.content === 'object' && edu.content !== null; }; -export const isFederationEDUResponse = ( - obj: unknown, -): obj is FederationEDUResponse => { +export const isFederationEDUResponse = (obj: unknown): obj is FederationEDUResponse => { if (typeof obj !== 'object' || obj === null) { return false; } @@ -48,9 +42,7 @@ export const isFederationEDUResponse = ( return 'edus' in response && Array.isArray(response.edus); }; -export const isFederationEventWithEDUs = ( - response: unknown, -): response is FederationEDUResponse => { +export const isFederationEventWithEDUs = (response: unknown): response is FederationEDUResponse => { if (typeof response !== 'object' || response === null) { return false; } diff --git a/packages/core/src/events/edu/index.spec.ts b/packages/core/src/events/edu/index.spec.ts index 5a3dd6d83..03c65f0ac 100644 --- a/packages/core/src/events/edu/index.spec.ts +++ b/packages/core/src/events/edu/index.spec.ts @@ -9,11 +9,7 @@ import { createTypingEDU, isTypingEDU } from './m.typing'; describe('EDU Index', () => { describe('MatrixEDUTypes union type', () => { test('accepts TypingEDU', () => { - const typingEDU = createTypingEDU( - '!room:example.com', - '@user:example.com', - true, - ); + const typingEDU = createTypingEDU('!room:example.com', '@user:example.com', true); const eduUnion: MatrixEDUTypes = typingEDU; @@ -44,11 +40,7 @@ describe('EDU Index', () => { }); test('union type preserves specific EDU properties', () => { - const typingEDU = createTypingEDU( - '!room:example.com', - '@user:example.com', - true, - ); + const typingEDU = createTypingEDU('!room:example.com', '@user:example.com', true); const presenceEDU = createPresenceEDU([ { user_id: '@user:example.com', @@ -66,9 +58,7 @@ describe('EDU Index', () => { test('can be used in discriminated union patterns', () => { const edus: MatrixEDUTypes[] = [ createTypingEDU('!room:example.com', '@user:example.com', true), - createPresenceEDU([ - { user_id: '@user:example.com', presence: 'online' }, - ]), + createPresenceEDU([{ user_id: '@user:example.com', presence: 'online' }]), ]; for (const edu of edus) { diff --git a/packages/core/src/events/edu/index.ts b/packages/core/src/events/edu/index.ts index f07dd993c..4f79aa4e1 100644 --- a/packages/core/src/events/edu/index.ts +++ b/packages/core/src/events/edu/index.ts @@ -1,10 +1,10 @@ +import type { BaseEDU } from './base'; +import type { PresenceEDU } from './m.presence'; +import type { TypingEDU } from './m.typing'; + export * from './base'; export * from './m.typing'; export * from './m.presence'; -import type { BaseEDU } from './base'; -import type { PresenceEDU } from './m.presence'; -import type { TypingEDU } from './m.typing'; - export type MatrixEDUTypes = TypingEDU | PresenceEDU | BaseEDU; diff --git a/packages/core/src/events/edu/m.presence.spec.ts b/packages/core/src/events/edu/m.presence.spec.ts index 10e0e9457..e70c28e1c 100644 --- a/packages/core/src/events/edu/m.presence.spec.ts +++ b/packages/core/src/events/edu/m.presence.spec.ts @@ -1,12 +1,7 @@ import { describe, expect, test } from 'bun:test'; import type { BaseEDU } from './base'; -import { - type PresenceEDU, - type PresenceUpdate, - createPresenceEDU, - isPresenceEDU, -} from './m.presence'; +import { type PresenceEDU, type PresenceUpdate, createPresenceEDU, isPresenceEDU } from './m.presence'; describe('PresenceEDU', () => { describe('createPresenceEDU', () => { @@ -162,11 +157,7 @@ describe('PresenceEDU', () => { }); test('validates presence state values', () => { - const validStates: Array<'online' | 'offline' | 'unavailable'> = [ - 'online', - 'offline', - 'unavailable', - ]; + const validStates: Array<'online' | 'offline' | 'unavailable'> = ['online', 'offline', 'unavailable']; for (const state of validStates) { const update: PresenceUpdate = { diff --git a/packages/core/src/events/edu/m.presence.ts b/packages/core/src/events/edu/m.presence.ts index 7e81bcbe2..7f542d592 100644 --- a/packages/core/src/events/edu/m.presence.ts +++ b/packages/core/src/events/edu/m.presence.ts @@ -32,10 +32,7 @@ export const isPresenceEDU = (edu: BaseEDU): edu is PresenceEDU => { return edu.edu_type === 'm.presence'; }; -export const createPresenceEDU = ( - presenceUpdates: PresenceUpdate[], - origin?: string, -): PresenceEDU => ({ +export const createPresenceEDU = (presenceUpdates: PresenceUpdate[], origin?: string): PresenceEDU => ({ edu_type: 'm.presence', content: { push: presenceUpdates, diff --git a/packages/core/src/events/edu/m.typing.spec.ts b/packages/core/src/events/edu/m.typing.spec.ts index 675fd0734..c2b1f33fa 100644 --- a/packages/core/src/events/edu/m.typing.spec.ts +++ b/packages/core/src/events/edu/m.typing.spec.ts @@ -96,11 +96,7 @@ describe('TypingEDU', () => { }); test('type guard correctly narrows type', () => { - const edu: BaseEDU = createTypingEDU( - '!room:example.com', - '@user:example.com', - true, - ); + const edu: BaseEDU = createTypingEDU('!room:example.com', '@user:example.com', true); if (isTypingEDU(edu)) { expect(edu.content.room_id).toBe('!room:example.com'); @@ -114,12 +110,7 @@ describe('TypingEDU', () => { describe('TypingEDU interface', () => { test('typing EDU has correct structure', () => { - const edu = createTypingEDU( - '!room:example.com', - '@user:example.com', - true, - 'example.com', - ); + const edu = createTypingEDU('!room:example.com', '@user:example.com', true, 'example.com'); expect(edu).toHaveProperty('edu_type', 'm.typing'); expect(edu).toHaveProperty('content'); diff --git a/packages/core/src/events/edu/m.typing.ts b/packages/core/src/events/edu/m.typing.ts index 4d06b73fa..fd56d6be0 100644 --- a/packages/core/src/events/edu/m.typing.ts +++ b/packages/core/src/events/edu/m.typing.ts @@ -21,12 +21,7 @@ export const isTypingEDU = (edu: BaseEDU): edu is TypingEDU => { return edu.edu_type === 'm.typing'; }; -export const createTypingEDU = ( - roomId: string, - userId: string, - typing: boolean, - origin?: string, -): TypingEDU => ({ +export const createTypingEDU = (roomId: string, userId: string, typing: boolean, origin?: string): TypingEDU => ({ edu_type: 'm.typing', content: { room_id: roomId, diff --git a/packages/core/src/events/eventBase.ts b/packages/core/src/events/eventBase.ts index 93916c355..a4e474759 100644 --- a/packages/core/src/events/eventBase.ts +++ b/packages/core/src/events/eventBase.ts @@ -1,5 +1,4 @@ -import { Pdu, PduForType } from '@rocket.chat/federation-room'; -import type { EventID } from '@rocket.chat/federation-room'; +import type { Pdu, PduForType, EventID } from '@rocket.chat/federation-room'; export type EventBase = { auth_events: EventID[]; @@ -37,12 +36,11 @@ export type RedactedEvent = EventBase & { type: 'm.room.redaction'; }; -export const isRedactedEvent = ( - event: Pdu, -): event is PduForType<'m.room.redaction'> => { +export const isRedactedEvent = (event: Pdu): event is PduForType<'m.room.redaction'> => { return event.type === 'm.room.redaction' && 'redacts' in event; }; +// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface Events {} type KeyEvent = keyof Events; @@ -63,21 +61,13 @@ export const createEventBase = ( ts?: number; }, ): Events[T] extends EventBase ? Events[T] : never => { - return _createEventBase< - Events[T] extends EventBase ? Events[T] : never, - Events[T]['content'], - Events[T]['unsigned'] - >({ + return _createEventBase({ type, ...props, }); }; -const _createEventBase = < - E extends EventBase, - TContent extends EventBase['content'], - TUnsigned extends EventBase['unsigned'], ->({ +const _createEventBase = ({ roomId, sender, auth_events = [], @@ -104,9 +94,7 @@ const _createEventBase = < origin?: string; ts?: number; }): E & { - unsigned: E['unsigned'] extends void - ? { age_ts: number } - : E['unsigned'] & { age_ts: number }; + unsigned: E['unsigned'] extends void ? { age_ts: number } : E['unsigned'] & { age_ts: number }; } => { if (!sender.includes(':') || !sender.includes('@')) { throw new Error('Invalid sender'); diff --git a/packages/core/src/events/isRoomMemberEvent.ts b/packages/core/src/events/isRoomMemberEvent.ts index 44ea9fec5..1905c5eff 100644 --- a/packages/core/src/events/isRoomMemberEvent.ts +++ b/packages/core/src/events/isRoomMemberEvent.ts @@ -1,12 +1,8 @@ -import { Pdu } from '@rocket.chat/federation-room'; +import type { Pdu } from '@rocket.chat/federation-room'; + import type { EventBase } from './eventBase'; -export type JoinRule = - | 'invite' - | 'knock' - | 'public' - | 'restricted' - | 'knock_restricted'; +export type JoinRule = 'invite' | 'knock' | 'public' | 'restricted' | 'knock_restricted'; export type Membership = 'join' | 'invite' | 'leave' | 'knock' | 'ban'; diff --git a/packages/core/src/events/m.reaction.spec.ts b/packages/core/src/events/m.reaction.spec.ts index 6cff7e53f..dbb0aa782 100644 --- a/packages/core/src/events/m.reaction.spec.ts +++ b/packages/core/src/events/m.reaction.spec.ts @@ -1,9 +1,9 @@ import { expect, test } from 'bun:test'; +import { reactionEvent } from './m.reaction'; import { generateId } from '../utils/generateId'; import { generateKeyPairsFromString } from '../utils/keys'; import { signEvent } from '../utils/signEvent'; -import { reactionEvent } from './m.reaction'; const finalEvent = { auth_events: [ @@ -28,9 +28,7 @@ const finalEvent = { }; test('reactionEvent', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw', - ); + const signature = await generateKeyPairsFromString('ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw'); const { state_key: reactionStateKey, ...reaction } = reactionEvent({ roomId: '!MZyyuzkUwHEaBBOXai:hs1', diff --git a/packages/core/src/events/m.reaction.ts b/packages/core/src/events/m.reaction.ts index 30ad68ed5..6585da38b 100644 --- a/packages/core/src/events/m.reaction.ts +++ b/packages/core/src/events/m.reaction.ts @@ -1,4 +1,5 @@ import type { EventID } from '@rocket.chat/federation-room'; + import { type EventBase, createEventBase } from './eventBase'; import { createEventWithId } from './utils/createSignedEvent'; @@ -43,9 +44,7 @@ export interface ReactionEvent extends EventBase { }; } -const isTruthy = ( - value: T | null | undefined | false | 0 | '', -): value is T => { +const isTruthy = (value: T | null | undefined | false | 0 | ''): value is T => { return Boolean(value); }; @@ -79,11 +78,7 @@ export const reactionEvent = ({ return createEventBase('m.reaction', { roomId, sender, - auth_events: [ - auth_events['m.room.create'], - auth_events['m.room.power_levels'], - auth_events['m.room.member'], - ].filter(isTruthy), + auth_events: [auth_events['m.room.create'], auth_events['m.room.power_levels'], auth_events['m.room.member']].filter(isTruthy), prev_events, depth, content, diff --git a/packages/core/src/events/m.room.create.spec.ts b/packages/core/src/events/m.room.create.spec.ts index 38658e284..364d23265 100644 --- a/packages/core/src/events/m.room.create.spec.ts +++ b/packages/core/src/events/m.room.create.spec.ts @@ -1,18 +1,13 @@ import { expect, test } from 'bun:test'; -import { - PersistentEventFactory, - RoomVersion, -} from '@rocket.chat/federation-room'; +import type { RoomVersion } from '@rocket.chat/federation-room'; +import { PersistentEventFactory } from '@rocket.chat/federation-room'; + import type { SignedEvent } from '../types'; +import { type RoomCreateEvent, isRoomCreateEvent, roomCreateEvent } from './m.room.create'; import { generateId } from '../utils/generateId'; import { generateKeyPairsFromString } from '../utils/keys'; import { signEvent } from '../utils/signEvent'; -import { - type RoomCreateEvent, - isRoomCreateEvent, - roomCreateEvent, -} from './m.room.create'; const finalEventId = '$0AQU5dG_mtjH6qavAxYrQsDC0a_-6T3DHs1yoxf5fz4'; const finalEvent = { @@ -34,17 +29,14 @@ const finalEvent = { signatures: { hs1: { - 'ed25519:a_HDhg': - 'rmnvsWlTL+JP8Sk9767UR0svF4IrzC9zhUPbT+y4u31r/qtIaF9OtT1FP8tD/yFGD92qoTcRb4Oo8DRbLRXcAg', + 'ed25519:a_HDhg': 'rmnvsWlTL+JP8Sk9767UR0svF4IrzC9zhUPbT+y4u31r/qtIaF9OtT1FP8tD/yFGD92qoTcRb4Oo8DRbLRXcAg', }, }, unsigned: { age_ts: 1733107418648 }, }; test('roomCreateEvent', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw', - ); + const signature = await generateKeyPairsFromString('ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw'); const event = roomCreateEvent({ roomId: '!uTqsSSWabZzthsSCNf:hs1', @@ -54,9 +46,7 @@ test('roomCreateEvent', async () => { const signed = await signEvent(event, signature, 'hs1'); - expect(signed).toStrictEqual( - finalEvent as unknown as SignedEvent, - ); + expect(signed).toStrictEqual(finalEvent as unknown as SignedEvent); expect(signed).toHaveProperty( 'signatures.hs1.ed25519:a_HDhg', 'rmnvsWlTL+JP8Sk9767UR0svF4IrzC9zhUPbT+y4u31r/qtIaF9OtT1FP8tD/yFGD92qoTcRb4Oo8DRbLRXcAg', @@ -86,9 +76,7 @@ const roomId = '!uTqsSSWabZzthsSCNf:hs1'; const timestamp = 1733107418648; test('roomCreateEvent with factory', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw', - ); + const signature = await generateKeyPairsFromString('ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw'); const sender = '@admin:hs1'; @@ -114,12 +102,7 @@ test('roomCreateEvent with factory', async () => { finalEvent.content.room_version as RoomVersion, ); - const signed = await signEvent( - createEvent.redactedEvent as any, - signature, - 'hs1', - false, - ); + const signed = await signEvent(createEvent.redactedEvent as any, signature, 'hs1', false); expect({ ...signed, @@ -131,7 +114,7 @@ test('roomCreateEvent with factory', async () => { 'rmnvsWlTL+JP8Sk9767UR0svF4IrzC9zhUPbT+y4u31r/qtIaF9OtT1FP8tD/yFGD92qoTcRb4Oo8DRbLRXcAg', ); - const eventId = createEvent.eventId; + const { eventId } = createEvent; expect(eventId).toBe(finalEventId); }); diff --git a/packages/core/src/events/m.room.create.ts b/packages/core/src/events/m.room.create.ts index 51ee05e27..f16f5884b 100644 --- a/packages/core/src/events/m.room.create.ts +++ b/packages/core/src/events/m.room.create.ts @@ -23,11 +23,7 @@ type RoomCreateEventProps = { ts?: number; }; -export const roomCreateEvent = ({ - roomId, - sender, - ts = Date.now(), -}: RoomCreateEventProps): RoomCreateEvent => +export const roomCreateEvent = ({ roomId, sender, ts = Date.now() }: RoomCreateEventProps): RoomCreateEvent => createEventBase('m.room.create', { roomId, sender, @@ -42,9 +38,6 @@ export const roomCreateEvent = ({ unsigned: { age_ts: ts }, }); -export const createRoomCreateEvent = createEventWithId((...args: any[]) => - roomCreateEvent(args[0]), -); +export const createRoomCreateEvent = createEventWithId((...args: any[]) => roomCreateEvent(args[0])); -export const isRoomCreateEvent = (event: EventBase): event is RoomCreateEvent => - event.type === 'm.room.create'; +export const isRoomCreateEvent = (event: EventBase): event is RoomCreateEvent => event.type === 'm.room.create'; diff --git a/packages/core/src/events/m.room.guest_access.spec.ts b/packages/core/src/events/m.room.guest_access.spec.ts index 9db0d580e..23c1d73bf 100644 --- a/packages/core/src/events/m.room.guest_access.spec.ts +++ b/packages/core/src/events/m.room.guest_access.spec.ts @@ -1,9 +1,9 @@ import { expect, test } from 'bun:test'; +import { roomGuestAccessEvent } from './m.room.guest_access'; import { generateId } from '../utils/generateId'; import { generateKeyPairsFromString } from '../utils/keys'; import { signEvent } from '../utils/signEvent'; -import { roomGuestAccessEvent } from './m.room.guest_access'; const finalEventId = '$gdAY3-3DdjuG-uyFkDn8q5wPS4fbymH__fch9BQmOas'; const finalEvent = { @@ -24,17 +24,14 @@ const finalEvent = { hashes: { sha256: 'ArUZZ33x+j5oMNWhWvHDXBH7qrMRMbsqig5XDM5jOac' }, signatures: { hs1: { - 'ed25519:a_HDhg': - 'PLaE7un6a+pzrsU/0kiB/tvneZp5/dEda4+uE7UK411hNaM4W4ZUo52ua6AGO9q5gLBjSmnR90/tPf714HiTBw', + 'ed25519:a_HDhg': 'PLaE7un6a+pzrsU/0kiB/tvneZp5/dEda4+uE7UK411hNaM4W4ZUo52ua6AGO9q5gLBjSmnR90/tPf714HiTBw', }, }, unsigned: { age_ts: 1733107418721 }, } as const; test('roomGuestAccessEvent', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw', - ); + const signature = await generateKeyPairsFromString('ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw'); const event = roomGuestAccessEvent({ roomId: '!uTqsSSWabZzthsSCNf:hs1', @@ -50,7 +47,7 @@ test('roomGuestAccessEvent', async () => { }); const signed = await signEvent(event, signature, 'hs1'); - // @ts-ignore + // @ts-expect-error --- IGNORE --- expect(signed).toStrictEqual(finalEvent); expect(signed).toHaveProperty( 'signatures.hs1.ed25519:a_HDhg', diff --git a/packages/core/src/events/m.room.guest_access.ts b/packages/core/src/events/m.room.guest_access.ts index 03d5368ff..5ee1d300c 100644 --- a/packages/core/src/events/m.room.guest_access.ts +++ b/packages/core/src/events/m.room.guest_access.ts @@ -1,4 +1,5 @@ -import { EventID } from '@rocket.chat/federation-room'; +import type { EventID } from '@rocket.chat/federation-room'; + import { type EventBase, createEventBase } from './eventBase'; import { createEventWithId } from './utils/createSignedEvent'; @@ -44,5 +45,4 @@ export const roomGuestAccessEvent = ({ }); }; -export const createRoomGuestAccessEvent = - createEventWithId(roomGuestAccessEvent); +export const createRoomGuestAccessEvent = createEventWithId(roomGuestAccessEvent); diff --git a/packages/core/src/events/m.room.history_visibility.spec.ts b/packages/core/src/events/m.room.history_visibility.spec.ts index 97cc5107b..7cdbb4c19 100644 --- a/packages/core/src/events/m.room.history_visibility.spec.ts +++ b/packages/core/src/events/m.room.history_visibility.spec.ts @@ -1,9 +1,9 @@ import { expect, test } from 'bun:test'; +import { roomHistoryVisibilityEvent } from './m.room.history_visibility'; import { generateId } from '../utils/generateId'; import { generateKeyPairsFromString } from '../utils/keys'; import { signEvent } from '../utils/signEvent'; -import { roomHistoryVisibilityEvent } from './m.room.history_visibility'; const finalEventId = '$a4hYydlvVc738DgFJA4hDHaIl_umBkHSV_efweAO5PE'; const finalEvent = { @@ -24,17 +24,14 @@ const finalEvent = { hashes: { sha256: 'H1w6a6qPvdTtb2WKqacazfgiZvVHgK9/Np5DorJIy40' }, signatures: { hs1: { - 'ed25519:a_HDhg': - 'ZHzOfPU2BYDilKSrt5zqMBC9ohZtHph4uLldOIzBY/oTO1pZCp3D9CRr04h5eJ7zkkuzkNv4y8+N0TDPNMHFBg', + 'ed25519:a_HDhg': 'ZHzOfPU2BYDilKSrt5zqMBC9ohZtHph4uLldOIzBY/oTO1pZCp3D9CRr04h5eJ7zkkuzkNv4y8+N0TDPNMHFBg', }, }, unsigned: { age_ts: 1733107418720 }, }; test('roomHistoryVisibilityEvent', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw', - ); + const signature = await generateKeyPairsFromString('ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw'); const event = roomHistoryVisibilityEvent({ roomId: '!uTqsSSWabZzthsSCNf:hs1', @@ -52,7 +49,7 @@ test('roomHistoryVisibilityEvent', async () => { const signed = await signEvent(event, signature, 'hs1'); event.content.history_visibility; signed.content.history_visibility; - // @ts-ignore + // @ts-expect-error --- IGNORE --- expect(signed).toStrictEqual(finalEvent); expect(signed).toHaveProperty( 'signatures.hs1.ed25519:a_HDhg', diff --git a/packages/core/src/events/m.room.history_visibility.ts b/packages/core/src/events/m.room.history_visibility.ts index 14c640efc..617a0d018 100644 --- a/packages/core/src/events/m.room.history_visibility.ts +++ b/packages/core/src/events/m.room.history_visibility.ts @@ -1,4 +1,5 @@ -import { EventID } from '@rocket.chat/federation-room'; +import type { EventID } from '@rocket.chat/federation-room'; + import { type EventBase, createEventBase } from './eventBase'; import { createEventWithId } from './utils/createSignedEvent'; @@ -46,6 +47,4 @@ export const roomHistoryVisibilityEvent = ({ }); }; -export const createRoomHistoryVisibilityEvent = createEventWithId( - roomHistoryVisibilityEvent, -); +export const createRoomHistoryVisibilityEvent = createEventWithId(roomHistoryVisibilityEvent); diff --git a/packages/core/src/events/m.room.join_rules.spec.ts b/packages/core/src/events/m.room.join_rules.spec.ts index 976e9c43e..c6a6b7a23 100644 --- a/packages/core/src/events/m.room.join_rules.spec.ts +++ b/packages/core/src/events/m.room.join_rules.spec.ts @@ -1,9 +1,9 @@ import { expect, test } from 'bun:test'; +import { isRoomJoinRulesEvent, roomJoinRulesEvent } from './m.room.join_rules'; import { generateId } from '../utils/generateId'; import { generateKeyPairsFromString } from '../utils/keys'; import { signEvent } from '../utils/signEvent'; -import { isRoomJoinRulesEvent, roomJoinRulesEvent } from './m.room.join_rules'; const finalEventId = '$Uxo9MgF-4HQNEZdkkQDzgh9wlZ1yJbDXTMXCh6aZBi4'; const finalEvent = { @@ -24,17 +24,14 @@ const finalEvent = { hashes: { sha256: 'd3g1gHQsf/chWvoUMLe9iJlQQoVxEm6ajBW4Wdq9LUQ' }, signatures: { hs1: { - 'ed25519:a_HDhg': - 'egXzghr88RZMZYG4/DUrIf92NiUiC59GhgmvB1zV5oSuDuCGXgYnVBmXOfQ54ElXx1AFc8ajwPmfupXoYkHaAg', + 'ed25519:a_HDhg': 'egXzghr88RZMZYG4/DUrIf92NiUiC59GhgmvB1zV5oSuDuCGXgYnVBmXOfQ54ElXx1AFc8ajwPmfupXoYkHaAg', }, }, unsigned: { age_ts: 1733107418719 }, }; test('roomJoinRulesEvent', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw', - ); + const signature = await generateKeyPairsFromString('ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw'); const event = roomJoinRulesEvent({ roomId: '!uTqsSSWabZzthsSCNf:hs1', @@ -50,7 +47,7 @@ test('roomJoinRulesEvent', async () => { }); const signed = await signEvent(event, signature, 'hs1'); - // @ts-ignore + // @ts-expect-error --- IGNORE --- expect(signed).toStrictEqual(finalEvent); expect(signed).toHaveProperty( 'signatures.hs1.ed25519:a_HDhg', diff --git a/packages/core/src/events/m.room.join_rules.ts b/packages/core/src/events/m.room.join_rules.ts index 1b3832da1..fccc40b2e 100644 --- a/packages/core/src/events/m.room.join_rules.ts +++ b/packages/core/src/events/m.room.join_rules.ts @@ -1,4 +1,5 @@ -import { EventID } from '@rocket.chat/federation-room'; +import type { EventID } from '@rocket.chat/federation-room'; + import { type EventBase, createEventBase } from './eventBase'; import { createEventWithId } from './utils/createSignedEvent'; @@ -8,12 +9,7 @@ declare module './eventBase' { } } -export type JoinRule = - | 'invite' - | 'knock' - | 'public' - | 'restricted' - | 'knock_restricted'; +export type JoinRule = 'invite' | 'knock' | 'public' | 'restricted' | 'knock_restricted'; export interface RoomJoinRulesEvent extends EventBase { content: { @@ -55,8 +51,6 @@ export const roomJoinRulesEvent = ({ export const createRoomJoinRulesEvent = createEventWithId(roomJoinRulesEvent); -export const isRoomJoinRulesEvent = ( - event: EventBase, -): event is RoomJoinRulesEvent => { +export const isRoomJoinRulesEvent = (event: EventBase): event is RoomJoinRulesEvent => { return event.type === 'm.room.join_rules'; }; diff --git a/packages/core/src/events/m.room.member-invite.spec.ts b/packages/core/src/events/m.room.member-invite.spec.ts index b7ab03283..a052bc989 100644 --- a/packages/core/src/events/m.room.member-invite.spec.ts +++ b/packages/core/src/events/m.room.member-invite.spec.ts @@ -1,9 +1,9 @@ import { expect, test } from 'bun:test'; +import { roomMemberEvent } from './m.room.member'; import { generateId } from '../utils/generateId'; import { generateKeyPairsFromString } from '../utils/keys'; import { signEvent } from '../utils/signEvent'; -import { roomMemberEvent } from './m.room.member'; const finalEventId = '$nzfaHPXjmyOQerkm4WOlFupYVq56ZDHqC42DlgPydaI'; const finalEvent = { @@ -30,8 +30,7 @@ const finalEvent = { hashes: { sha256: '1tVvOpcM7iQz9kacDoUa8vhNMQVZZhA+5txzwsH8NR8' }, signatures: { hs1: { - 'ed25519:a_HDhg': - '8/qPp2d0PTc4bVMNdbTl32OSFnNqXan9ACQr1QcDV3SgdsDnm+sZv2mXW8rdhIOLOohRG2cED0+1aNxV7VH2Cw', + 'ed25519:a_HDhg': '8/qPp2d0PTc4bVMNdbTl32OSFnNqXan9ACQr1QcDV3SgdsDnm+sZv2mXW8rdhIOLOohRG2cED0+1aNxV7VH2Cw', }, }, unsigned: { @@ -61,9 +60,7 @@ const finalEvent = { }; test('roomMemberInviteEvent', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw', - ); + const signature = await generateKeyPairsFromString('ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw'); const memberEvent = roomMemberEvent({ membership: 'invite', @@ -76,10 +73,8 @@ test('roomMemberInviteEvent', async () => { 'm.room.create': '$0AQU5dG_mtjH6qavAxYrQsDC0a_-6T3DHs1yoxf5fz4', 'm.room.power_levels': '$T20EETjD2OuaC1OVyg8iIbJGTNeGBsMiWoAagBOVRNE', 'm.room.join_rules': '$Uxo9MgF-4HQNEZdkkQDzgh9wlZ1yJbDXTMXCh6aZBi4', - 'm.room.member:@admin:hs1': - '$tZRt2bwceX4sG913Ee67tJiwe-gk859kY2mCeYSncw8', - 'm.room.history_visibility': - '$HistVisAuthEvent123456789012345678901234567890', + 'm.room.member:@admin:hs1': '$tZRt2bwceX4sG913Ee67tJiwe-gk859kY2mCeYSncw8', + 'm.room.history_visibility': '$HistVisAuthEvent123456789012345678901234567890', }, prev_events: ['$gdAY3-3DdjuG-uyFkDn8q5wPS4fbymH__fch9BQmOas'], content: { @@ -116,7 +111,6 @@ test('roomMemberInviteEvent', async () => { } as const); const signed = await signEvent(memberEvent, signature, 'hs1'); - // @ts-ignore expect(signed).toStrictEqual(finalEvent); expect(signed).toHaveProperty( 'signatures.hs1.ed25519:a_HDhg', diff --git a/packages/core/src/events/m.room.member.spec.ts b/packages/core/src/events/m.room.member.spec.ts index f4cfbaf12..d5c6d4457 100644 --- a/packages/core/src/events/m.room.member.spec.ts +++ b/packages/core/src/events/m.room.member.spec.ts @@ -1,10 +1,10 @@ import { expect, test } from 'bun:test'; +import { roomCreateEvent } from './m.room.create'; +import { roomMemberEvent } from './m.room.member'; import { generateId } from '../utils/generateId'; import { generateKeyPairsFromString } from '../utils/keys'; import { signEvent } from '../utils/signEvent'; -import { roomCreateEvent } from './m.room.create'; -import { roomMemberEvent } from './m.room.member'; const finalEventId = '$tZRt2bwceX4sG913Ee67tJiwe-gk859kY2mCeYSncw8'; const finalEvent = { @@ -21,17 +21,14 @@ const finalEvent = { hashes: { sha256: '7qLYbHf6z6nLGkN0DABO89wgDjaeZwq0ma7GsPbhZ8I' }, signatures: { hs1: { - 'ed25519:a_HDhg': - 'y/qV5T9PeXvqgwRafZDSygtk4XRMstdt04qusZWJSu77Juxzzz4Ijyk+JsJ5NNV0/WWYMT9IhmVb7/EEBH4vDQ', + 'ed25519:a_HDhg': 'y/qV5T9PeXvqgwRafZDSygtk4XRMstdt04qusZWJSu77Juxzzz4Ijyk+JsJ5NNV0/WWYMT9IhmVb7/EEBH4vDQ', }, }, unsigned: { age_ts: 1733107418672 }, }; test('roomMemberEvent', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw', - ); + const signature = await generateKeyPairsFromString('ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw'); const createEvent = roomCreateEvent({ roomId: '!uTqsSSWabZzthsSCNf:hs1', @@ -58,7 +55,7 @@ test('roomMemberEvent', async () => { }); const signed = await signEvent(memberEvent, signature, 'hs1'); - // @ts-ignore + // @ts-expect-error --- IGNORE --- expect(signed).toStrictEqual(finalEvent); expect(signed).toHaveProperty( 'signatures.hs1.ed25519:a_HDhg', @@ -71,9 +68,7 @@ test('roomMemberEvent', async () => { }); test('roomMemberEvent - leave', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw', - ); + const signature = await generateKeyPairsFromString('ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw'); const serverName = 'hs1'; const roomId = '!leaveRoomTest:hs1'; const userId = '@user_to_leave:hs1'; @@ -84,11 +79,7 @@ test('roomMemberEvent - leave', async () => { sender: userId, ts: ts - 1000, }); - const signedCreateEvent = await signEvent( - createEventPayload, - signature, - serverName, - ); + const signedCreateEvent = await signEvent(createEventPayload, signature, serverName); const createEventId = generateId(signedCreateEvent); // A user usually joins before they can leave @@ -104,11 +95,7 @@ test('roomMemberEvent - leave', async () => { ts: ts - 500, origin: serverName, }); - const signedJoinEvent = await signEvent( - joinMemberEventPayload, - signature, - serverName, - ); + const signedJoinEvent = await signEvent(joinMemberEventPayload, signature, serverName); const joinEventId = generateId(signedJoinEvent); // Now, the leave event @@ -130,11 +117,7 @@ test('roomMemberEvent - leave', async () => { }, }); - const signedLeaveEvent = await signEvent( - leaveMemberEventPayload, - signature, - serverName, - ); + const signedLeaveEvent = await signEvent(leaveMemberEventPayload, signature, serverName); const leaveEventId = generateId(signedLeaveEvent); expect(signedLeaveEvent.type).toBe('m.room.member'); @@ -148,21 +131,13 @@ test('roomMemberEvent - leave', async () => { expect(signedLeaveEvent.auth_events).toContain(createEventId); expect(signedLeaveEvent.auth_events).toContain(joinEventId); expect(leaveEventId).toBeDefined(); - expect( - signedLeaveEvent.signatures[serverName][ - `${signature.algorithm}:${signature.version}` - ], - ).toBeString(); + expect(signedLeaveEvent.signatures[serverName][`${signature.algorithm}:${signature.version}`]).toBeString(); expect(Object.keys(signedLeaveEvent.content).length).toBe(1); }); test('roomMemberEvent - kick', async () => { - const kickerSignature = await generateKeyPairsFromString( - 'ed25519 kicker_key WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw', - ); - const userToKickSignature = await generateKeyPairsFromString( - 'ed25519 kicked_key WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw', - ); + const kickerSignature = await generateKeyPairsFromString('ed25519 kicker_key WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw'); + const userToKickSignature = await generateKeyPairsFromString('ed25519 kicked_key WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw'); const serverName = 'hs1'; const roomId = '!kickRoomTest:hs1'; const kickerId = '@kicker:hs1'; @@ -176,11 +151,7 @@ test('roomMemberEvent - kick', async () => { sender: kickerId, ts: ts - 4000, }); - const signedCreateEvent = await signEvent( - createEventPayload, - kickerSignature, - serverName, - ); + const signedCreateEvent = await signEvent(createEventPayload, kickerSignature, serverName); const createEventId = generateId(signedCreateEvent); let lastEventId = createEventId; let currentDepth = 1; @@ -214,11 +185,7 @@ test('roomMemberEvent - kick', async () => { origin: serverName, origin_server_ts: ts - 3000, }; - const signedPowerLevelsEvent = await signEvent( - powerLevelsEventPayload, - kickerSignature, - serverName, - ); + const signedPowerLevelsEvent = await signEvent(powerLevelsEventPayload, kickerSignature, serverName); const powerLevelsEventId = generateId(signedPowerLevelsEvent); lastEventId = powerLevelsEventId; @@ -238,11 +205,7 @@ test('roomMemberEvent - kick', async () => { ts: ts - 2000, origin: serverName, }); - const signedKickerJoinEvent = await signEvent( - kickerJoinEventPayload, - kickerSignature, - serverName, - ); + const signedKickerJoinEvent = await signEvent(kickerJoinEventPayload, kickerSignature, serverName); const kickerJoinEventId = generateId(signedKickerJoinEvent); lastEventId = kickerJoinEventId; @@ -262,11 +225,7 @@ test('roomMemberEvent - kick', async () => { ts: ts - 1000, origin: serverName, }); - const signedUserToKickJoinEvent = await signEvent( - userToKickJoinEventPayload, - userToKickSignature, - serverName, - ); + const signedUserToKickJoinEvent = await signEvent(userToKickJoinEventPayload, userToKickSignature, serverName); const userToKickJoinEventId = generateId(signedUserToKickJoinEvent); lastEventId = userToKickJoinEventId; @@ -292,11 +251,7 @@ test('roomMemberEvent - kick', async () => { }, }); - const signedKickEvent = await signEvent( - kickMemberEventPayload, - kickerSignature, - serverName, - ); + const signedKickEvent = await signEvent(kickMemberEventPayload, kickerSignature, serverName); const kickEventId = generateId(signedKickEvent); // Assertions @@ -316,21 +271,13 @@ test('roomMemberEvent - kick', async () => { expect(signedKickEvent.auth_events).toContain(kickerJoinEventId); expect(kickEventId).toBeDefined(); - expect( - signedKickEvent.signatures[serverName][ - `${kickerSignature.algorithm}:${kickerSignature.version}` - ], - ).toBeString(); + expect(signedKickEvent.signatures[serverName][`${kickerSignature.algorithm}:${kickerSignature.version}`]).toBeString(); expect(Object.keys(signedKickEvent.content).length).toBe(2); // membership + reason }); test('roomMemberEvent - ban', async () => { - const bannerSignature = await generateKeyPairsFromString( - 'ed25519 banner_key WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw', - ); - const userToBanSignature = await generateKeyPairsFromString( - 'ed25519 banned_key WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw', - ); + const bannerSignature = await generateKeyPairsFromString('ed25519 banner_key WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw'); + const userToBanSignature = await generateKeyPairsFromString('ed25519 banned_key WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw'); const serverName = 'hs1'; const roomId = '!banRoomTest:hs1'; const bannerId = '@banner:hs1'; @@ -344,11 +291,7 @@ test('roomMemberEvent - ban', async () => { sender: bannerId, ts: ts - 4000, }); - const signedCreateEvent = await signEvent( - createEventPayload, - bannerSignature, - serverName, - ); + const signedCreateEvent = await signEvent(createEventPayload, bannerSignature, serverName); const createEventId = generateId(signedCreateEvent); let lastEventId = createEventId; let currentDepth = 1; @@ -382,11 +325,7 @@ test('roomMemberEvent - ban', async () => { origin: serverName, origin_server_ts: ts - 3000, }; - const signedPowerLevelsEvent = await signEvent( - powerLevelsEventPayload, - bannerSignature, - serverName, - ); + const signedPowerLevelsEvent = await signEvent(powerLevelsEventPayload, bannerSignature, serverName); const powerLevelsEventId = generateId(signedPowerLevelsEvent); lastEventId = powerLevelsEventId; @@ -406,11 +345,7 @@ test('roomMemberEvent - ban', async () => { ts: ts - 2000, origin: serverName, }); - const signedBannerJoinEvent = await signEvent( - bannerJoinEventPayload, - bannerSignature, - serverName, - ); + const signedBannerJoinEvent = await signEvent(bannerJoinEventPayload, bannerSignature, serverName); const bannerJoinEventId = generateId(signedBannerJoinEvent); lastEventId = bannerJoinEventId; @@ -430,11 +365,7 @@ test('roomMemberEvent - ban', async () => { ts: ts - 1000, origin: serverName, }); - const signedUserToBanJoinEvent = await signEvent( - userToBanJoinEventPayload, - userToBanSignature, - serverName, - ); + const signedUserToBanJoinEvent = await signEvent(userToBanJoinEventPayload, userToBanSignature, serverName); const userToBanJoinEventId = generateId(signedUserToBanJoinEvent); lastEventId = userToBanJoinEventId; @@ -460,11 +391,7 @@ test('roomMemberEvent - ban', async () => { }, }); - const signedBanEvent = await signEvent( - banMemberEventPayload, - bannerSignature, - serverName, - ); + const signedBanEvent = await signEvent(banMemberEventPayload, bannerSignature, serverName); const banEventId = generateId(signedBanEvent); // Assertions @@ -484,10 +411,6 @@ test('roomMemberEvent - ban', async () => { expect(signedBanEvent.auth_events).toContain(bannerJoinEventId); expect(banEventId).toBeDefined(); - expect( - signedBanEvent.signatures[serverName][ - `${bannerSignature.algorithm}:${bannerSignature.version}` - ], - ).toBeString(); + expect(signedBanEvent.signatures[serverName][`${bannerSignature.algorithm}:${bannerSignature.version}`]).toBeString(); expect(Object.keys(signedBanEvent.content).length).toBe(2); // membership + reason }); diff --git a/packages/core/src/events/m.room.member.ts b/packages/core/src/events/m.room.member.ts index 571662b44..b84ee6032 100644 --- a/packages/core/src/events/m.room.member.ts +++ b/packages/core/src/events/m.room.member.ts @@ -1,4 +1,5 @@ -import { EventID } from '@rocket.chat/federation-room'; +import type { EventID } from '@rocket.chat/federation-room'; + import { createEventBase } from './eventBase'; import type { Membership, RoomMemberEvent } from './isRoomMemberEvent'; import { createEventWithId } from './utils/createSignedEvent'; @@ -27,9 +28,7 @@ export type AuthEvents = { [K in `m.room.member:${string}`]?: EventID; }; -const isTruthy = ( - value: T | null | undefined | false | 0 | '', -): value is T => { +const isTruthy = (value: T | null | undefined | false | 0 | ''): value is T => { return Boolean(value); }; diff --git a/packages/core/src/events/m.room.message.ts b/packages/core/src/events/m.room.message.ts index d4a18dd93..71813feb8 100644 --- a/packages/core/src/events/m.room.message.ts +++ b/packages/core/src/events/m.room.message.ts @@ -1,21 +1,19 @@ import type { EventID } from '@rocket.chat/federation-room'; + import { type EventBase, createEventBase } from './eventBase'; import { createEventWithId } from './utils/createSignedEvent'; export type TextMessageType = 'm.text' | 'm.emote' | 'm.notice'; export type FileMessageType = 'm.image' | 'm.file' | 'm.audio' | 'm.video'; export type LocationMessageType = 'm.location'; -export type MessageType = - | TextMessageType - | FileMessageType - | LocationMessageType; +export type MessageType = TextMessageType | FileMessageType | LocationMessageType; // Base message content type BaseMessageContent = { - body: string; + 'body': string; 'm.mentions'?: Record; - format?: string; - formatted_body?: string; + 'format'?: string; + 'formatted_body'?: string; 'm.relates_to'?: MessageRelation; }; @@ -62,11 +60,7 @@ declare module './eventBase' { unsigned: { age_ts: number; }; - content: ( - | TextMessageContent - | FileMessageContent - | LocationMessageContent - ) & { + content: (TextMessageContent | FileMessageContent | LocationMessageContent) & { 'm.new_content'?: NewContent; }; }; @@ -76,18 +70,13 @@ declare module './eventBase' { export type MessageRelation = { rel_type: RelationType; event_id: EventID; -} & ( - | RelationTypeReplace - | RelationTypeAnnotation - | RelationTypeThread - | Record -); +} & (RelationTypeReplace | RelationTypeAnnotation | RelationTypeThread | Record); export type RelationType = 'm.replace' | 'm.annotation' | 'm.thread'; export type RelationTypeReplace = { - rel_type: 'm.replace'; - event_id: EventID; + 'rel_type': 'm.replace'; + 'event_id': EventID; 'm.new_content'?: { body: string; msgtype: MessageType; @@ -103,15 +92,15 @@ export type RelationTypeAnnotation = { }; export type RelationTypeThread = { - rel_type: 'm.thread'; - event_id: EventID; + 'rel_type': 'm.thread'; + 'event_id': EventID; 'm.in_reply_to'?: { event_id: EventID; room_id: string; sender: string; origin_server_ts: number; }; - is_falling_back?: boolean; + 'is_falling_back'?: boolean; }; export type MessageAuthEvents = { @@ -120,19 +109,13 @@ export type MessageAuthEvents = { 'm.room.member': EventID; }; -export const isRoomMessageEvent = ( - event: EventBase, -): event is RoomMessageEvent => { +export const isRoomMessageEvent = (event: EventBase): event is RoomMessageEvent => { return event.type === 'm.room.message'; }; export interface RoomMessageEvent extends EventBase { type: 'm.room.message'; - content: ( - | TextMessageContent - | FileMessageContent - | LocationMessageContent - ) & { + content: (TextMessageContent | FileMessageContent | LocationMessageContent) & { 'm.new_content'?: NewContent; }; unsigned: { @@ -141,9 +124,7 @@ export interface RoomMessageEvent extends EventBase { }; } -const isTruthy = ( - value: T | null | undefined | false | 0 | '', -): value is T => { +const isTruthy = (value: T | null | undefined | false | 0 | ''): value is T => { return Boolean(value); }; @@ -164,11 +145,7 @@ export const roomMessageEvent = ({ prev_events: EventID[]; depth: number; unsigned?: RoomMessageEvent['unsigned']; - content: ( - | TextMessageContent - | FileMessageContent - | LocationMessageContent - ) & { + content: (TextMessageContent | FileMessageContent | LocationMessageContent) & { 'm.new_content'?: NewContent; }; origin?: string; @@ -177,11 +154,7 @@ export const roomMessageEvent = ({ return createEventBase('m.room.message', { roomId, sender, - auth_events: [ - auth_events['m.room.create'], - auth_events['m.room.power_levels'], - auth_events['m.room.member'], - ].filter(isTruthy), + auth_events: [auth_events['m.room.create'], auth_events['m.room.power_levels'], auth_events['m.room.member']].filter(isTruthy), prev_events, depth, content, diff --git a/packages/core/src/events/m.room.name.spec.ts b/packages/core/src/events/m.room.name.spec.ts index eb37befd2..e3e3bd83c 100644 --- a/packages/core/src/events/m.room.name.spec.ts +++ b/packages/core/src/events/m.room.name.spec.ts @@ -1,12 +1,12 @@ import { describe, expect, test } from 'bun:test'; import type { SignedEvent } from '../types'; -import { generateId } from '../utils/generateId'; -import { generateKeyPairsFromString } from '../utils/keys'; -import { signEvent } from '../utils/signEvent'; import { roomCreateEvent } from './m.room.create'; import { roomMemberEvent } from './m.room.member'; import { type RoomNameEvent, roomNameEvent } from './m.room.name'; +import { generateId } from '../utils/generateId'; +import { generateKeyPairsFromString } from '../utils/keys'; +import { signEvent } from '../utils/signEvent'; const finalEventId = '$JdX_s3d4CORV_BfkatJxF_lUfzJoKjzQXTP0NGtVj1E'; const finalEventPlaceholder: SignedEvent = { @@ -35,17 +35,14 @@ const finalEventPlaceholder: SignedEvent = { }, signatures: { rc1: { - 'ed25519:a_HDhg': - 'ZHKRV0W5bGYNdyP1CZ3PPjc/3ZC6DUOiEBtCpNwvh2aDnrNly7SxjjYHdkAi5+ZpZVB57o8AKsVuDoGnfV2cDQ', + 'ed25519:a_HDhg': 'ZHKRV0W5bGYNdyP1CZ3PPjc/3ZC6DUOiEBtCpNwvh2aDnrNly7SxjjYHdkAi5+ZpZVB57o8AKsVuDoGnfV2cDQ', }, }, }; describe('roomNameEvent', () => { test('it should correctly create, sign, and generate an ID for an m.room.name event', async () => { - const signingKey = await generateKeyPairsFromString( - 'ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw', - ); + const signingKey = await generateKeyPairsFromString('ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw'); const serverName = 'rc1'; const roomId = '!MZyyuzkUwHEaBBOXai:hs1'; const userId = '@user1:rc1'; @@ -57,11 +54,7 @@ describe('roomNameEvent', () => { sender: userId, ts: exampleTimestamp - 2000, // No explicit content needed here }); - const signedCreateEvent = await signEvent( - createEventPayload, - signingKey, - serverName, - ); + const signedCreateEvent = await signEvent(createEventPayload, signingKey, serverName); const createEventId = generateId(signedCreateEvent); // 2. Mock Power Levels Event ID (as in m.room.message.spec.ts) @@ -79,11 +72,7 @@ describe('roomNameEvent', () => { auth_events: { 'm.room.create': createEventId }, prev_events: [createEventId], }); - const signedMemberEvent = await signEvent( - memberEventPayload, - signingKey, - serverName, - ); + const signedMemberEvent = await signEvent(memberEventPayload, signingKey, serverName); const memberEventId = generateId(signedMemberEvent); // 4. Room Name Event @@ -102,11 +91,7 @@ describe('roomNameEvent', () => { origin: serverName, }); - const signedRoomNameEvent = await signEvent( - roomNamePayload, - signingKey, - serverName, - ); + const signedRoomNameEvent = await signEvent(roomNamePayload, signingKey, serverName); const generatedEventId = generateId(signedRoomNameEvent); expect(finalEventId).toBe(generatedEventId); diff --git a/packages/core/src/events/m.room.name.ts b/packages/core/src/events/m.room.name.ts index 0ee459704..264c4456b 100644 --- a/packages/core/src/events/m.room.name.ts +++ b/packages/core/src/events/m.room.name.ts @@ -1,4 +1,5 @@ -import { EventID } from '@rocket.chat/federation-room'; +import type { EventID } from '@rocket.chat/federation-room'; + import { type EventBase, createEventBase } from './eventBase'; import { createEventWithId } from './utils/createSignedEvent'; @@ -34,9 +35,7 @@ export interface RoomNameEvent extends EventBase { }; } -const isTruthy = ( - value: T | null | undefined | false | 0 | '', -): value is T => { +const isTruthy = (value: T | null | undefined | false | 0 | ''): value is T => { return Boolean(value); }; @@ -65,11 +64,7 @@ export const roomNameEvent = ({ roomId, sender, state_key: '', - auth_events: [ - auth_events['m.room.create'], - auth_events['m.room.power_levels'], - auth_events['m.room.member'], - ].filter(isTruthy), + auth_events: [auth_events['m.room.create'], auth_events['m.room.power_levels'], auth_events['m.room.member']].filter(isTruthy), prev_events, depth, content, diff --git a/packages/core/src/events/m.room.power_levels.spec.ts b/packages/core/src/events/m.room.power_levels.spec.ts index 8453cb5e9..60e477bd2 100644 --- a/packages/core/src/events/m.room.power_levels.spec.ts +++ b/packages/core/src/events/m.room.power_levels.spec.ts @@ -1,20 +1,14 @@ import { expect, test } from 'bun:test'; import type { SignedEvent } from '../types'; +import { type RoomPowerLevelsEvent, roomPowerLevelsEvent } from './m.room.power_levels'; import { generateId } from '../utils/generateId'; import { generateKeyPairsFromString } from '../utils/keys'; import { signEvent } from '../utils/signEvent'; -import { - type RoomPowerLevelsEvent, - roomPowerLevelsEvent, -} from './m.room.power_levels'; const finalEventId = '$T20EETjD2OuaC1OVyg8iIbJGTNeGBsMiWoAagBOVRNE'; const finalEvent = { - auth_events: [ - '$0AQU5dG_mtjH6qavAxYrQsDC0a_-6T3DHs1yoxf5fz4', - '$tZRt2bwceX4sG913Ee67tJiwe-gk859kY2mCeYSncw8', - ], + auth_events: ['$0AQU5dG_mtjH6qavAxYrQsDC0a_-6T3DHs1yoxf5fz4', '$tZRt2bwceX4sG913Ee67tJiwe-gk859kY2mCeYSncw8'], prev_events: ['$tZRt2bwceX4sG913Ee67tJiwe-gk859kY2mCeYSncw8'], type: 'm.room.power_levels', room_id: '!uTqsSSWabZzthsSCNf:hs1', @@ -47,8 +41,7 @@ const finalEvent = { hashes: { sha256: '7Sv2UTnpNI9qnVO1oXaNoj1SEraxoWTm9uloqm3Oqho' }, signatures: { hs1: { - 'ed25519:a_HDhg': - 'UBNpsQBCDX7t6cPHSj+g4bfAf/9Gb1TxYnme2MCXF4JgN7P3X0OUq0leFjrI5p/+sTR60/nuaZCX7OUYWTTLDA', + 'ed25519:a_HDhg': 'UBNpsQBCDX7t6cPHSj+g4bfAf/9Gb1TxYnme2MCXF4JgN7P3X0OUq0leFjrI5p/+sTR60/nuaZCX7OUYWTTLDA', }, }, unsigned: { age_ts: 1733107418713 }, @@ -92,17 +85,14 @@ const finalCustomEvent: Omit, 'event_id'> = { hashes: { sha256: 'y0ffUmoWZ9WYPiGk8fdrDyu0Sc7JRpTBsmnjRUoPL7I' }, signatures: { hs1: { - 'ed25519:test_key_custom': - 'x8W5woA58MTdNlxF5PY+m3MvrJVOmBOVuB/xG3+kQ/pX6EEdmAexVUYGCtzf7GcIk9TsGGG6Q1NmJOxyH6PrBQ', + 'ed25519:test_key_custom': 'x8W5woA58MTdNlxF5PY+m3MvrJVOmBOVuB/xG3+kQ/pX6EEdmAexVUYGCtzf7GcIk9TsGGG6Q1NmJOxyH6PrBQ', }, }, unsigned: { age_ts: 1748224026175 }, }; test('roomPowerLevelsEvent', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw', - ); + const signature = await generateKeyPairsFromString('ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw'); const event = roomPowerLevelsEvent({ roomId: '!uTqsSSWabZzthsSCNf:hs1', @@ -130,9 +120,7 @@ test('roomPowerLevelsEvent', async () => { }); test('roomPowerLevelsEvent with custom content', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 test_key_custom WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw', - ); + const signature = await generateKeyPairsFromString('ed25519 test_key_custom WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw'); const roomId = '!customRoom:hs1'; const senderId = '@customSender:hs1'; @@ -179,10 +167,7 @@ test('roomPowerLevelsEvent with custom content', async () => { ts: 1748224026175, }); - const signed: Omit< - SignedEvent, - 'event_id' - > = await signEvent(event, signature, 'hs1'); + const signed: Omit, 'event_id'> = await signEvent(event, signature, 'hs1'); const eventId = generateId(signed); expect(signed).toStrictEqual(finalCustomEvent); diff --git a/packages/core/src/events/m.room.power_levels.ts b/packages/core/src/events/m.room.power_levels.ts index 81d025a82..634e6cd38 100644 --- a/packages/core/src/events/m.room.power_levels.ts +++ b/packages/core/src/events/m.room.power_levels.ts @@ -1,4 +1,5 @@ -import { EventID } from '@rocket.chat/federation-room'; +import type { EventID } from '@rocket.chat/federation-room'; + import { type EventBase, createEventBase } from './eventBase'; import { createEventWithId } from './utils/createSignedEvent'; @@ -8,14 +9,7 @@ declare module './eventBase' { } } -export type PowerLevelNames = - | 'events_default' - | 'state_default' - | 'ban' - | 'kick' - | 'redact' - | 'invite' - | 'historical'; +export type PowerLevelNames = 'events_default' | 'state_default' | 'ban' | 'kick' | 'redact' | 'invite' | 'historical'; export interface RoomPowerLevelsEvent extends EventBase { content: { @@ -67,11 +61,9 @@ export const roomPowerLevelsEvent = ({ return createEventBase('m.room.power_levels', { roomId, sender, - auth_events: [ - auth_events['m.room.create'], - auth_events['m.room.power_levels'], - auth_events['m.room.member'], - ].filter(Boolean) as EventID[], + auth_events: [auth_events['m.room.create'], auth_events['m.room.power_levels'], auth_events['m.room.member']].filter( + Boolean, + ) as EventID[], prev_events, depth, ts, @@ -107,11 +99,8 @@ export const roomPowerLevelsEvent = ({ }); }; -export const createRoomPowerLevelsEvent = - createEventWithId(roomPowerLevelsEvent); +export const createRoomPowerLevelsEvent = createEventWithId(roomPowerLevelsEvent); -export const isRoomPowerLevelsEvent = ( - event: EventBase, -): event is RoomPowerLevelsEvent => { +export const isRoomPowerLevelsEvent = (event: EventBase): event is RoomPowerLevelsEvent => { return event.type === 'm.room.power_levels'; }; diff --git a/packages/core/src/events/m.room.redaction.spec.ts b/packages/core/src/events/m.room.redaction.spec.ts index 34475b853..834b1962d 100644 --- a/packages/core/src/events/m.room.redaction.spec.ts +++ b/packages/core/src/events/m.room.redaction.spec.ts @@ -1,10 +1,10 @@ import { expect, test } from 'bun:test'; +import type { EventBase } from './eventBase'; +import { isRedactionEvent, redactionEvent } from './m.room.redaction'; import { generateId } from '../utils/generateId'; import { generateKeyPairsFromString } from '../utils/keys'; import { signEvent } from '../utils/signEvent'; -import type { EventBase } from './eventBase'; -import { isRedactionEvent, redactionEvent } from './m.room.redaction'; test('isRedactionEvent', () => { // Test case 1: Should return true for a redaction event @@ -55,9 +55,7 @@ test('isRedactionEvent', () => { }); test('redactionEvent', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw', - ); + const signature = await generateKeyPairsFromString('ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw'); const { state_key: redactionStateKey, ...redaction } = redactionEvent({ roomId: '!MZyyuzkUwHEaBBOXai:hs1', @@ -78,9 +76,7 @@ test('redactionEvent', async () => { }); // Verify that the redacts property is in the original event - expect(redaction.redacts).toBe( - '$8ftnUd9WTPTQGbdPgfOPea8bOEQ21qPvbcGqeOApQxA', - ); + expect(redaction.redacts).toBe('$8ftnUd9WTPTQGbdPgfOPea8bOEQ21qPvbcGqeOApQxA'); const signedRedaction = await signEvent(redaction, signature, 'rc1'); const redactionEventId = generateId(signedRedaction); diff --git a/packages/core/src/events/m.room.redaction.ts b/packages/core/src/events/m.room.redaction.ts index 61b23649a..2a09ba680 100644 --- a/packages/core/src/events/m.room.redaction.ts +++ b/packages/core/src/events/m.room.redaction.ts @@ -1,4 +1,5 @@ -import { EventID } from '@rocket.chat/federation-room'; +import type { EventID } from '@rocket.chat/federation-room'; + import { type EventBase, createEventBase } from './eventBase'; import { createEventWithId } from './utils/createSignedEvent'; @@ -28,9 +29,7 @@ export interface RedactionEvent extends EventBase { redacts: EventID; } -const isTruthy = ( - value: T | null | undefined | false | 0 | '', -): value is T => { +const isTruthy = (value: T | null | undefined | false | 0 | ''): value is T => { return Boolean(value); }; @@ -65,11 +64,7 @@ export const redactionEvent = ({ const baseEvent = createEventBase('m.room.redaction', { roomId, sender, - auth_events: [ - auth_events['m.room.create'], - auth_events['m.room.power_levels'], - auth_events['m.room.member'], - ].filter(isTruthy), + auth_events: [auth_events['m.room.create'], auth_events['m.room.power_levels'], auth_events['m.room.member']].filter(isTruthy), prev_events, depth, content: { diff --git a/packages/core/src/events/m.room.third_party_invite.ts b/packages/core/src/events/m.room.third_party_invite.ts index 3eca7f952..c82351ce8 100644 --- a/packages/core/src/events/m.room.third_party_invite.ts +++ b/packages/core/src/events/m.room.third_party_invite.ts @@ -22,8 +22,6 @@ export interface RoomThirdPartyInviteEvent extends EventBase { }; } -export const isRoomThirdPartyInviteEvent = ( - event: EventBase, -): event is RoomThirdPartyInviteEvent => { +export const isRoomThirdPartyInviteEvent = (event: EventBase): event is RoomThirdPartyInviteEvent => { return event.type === 'm.room.third_party_invite'; }; diff --git a/packages/core/src/events/m.room.tombstone.spec.ts b/packages/core/src/events/m.room.tombstone.spec.ts index 294a5ced2..70173cbf6 100644 --- a/packages/core/src/events/m.room.tombstone.spec.ts +++ b/packages/core/src/events/m.room.tombstone.spec.ts @@ -1,13 +1,10 @@ import { describe, expect, test } from 'bun:test'; + +import type { EventBase } from './eventBase'; +import { createRoomTombstoneEvent, isRoomTombstoneEvent, roomTombstoneEvent } from './m.room.tombstone'; import { generateId } from '../utils/generateId'; import { generateKeyPairsFromString } from '../utils/keys'; import { signEvent } from '../utils/signEvent'; -import type { EventBase } from './eventBase'; -import { - createRoomTombstoneEvent, - isRoomTombstoneEvent, - roomTombstoneEvent, -} from './m.room.tombstone'; describe('m.room.tombstone', () => { const roomId = '!someroom:example.com'; @@ -69,9 +66,7 @@ describe('m.room.tombstone', () => { }); expect(isRoomTombstoneEvent(event)).toBe(true); - expect(isRoomTombstoneEvent({ ...event, type: 'm.room.message' })).toBe( - false, - ); + expect(isRoomTombstoneEvent({ ...event, type: 'm.room.message' })).toBe(false); }); test('should thoroughly validate different types of events with isRoomTombstoneEvent', () => { @@ -97,27 +92,18 @@ describe('m.room.tombstone', () => { expect(isRoomTombstoneEvent(messageEvent)).toBe(false); expect(isRoomTombstoneEvent(createEvent)).toBe(false); expect(isRoomTombstoneEvent(memberEvent)).toBe(false); - expect(isRoomTombstoneEvent(malformedEvent as unknown as EventBase)).toBe( - false, - ); - expect( - isRoomTombstoneEvent(objectWithTypeOnly as unknown as EventBase), - ).toBe(true); + expect(isRoomTombstoneEvent(malformedEvent as unknown as EventBase)).toBe(false); + expect(isRoomTombstoneEvent(objectWithTypeOnly as unknown as EventBase)).toBe(true); expect(isRoomTombstoneEvent(null as unknown as EventBase)).toBe(false); expect(isRoomTombstoneEvent(undefined as unknown as EventBase)).toBe(false); expect(isRoomTombstoneEvent({} as unknown as EventBase)).toBe(false); - expect( - isRoomTombstoneEvent('m.room.tombstone' as unknown as EventBase), - ).toBe(false); + expect(isRoomTombstoneEvent('m.room.tombstone' as unknown as EventBase)).toBe(false); }); test('should validate event signature and ID', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw', - ); + const signature = await generateKeyPairsFromString('ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw'); - const expectedSignature = - 'leZQvRcjV9GwnSDtmuUtheOODSVpb98kszzyEw0X8ScqEYx0Z1eOIXhOFL3jcolFHGoFZMGYC5GGNN44x1BWBA'; + const expectedSignature = 'leZQvRcjV9GwnSDtmuUtheOODSVpb98kszzyEw0X8ScqEYx0Z1eOIXhOFL3jcolFHGoFZMGYC5GGNN44x1BWBA'; const expectedEventId = '$3Sw4iBWv9pil8BRt3ojPIWGLNQGpsKJoImxNoCmz7ME'; const event = roomTombstoneEvent({ @@ -142,9 +128,7 @@ describe('m.room.tombstone', () => { }); test('should create a room tombstone event with ID', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw', - ); + const signature = await generateKeyPairsFromString('ed25519 a_HDhg WntaJ4JP5WbZZjDShjeuwqCybQ5huaZAiowji7tnIEw'); const eventProps = { roomId, diff --git a/packages/core/src/events/m.room.tombstone.ts b/packages/core/src/events/m.room.tombstone.ts index 6b4769b89..12aaf9a28 100644 --- a/packages/core/src/events/m.room.tombstone.ts +++ b/packages/core/src/events/m.room.tombstone.ts @@ -1,4 +1,5 @@ -import { EventID } from '@rocket.chat/federation-room'; +import type { EventID } from '@rocket.chat/federation-room'; + import { type EventBase, createEventBase } from './eventBase'; import { createEventWithId } from './utils/createSignedEvent'; @@ -38,9 +39,7 @@ type RoomTombstoneEventProps = { origin?: string; }; -const isTruthy = ( - value: T | null | undefined | false | 0 | '', -): value is T => { +const isTruthy = (value: T | null | undefined | false | 0 | ''): value is T => { return Boolean(value); }; @@ -66,11 +65,7 @@ export const roomTombstoneEvent = ({ return createEventBase('m.room.tombstone', { roomId, sender, - auth_events: [ - auth_events['m.room.create'], - auth_events['m.room.power_levels'], - auth_events['m.room.member'], - ].filter(isTruthy), + auth_events: [auth_events['m.room.create'], auth_events['m.room.power_levels'], auth_events['m.room.member']].filter(isTruthy), prev_events, depth, content, @@ -84,9 +79,5 @@ export const roomTombstoneEvent = ({ export const createRoomTombstoneEvent = createEventWithId(roomTombstoneEvent); -export const isRoomTombstoneEvent = ( - event: EventBase | null | undefined, -): event is RoomTombstoneEvent => - Boolean( - event && typeof event === 'object' && event.type === 'm.room.tombstone', - ); +export const isRoomTombstoneEvent = (event: EventBase | null | undefined): event is RoomTombstoneEvent => + Boolean(event && typeof event === 'object' && event.type === 'm.room.tombstone'); diff --git a/packages/core/src/events/pdu.spec.ts b/packages/core/src/events/pdu.spec.ts index 19c1f71b7..63cf4e376 100644 --- a/packages/core/src/events/pdu.spec.ts +++ b/packages/core/src/events/pdu.spec.ts @@ -1,10 +1,7 @@ import { describe, expect, it } from 'bun:test'; + import type { EventBase } from './eventBase'; -import { - type FederationEventResponse, - type MatrixPDU, - isFederationEventWithPDUs, -} from './pdu'; +import { type FederationEventResponse, type MatrixPDU, isFederationEventWithPDUs } from './pdu'; describe('PDU', () => { const samplePDU: MatrixPDU = { diff --git a/packages/core/src/events/pdu.ts b/packages/core/src/events/pdu.ts index 2617cd18b..81443f3fc 100644 --- a/packages/core/src/events/pdu.ts +++ b/packages/core/src/events/pdu.ts @@ -1,4 +1,5 @@ -import { Pdu } from '@rocket.chat/federation-room'; +import type { Pdu } from '@rocket.chat/federation-room'; + import type { EventBase } from './eventBase'; /** @@ -24,13 +25,6 @@ export interface FederationEventResponse { pdus: Pdu[]; } -export const isFederationEventWithPDUs = ( - response: unknown, -): response is FederationEventResponse => { - return ( - response instanceof Object && - 'pdus' in response && - Array.isArray(response.pdus) && - response.pdus.length > 0 - ); +export const isFederationEventWithPDUs = (response: unknown): response is FederationEventResponse => { + return response instanceof Object && 'pdus' in response && Array.isArray(response.pdus) && response.pdus.length > 0; }; diff --git a/packages/core/src/events/utils/createEventWithId.spec.ts b/packages/core/src/events/utils/createEventWithId.spec.ts index c4570ff3d..29f818adf 100644 --- a/packages/core/src/events/utils/createEventWithId.spec.ts +++ b/packages/core/src/events/utils/createEventWithId.spec.ts @@ -1,13 +1,12 @@ import { describe, expect, test } from 'bun:test'; + +import { createEventWithId, createSignedEvent } from './createSignedEvent'; import { generateId } from '../../utils/generateId'; import { generateKeyPairsFromString } from '../../utils/keys'; -import { createEventWithId, createSignedEvent } from './createSignedEvent'; describe('createEventWithId', () => { test('it should add an ID to a signed event', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 a_XRhW YjbSyfqQeGto+OFswt+XwtJUUooHXH5w+czSgawN63U', - ); + const signature = await generateKeyPairsFromString('ed25519 a_XRhW YjbSyfqQeGto+OFswt+XwtJUUooHXH5w+czSgawN63U'); const testEvent = () => ({ type: 'm.test.event', @@ -29,9 +28,7 @@ describe('createEventWithId', () => { }); test('it should generate unique IDs for different events', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 a_XRhW YjbSyfqQeGto+OFswt+XwtJUUooHXH5w+czSgawN63U', - ); + const signature = await generateKeyPairsFromString('ed25519 a_XRhW YjbSyfqQeGto+OFswt+XwtJUUooHXH5w+czSgawN63U'); const testEvent1 = () => ({ type: 'm.test.event', @@ -63,9 +60,7 @@ describe('createEventWithId', () => { }); test('it should work with async event generators', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 a_XRhW YjbSyfqQeGto+OFswt+XwtJUUooHXH5w+czSgawN63U', - ); + const signature = await generateKeyPairsFromString('ed25519 a_XRhW YjbSyfqQeGto+OFswt+XwtJUUooHXH5w+czSgawN63U'); const asyncTestEvent = async () => { await new Promise((resolve) => setTimeout(resolve, 10)); diff --git a/packages/core/src/events/utils/createSignedEvent.spec.ts b/packages/core/src/events/utils/createSignedEvent.spec.ts index 88d17b399..7ebfa1437 100644 --- a/packages/core/src/events/utils/createSignedEvent.spec.ts +++ b/packages/core/src/events/utils/createSignedEvent.spec.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'bun:test'; + import { generateId } from '../../utils/generateId'; import { generateKeyPairsFromString } from '../../utils/keys'; import { signEvent } from '../../utils/signEvent'; @@ -7,9 +8,7 @@ import { createSignedEvent } from './createSignedEvent'; describe('makeSignedEvent', () => { test('it should return the same payload, following create event > sign > generate id', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 a_XRhW YjbSyfqQeGto+OFswt+XwtJUUooHXH5w+czSgawN63U', - ); + const signature = await generateKeyPairsFromString('ed25519 a_XRhW YjbSyfqQeGto+OFswt+XwtJUUooHXH5w+czSgawN63U'); const event = roomCreateEvent({ roomId: '!uTqsSSWabZzthsSCNf:hs1', @@ -29,7 +28,7 @@ describe('makeSignedEvent', () => { expect({ event: signed, _id: id, - // @ts-ignore + // @ts-expect-error --- IGNORE --- }).toStrictEqual(result); }); }); diff --git a/packages/core/src/events/utils/createSignedEvent.ts b/packages/core/src/events/utils/createSignedEvent.ts index 0cbb80bbb..710e5d7fb 100644 --- a/packages/core/src/events/utils/createSignedEvent.ts +++ b/packages/core/src/events/utils/createSignedEvent.ts @@ -1,31 +1,19 @@ -import type { SigningKey } from '../../types'; -import type { SignedEvent } from '../../types'; +import type { SigningKey, SignedEvent } from '../../types'; import { generateId } from '../../utils/generateId'; import { signEvent } from '../../utils/signEvent'; -export const createSignedEvent = ( - signature: SigningKey, - signingName: string, -) => { - // biome-ignore lint/suspicious/noExplicitAny: +export const createSignedEvent = (signature: SigningKey, signingName: string) => { return any>(fn: F) => { - return async ( - ...args: Parameters - ): Promise>> => { + return async (...args: Parameters): Promise>> => { const event = await fn(...args); - return signEvent(event, signature, signingName) as Promise< - SignedEvent> - >; + return signEvent(event, signature, signingName) as Promise>>; }; }; }; -// biome-ignore lint/suspicious/noExplicitAny: export const createEventWithId = any>(fn: F) => { return >(sign: S) => { - return async ( - ...args: Parameters - ): Promise<{ event: SignedEvent>; _id: string }> => { + return async (...args: Parameters): Promise<{ event: SignedEvent>; _id: string }> => { const event = await sign(fn)(...args); const id = generateId(event); return { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5fef90178..03f7328ac 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -33,11 +33,7 @@ export { } from './utils/signJson'; // Binary data utilities -export { - toBinaryData, - fromBinaryData, - toUnpaddedBase64, -} from './utils/binaryData'; +export { toBinaryData, fromBinaryData, toUnpaddedBase64 } from './utils/binaryData'; // Keys utilities export * from './utils/keys'; diff --git a/packages/core/src/models/event.model.ts b/packages/core/src/models/event.model.ts index a28a2b517..1f89916f2 100644 --- a/packages/core/src/models/event.model.ts +++ b/packages/core/src/models/event.model.ts @@ -1,9 +1,5 @@ -import type { - EventID, - Pdu, - RejectCode, - StateID, -} from '@rocket.chat/federation-room'; +import type { EventID, Pdu, RejectCode, StateID } from '@rocket.chat/federation-room'; + import type { EventBase as CoreEventBase } from '../events/eventBase'; // TODO: use room package diff --git a/packages/core/src/procedures/getPublicKeyFromServer.spec.ts b/packages/core/src/procedures/getPublicKeyFromServer.spec.ts index 4da5e4aa0..47c8a3486 100644 --- a/packages/core/src/procedures/getPublicKeyFromServer.spec.ts +++ b/packages/core/src/procedures/getPublicKeyFromServer.spec.ts @@ -1,9 +1,11 @@ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; + import { encodeCanonicalJson } from '@rocket.chat/federation-crypto'; import nacl from 'tweetnacl'; + import { EncryptionValidAlgorithm } from '../types'; -import { generateKeyPairs } from '../utils/keys'; import { getPublicKeyFromRemoteServer } from './getPublicKeyFromServer'; +import { generateKeyPairs } from '../utils/keys'; describe('getPublicKeyFromRemoteServer', () => { let originalFetch: typeof globalThis.fetch; @@ -17,11 +19,7 @@ describe('getPublicKeyFromRemoteServer', () => { const createValidServerKeyResponse = async () => { const seed = nacl.randomBytes(32); - const keyPair = await generateKeyPairs( - seed, - EncryptionValidAlgorithm.ed25519, - 'test_key', - ); + const keyPair = await generateKeyPairs(seed, EncryptionValidAlgorithm.ed25519, 'test_key'); const serverName = 'test.server'; const keyId = `${keyPair.algorithm}:${keyPair.version}`; const validUntil = Date.now() + 24 * 60 * 60 * 1000; @@ -38,9 +36,7 @@ describe('getPublicKeyFromRemoteServer', () => { }; const canonicalJson = encodeCanonicalJson(serverKey); - const signature = await keyPair.sign( - new TextEncoder().encode(canonicalJson), - ); + const signature = await keyPair.sign(new TextEncoder().encode(canonicalJson)); return { ...serverKey, @@ -79,11 +75,7 @@ describe('getPublicKeyFromRemoteServer', () => { const origin = 'test.server'; const algorithmAndVersion = `${EncryptionValidAlgorithm.ed25519}:test_key`; - const result = await getPublicKeyFromRemoteServer( - domain, - origin, - algorithmAndVersion, - ); + const result = await getPublicKeyFromRemoteServer(domain, origin, algorithmAndVersion); expect(result).toBeDefined(); expect(result.key).toBeDefined(); @@ -95,11 +87,7 @@ describe('getPublicKeyFromRemoteServer', () => { mockServerKeys.valid_until_ts = Date.now() - 1000; await expect( - getPublicKeyFromRemoteServer( - 'test.server', - 'test.server', - `${EncryptionValidAlgorithm.ed25519}:test_key`, - ), + getPublicKeyFromRemoteServer('test.server', 'test.server', `${EncryptionValidAlgorithm.ed25519}:test_key`), ).rejects.toThrow('Expired remote public key'); }); @@ -107,32 +95,18 @@ describe('getPublicKeyFromRemoteServer', () => { mockServerKeys.verify_keys = {}; await expect( - getPublicKeyFromRemoteServer( - 'test.server', - 'test.server', - `${EncryptionValidAlgorithm.ed25519}:test_key`, - ), + getPublicKeyFromRemoteServer('test.server', 'test.server', `${EncryptionValidAlgorithm.ed25519}:test_key`), ).rejects.toThrow('Public key not found'); }); it('should throw error for invalid algorithm', async () => { - await expect( - getPublicKeyFromRemoteServer( - 'test.server', - 'test.server', - 'invalid_alg:test_key', - ), - ).rejects.toThrow('Invalid algorithm'); + await expect(getPublicKeyFromRemoteServer('test.server', 'test.server', 'invalid_alg:test_key')).rejects.toThrow('Invalid algorithm'); }); it('should throw error for invalid algorithm format', async () => { - await expect( - getPublicKeyFromRemoteServer( - 'test.server', - 'test.server', - 'invalid_algorithm_format', - ), - ).rejects.toThrow('Invalid algorithm and version format'); + await expect(getPublicKeyFromRemoteServer('test.server', 'test.server', 'invalid_algorithm_format')).rejects.toThrow( + 'Invalid algorithm and version format', + ); }); it('should handle network errors gracefully', async () => { @@ -143,11 +117,7 @@ describe('getPublicKeyFromRemoteServer', () => { globalThis.fetch = errorFetch; await expect( - getPublicKeyFromRemoteServer( - 'test.server', - 'test.server', - `${EncryptionValidAlgorithm.ed25519}:test_key`, - ), + getPublicKeyFromRemoteServer('test.server', 'test.server', `${EncryptionValidAlgorithm.ed25519}:test_key`), ).rejects.toThrow(); }); @@ -162,21 +132,13 @@ describe('getPublicKeyFromRemoteServer', () => { globalThis.fetch = invalidJsonFetch; await expect( - getPublicKeyFromRemoteServer( - 'test.server', - 'test.server', - `${EncryptionValidAlgorithm.ed25519}:test_key`, - ), + getPublicKeyFromRemoteServer('test.server', 'test.server', `${EncryptionValidAlgorithm.ed25519}:test_key`), ).rejects.toThrow(); }); it('should successfully retrieve and validate a public key with different algorithm version', async () => { const seed = nacl.randomBytes(32); - const keyPair = await generateKeyPairs( - seed, - EncryptionValidAlgorithm.ed25519, - 'another_version', - ); + const keyPair = await generateKeyPairs(seed, EncryptionValidAlgorithm.ed25519, 'another_version'); const serverName = 'test.server'; const keyId = `${keyPair.algorithm}:${keyPair.version}`; @@ -195,9 +157,7 @@ describe('getPublicKeyFromRemoteServer', () => { }; const canonicalJson = encodeCanonicalJson(baseServerKey); - const signature = await keyPair.sign( - new TextEncoder().encode(canonicalJson), - ); + const signature = await keyPair.sign(new TextEncoder().encode(canonicalJson)); const fullServerKey = { ...baseServerKey, @@ -218,11 +178,7 @@ describe('getPublicKeyFromRemoteServer', () => { specialMockFetch.preconnect = async () => undefined; globalThis.fetch = specialMockFetch; - const result = await getPublicKeyFromRemoteServer( - serverName, - serverName, - keyId, - ); + const result = await getPublicKeyFromRemoteServer(serverName, serverName, keyId); expect(result).toBeDefined(); expect(result.key).toBeDefined(); diff --git a/packages/core/src/procedures/getPublicKeyFromServer.ts b/packages/core/src/procedures/getPublicKeyFromServer.ts index 0819a7a1b..9483b872b 100644 --- a/packages/core/src/procedures/getPublicKeyFromServer.ts +++ b/packages/core/src/procedures/getPublicKeyFromServer.ts @@ -1,17 +1,8 @@ import type { ServerKey } from '../server'; import { makeRequest } from '../utils/makeRequest'; +import { getSignaturesFromRemote, isValidAlgorithm, verifyJsonSignature } from '../utils/signJson'; -import { - getSignaturesFromRemote, - isValidAlgorithm, - verifyJsonSignature, -} from '../utils/signJson'; - -export const getPublicKeyFromRemoteServer = async ( - domain: string, - origin: string, - algorithmAndVersion: string, -) => { +export const getPublicKeyFromRemoteServer = async (domain: string, origin: string, algorithmAndVersion: string) => { const [algorithm, version] = algorithmAndVersion.split(':'); if (!algorithm || !version) { throw new Error('Invalid algorithm and version format'); @@ -42,23 +33,10 @@ export const getPublicKeyFromRemoteServer = async ( throw new Error(`No valid signature found for ${domain}`); } - const publicKeyBytes = Uint8Array.from(atob(publickey), (c) => - c.charCodeAt(0), - ); - const signatureBytes = Uint8Array.from(atob(signature.signature), (c) => - c.charCodeAt(0), - ); + const publicKeyBytes = Uint8Array.from(atob(publickey), (c) => c.charCodeAt(0)); + const signatureBytes = Uint8Array.from(atob(signature.signature), (c) => c.charCodeAt(0)); - if ( - !verifyJsonSignature( - result, - domain, - signatureBytes, - publicKeyBytes, - signature.algorithm, - signature.version, - ) - ) { + if (!verifyJsonSignature(result, domain, signatureBytes, publicKeyBytes, signature.algorithm, signature.version)) { throw new Error('Invalid signature'); } diff --git a/packages/core/src/procedures/makeJoin.spec.ts b/packages/core/src/procedures/makeJoin.spec.ts index b97992969..76f6b0393 100644 --- a/packages/core/src/procedures/makeJoin.spec.ts +++ b/packages/core/src/procedures/makeJoin.spec.ts @@ -1,9 +1,10 @@ import { describe, expect, it, mock } from 'bun:test'; + // Note: Error classes are now generic Error instances in the core package import { IncompatibleRoomVersionError, NotFoundError } from '../errors'; +import { makeJoinEventBuilder } from './makeJoin'; import type { AuthEvents } from '../events/m.room.member'; import type { EventStore } from '../models/event.model'; -import { makeJoinEventBuilder } from './makeJoin'; describe('makeJoinEventBuilder', () => { const mockRoomId = '!roomId:example.org'; @@ -12,27 +13,25 @@ describe('makeJoinEventBuilder', () => { const mockDate = 1620000000000; Date.now = () => mockDate; - const mockGetLastEvent = mock( - (roomId: string): Promise => { - return Promise.resolve({ - _id: 'lastEventId', - event: { - depth: 10, - room_id: roomId, - sender: '@otheruser:example.org', - type: 'm.room.message', - origin: 'example.org', - origin_server_ts: 1600000000000, - prev_events: [], - auth_events: [], - }, - stateId: 'stateId', - createdAt: new Date(mockDate), - nextEventId: 'nextEventId', - eventId: 'eventId', - }); - }, - ); + const mockGetLastEvent = mock((roomId: string): Promise => { + return Promise.resolve({ + _id: 'lastEventId', + event: { + depth: 10, + room_id: roomId, + sender: '@otheruser:example.org', + type: 'm.room.message', + origin: 'example.org', + origin_server_ts: 1600000000000, + prev_events: [], + auth_events: [], + }, + stateId: 'stateId', + createdAt: new Date(mockDate), + nextEventId: 'nextEventId', + eventId: 'eventId', + }); + }); const mockGetAuthEvents = mock((): Promise => { return Promise.resolve({ @@ -45,44 +44,25 @@ describe('makeJoinEventBuilder', () => { it('should throw IncompatibleRoomVersionError when room version is not supported', async () => { const makeJoin = makeJoinEventBuilder(mockGetLastEvent, mockGetAuthEvents); - await expect( - makeJoin(mockRoomId, mockUserId, ['1', '2', '9'], mockOrigin), - ).rejects.toEqual( - new IncompatibleRoomVersionError( - 'Your homeserver does not support the features required to join this room', - { roomVersion: '10' }, - ), + await expect(makeJoin(mockRoomId, mockUserId, ['1', '2', '9'], mockOrigin)).rejects.toEqual( + new IncompatibleRoomVersionError('Your homeserver does not support the features required to join this room', { roomVersion: '10' }), ); }); it('should throw NotFoundError when no events found for room', async () => { - const mockEmptyGetLastEvent = mock( - (_: string): Promise => Promise.resolve(null), - ); - const makeJoin = makeJoinEventBuilder( - mockEmptyGetLastEvent, - mockGetAuthEvents, - ); + const mockEmptyGetLastEvent = mock((_: string): Promise => Promise.resolve(null)); + const makeJoin = makeJoinEventBuilder(mockEmptyGetLastEvent, mockGetAuthEvents); - await expect( - makeJoin(mockRoomId, mockUserId, ['10'], mockOrigin), - ).rejects.toEqual( + await expect(makeJoin(mockRoomId, mockUserId, ['10'], mockOrigin)).rejects.toEqual( new NotFoundError(`No events found for room ${mockRoomId}`), ); }); it('should throw NotFoundError when no create event found for room', async () => { - const mockEmptyGetAuthEvents = mock( - (_: string): Promise => Promise.resolve({} as AuthEvents), - ); - const makeJoin = makeJoinEventBuilder( - mockGetLastEvent, - mockEmptyGetAuthEvents, - ); + const mockEmptyGetAuthEvents = mock((_: string): Promise => Promise.resolve({} as AuthEvents)); + const makeJoin = makeJoinEventBuilder(mockGetLastEvent, mockEmptyGetAuthEvents); - await expect( - makeJoin(mockRoomId, mockUserId, ['10'], mockOrigin), - ).rejects.toEqual( + await expect(makeJoin(mockRoomId, mockUserId, ['10'], mockOrigin)).rejects.toEqual( new NotFoundError(`No create event found for room ${mockRoomId}`), ); }); @@ -90,12 +70,7 @@ describe('makeJoinEventBuilder', () => { it('should create a valid join event', async () => { const makeJoin = makeJoinEventBuilder(mockGetLastEvent, mockGetAuthEvents); - const result = await makeJoin( - mockRoomId, - mockUserId, - ['10', '11'], - mockOrigin, - ); + const result = await makeJoin(mockRoomId, mockUserId, ['10', '11'], mockOrigin); expect(result).toEqual({ event: { @@ -104,11 +79,7 @@ describe('makeJoinEventBuilder', () => { room_id: mockRoomId, sender: mockUserId, state_key: mockUserId, - auth_events: [ - 'createEventId', - 'powerLevelsEventId', - 'joinRulesEventId', - ], + auth_events: ['createEventId', 'powerLevelsEventId', 'joinRulesEventId'], prev_events: ['lastEventId'], depth: 11, origin: mockOrigin, @@ -125,12 +96,7 @@ describe('makeJoinEventBuilder', () => { it('should work with multiple room versions', async () => { const makeJoin = makeJoinEventBuilder(mockGetLastEvent, mockGetAuthEvents); - const result = await makeJoin( - mockRoomId, - mockUserId, - ['9', '10', '11'], - mockOrigin, - ); + const result = await makeJoin(mockRoomId, mockUserId, ['9', '10', '11'], mockOrigin); expect(result.room_version).toEqual('10'); }); diff --git a/packages/core/src/procedures/makeJoin.ts b/packages/core/src/procedures/makeJoin.ts index 75fae6797..69fb3fd4b 100644 --- a/packages/core/src/procedures/makeJoin.ts +++ b/packages/core/src/procedures/makeJoin.ts @@ -7,21 +7,12 @@ import type { EventStore } from '../models/event.model'; // "url":"http://rc1:443/_matrix/federation/v1/make_join/%21kwkcWPpOXEJvlcollu%3Arc1/%40admin%3Ahs1?ver=1&ver=2&ver=3&ver=4&ver=5&ver=6&ver=7&ver=8&ver=9&ver=10&ver=11&ver=org.matrix.msc3757.10&ver=org.matrix.msc3757.11", export const makeJoinEventBuilder = - ( - getLastEvent: (roomId: string) => Promise, - getAuthEvents: (roomId: string) => Promise, - ) => - async ( - roomId: string, - userId: string, - roomVersions: string[], - origin: string, - ) => { + (getLastEvent: (roomId: string) => Promise, getAuthEvents: (roomId: string) => Promise) => + async (roomId: string, userId: string, roomVersions: string[], origin: string) => { if (!roomVersions.includes('10')) { - throw new IncompatibleRoomVersionError( - 'Your homeserver does not support the features required to join this room', - { roomVersion: '10' }, - ); + throw new IncompatibleRoomVersionError('Your homeserver does not support the features required to join this room', { + roomVersion: '10', + }); } const lastEvent = await getLastEvent(roomId); diff --git a/packages/core/src/server-discovery/discovery.spec.ts b/packages/core/src/server-discovery/discovery.spec.ts index 091fc8ab0..1d97c232c 100644 --- a/packages/core/src/server-discovery/discovery.spec.ts +++ b/packages/core/src/server-discovery/discovery.spec.ts @@ -1,20 +1,6 @@ -import { - afterAll, - afterEach, - describe, - expect, - it, - jest, - mock, -} from 'bun:test'; - -/** - * This is a workaround restore the original module, since bun module mock is a bit weird - */ +import { afterAll, afterEach, describe, expect, it, jest, mock } from 'bun:test'; import ResolverModule from 'node:dns/promises'; -const { Resolver } = ResolverModule; - import { addressWithDefaultPort, getAddressFromTargetWellKnownEndpoint, @@ -27,6 +13,12 @@ import { wellKnownCache, } from './discovery'; +/** + * This is a workaround restore the original module, since bun module mock is a bit weird + */ + +const { Resolver } = ResolverModule; + const mockResolver = { resolveAny: jest.fn(), resolveSrv: jest.fn(), @@ -69,9 +61,7 @@ describe('#isIpLiteral()', () => { it('should return true for IP addresses with ports', () => { expect(isIpLiteral('192.168.1.1:8080')).toBe(true); - expect(isIpLiteral('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:443')).toBe( - true, - ); + expect(isIpLiteral('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:443')).toBe(true); }); }); @@ -81,9 +71,7 @@ describe('#addressWithDefaultPort()', () => { }); it('should append default port 8448 to IPv6 address', () => { - expect( - addressWithDefaultPort('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]'), - ).toBe('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8448'); + expect(addressWithDefaultPort('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]')).toBe('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8448'); }); it('should append default port 8448 to domain name', () => { @@ -98,9 +86,7 @@ describe('#resolveWhenServerNameIsIpAddress()', () => { }); it('should return the same address with port if IPv6 address has a port', async () => { - const result = await resolveWhenServerNameIsIpAddress( - '[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:443', - ); + const result = await resolveWhenServerNameIsIpAddress('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:443'); expect(result).toBe('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:443'); }); @@ -110,9 +96,7 @@ describe('#resolveWhenServerNameIsIpAddress()', () => { }); it('should append default port 8448 if IPv6 address does not have a port', async () => { - const result = await resolveWhenServerNameIsIpAddress( - '2001:0db8:85a3:0000:0000:8a2e:0370:7334', - ); + const result = await resolveWhenServerNameIsIpAddress('2001:0db8:85a3:0000:0000:8a2e:0370:7334'); expect(result).toBe('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8448'); }); @@ -122,9 +106,7 @@ describe('#resolveWhenServerNameIsIpAddress()', () => { }); it('should return the same address if it is a valid IPv6 address with default port', async () => { - const result = await resolveWhenServerNameIsIpAddress( - '[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8448', - ); + const result = await resolveWhenServerNameIsIpAddress('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8448'); expect(result).toBe('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8448'); }); @@ -134,9 +116,7 @@ describe('#resolveWhenServerNameIsIpAddress()', () => { }); it('should throw an error if the URL is not parseable', async () => { - await expect( - resolveWhenServerNameIsIpAddress('::invalid'), - ).rejects.toThrow(); + await expect(resolveWhenServerNameIsIpAddress('::invalid')).rejects.toThrow(); }); }); @@ -146,12 +126,9 @@ describe('#resolveWhenServerNameIsAddressWithPort()', () => { }); it('should resolve to CNAME record with port', async () => { - mockResolver.resolveAny.mockResolvedValueOnce([ - { type: 'CNAME', value: 'alias.example.com' }, - ]); + mockResolver.resolveAny.mockResolvedValueOnce([{ type: 'CNAME', value: 'alias.example.com' }]); - const result = - await resolveWhenServerNameIsAddressWithPort('example.com:8080'); + const result = await resolveWhenServerNameIsAddressWithPort('example.com:8080'); expect(result).toBe('alias.example.com:8080'); }); @@ -164,37 +141,28 @@ describe('#resolveWhenServerNameIsAddressWithPort()', () => { }, ]); - const result = - await resolveWhenServerNameIsAddressWithPort('example.com:8080'); + const result = await resolveWhenServerNameIsAddressWithPort('example.com:8080'); expect(result).toBe('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8080'); }); it('should resolve to A record with port', async () => { - mockResolver.resolveAny.mockResolvedValueOnce([ - { type: 'A', address: '192.168.1.1', ttl: 300 }, - ]); + mockResolver.resolveAny.mockResolvedValueOnce([{ type: 'A', address: '192.168.1.1', ttl: 300 }]); - const result = - await resolveWhenServerNameIsAddressWithPort('example.com:8080'); + const result = await resolveWhenServerNameIsAddressWithPort('example.com:8080'); expect(result).toBe('192.168.1.1:8080'); }); it('should return the same address with its port if no records are found', async () => { mockResolver.resolveAny.mockResolvedValueOnce([]); - const result = - await resolveWhenServerNameIsAddressWithPort('example.com:8080'); + const result = await resolveWhenServerNameIsAddressWithPort('example.com:8080'); expect(result).toBe('example.com:8080'); }); it('should throw an error if an error occurs during resolution', async () => { - mockResolver.resolveAny.mockRejectedValueOnce( - new Error('DNS resolution error'), - ); + mockResolver.resolveAny.mockRejectedValueOnce(new Error('DNS resolution error')); - await expect( - resolveWhenServerNameIsAddressWithPort('example.com:8080'), - ).rejects.toThrow('DNS resolution error'); + await expect(resolveWhenServerNameIsAddressWithPort('example.com:8080')).rejects.toThrow('DNS resolution error'); }); }); @@ -204,32 +172,18 @@ describe('#resolveUsingSRVRecordsOrFallbackToOtherRecords()', () => { }); it('should resolve to SRV record target with default port', async () => { - mockResolver.resolveSrv = jest - .fn() - .mockResolvedValue([ - { name: 'srv.example.com', port: 8448, priority: 10, weight: 5 }, - ]); - mockResolver.resolveAny.mockResolvedValueOnce([ - { type: 'A', address: '192.168.1.1', ttl: 300 }, - ]); + mockResolver.resolveSrv = jest.fn().mockResolvedValue([{ name: 'srv.example.com', port: 8448, priority: 10, weight: 5 }]); + mockResolver.resolveAny.mockResolvedValueOnce([{ type: 'A', address: '192.168.1.1', ttl: 300 }]); - const result = - await resolveUsingSRVRecordsOrFallbackToOtherRecords('example.com'); + const result = await resolveUsingSRVRecordsOrFallbackToOtherRecords('example.com'); expect(result).toBe('192.168.1.1:8448'); }); it('should resolve to SRV record target with specified port', async () => { - mockResolver.resolveSrv = jest - .fn() - .mockResolvedValue([ - { name: 'srv.example.com', port: 8448, priority: 10, weight: 5 }, - ]); - mockResolver.resolveAny.mockResolvedValueOnce([ - { type: 'A', address: '192.168.1.1', ttl: 300 }, - ]); + mockResolver.resolveSrv = jest.fn().mockResolvedValue([{ name: 'srv.example.com', port: 8448, priority: 10, weight: 5 }]); + mockResolver.resolveAny.mockResolvedValueOnce([{ type: 'A', address: '192.168.1.1', ttl: 300 }]); - const result = - await resolveUsingSRVRecordsOrFallbackToOtherRecords('example.com'); + const result = await resolveUsingSRVRecordsOrFallbackToOtherRecords('example.com'); expect(result).toBe('192.168.1.1:8448'); }); @@ -243,19 +197,15 @@ describe('#resolveUsingSRVRecordsOrFallbackToOtherRecords()', () => { }, ]); - const result = - await resolveUsingSRVRecordsOrFallbackToOtherRecords('example.com'); + const result = await resolveUsingSRVRecordsOrFallbackToOtherRecords('example.com'); expect(result).toBe('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8448'); }); it('should resolve to A record if no SRV records are found', async () => { mockResolver.resolveSrv = jest.fn().mockResolvedValue([]); - mockResolver.resolveAny.mockResolvedValueOnce([ - { type: 'A', address: '192.168.1.1', ttl: 300 }, - ]); + mockResolver.resolveAny.mockResolvedValueOnce([{ type: 'A', address: '192.168.1.1', ttl: 300 }]); - const result = - await resolveUsingSRVRecordsOrFallbackToOtherRecords('example.com'); + const result = await resolveUsingSRVRecordsOrFallbackToOtherRecords('example.com'); expect(result).toBe('192.168.1.1:8448'); }); @@ -263,30 +213,21 @@ describe('#resolveUsingSRVRecordsOrFallbackToOtherRecords()', () => { mockResolver.resolveSrv = jest.fn().mockResolvedValue([]); mockResolver.resolveAny.mockResolvedValueOnce([]); - const result = - await resolveUsingSRVRecordsOrFallbackToOtherRecords('example.com'); + const result = await resolveUsingSRVRecordsOrFallbackToOtherRecords('example.com'); expect(result).toBe('example.com:8448'); }); it('should throw an error if an error occurs during SRV resolution', async () => { - mockResolver.resolveSrv = jest - .fn() - .mockRejectedValue(new Error('DNS resolution error')); + mockResolver.resolveSrv = jest.fn().mockRejectedValue(new Error('DNS resolution error')); - await expect( - resolveUsingSRVRecordsOrFallbackToOtherRecords('example.com'), - ).rejects.toThrow('DNS resolution error'); + await expect(resolveUsingSRVRecordsOrFallbackToOtherRecords('example.com')).rejects.toThrow('DNS resolution error'); }); it('should throw an error if an error if an error occurs during ANY resolution', async () => { mockResolver.resolveSrv = jest.fn().mockResolvedValue([]); - mockResolver.resolveAny.mockRejectedValueOnce( - new Error('DNS resolution error'), - ); + mockResolver.resolveAny.mockRejectedValueOnce(new Error('DNS resolution error')); - await expect( - resolveUsingSRVRecordsOrFallbackToOtherRecords('example.com'), - ).rejects.toThrow('DNS resolution error'); + await expect(resolveUsingSRVRecordsOrFallbackToOtherRecords('example.com')).rejects.toThrow('DNS resolution error'); }); }); @@ -333,9 +274,7 @@ describe('#getAddressFromTargetWellKnownEndpoint()', () => { }; global.fetch = jest.fn().mockResolvedValueOnce(mockResponse) as any; - await expect( - getAddressFromTargetWellKnownEndpoint('example.com'), - ).rejects.toThrow('No address found'); + await expect(getAddressFromTargetWellKnownEndpoint('example.com')).rejects.toThrow('No address found'); }); it('should throw an error if json() throws', async () => { @@ -348,9 +287,7 @@ describe('#getAddressFromTargetWellKnownEndpoint()', () => { }; global.fetch = jest.fn().mockResolvedValueOnce(mockResponse) as any; - await expect( - getAddressFromTargetWellKnownEndpoint('example.com'), - ).rejects.toThrow('No address found'); + await expect(getAddressFromTargetWellKnownEndpoint('example.com')).rejects.toThrow('No address found'); }); it('should throw an error if the well-known response does not contain m.server', async () => { @@ -363,9 +300,7 @@ describe('#getAddressFromTargetWellKnownEndpoint()', () => { }; global.fetch = jest.fn().mockResolvedValueOnce(mockResponse) as any; - await expect( - getAddressFromTargetWellKnownEndpoint('example.com'), - ).rejects.toThrow('No address found'); + await expect(getAddressFromTargetWellKnownEndpoint('example.com')).rejects.toThrow('No address found'); }); it('should limit maxAge to 48 hours if cache-control header exceeds the limit', async () => { @@ -431,10 +366,7 @@ describe('#resolveHostAddressByServerName()', () => { }); it('should resolve IP literal addresses directly', async () => { - const { address, headers } = await resolveHostAddressByServerName( - '192.168.1.1', - localHomeServerName, - ); + const { address, headers } = await resolveHostAddressByServerName('192.168.1.1', localHomeServerName); expect(address).toBe('192.168.1.1:8448'); expect(headers).toEqual({ Host: localHomeServerNameWithPort }); }); @@ -447,10 +379,7 @@ describe('#resolveHostAddressByServerName()', () => { ttl: 300, }, ]); - const { address, headers } = await resolveHostAddressByServerName( - 'example.com:8080', - localHomeServerName, - ); + const { address, headers } = await resolveHostAddressByServerName('example.com:8080', localHomeServerName); expect(address).toBe('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8080'); expect(headers).toEqual({ Host: localHomeServerNameWithPort }); }); @@ -472,10 +401,7 @@ describe('#resolveHostAddressByServerName()', () => { }; wellKnownCache.set(serverName, cachedData); - const { address, headers } = await resolveHostAddressByServerName( - serverName, - localHomeServerName, - ); + const { address, headers } = await resolveHostAddressByServerName(serverName, localHomeServerName); expect(address).toBe('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8448'); expect(headers).toEqual({ Host: 'cached.example.com:8448' }); }); @@ -498,27 +424,17 @@ describe('#resolveHostAddressByServerName()', () => { }; global.fetch = jest.fn().mockResolvedValueOnce(mockResponse) as any; - const { address, headers } = await resolveHostAddressByServerName( - 'example.com', - localHomeServerName, - ); + const { address, headers } = await resolveHostAddressByServerName('example.com', localHomeServerName); expect(address).toBe('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8448'); expect(headers).toEqual({ Host: 'example.com:8448' }); }); it('should fallback to SRV records if well-known address is not available', async () => { - global.fetch = jest - .fn() - .mockRejectedValueOnce(new Error('Fetch error')) as any; + global.fetch = jest.fn().mockRejectedValueOnce(new Error('Fetch error')) as any; mockResolver.resolveSrv = jest.fn().mockResolvedValue([]); - mockResolver.resolveAny.mockResolvedValueOnce([ - { type: 'A', address: '192.168.1.1', ttl: 300 }, - ]); + mockResolver.resolveAny.mockResolvedValueOnce([{ type: 'A', address: '192.168.1.1', ttl: 300 }]); - const { address, headers } = await resolveHostAddressByServerName( - 'example.com', - localHomeServerName, - ); + const { address, headers } = await resolveHostAddressByServerName('example.com', localHomeServerName); expect(address).toBe('192.168.1.1:8448'); expect(headers).toEqual({ Host: 'example.com' }); }); @@ -536,10 +452,7 @@ describe('#resolveHostAddressByServerName()', () => { }; global.fetch = jest.fn().mockResolvedValueOnce(mockResponse) as any; - const { address, headers } = await resolveHostAddressByServerName( - 'example.com', - localHomeServerName, - ); + const { address, headers } = await resolveHostAddressByServerName('example.com', localHomeServerName); expect(address).toBe('example.com:8448'); expect(headers).toEqual({ Host: 'example.com' }); }); @@ -557,41 +470,26 @@ describe('#resolveHostAddressByServerName()', () => { }; global.fetch = jest.fn().mockResolvedValueOnce(mockResponse) as any; - const { address, headers } = await resolveHostAddressByServerName( - 'example.com', - localHomeServerName, - ); + const { address, headers } = await resolveHostAddressByServerName('example.com', localHomeServerName); expect(address).toBe('example.com:8448'); expect(headers).toEqual({ Host: 'example.com' }); }); it('should fallback to default port if no SRV, CNAME, AAAA, not A records are found', async () => { - global.fetch = jest - .fn() - .mockRejectedValueOnce(new Error('Fetch error')) as any; + global.fetch = jest.fn().mockRejectedValueOnce(new Error('Fetch error')) as any; mockResolver.resolveSrv = jest.fn().mockResolvedValue([]); mockResolver.resolveAny.mockResolvedValueOnce([]); - const { address, headers } = await resolveHostAddressByServerName( - 'example.com', - localHomeServerName, - ); + const { address, headers } = await resolveHostAddressByServerName('example.com', localHomeServerName); expect(address).toBe('example.com:8448'); expect(headers).toEqual({ Host: 'example.com' }); }); it('should handle errors gracefully and return address with default port', async () => { - global.fetch = jest - .fn() - .mockRejectedValueOnce(new Error('Fetch error')) as any; - mockResolver.resolveSrv = jest - .fn() - .mockRejectedValue(new Error('DNS resolution error')); - - const { address, headers } = await resolveHostAddressByServerName( - 'example.com', - localHomeServerName, - ); + global.fetch = jest.fn().mockRejectedValueOnce(new Error('Fetch error')) as any; + mockResolver.resolveSrv = jest.fn().mockRejectedValue(new Error('DNS resolution error')); + + const { address, headers } = await resolveHostAddressByServerName('example.com', localHomeServerName); expect(address).toBe('example.com:8448'); expect(headers).toEqual({ Host: 'example.com' }); }); diff --git a/packages/core/src/server-discovery/discovery.ts b/packages/core/src/server-discovery/discovery.ts index ab07047f5..85842e914 100644 --- a/packages/core/src/server-discovery/discovery.ts +++ b/packages/core/src/server-discovery/discovery.ts @@ -5,8 +5,8 @@ const ipv4Regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(:\d{1,5})?$/; const ipv6Regex = /^(\[([a-fA-F0-9:]+)\]|([a-fA-F0-9:]+))(?::(\d{1,5}))?$/; const DEFAULT_SECURE_PORT = 8448; -const MAX_AGE_HOURS_IN_SECONDS = 86400; //24 hours -const MAX_CACHE_ALLOWED_IN_SECONDS = 172800; //48 hours +const MAX_AGE_HOURS_IN_SECONDS = 86400; // 24 hours +const MAX_CACHE_ALLOWED_IN_SECONDS = 172800; // 48 hours export const isIpLiteral = (ip: string): boolean => { if (ipv4Regex.test(ip)) { @@ -22,9 +22,7 @@ export const addressWithDefaultPort = (address: string): string => { return `${address}:${DEFAULT_SECURE_PORT}`; }; -export const resolveWhenServerNameIsIpAddress = async ( - serverName: string, -): Promise => { +export const resolveWhenServerNameIsIpAddress = async (serverName: string): Promise => { if (ipv6Regex.test(serverName)) { // Ensure IPv6 addresses are enclosed in square brackets const ipv6Match = serverName.match(ipv6Regex); @@ -39,16 +37,11 @@ export const resolveWhenServerNameIsIpAddress = async ( return url.port ? serverName : addressWithDefaultPort(serverName); }; -const formatIpAddressWithOptionalPort = ( - address: string, - port = '', -): string => { +const formatIpAddressWithOptionalPort = (address: string, port = ''): string => { return isIPv6(address) ? `[${address}]${port}` : `${address}${port}`; }; -export const resolveWhenServerNameIsAddressWithPort = async ( - serverName: string, -): Promise => { +export const resolveWhenServerNameIsAddressWithPort = async (serverName: string): Promise => { const [hostname, port] = serverName.split(':'); const resolver = new Resolver(); @@ -71,26 +64,18 @@ export const resolveWhenServerNameIsAddressWithPort = async ( return addressWithDefaultPort(hostname); }; -export const resolveUsingSRVRecordsOrFallbackToOtherRecords = async ( - serverName: string, -): Promise => { +export const resolveUsingSRVRecordsOrFallbackToOtherRecords = async (serverName: string): Promise => { const resolver = new Resolver(); - const srvRecords = await resolver.resolveSrv( - `_matrix-fed._tcp.${serverName}`, - ); + const srvRecords = await resolver.resolveSrv(`_matrix-fed._tcp.${serverName}`); if (srvRecords.length > 0) { - for (const srv of srvRecords) { + for await (const srv of srvRecords) { const addresses = await resolver.resolveAny(srv.name); for (const address of addresses) { if (address.type === 'AAAA' || address.type === 'A') { - const ipAddress = isIPv6(address.address) - ? `[${address.address}]` - : address.address; + const ipAddress = isIPv6(address.address) ? `[${address.address}]` : address.address; - return srv.port - ? `${ipAddress}:${srv.port}` - : addressWithDefaultPort(`[${ipAddress}]`); + return srv.port ? `${ipAddress}:${srv.port}` : addressWithDefaultPort(`[${ipAddress}]`); } } } @@ -100,13 +85,8 @@ export const resolveUsingSRVRecordsOrFallbackToOtherRecords = async ( const addresses = await resolver.resolveAny(serverName); for (const address of addresses) { - if ( - address.type === 'CNAME' || - address.type === 'AAAA' || - address.type === 'A' - ) { - const ipAddress = - address.type === 'CNAME' ? address.value : address.address; + if (address.type === 'CNAME' || address.type === 'AAAA' || address.type === 'A') { + const ipAddress = address.type === 'CNAME' ? address.value : address.address; const formattedIpAddress = formatIpAddressWithOptionalPort(ipAddress); return addressWithDefaultPort(formattedIpAddress); @@ -116,9 +96,7 @@ export const resolveUsingSRVRecordsOrFallbackToOtherRecords = async ( return addressWithDefaultPort(serverName); }; -export const getAddressFromTargetWellKnownEndpoint = async ( - serverName: string, -): Promise<{ address: string; maxAge: number }> => { +export const getAddressFromTargetWellKnownEndpoint = async (serverName: string): Promise<{ address: string; maxAge: number }> => { let response: Response | undefined; let data: unknown; try { @@ -133,14 +111,7 @@ export const getAddressFromTargetWellKnownEndpoint = async ( throw new Error('No address found'); } - if ( - !( - typeof data === 'object' && - data !== null && - 'm.server' in data && - typeof data['m.server'] === 'string' - ) - ) { + if (!(typeof data === 'object' && data !== null && 'm.server' in data && typeof data['m.server'] === 'string')) { throw new Error('No address found'); } @@ -151,10 +122,7 @@ export const getAddressFromTargetWellKnownEndpoint = async ( if (cacheControl) { const match = cacheControl.match(/max-age=(\d+)/); if (match) { - maxAge = Math.min( - Number.parseInt(match[1], 10), - MAX_CACHE_ALLOWED_IN_SECONDS, - ); + maxAge = Math.min(Number.parseInt(match[1], 10), MAX_CACHE_ALLOWED_IN_SECONDS); } } @@ -163,17 +131,11 @@ export const getAddressFromTargetWellKnownEndpoint = async ( return { address, maxAge }; }; -const addressHasExplicitPort = (address: string): boolean => - !isIpLiteral(address) && new URL(`http://${address}`).port !== ''; +const addressHasExplicitPort = (address: string): boolean => !isIpLiteral(address) && new URL(`http://${address}`).port !== ''; -export const wellKnownCache = new Map< - string, - { address: string; maxAge: number; timestamp: number } ->(); +export const wellKnownCache = new Map(); -export const getWellKnownCachedAddress = ( - serverName: string, -): string | null => { +export const getWellKnownCachedAddress = (serverName: string): string | null => { const cached = wellKnownCache.get(serverName); if (cached && Date.now() < cached.timestamp + cached.maxAge * 1000) { return cached.address; @@ -199,16 +161,13 @@ export const getWellKnownCachedAddress = ( // return resolveUsingSRVRecordsOrFallbackToOtherRecords(serverName); // }; -const getAddressFromWellKnownData = async ( - serverName: string, -): Promise => { +const getAddressFromWellKnownData = async (serverName: string): Promise => { const cachedAddress = getWellKnownCachedAddress(serverName); if (cachedAddress) { return cachedAddress; } - const { address, maxAge } = - await getAddressFromTargetWellKnownEndpoint(serverName); + const { address, maxAge } = await getAddressFromTargetWellKnownEndpoint(serverName); wellKnownCache.set(serverName, { address, maxAge, timestamp: Date.now() }); return address; @@ -246,9 +205,7 @@ export const resolveHostAddressByServerName = async ( return { address: rawAddress, headers: { Host: rawAddress } }; } catch (error) { if (error instanceof Error && error.message === 'No address found') { - const address = await resolveUsingSRVRecordsOrFallbackToOtherRecords( - serverName, - ).catch(() => addressWithDefaultPort(serverName)); + const address = await resolveUsingSRVRecordsOrFallbackToOtherRecords(serverName).catch(() => addressWithDefaultPort(serverName)); return { address, headers: { Host: serverName } }; } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 1400cee3f..479064f0b 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,5 +1,4 @@ -import { Pdu } from '@rocket.chat/federation-room'; -import type { EventID } from '@rocket.chat/federation-room'; +import type { Pdu, EventID } from '@rocket.chat/federation-room'; export enum EncryptionValidAlgorithm { ed25519 = 'ed25519', diff --git a/packages/core/src/url.spec.ts b/packages/core/src/url.spec.ts index cbec27ba8..b6b5d2367 100644 --- a/packages/core/src/url.spec.ts +++ b/packages/core/src/url.spec.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'bun:test'; + import { extractURIfromURL } from './url'; describe('extractURIfromURL', () => { @@ -27,12 +28,8 @@ describe('extractURIfromURL', () => { }); it('should handle URLs with special characters in pathname and search params', () => { - const url = new URL( - 'https://example.com/path/to/resource%20with%20spaces?query=123&another=456', - ); + const url = new URL('https://example.com/path/to/resource%20with%20spaces?query=123&another=456'); const result = extractURIfromURL(url); - expect(result).toBe( - '/path/to/resource%20with%20spaces?query=123&another=456', - ); + expect(result).toBe('/path/to/resource%20with%20spaces?query=123&another=456'); }); }); diff --git a/packages/core/src/utils/authentication.spec.ts b/packages/core/src/utils/authentication.spec.ts index 35d8b09c2..dee7292a7 100644 --- a/packages/core/src/utils/authentication.spec.ts +++ b/packages/core/src/utils/authentication.spec.ts @@ -8,7 +8,6 @@ import { signRequest, validateAuthorizationHeader, } from './authentication'; - import { generateId } from './generateId'; import { generateKeyPairsFromString } from './keys'; import { signJson } from './signJson'; @@ -58,9 +57,7 @@ import { signJson } from './signJson'; // } test('signRequest', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 a_XRhW YjbSyfqQeGto+OFswt+XwtJUUooHXH5w+czSgawN63U', - ); + const signature = await generateKeyPairsFromString('ed25519 a_XRhW YjbSyfqQeGto+OFswt+XwtJUUooHXH5w+czSgawN63U'); const event = Object.freeze({ auth_events: [ @@ -90,8 +87,7 @@ test('signRequest', async () => { state_key: '@admin:hs2', signatures: { hs2: { - 'ed25519:a_XRhW': - 'DR+DBqFTm7IUa35pFeOczsNw4shglIXW+3Ze63wC3dqQ4okzaSRgLuAUkYnVyxM2sZkSvlbeSBS7G6DeeaDEAA', + 'ed25519:a_XRhW': 'DR+DBqFTm7IUa35pFeOczsNw4shglIXW+3Ze63wC3dqQ4okzaSRgLuAUkYnVyxM2sZkSvlbeSBS7G6DeeaDEAA', }, }, unsigned: { diff --git a/packages/core/src/utils/authentication.ts b/packages/core/src/utils/authentication.ts index abbe99e3d..2038c8814 100644 --- a/packages/core/src/utils/authentication.ts +++ b/packages/core/src/utils/authentication.ts @@ -1,6 +1,7 @@ import { encodeCanonicalJson } from '@rocket.chat/federation-crypto'; import { type Pdu, PersistentEventBase } from '@rocket.chat/federation-room'; import nacl from 'tweetnacl'; + import { type SigningKey } from '../types'; import { signJson } from './signJson'; @@ -20,11 +21,7 @@ export const extractSignaturesFromHeader = (authorizationHeader: string) => { key, sig: signature, ...rest - } = Object.fromEntries( - [...authorizationHeader.matchAll(regex)].map( - ([, key, value]) => [key, value] as const, - ), - ); + } = Object.fromEntries([...authorizationHeader.matchAll(regex)].map(([, key, value]) => [key, value] as const)); if (Object.keys(rest).length) { // it should never happen since the regex should match all the parameters @@ -51,14 +48,7 @@ export async function authorizationHeaders( uri: string, content?: T, ): Promise { - const signedJson = await signRequest( - origin, - signingKey, - destination, - method, - uri, - content, - ); + const signedJson = await signRequest(origin, signingKey, destination, method, uri, content); const key = `${signingKey.algorithm}:${signingKey.version}` as const; const signed = signedJson.signatures[origin][key]; @@ -83,23 +73,13 @@ export const validateAuthorizationHeader = async ( ...(content && { content }), }); - const signature = Uint8Array.from(atob(hash as string), (c) => - c.charCodeAt(0), - ); - const signingKeyBytes = Uint8Array.from(atob(signingKey as string), (c) => - c.charCodeAt(0), - ); + const signature = Uint8Array.from(atob(hash as string), (c) => c.charCodeAt(0)); + const signingKeyBytes = Uint8Array.from(atob(signingKey as string), (c) => c.charCodeAt(0)); const messageBytes = new TextEncoder().encode(canonicalJson); - const isValid = nacl.sign.detached.verify( - messageBytes, - signature, - signingKeyBytes, - ); + const isValid = nacl.sign.detached.verify(messageBytes, signature, signingKeyBytes); if (!isValid) { - throw new Error( - `Invalid signature from ${origin} for request to ${destination}`, - ); + throw new Error(`Invalid signature from ${origin} for request to ${destination}`); } return true; @@ -135,19 +115,9 @@ export type HashedEvent> = T & { }; }; -export function computeAndMergeHash>( - content: T, -): HashedEvent { +export function computeAndMergeHash>(content: T): HashedEvent { // remove the fields that are not part of the hash - const { - age_ts, - unsigned, - signatures, - hashes, - outlier, - destinations, - ...toHash - } = content; + const { age_ts, unsigned, signatures, hashes, outlier, destinations, ...toHash } = content; const [algorithm, hash] = computeHash(toHash); @@ -159,10 +129,7 @@ export function computeAndMergeHash>( }; } -export function computeHash>( - content: T, - algorithm: 'sha256' = 'sha256', -): ['sha256', string] { +export function computeHash>(content: T, algorithm: 'sha256' = 'sha256'): ['sha256', string] { // remove the fields that are not part of the hash return [ algorithm, diff --git a/packages/core/src/utils/binaryData.ts b/packages/core/src/utils/binaryData.ts index b7dd667cb..284e58527 100644 --- a/packages/core/src/utils/binaryData.ts +++ b/packages/core/src/utils/binaryData.ts @@ -1,6 +1,4 @@ -export function toBinaryData( - value: string | Uint8Array | ArrayBuffer | ArrayBufferView, -): Uint8Array { +export function toBinaryData(value: string | Uint8Array | ArrayBuffer | ArrayBufferView): Uint8Array { if (typeof value === 'string') { return new TextEncoder().encode(value); } @@ -16,9 +14,7 @@ export function toBinaryData( return new Uint8Array(value.buffer, value.byteOffset, value.byteLength); } -export function fromBinaryData( - value: string | Uint8Array | ArrayBuffer, -): string { +export function fromBinaryData(value: string | Uint8Array | ArrayBuffer): string { if (typeof value === 'string') { return value; } diff --git a/packages/core/src/utils/checkSignAndHashes.spec.ts b/packages/core/src/utils/checkSignAndHashes.spec.ts index 1268e5cb3..b7121281c 100644 --- a/packages/core/src/utils/checkSignAndHashes.spec.ts +++ b/packages/core/src/utils/checkSignAndHashes.spec.ts @@ -1,9 +1,10 @@ import 'reflect-metadata'; import { afterAll, beforeAll, describe, expect, it, spyOn } from 'bun:test'; + import type { EventBase } from '../events/eventBase'; import { EncryptionValidAlgorithm } from '../types'; -import * as authentication from '../utils/authentication'; import type { HashedEvent } from './authentication'; +import * as authentication from './authentication'; import { checkSignAndHashes } from './checkSignAndHashes'; import { MatrixError } from './errors'; import * as signJson from './signJson'; @@ -34,10 +35,7 @@ describe('checkSignAndHashes', () => { }, } as unknown as HashedEvent>; - const getPublicKeyFromServerMock = ( - _origin: string, - _key: string, - ): Promise => { + const getPublicKeyFromServerMock = (_origin: string, _key: string): Promise => { return Promise.resolve(mockPublicKey); }; @@ -59,23 +57,11 @@ describe('checkSignAndHashes', () => { }); it('should validate signature and hash successfully', async () => { - const getSignaturesSpy = spyOn( - signJson, - 'getSignaturesFromRemote', - ).mockResolvedValue([mockSignature]); - const verifyJsonSpy = spyOn( - signJson, - 'verifyJsonSignature', - ).mockReturnValue(true); - const computeHashSpy = spyOn(authentication, 'computeHash').mockReturnValue( - ['sha256', mockHash], - ); - - const result = await checkSignAndHashes( - mockPdu, - mockOrigin, - getPublicKeyFromServerMock, - ); + const getSignaturesSpy = spyOn(signJson, 'getSignaturesFromRemote').mockResolvedValue([mockSignature]); + const verifyJsonSpy = spyOn(signJson, 'verifyJsonSignature').mockReturnValue(true); + const computeHashSpy = spyOn(authentication, 'computeHash').mockReturnValue(['sha256', mockHash]); + + const result = await checkSignAndHashes(mockPdu, mockOrigin, getPublicKeyFromServerMock); expect(getSignaturesSpy).toHaveBeenCalledWith(mockPdu, mockOrigin); expect(verifyJsonSpy).toHaveBeenCalled(); @@ -89,17 +75,9 @@ describe('checkSignAndHashes', () => { }); it('should throw error for invalid signature', async () => { - const getSignaturesSpy = spyOn( - signJson, - 'getSignaturesFromRemote', - ).mockResolvedValue([mockSignature]); - const verifyJsonSpy = spyOn( - signJson, - 'verifyJsonSignature', - ).mockReturnValue(false); - const computeHashSpy = spyOn(authentication, 'computeHash').mockReturnValue( - ['sha256', mockHash], - ); + const getSignaturesSpy = spyOn(signJson, 'getSignaturesFromRemote').mockResolvedValue([mockSignature]); + const verifyJsonSpy = spyOn(signJson, 'verifyJsonSignature').mockReturnValue(false); + const computeHashSpy = spyOn(authentication, 'computeHash').mockReturnValue(['sha256', mockHash]); let error: Error | undefined; try { @@ -117,17 +95,9 @@ describe('checkSignAndHashes', () => { }); it('should throw error for invalid hash', async () => { - const getSignaturesSpy = spyOn( - signJson, - 'getSignaturesFromRemote', - ).mockResolvedValue([mockSignature]); - const verifyJsonSpy = spyOn( - signJson, - 'verifyJsonSignature', - ).mockReturnValue(true); - const computeHashSpy = spyOn(authentication, 'computeHash').mockReturnValue( - ['sha256', 'differentHash'], - ); + const getSignaturesSpy = spyOn(signJson, 'getSignaturesFromRemote').mockResolvedValue([mockSignature]); + const verifyJsonSpy = spyOn(signJson, 'verifyJsonSignature').mockReturnValue(true); + const computeHashSpy = spyOn(authentication, 'computeHash').mockReturnValue(['sha256', 'differentHash']); let error: Error | undefined; try { diff --git a/packages/core/src/utils/checkSignAndHashes.ts b/packages/core/src/utils/checkSignAndHashes.ts index 58171d0a4..bb6acf4cc 100644 --- a/packages/core/src/utils/checkSignAndHashes.ts +++ b/packages/core/src/utils/checkSignAndHashes.ts @@ -1,13 +1,10 @@ -import { Pdu } from '@rocket.chat/federation-room'; +import type { Pdu } from '@rocket.chat/federation-room'; + import { type HashedEvent, computeHash } from './authentication'; import { MatrixError } from './errors'; import { logger } from './logger'; import { pruneEventDict } from './pruneEventDict'; -import { - type SignedJson, - getSignaturesFromRemote, - verifyJsonSignature, -} from './signJson'; +import { type SignedJson, getSignaturesFromRemote, verifyJsonSignature } from './signJson'; export async function checkSignAndHashes>( pdu: HashedEvent, @@ -15,10 +12,7 @@ export async function checkSignAndHashes>( getPublicKeyFromServer: (origin: string, key: string) => Promise, ) { const [signature] = await getSignaturesFromRemote(pdu, origin); - const publicKey = await getPublicKeyFromServer( - origin, - `${signature.algorithm}:${signature.version}`, - ); + const publicKey = await getPublicKeyFromServer(origin, `${signature.algorithm}:${signature.version}`); if ( !verifyJsonSignature( diff --git a/packages/core/src/utils/errors.ts b/packages/core/src/utils/errors.ts index 88a0b5abb..df42696b0 100644 --- a/packages/core/src/utils/errors.ts +++ b/packages/core/src/utils/errors.ts @@ -1,8 +1,5 @@ export class MatrixError extends Error { - constructor( - public readonly code: TCode, - message: string, - ) { + constructor(public readonly code: TCode, message: string) { super(message); this.name = 'MatrixError'; } diff --git a/packages/core/src/utils/fetch.ts b/packages/core/src/utils/fetch.ts index 85fe861b9..f5477b38b 100644 --- a/packages/core/src/utils/fetch.ts +++ b/packages/core/src/utils/fetch.ts @@ -38,18 +38,12 @@ function parseMultipart(buffer: Buffer, boundary: string): MultipartResult { const binaryStart = lastHeaderEnd + 4; const closingBoundary = buffer.lastIndexOf(`\r\n--${boundary}`); - const content = - closingBoundary > binaryStart - ? buffer.subarray(binaryStart, closingBoundary) - : buffer.subarray(binaryStart); + const content = closingBoundary > binaryStart ? buffer.subarray(binaryStart, closingBoundary) : buffer.subarray(binaryStart); return { content }; } -async function handleJson( - contentType: string, - body: () => Promise, -): Promise { +async function handleJson(contentType: string, body: () => Promise): Promise { if (!contentType.includes('application/json')) { throw new Error('Content-Type is not application/json'); } @@ -61,10 +55,7 @@ async function handleJson( } } -async function handleText( - contentType: string, - body: () => Promise, -): Promise { +async function handleText(contentType: string, body: () => Promise): Promise { if (!contentType.includes('text/')) { return ''; } @@ -74,9 +65,7 @@ async function handleText( // the redirect URL should be fetched without Matrix auth // and will only occur for media downloads as per Matrix spec -async function handleMultipartRedirect( - redirect: string, -): Promise> { +async function handleMultipartRedirect(redirect: string): Promise> { const redirectResponse = await fetch(new URL(redirect), { method: 'GET', headers: {}, @@ -89,11 +78,7 @@ async function handleMultipartRedirect( return redirectResponse; } -async function handleMultipart( - contentType: string, - body: () => Promise, - depth = 0, -): Promise { +async function handleMultipart(contentType: string, body: () => Promise, depth = 0): Promise { if (!/\bmultipart\b/i.test(contentType)) { throw new Error('Content-Type is not multipart'); } @@ -113,14 +98,8 @@ async function handleMultipart( throw new Error('Too many redirects in multipart response'); } - const redirectResponse = await handleMultipartRedirect( - multipart.redirect, - ); - return handleMultipart( - redirectResponse.headers['content-type'] || '', - redirectResponse.body, - depth + 1, - ); + const redirectResponse = await handleMultipartRedirect(multipart.redirect); + return handleMultipart(redirectResponse.headers['content-type'] || '', redirectResponse.body, depth + 1); } return multipart; @@ -138,13 +117,8 @@ export type FetchResponse = { }; // this fetch is used when connecting to a multihome server, same server hosting multiple homeservers, and we need to verify the cert with the right SNI (hostname), or else, cert check will fail due to connecting through ip and not hostname (due to matrix spec). -export async function fetch( - url: URL, - options: RequestInit, -): Promise> { - const serverName = new URL( - `http://${(options.headers as IncomingHttpHeaders).Host}` as string, - ).hostname; +export async function fetch(url: URL, options: RequestInit): Promise> { + const serverName = new URL(`http://${(options.headers as IncomingHttpHeaders).Host}` as string).hostname; const requestParams: RequestOptions = { // for ipv6 remove square brackets as they come due to url standard @@ -220,13 +194,11 @@ export async function fetch( }); }); - const signal = options.signal; + const { signal } = options; if (signal) { const onAbort = () => request.destroy(new Error('Aborted')); signal.addEventListener('abort', onAbort, { once: true }); - request.once('close', () => - signal.removeEventListener('abort', onAbort), - ); + request.once('close', () => signal.removeEventListener('abort', onAbort)); } request.on('error', (err) => { @@ -244,9 +216,7 @@ export async function fetch( const contentType = response.headers['content-type'] || ''; return { - ok: response.statusCode - ? response.statusCode >= 200 && response.statusCode < 300 - : false, + ok: response.statusCode ? response.statusCode >= 200 && response.statusCode < 300 : false, buffer: () => response.body(), json: () => handleJson(contentType, response.body), text: () => handleText(contentType, response.body), diff --git a/packages/core/src/utils/generateId.ts b/packages/core/src/utils/generateId.ts index e3520183c..0d19960a4 100644 --- a/packages/core/src/utils/generateId.ts +++ b/packages/core/src/utils/generateId.ts @@ -1,15 +1,16 @@ import crypto from 'node:crypto'; + import { encodeCanonicalJson } from '@rocket.chat/federation-crypto'; -import { EventID } from '@rocket.chat/federation-room'; +import type { EventID, Pdu } from '@rocket.chat/federation-room'; + +import type { EventBase } from '@rocket.chat/federation-core'; + import { toUnpaddedBase64 } from './binaryData'; import { pruneEventDict } from './pruneEventDict'; -export function generateId(content: T): EventID { +export function generateId(content: T): EventID { // remove the fields that are not part of the hash - const { unsigned, signatures, ...toHash } = pruneEventDict(content as any); + const { unsigned, signatures, ...toHash } = pruneEventDict(content); - return `\$${toUnpaddedBase64( - crypto.createHash('sha256').update(encodeCanonicalJson(toHash)).digest(), - { urlSafe: true }, - )}` as EventID; + return `\$${toUnpaddedBase64(crypto.createHash('sha256').update(encodeCanonicalJson(toHash)).digest(), { urlSafe: true })}` as EventID; } diff --git a/packages/core/src/utils/keys.spec.ts b/packages/core/src/utils/keys.spec.ts index db5e40cc7..413a3785d 100644 --- a/packages/core/src/utils/keys.spec.ts +++ b/packages/core/src/utils/keys.spec.ts @@ -1,13 +1,11 @@ import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test'; import fs from 'node:fs/promises'; + import nacl from 'tweetnacl'; + import { EncryptionValidAlgorithm } from '../types'; import { toUnpaddedBase64 } from './binaryData'; -import { - generateKeyPairs, - generateKeyPairsFromString, - getKeyPair, -} from './keys'; +import { generateKeyPairs, generateKeyPairsFromString, getKeyPair } from './keys'; describe('keys', () => { describe('generateKeyPairs', () => { @@ -39,11 +37,7 @@ describe('keys', () => { expect(signature).toBeInstanceOf(Uint8Array); - const isValid = nacl.sign.detached.verify( - data, - signature, - keyPair.publicKey, - ); + const isValid = nacl.sign.detached.verify(data, signature, keyPair.publicKey); expect(isValid).toBe(true); }); diff --git a/packages/core/src/utils/keys.ts b/packages/core/src/utils/keys.ts index 39fc9bf3c..d7400d583 100644 --- a/packages/core/src/utils/keys.ts +++ b/packages/core/src/utils/keys.ts @@ -1,15 +1,13 @@ import fs from 'node:fs/promises'; + import nacl from 'tweetnacl'; + import { EncryptionValidAlgorithm } from '../types'; import type { SigningKey } from '../types'; import { toUnpaddedBase64 } from './binaryData'; import { signData } from './signJson'; -export async function generateKeyPairs( - seed: Uint8Array, - algorithm = EncryptionValidAlgorithm.ed25519, - version = '0', -): Promise { +export async function generateKeyPairs(seed: Uint8Array, algorithm = EncryptionValidAlgorithm.ed25519, version = '0'): Promise { // Generate an Ed25519 key pair const keyPair = await nacl.sign.keyPair.fromSeed(seed); @@ -29,7 +27,7 @@ export async function generateKeyPairs( export async function generateKeyPairsFromString(content: string) { const [algorithm, version, seed] = content.trim().split(' '); - return await generateKeyPairs( + return generateKeyPairs( Uint8Array.from(atob(seed), (c) => c.charCodeAt(0)), algorithm as EncryptionValidAlgorithm, version, @@ -45,23 +43,16 @@ async function storeKeyPairs( path: string, ) { for await (const keyPair of seeds) { - await fs.writeFile( - path, - `${keyPair.algorithm} ${keyPair.version} ${toUnpaddedBase64(keyPair.seed)}`, - ); + await fs.writeFile(path, `${keyPair.algorithm} ${keyPair.version} ${toUnpaddedBase64(keyPair.seed)}`); } } -export const getKeyPair = async (config: { - signingKeyPath: string; -}): Promise => { +export const getKeyPair = async (config: { signingKeyPath: string }): Promise => { const { signingKeyPath } = config; const seeds = []; - const existingKeyContent = await fs - .readFile(signingKeyPath, 'utf8') - .catch(() => null); + const existingKeyContent = await fs.readFile(signingKeyPath, 'utf8').catch(() => null); if (existingKeyContent) { const [algorithm, version, seed] = existingKeyContent.trim().split(' '); @@ -80,12 +71,7 @@ export const getKeyPair = async (config: { await storeKeyPairs(seeds, signingKeyPath); } - return Promise.all( - seeds.map( - async (seed) => - await generateKeyPairs(seed.seed, seed.algorithm, seed.version), - ), - ); + return Promise.all(seeds.map(async (seed) => generateKeyPairs(seed.seed, seed.algorithm, seed.version))); }; export const convertSigningKeyToBase64 = (signingKey: SigningKey): string => diff --git a/packages/core/src/utils/logger.ts b/packages/core/src/utils/logger.ts index 373bc97c0..aca776452 100644 --- a/packages/core/src/utils/logger.ts +++ b/packages/core/src/utils/logger.ts @@ -9,7 +9,7 @@ const logger = pino({ ? { target: 'pino-pretty', options: { colorize: true }, - } + } : undefined, }); diff --git a/packages/core/src/utils/makeRequest.ts b/packages/core/src/utils/makeRequest.ts index b45026693..ecbaf4739 100644 --- a/packages/core/src/utils/makeRequest.ts +++ b/packages/core/src/utils/makeRequest.ts @@ -21,10 +21,7 @@ export const makeRequest = async >({ options?: Record; queryString?: string; }): Promise => { - const { address, headers } = await resolveHostAddressByServerName( - domain, - signingName, - ); + const { address, headers } = await resolveHostAddressByServerName(domain, signingName); const url = new URL(`https://${address}${uri}`); if (queryString) { url.search = queryString; @@ -64,19 +61,9 @@ export const makeUnsignedRequest = async >({ signingName: string; queryString?: string; }): Promise => { - const auth = await authorizationHeaders>( - signingName, - signingKey, - domain, - method, - uri, - body, - ); + const auth = await authorizationHeaders>(signingName, signingKey, domain, method, uri, body); - const { address, headers } = await resolveHostAddressByServerName( - domain, - signingName, - ); + const { address, headers } = await resolveHostAddressByServerName(domain, signingName); const url = new URL(`https://${address}${uri}`); if (queryString) { url.search = queryString; @@ -86,7 +73,7 @@ export const makeUnsignedRequest = async >({ ...options, method, headers: { - Authorization: auth, + 'Authorization': auth, ...headers, 'content-type': 'application/json', }, diff --git a/packages/core/src/utils/pruneEventDict.spec.ts b/packages/core/src/utils/pruneEventDict.spec.ts index c78a0e186..ab5c804bf 100644 --- a/packages/core/src/utils/pruneEventDict.spec.ts +++ b/packages/core/src/utils/pruneEventDict.spec.ts @@ -8,8 +8,7 @@ const event = { destination: 'synapse1', signatures: { synapse2: { - 'ed25519:a_yNbw': - '4rVyjfM9Apz6O93HvqkeidmsjwNGHD9WbEA1AdUsjdbACPM67iJK75BQLBLcw5NQj3q/eL7+kGaknkqJH8kEAw', + 'ed25519:a_yNbw': '4rVyjfM9Apz6O93HvqkeidmsjwNGHD9WbEA1AdUsjdbACPM67iJK75BQLBLcw5NQj3q/eL7+kGaknkqJH8kEAw', }, }, content: { @@ -29,13 +28,11 @@ const event = { sender: '@rodrigo2:synapse2', signatures: { synapse2: { - 'ed25519:a_yNbw': - '0mp0rfrdjPhJFK603sAjCp/iau2cGnxTXhwJoyyrnLw7uqhVs/1vzNxjnntU2G5GFtKLa6YqNzSooLsqhptgBQ', + 'ed25519:a_yNbw': '0mp0rfrdjPhJFK603sAjCp/iau2cGnxTXhwJoyyrnLw7uqhVs/1vzNxjnntU2G5GFtKLa6YqNzSooLsqhptgBQ', }, }, state_key: '@rodrigo2:synapse2', type: 'm.room.member', - // @ts-ignore unsigned: { age: 1 }, }, origin: 'synapse2', @@ -52,7 +49,6 @@ describe('pruneEventDict', () => { '$brN97rWTFjQbIQHy2FNwg4BHc5HbhruxQHdCdk__Lb0', '$7tKDuHnd8QKPx_T_9-2WGAgJLqjfOndQgzWM1afTQLQ', ], - // @ts-ignore content: { // avatar_url: null, --> it should be removed // displayname: "rodrigo2", --> it should be removed @@ -67,13 +63,11 @@ describe('pruneEventDict', () => { sender: '@rodrigo2:synapse2', signatures: { synapse2: { - 'ed25519:a_yNbw': - '0mp0rfrdjPhJFK603sAjCp/iau2cGnxTXhwJoyyrnLw7uqhVs/1vzNxjnntU2G5GFtKLa6YqNzSooLsqhptgBQ', + 'ed25519:a_yNbw': '0mp0rfrdjPhJFK603sAjCp/iau2cGnxTXhwJoyyrnLw7uqhVs/1vzNxjnntU2G5GFtKLa6YqNzSooLsqhptgBQ', }, }, state_key: '@rodrigo2:synapse2', type: 'm.room.member', - // @ts-ignore unsigned: { // age: 1 --> it should be removed }, diff --git a/packages/core/src/utils/pruneEventDict.ts b/packages/core/src/utils/pruneEventDict.ts index 2151eb01d..85dfb93a6 100644 --- a/packages/core/src/utils/pruneEventDict.ts +++ b/packages/core/src/utils/pruneEventDict.ts @@ -1,7 +1,7 @@ -import { isRoomMemberEvent } from '../events/isRoomMemberEvent'; +import type { Pdu } from '@rocket.chat/federation-room'; -import { Pdu } from '@rocket.chat/federation-room'; import type { EventBase } from '../events/eventBase'; +import { isRoomMemberEvent } from '../events/isRoomMemberEvent'; interface RoomVersion { updated_redaction_rules: boolean; @@ -34,22 +34,15 @@ type AllowedKeysDefault = Extract< type AllowedKeysPowerLevels = Extract< keyof Pdu, - | 'users' - | 'users_default' - | 'events' - | 'events_default' - | 'state_default' - | 'ban' - | 'kick' - | 'redact' + 'users' | 'users_default' | 'events' | 'events_default' | 'state_default' | 'ban' | 'kick' | 'redact' >; type MergeMultipleKeys = T | U; export type Prettify = { [K in keyof T]: T[K]; -} & {}; +}; -export function pruneEventDict( +export function pruneEventDict( eventDict: T, roomVersion: RoomVersion = { updated_redaction_rules: false, @@ -59,15 +52,7 @@ export function pruneEventDict( special_case_aliases_auth: false, msc3389_relation_redactions: false, }, -): Prettify< - Pick< - T, - MergeMultipleKeys< - AllowedKeysDefault, - T['type'] extends 'm.room.power_levels' ? AllowedKeysPowerLevels : never - > & {} - > -> { +): Prettify>> { /** * Redacts the eventDict in the same way as `prune_event`, except it * operates on objects rather than event instances. @@ -91,18 +76,14 @@ export function pruneEventDict( 'auth_events', 'origin_server_ts', // Earlier room versions had additional allowed keys. - ...(!roomVersion.updated_redaction_rules - ? ['prev_state', 'membership', 'origin'] - : []), + ...(!roomVersion.updated_redaction_rules ? ['prev_state', 'membership', 'origin'] : []), ]; const content: JsonDict = {}; if (roomVersion.msc3389_relation_redactions) { const relatesTo = - eventDict.content && - 'm.relates_to' in eventDict.content && - (eventDict.content['m.relates_to'] as Record); + eventDict.content && 'm.relates_to' in eventDict.content && (eventDict.content['m.relates_to'] as Record); if (relatesTo && typeof relatesTo === 'object') { const newRelatesTo: JsonDict = {}; @@ -117,9 +98,7 @@ export function pruneEventDict( } } - const allowedFields: JsonDict = Object.fromEntries( - Object.entries(eventDict).filter(([key]) => allowedKeys.includes(key)), - ); + const allowedFields: JsonDict = Object.fromEntries(Object.entries(eventDict).filter(([key]) => allowedKeys.includes(key))); const unsigned: JsonDict = {}; allowedFields.unsigned = unsigned; @@ -139,23 +118,18 @@ export function pruneEventDict( for (const field of fields) { if (field in eventDict.content) { - // @ts-ignore - content[field] = eventDict.content[field]; + content[field] = (eventDict.content as Record)[field]; } } } - if (isRoomMemberEvent(eventDict)) { - const contentKeys = [ - 'membership', - ...(roomVersion.restricted_join_rule_fix ? ['authorising_user'] : []), - ]; + if (eventType === 'm.room.member') { + const contentKeys = ['membership', ...(roomVersion.restricted_join_rule_fix ? ['authorising_user'] : [])]; addFields(...contentKeys); if (roomVersion.updated_redaction_rules) { - // @ts-ignore - const thirdPartyInvite = eventDict.content.third_party_invite; + const thirdPartyInvite = (eventDict.content as Record)?.third_party_invite as Record | undefined; if (thirdPartyInvite && typeof thirdPartyInvite === 'object') { content.third_party_invite = {}; if ('signed' in thirdPartyInvite) { @@ -178,10 +152,7 @@ export function pruneEventDict( } if (eventType === 'm.room.join_rules') { - addFields( - 'join_rule', - ...(roomVersion.restricted_join_rule ? ['allow'] : []), - ); + addFields('join_rule', ...(roomVersion.restricted_join_rule ? ['allow'] : [])); } if (eventType === 'm.room.power_levels') { diff --git a/packages/core/src/utils/signEvent.ts b/packages/core/src/utils/signEvent.ts index 5ee2ab4d6..3357b7398 100644 --- a/packages/core/src/utils/signEvent.ts +++ b/packages/core/src/utils/signEvent.ts @@ -1,6 +1,6 @@ -import { Pdu } from '@rocket.chat/federation-room'; -import type { SignedEvent } from '../types'; -import type { SigningKey } from '../types'; +import type { Pdu } from '@rocket.chat/federation-room'; + +import type { SignedEvent, SigningKey } from '../types'; import { computeAndMergeHash } from './authentication'; import { pruneEventDict } from './pruneEventDict'; import { signJson } from './signJson'; @@ -12,9 +12,7 @@ export const signEvent = async ( prune = true, ): Promise> => { // Compute hash and sign - const eventToSign = prune - ? pruneEventDict(computeAndMergeHash(event)) - : event; + const eventToSign = prune ? pruneEventDict(computeAndMergeHash(event)) : event; const signedJsonResult = await signJson(eventToSign, signature, signingName); // For non-redaction events, restore the original content diff --git a/packages/core/src/utils/signJson.ts b/packages/core/src/utils/signJson.ts index dc192b85a..a3d4669a9 100644 --- a/packages/core/src/utils/signJson.ts +++ b/packages/core/src/utils/signJson.ts @@ -1,5 +1,6 @@ import { encodeCanonicalJson } from '@rocket.chat/federation-crypto'; import nacl from 'tweetnacl'; + import type { SigningKey } from '../types'; import { EncryptionValidAlgorithm } from '../types'; import { toBinaryData, toUnpaddedBase64 } from './binaryData'; @@ -19,13 +20,8 @@ export async function signJson< signatures?: Record>; unsigned?: Record; }, ->( - jsonObject: T, - signingKey: SigningKey, - signingName: string, -): Promise> { - const keyId = - `${signingKey.algorithm}:${signingKey.version}` as ProtocolVersionKey; +>(jsonObject: T, signingKey: SigningKey, signingName: string): Promise> { + const keyId = `${signingKey.algorithm}:${signingKey.version}` as ProtocolVersionKey; const { signatures = {}, unsigned, ...rest } = jsonObject; const data = encodeCanonicalJson(rest); @@ -49,9 +45,7 @@ export async function signJson< }; } -export const isValidAlgorithm = ( - algorithm: string, -): algorithm is EncryptionValidAlgorithm => { +export const isValidAlgorithm = (algorithm: string): algorithm is EncryptionValidAlgorithm => { return Object.values(EncryptionValidAlgorithm).includes(algorithm as any); }; @@ -61,7 +55,7 @@ export async function getSignaturesFromRemote< unsigned?: unknown; }, >(jsonObject: T, signingName: string) { - const { signatures, unsigned: _unsigned /*..._rest */ } = jsonObject; + const { signatures, unsigned: _unsigned /* ..._rest */ } = jsonObject; const remoteSignatures = signatures?.[signingName] && Object.entries(signatures[signingName]) @@ -77,9 +71,7 @@ export async function getSignaturesFromRemote< signature, }; }) - .filter(({ algorithm }) => - Object.values(EncryptionValidAlgorithm).includes(algorithm as any), - ); + .filter(({ algorithm }) => Object.values(EncryptionValidAlgorithm).includes(algorithm as any)); if (!remoteSignatures?.length) { throw new Error(`Signatures not found for ${signingName}`); @@ -100,13 +92,7 @@ export const verifySignature = ( throw new Error(`Invalid algorithm ${algorithm} for ${signingName}`); } - if ( - !nacl.sign.detached.verify( - new TextEncoder().encode(content), - signature, - publicKey, - ) - ) { + if (!nacl.sign.detached.verify(new TextEncoder().encode(content), signature, publicKey)) { throw new Error(`Invalid signature for ${signingName}`); } return true; @@ -123,14 +109,7 @@ export const verifyJsonSignature = ( const { signatures: _, unsigned: _unsigned, ...__rest } = content as any; const canonicalJson = encodeCanonicalJson(__rest); - return verifySignature( - canonicalJson, - signingName, - signature, - publicKey, - algorithm, - version, - ); + return verifySignature(canonicalJson, signingName, signature, publicKey, algorithm, version); }; export async function verifySignaturesFromRemote< @@ -138,14 +117,7 @@ export async function verifySignaturesFromRemote< signatures?: Record>; unsigned?: unknown; }, ->( - jsonObject: T, - signingName: string, - getPublicKey: ( - algorithm: EncryptionValidAlgorithm, - version: string, - ) => Promise, -) { +>(jsonObject: T, signingName: string, getPublicKey: (algorithm: EncryptionValidAlgorithm, version: string) => Promise) { const { signatures: _, unsigned: _unsigned, ...__rest } = jsonObject; const canonicalJson = encodeCanonicalJson(__rest); @@ -153,18 +125,9 @@ export async function verifySignaturesFromRemote< const signatures = await getSignaturesFromRemote(jsonObject, signingName); for await (const { algorithm, version, signature } of signatures) { - const publicKey = await getPublicKey( - algorithm as EncryptionValidAlgorithm, - version, - ); - - if ( - !nacl.sign.detached.verify( - new TextEncoder().encode(canonicalJson), - new Uint8Array(Buffer.from(signature, 'base64')), - publicKey, - ) - ) { + const publicKey = await getPublicKey(algorithm as EncryptionValidAlgorithm, version); + + if (!nacl.sign.detached.verify(new TextEncoder().encode(canonicalJson), new Uint8Array(Buffer.from(signature, 'base64')), publicKey)) { throw new Error(`Invalid signature for ${signingName}`); } } @@ -172,26 +135,14 @@ export async function verifySignaturesFromRemote< return true; } -export async function signText( - data: string | Uint8Array, - signingKey: Uint8Array, -) { - const signature = nacl.sign.detached( - typeof data === 'string' ? new TextEncoder().encode(data) : data, - signingKey, - ); +export async function signText(data: string | Uint8Array, signingKey: Uint8Array) { + const signature = nacl.sign.detached(typeof data === 'string' ? new TextEncoder().encode(data) : data, signingKey); return toUnpaddedBase64(signature); } -export async function signData( - data: string | Uint8Array, - signingKey: Uint8Array, -): Promise { - const signature = nacl.sign.detached( - typeof data === 'string' ? new TextEncoder().encode(data) : data, - signingKey, - ); +export async function signData(data: string | Uint8Array, signingKey: Uint8Array): Promise { + const signature = nacl.sign.detached(typeof data === 'string' ? new TextEncoder().encode(data) : data, signingKey); return signature; } diff --git a/packages/crypto/.eslintrc.json b/packages/crypto/.eslintrc.json new file mode 100644 index 000000000..be97c53fb --- /dev/null +++ b/packages/crypto/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.eslintrc.json" +} diff --git a/packages/crypto/src/contracts/key.ts b/packages/crypto/src/contracts/key.ts index e645ece12..e70338d38 100644 --- a/packages/crypto/src/contracts/key.ts +++ b/packages/crypto/src/contracts/key.ts @@ -1,8 +1,4 @@ -import type { - DataType, - EncryptionValidAlgorithm, - SignatureType, -} from '../utils/constants'; +import type { DataType, EncryptionValidAlgorithm, SignatureType } from '../utils/constants'; type KeyId = `${EncryptionValidAlgorithm}:${string}`; diff --git a/packages/crypto/src/der/index.ts b/packages/crypto/src/der/index.ts index fa912a0e9..4c39c7f9f 100644 --- a/packages/crypto/src/der/index.ts +++ b/packages/crypto/src/der/index.ts @@ -30,7 +30,7 @@ function encodeLength(len: number): Uint8Array { export const privateKeyVersionTlv = Uint8Array.of( 0x02 /* tag for int */, 0x01 /* legth */, - 0x00 /*version of private key, we are not using extensions*/, + 0x00 /* version of private key, we are not using extensions*/, ); // @@ -49,22 +49,16 @@ export const oidTlv = Uint8Array.of( ); // https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der/#null-encoding -export const nullTlv = Uint8Array.of( - 0x05 /* tag for null */, - 0x00 /* length 0 */, -); +export const nullTlv = Uint8Array.of(0x05 /* tag for null */, 0x00 /* length 0 */); // https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der/#sequence-encoding export function sequenceOrderedTlv(elements: Uint8Array[]): Uint8Array { const totalLengthInBytes = elements.reduce((a, b) => a + b.length, 0); - const bytesRepresentationForTotalLengthRequired = - encodeLength(totalLengthInBytes); + const bytesRepresentationForTotalLengthRequired = encodeLength(totalLengthInBytes); const numberOfBytesRequired = - 1 + - bytesRepresentationForTotalLengthRequired.length /*bytes needed for the length itself*/ + - totalLengthInBytes; + 1 + bytesRepresentationForTotalLengthRequired.length /* bytes needed for the length itself*/ + totalLengthInBytes; const sequence = new Uint8Array(numberOfBytesRequired); @@ -89,11 +83,7 @@ export function sequenceOrderedTlv(elements: Uint8Array[]): Uint8Array { export function octetStringTlv(data: Uint8Array): Uint8Array { const encodedLength = encodeLength(data.length); - const octet = new Uint8Array( - 1 + - encodedLength.length /* space just for the length representation */ + - data.length, - ); + const octet = new Uint8Array(1 + encodedLength.length /* space just for the length representation */ + data.length); // TAG octet[0] = 0x04; // OCTET STRING tag diff --git a/packages/crypto/src/index.spec.ts b/packages/crypto/src/index.spec.ts index c689ddcd7..425932ab4 100644 --- a/packages/crypto/src/index.spec.ts +++ b/packages/crypto/src/index.spec.ts @@ -1,11 +1,7 @@ import { describe, expect, it } from 'bun:test'; + import { fromBase64ToBytes } from './utils/data-types'; -import { - loadEd25519SignerFromSeed, - loadEd25519VerifierFromPublicKey, - signJson, - verifyJsonSignature, -} from './utils/keys'; +import { loadEd25519SignerFromSeed, loadEd25519VerifierFromPublicKey, signJson, verifyJsonSignature } from './utils/keys'; describe('signJson', () => { it('should sign a json object', async () => { @@ -46,9 +42,7 @@ describe('signJson', () => { const signature = await signJson(json, signer); - expect(signature).toBe( - 'ZDz7K7NRz0OwgR6n96YMIyt9h8KUCb7T9TklId7S1UDVOwc2y45+tC12/51kxRxpUkaOgr+iBtSBBh74BIrsBQ', - ); + expect(signature).toBe('ZDz7K7NRz0OwgR6n96YMIyt9h8KUCb7T9TklId7S1UDVOwc2y45+tC12/51kxRxpUkaOgr+iBtSBBh74BIrsBQ'); }); it('should verify a signature', async () => { @@ -78,16 +72,14 @@ describe('signJson', () => { }, }; - const signature = - 'ZDz7K7NRz0OwgR6n96YMIyt9h8KUCb7T9TklId7S1UDVOwc2y45+tC12/51kxRxpUkaOgr+iBtSBBh74BIrsBQ'; + const signature = 'ZDz7K7NRz0OwgR6n96YMIyt9h8KUCb7T9TklId7S1UDVOwc2y45+tC12/51kxRxpUkaOgr+iBtSBBh74BIrsBQ'; const keyv2serverresponsefromorigin = { old_verify_keys: {}, server_name: 'syn1.tunnel.dev.rocket.chat', signatures: { 'syn1.tunnel.dev.rocket.chat': { - 'ed25519:a_FAET': - 'MZF+8pncxhUNp7JzdSTIqriaANQ4QTYTe1AIqBNAtVhWcKz1Mc/6nzkP3/1HXZHAzCLYrmuFnTGb874XT4TJDg', + 'ed25519:a_FAET': 'MZF+8pncxhUNp7JzdSTIqriaANQ4QTYTe1AIqBNAtVhWcKz1Mc/6nzkP3/1HXZHAzCLYrmuFnTGb874XT4TJDg', }, }, valid_until_ts: 1747753307525, @@ -98,12 +90,9 @@ describe('signJson', () => { }, }; - const verifyKey = - keyv2serverresponsefromorigin.verify_keys['ed25519:a_FAET'].key; + const verifyKey = keyv2serverresponsefromorigin.verify_keys['ed25519:a_FAET'].key; - const verifier = await loadEd25519VerifierFromPublicKey( - fromBase64ToBytes(verifyKey), - ); + const verifier = await loadEd25519VerifierFromPublicKey(fromBase64ToBytes(verifyKey)); expect(verifyJsonSignature(json, signature, verifier)).resolves; }); diff --git a/packages/crypto/src/keys/ed25519.ts b/packages/crypto/src/keys/ed25519.ts index 70cc65906..0ba1cfc5b 100644 --- a/packages/crypto/src/keys/ed25519.ts +++ b/packages/crypto/src/keys/ed25519.ts @@ -1,10 +1,7 @@ import crypto from 'node:crypto'; import type { Signer, VerifierKey } from '../contracts/key'; -import { - ed25519PrivateKeyRawToPem, - ed25519PublicKeyRawToPem, -} from '../rfc/8410/ed25519-pem'; +import { ed25519PrivateKeyRawToPem, ed25519PublicKeyRawToPem } from '../rfc/8410/ed25519-pem'; import { EncryptionValidAlgorithm } from '../utils/constants'; export class Ed25519VerifierKeyImpl implements VerifierKey { @@ -24,38 +21,26 @@ export class Ed25519VerifierKeyImpl implements VerifierKey { return this._publicKeyPem; } - constructor( - public version: string, - public readonly publicKey: Uint8Array, - ) { + constructor(public version: string, public readonly publicKey: Uint8Array) { this._publicKeyPem = ed25519PublicKeyRawToPem(this.publicKey); } public async verify(data: Uint8Array, signature: Uint8Array): Promise { return new Promise((resolve, reject) => { - crypto.verify( - null, - data, - this._publicKeyPem, - signature, - (err, verified) => { - if (err) { - reject(err); - } else if (verified) { - resolve(); - } else { - reject(new Error('Invalid signature')); - } - }, - ); + crypto.verify(null, data, this._publicKeyPem, signature, (err, verified) => { + if (err) { + reject(err); + } else if (verified) { + resolve(); + } else { + reject(new Error('Invalid signature')); + } + }); }); } } -export class Ed25519SigningKeyImpl - extends Ed25519VerifierKeyImpl - implements Signer -{ +export class Ed25519SigningKeyImpl extends Ed25519VerifierKeyImpl implements Signer { public async sign(data: Uint8Array) { return new Promise((resolve, reject) => { crypto.sign(null, data, this._privateKeyPem, (err, signature) => { @@ -68,11 +53,8 @@ export class Ed25519SigningKeyImpl } private _privateKeyPem!: string; - constructor( - public version: string, - public readonly privateKey: Uint8Array, - publicKey: Uint8Array, - ) { + + constructor(public version: string, public readonly privateKey: Uint8Array, publicKey: Uint8Array) { super(version, publicKey); this._privateKeyPem = ed25519PrivateKeyRawToPem(privateKey); } diff --git a/packages/crypto/src/rfc/8410/ed25519-pem.ts b/packages/crypto/src/rfc/8410/ed25519-pem.ts index a06beee20..ed75f39dd 100644 --- a/packages/crypto/src/rfc/8410/ed25519-pem.ts +++ b/packages/crypto/src/rfc/8410/ed25519-pem.ts @@ -1,10 +1,4 @@ -import { - algorithmIdentifierTlv, - bitStringTlv, - octetStringTlv, - privateKeyVersionTlv, - sequenceOrderedTlv, -} from '../../der'; +import { algorithmIdentifierTlv, bitStringTlv, octetStringTlv, privateKeyVersionTlv, sequenceOrderedTlv } from '../../der'; enum KeyType { private = 'PRIVATE KEY', @@ -45,9 +39,7 @@ export function ed25519PrivateKeyRawToPem(rawKey: Uint8Array): string { const algId = algorithmIdentifierTlv; // privateKey PrivateKey -> OCTET STRING - const privKeyOctet = octetStringTlv( - octetStringTlv(rawKey), - ); /* The ASN.1 type CurvePrivateKey is defined in + const privKeyOctet = octetStringTlv(octetStringTlv(rawKey)); /* The ASN.1 type CurvePrivateKey is defined in this document to hold the byte sequence. Thus, when encoding a OneAsymmetricKey object, the private key is wrapped in a CurvePrivateKey object and wrapped by the OCTET STRING of the @@ -56,10 +48,7 @@ export function ed25519PrivateKeyRawToPem(rawKey: Uint8Array): string { // OneAsymmetricKey -> SEQUENCE const oneAsymmetricKey = sequenceOrderedTlv([version, algId, privKeyOctet]); // :) - return toPem( - Buffer.from(oneAsymmetricKey).toString('base64'), - KeyType.private, - ); + return toPem(Buffer.from(oneAsymmetricKey).toString('base64'), KeyType.private); } /* diff --git a/packages/crypto/src/utils/data-types.ts b/packages/crypto/src/utils/data-types.ts index d53537dce..58db6cf0c 100644 --- a/packages/crypto/src/utils/data-types.ts +++ b/packages/crypto/src/utils/data-types.ts @@ -1,26 +1,18 @@ import crypto from 'node:crypto'; // computeJsonHashBuffer computes the hash of a JSON object using the specified algorithm (default is sha256). -export function computeHashBuffer>( - content: T, - algorithm: 'sha256' = 'sha256', -): Buffer { +export function computeHashBuffer>(content: T, algorithm: 'sha256' = 'sha256'): Buffer { // making sure same JSON always results in same hash const canonicalisedJson = encodeCanonicalJson(content); return crypto.createHash(algorithm).update(canonicalisedJson).digest(); } // computeJsonHashString computes the hash of a JSON object and returns it as a UNPADDED base64 string. -export function computeHashString>( - content: T, - algorithm = 'sha256' as const, -) { +export function computeHashString>(content: T, algorithm = 'sha256' as const) { return toUnpaddedBase64(computeHashBuffer(content, algorithm)); } -export function toBinaryData( - value: string | Uint8Array | ArrayBuffer | ArrayBufferView, -): Uint8Array { +export function toBinaryData(value: string | Uint8Array | ArrayBuffer | ArrayBufferView): Uint8Array { if (typeof value === 'string') { return new TextEncoder().encode(value); } @@ -36,9 +28,7 @@ export function toBinaryData( return new Uint8Array(value.buffer, value.byteOffset, value.byteLength); } -export function fromBinaryData( - value: string | Uint8Array | ArrayBuffer, -): string { +export function fromBinaryData(value: string | Uint8Array | ArrayBuffer): string { if (typeof value === 'string') { return value; } @@ -70,11 +60,7 @@ export function encodeCanonicalJson(value: unknown): string { // Handle arrays recursively const serializedArray = value.map((value) => { // can't be in top level since encodeCanonicalJson(function() {}) should be undefined, just not as part of an array - if ( - value === undefined || - typeof value === 'function' || - typeof value === 'symbol' - ) { + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { return 'null'; } return encodeCanonicalJson(value); @@ -85,9 +71,7 @@ export function encodeCanonicalJson(value: unknown): string { // Handle objects: sort keys lexicographically const sortedKeys = Object.keys(value).sort(); const serializedEntries = sortedKeys.reduce((accum, key) => { - const encodedValue = encodeCanonicalJson( - (value as Record)[key], - ); + const encodedValue = encodeCanonicalJson((value as Record)[key]); if (encodedValue === undefined) { return accum; } diff --git a/packages/crypto/src/utils/keys.ts b/packages/crypto/src/utils/keys.ts index eeded239d..b5942f0ce 100644 --- a/packages/crypto/src/utils/keys.ts +++ b/packages/crypto/src/utils/keys.ts @@ -1,33 +1,20 @@ import * as ed25519 from '@noble/ed25519'; + +import { encodeCanonicalJson, fromBase64ToBytes, toBinaryData, toUnpaddedBase64 } from './data-types'; import type { Signer, VerifierKey } from '../contracts/key'; import { Ed25519SigningKeyImpl, Ed25519VerifierKeyImpl } from '../keys/ed25519'; -import { - encodeCanonicalJson, - fromBase64ToBytes, - toBinaryData, - toUnpaddedBase64, -} from './data-types'; // -export async function loadEd25519SignerFromSeed( - seed?: Uint8Array, - version = '0', -): Promise { +export async function loadEd25519SignerFromSeed(seed?: Uint8Array, version = '0'): Promise { const { secretKey, publicKey } = await ed25519.keygenAsync(seed); return new Ed25519SigningKeyImpl(version, secretKey, publicKey); } -export async function loadEd25519VerifierFromPublicKey( - publicKey: Uint8Array, - version = '0', -): Promise { +export async function loadEd25519VerifierFromPublicKey(publicKey: Uint8Array, version = '0'): Promise { return new Ed25519VerifierKeyImpl(version, publicKey); } -export async function signJson( - jsonObject: T, - key: Signer, -): Promise { +export async function signJson(jsonObject: T, key: Signer): Promise { const sortedSerializedForm = encodeCanonicalJson(jsonObject); const signature = await key.sign(toBinaryData(sortedSerializedForm)); @@ -36,11 +23,7 @@ export async function signJson( } // throws if invalid -export async function verifyJsonSignature( - jsonObject: T, - signature: string, - key: VerifierKey, -): Promise { +export async function verifyJsonSignature(jsonObject: T, signature: string, key: VerifierKey): Promise { const sortedSerializedForm = encodeCanonicalJson(jsonObject); const signatureBuffer = fromBase64ToBytes(signature); diff --git a/packages/crypto/src/utils/utils.spec.ts b/packages/crypto/src/utils/utils.spec.ts index a1182cfe2..90474dc52 100644 --- a/packages/crypto/src/utils/utils.spec.ts +++ b/packages/crypto/src/utils/utils.spec.ts @@ -1,11 +1,7 @@ import { describe, expect, it } from 'bun:test'; + import { encodeCanonicalJson, fromBase64ToBytes } from './data-types'; -import { - loadEd25519SignerFromSeed, - loadEd25519VerifierFromPublicKey, - signJson, - verifyJsonSignature, -} from './keys'; +import { loadEd25519SignerFromSeed, loadEd25519VerifierFromPublicKey, signJson, verifyJsonSignature } from './keys'; // NOTE(deb): listing file names as I port the tests // can and should be removed later, won't have must point later @@ -15,7 +11,7 @@ async function getSignerFromKeyContent(content: string) { const seed = content.split(' ').pop()!; const seedBytes = fromBase64ToBytes(seed); - //vvv + // vvv const signer = await loadEd25519SignerFromSeed(seedBytes); return signer; @@ -23,8 +19,7 @@ async function getSignerFromKeyContent(content: string) { // authentication.spec.ts (packages/core) describe('Signing and verifying payloads', async () => { - const seedFileContent = - 'ed25519 a_XRhW YjbSyfqQeGto+OFswt+XwtJUUooHXH5w+czSgawN63U'; + const seedFileContent = 'ed25519 a_XRhW YjbSyfqQeGto+OFswt+XwtJUUooHXH5w+czSgawN63U'; const signer = await getSignerFromKeyContent(seedFileContent); @@ -56,8 +51,7 @@ describe('Signing and verifying payloads', async () => { state_key: '@admin:hs2', signatures: { hs2: { - 'ed25519:a_XRhW': - 'DR+DBqFTm7IUa35pFeOczsNw4shglIXW+3Ze63wC3dqQ4okzaSRgLuAUkYnVyxM2sZkSvlbeSBS7G6DeeaDEAA', + 'ed25519:a_XRhW': 'DR+DBqFTm7IUa35pFeOczsNw4shglIXW+3Ze63wC3dqQ4okzaSRgLuAUkYnVyxM2sZkSvlbeSBS7G6DeeaDEAA', }, }, unsigned: { @@ -75,8 +69,7 @@ describe('Signing and verifying payloads', async () => { // signJson.spec.ts (packages/federation-sdk) it('should verify a valid signature', async () => { - const content = - 'ed25519 a_yNbw tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8'; + const content = 'ed25519 a_yNbw tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8'; const signer = await getSignerFromKeyContent(content); const signature = await signJson({}, signer); @@ -86,261 +79,277 @@ describe('Signing and verifying payloads', async () => { }); it('should throw an error if the signature is invalid', async () => { - const content = - 'ed25519 a_yNbw tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8'; + const content = 'ed25519 a_yNbw tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8'; const signer = await getSignerFromKeyContent(content); const signature = await signJson({}, signer); const diffPublicKeyContent = 'tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8'; - const verifier = await loadEd25519VerifierFromPublicKey( - fromBase64ToBytes(diffPublicKeyContent), - '0', - ); + const verifier = await loadEd25519VerifierFromPublicKey(fromBase64ToBytes(diffPublicKeyContent), '0'); - expect(verifyJsonSignature({}, signature, verifier)).rejects.toThrowError( - 'Invalid signature', - ); + expect(verifyJsonSignature({}, signature, verifier)).rejects.toThrowError('Invalid signature'); }); }); describe('Canonical json serialization', () => { // these cases are all llm generated FYI // write all input and expected outputs, be verbose, cater to all problematic spaces when it comes to json serialization - const testCases: Array<{ input: Record; expected: string }> = - [ - { - input: { b: 1, a: 2 }, - expected: '{"a":2,"b":1}', - }, - { - input: { a: { d: 4, c: 3 }, b: 2 }, - expected: '{"a":{"c":3,"d":4},"b":2}', - }, - { - input: { a: [2, 1], b: 3 }, - expected: '{"a":[2,1],"b":3}', - }, - { - input: { a: 'string with spaces', b: 2 }, - expected: '{"a":"string with spaces","b":2}', - }, - { - input: { a: 'string with "quotes"', b: 2 }, - expected: '{"a":"string with \\"quotes\\"","b":2}', - }, - { - input: { a: 'string with \\ backslash', b: 2 }, - expected: '{"a":"string with \\\\ backslash","b":2}', - }, - { - input: { a: 'string with \n new line', b: 2 }, - expected: '{"a":"string with \\n new line","b":2}', - }, - { - input: { a: 'string with \t tab', b: 2 }, - expected: '{"a":"string with \\t tab","b":2}', - }, - // add more, play with long spaces - { - input: { a: ' leading and trailing spaces ', b: 2 }, - expected: '{"a":" leading and trailing spaces ","b":2}', - }, - { - input: { a: 'multiple spaces', b: 2 }, - expected: '{"a":"multiple spaces","b":2}', - }, - { - input: { a: { b: { c: { d: 'deep' } } }, e: [5, 4, 3, 2, 1] }, - expected: '{"a":{"b":{"c":{"d":"deep"}}},"e":[5,4,3,2,1]}', - }, - { - input: { a: null, b: true, c: false, d: 0, e: '' }, - expected: '{"a":null,"b":true,"c":false,"d":0,"e":""}', - }, - { - input: { - a: [ - { b: 2, a: 1 }, - { d: 4, c: 3 }, - ], - e: 5, - }, - expected: '{"a":[{"a":1,"b":2},{"c":3,"d":4}],"e":5}', - }, - // add more with numbers and letters mixed together - { - input: { a1: 'value1', a10: 'value10', a2: 'value2' }, - expected: '{"a1":"value1","a10":"value10","a2":"value2"}', - }, - { - input: { z: 1, y: { b: 2, a: 1 }, x: [3, 2, 1] }, - expected: '{"x":[3,2,1],"y":{"a":1,"b":2},"z":1}', - }, - { - input: { ' key with spaces ': 'value', normalKey: 'value2' }, - expected: '{" key with spaces ":"value","normalKey":"value2"}', - }, - // add more with unicode characters - { - input: { a: 'üñîçødé', b: '测试', c: 'тест' }, - expected: '{"a":"üñîçødé","b":"测试","c":"тест"}', - }, - { - input: { a: 'emoji 😊', b: '🚀🌟' }, - expected: '{"a":"emoji 😊","b":"🚀🌟"}', - }, - // empty structures - { input: {}, expected: '{}' }, - { input: { a: {} }, expected: '{"a":{}}' }, - { input: { a: [] }, expected: '{"a":[]}' }, - { input: { a: { b: [] } }, expected: '{"a":{"b":[]}}' }, - { input: { a: [1, { b: 2 }, []] }, expected: '{"a":[1,{"b":2},[]]}' }, - // ---------- - // Nested arrays with mixed types - { - input: { arr: [1, 'two', { b: 2, a: 1 }, [3, 4]] }, - expected: '{"arr":[1,"two",{"a":1,"b":2},[3,4]]}', - }, - // Boolean values mixed in arrays and objects - { - input: { a: true, b: false, c: [false, true, { d: false, e: true }] }, - expected: '{"a":true,"b":false,"c":[false,true,{"d":false,"e":true}]}', - }, - // Numbers – including floating points, negative, zero, exponential notation - { - input: { a: 0, b: -0, c: 1.234, d: -5.67, e: 1e3, f: -2.5e-2 }, - expected: '{"a":0,"b":0,"c":1.234,"d":-5.67,"e":1000,"f":-0.025}', - }, - // Empty strings and keys with empty string - { - input: { '': 'emptyKey', a: '', b: 'non-empty' }, - expected: '{"":"emptyKey","a":"","b":"non-empty"}', - }, - // Null values nested deeply - { - input: { a: null, b: { c: null, d: [null, { e: null }] } }, - expected: '{"a":null,"b":{"c":null,"d":[null,{"e":null}]}}', - }, - // Escape sequences inside strings including backspace, form feed, carriage return - { - input: { a: 'backspace\b', b: 'formfeed\f', c: 'carriage\r' }, - expected: '{"a":"backspace\\b","b":"formfeed\\f","c":"carriage\\r"}', - }, - // Unicode escape sequences in strings should remain as characters (not \u escapes) - { - input: { a: '\u2603', b: '\uD83D\uDE00' }, // ☃ and 😀 - expected: '{"a":"☃","b":"😀"}', - }, - // Object keys with numeric strings and sort order (should be lex sorted) - { - input: { '1': 'one', '10': 'ten', '2': 'two' }, - expected: '{"1":"one","10":"ten","2":"two"}', - }, - // Mix of array and object nesting with empty elements - { - input: { a: [], b: {}, c: [{}, [], [{}]] }, - expected: '{"a":[],"b":{},"c":[{},[],[{}]]}', - }, - // Large nested structure with mixed types - { - input: { - z: [5, { x: 10, y: [1, 2, 3], a: { b: 'c' } }], - a: 'test', - m: false, - n: null, - }, - expected: - '{"a":"test","m":false,"n":null,"z":[5,{"a":{"b":"c"},"x":10,"y":[1,2,3]}]}', - }, - // Special number values for JSON (NaN, Infinity) should be omitted or converted to null - // This is tricky because canonical JSON doesn't support them; test if expected behavior is null - { - input: { - a: Number.NaN, - b: Number.POSITIVE_INFINITY, - c: Number.NEGATIVE_INFINITY, - d: 1, - }, - expected: '{"a":null,"b":null,"c":null,"d":1}', - }, - // Keys with special characters - { - input: { 'key\nnewLine': 1, 'key"quote': 2, 'key\\backslash': 3 }, - expected: '{"key\\nnewLine":1,"key\\"quote":2,"key\\\\backslash":3}', - }, - // Deeply nested empty objects and arrays - { - input: { a: { b: { c: { d: {} } } }, e: [[[[]]]] }, - expected: '{"a":{"b":{"c":{"d":{}}}},"e":[[[[]]]]}', - }, - // Keys that differ only by case (JSON keys are case-sensitive) - { - input: { a: 1, A: 2 }, - expected: '{"A":2,"a":1}', - }, - // Array of mixed empty values - { - input: { a: [null, '', 0, false, {}, []] }, - expected: '{"a":[null,"",0,false,{},[]]}', - }, - // Very long string with mixed whitespace characters - { - input: { a: ' \t\n\r mixed\t whitespace\n\r ' }, - expected: '{"a":" \\t\\n\\r mixed\\t whitespace\\n\\r "}', - }, - { - input: { 'key\nnewLine': 1, 'key"quote': 2, 'key\\backslash': 3 }, - expected: '{"key\\nnewLine":1,"key\\"quote":2,"key\\\\backslash":3}', - }, - { - input: { 'line\rreturn': 10, 'tab\tkey': 20, 'form\ffeed': 30 }, - expected: '{"form\\ffeed":30,"line\\rreturn":10,"tab\\tkey":20}', + const testCases: Array<{ input: Record; expected: string }> = [ + { + input: { b: 1, a: 2 }, + expected: '{"a":2,"b":1}', + }, + { + input: { a: { d: 4, c: 3 }, b: 2 }, + expected: '{"a":{"c":3,"d":4},"b":2}', + }, + { + input: { a: [2, 1], b: 3 }, + expected: '{"a":[2,1],"b":3}', + }, + { + input: { a: 'string with spaces', b: 2 }, + expected: '{"a":"string with spaces","b":2}', + }, + { + input: { a: 'string with "quotes"', b: 2 }, + expected: '{"a":"string with \\"quotes\\"","b":2}', + }, + { + input: { a: 'string with \\ backslash', b: 2 }, + expected: '{"a":"string with \\\\ backslash","b":2}', + }, + { + input: { a: 'string with \n new line', b: 2 }, + expected: '{"a":"string with \\n new line","b":2}', + }, + { + input: { a: 'string with \t tab', b: 2 }, + expected: '{"a":"string with \\t tab","b":2}', + }, + // add more, play with long spaces + { + input: { a: ' leading and trailing spaces ', b: 2 }, + expected: '{"a":" leading and trailing spaces ","b":2}', + }, + { + input: { a: 'multiple spaces', b: 2 }, + expected: '{"a":"multiple spaces","b":2}', + }, + { + input: { a: { b: { c: { d: 'deep' } } }, e: [5, 4, 3, 2, 1] }, + expected: '{"a":{"b":{"c":{"d":"deep"}}},"e":[5,4,3,2,1]}', + }, + { + input: { a: null, b: true, c: false, d: 0, e: '' }, + expected: '{"a":null,"b":true,"c":false,"d":0,"e":""}', + }, + { + input: { + a: [ + { b: 2, a: 1 }, + { d: 4, c: 3 }, + ], + e: 5, + }, + expected: '{"a":[{"a":1,"b":2},{"c":3,"d":4}],"e":5}', + }, + // add more with numbers and letters mixed together + { + input: { a1: 'value1', a10: 'value10', a2: 'value2' }, + expected: '{"a1":"value1","a10":"value10","a2":"value2"}', + }, + { + input: { z: 1, y: { b: 2, a: 1 }, x: [3, 2, 1] }, + expected: '{"x":[3,2,1],"y":{"a":1,"b":2},"z":1}', + }, + { + input: { ' key with spaces ': 'value', 'normalKey': 'value2' }, + expected: '{" key with spaces ":"value","normalKey":"value2"}', + }, + // add more with unicode characters + { + input: { a: 'üñîçødé', b: '测试', c: 'тест' }, + expected: '{"a":"üñîçødé","b":"测试","c":"тест"}', + }, + { + input: { a: 'emoji 😊', b: '🚀🌟' }, + expected: '{"a":"emoji 😊","b":"🚀🌟"}', + }, + // empty structures + { input: {}, expected: '{}' }, + { input: { a: {} }, expected: '{"a":{}}' }, + { input: { a: [] }, expected: '{"a":[]}' }, + { input: { a: { b: [] } }, expected: '{"a":{"b":[]}}' }, + { input: { a: [1, { b: 2 }, []] }, expected: '{"a":[1,{"b":2},[]]}' }, + // ---------- + // Nested arrays with mixed types + { + input: { arr: [1, 'two', { b: 2, a: 1 }, [3, 4]] }, + expected: '{"arr":[1,"two",{"a":1,"b":2},[3,4]]}', + }, + // Boolean values mixed in arrays and objects + { + input: { a: true, b: false, c: [false, true, { d: false, e: true }] }, + expected: '{"a":true,"b":false,"c":[false,true,{"d":false,"e":true}]}', + }, + // Numbers – including floating points, negative, zero, exponential notation + { + input: { a: 0, b: -0, c: 1.234, d: -5.67, e: 1e3, f: -2.5e-2 }, + expected: '{"a":0,"b":0,"c":1.234,"d":-5.67,"e":1000,"f":-0.025}', + }, + // Empty strings and keys with empty string + { + input: { '': 'emptyKey', 'a': '', 'b': 'non-empty' }, + expected: '{"":"emptyKey","a":"","b":"non-empty"}', + }, + // Null values nested deeply + { + input: { a: null, b: { c: null, d: [null, { e: null }] } }, + expected: '{"a":null,"b":{"c":null,"d":[null,{"e":null}]}}', + }, + // Escape sequences inside strings including backspace, form feed, carriage return + { + input: { a: 'backspace\b', b: 'formfeed\f', c: 'carriage\r' }, + expected: '{"a":"backspace\\b","b":"formfeed\\f","c":"carriage\\r"}', + }, + // Unicode escape sequences in strings should remain as characters (not \u escapes) + { + input: { a: '\u2603', b: '\uD83D\uDE00' }, // ☃ and 😀 + expected: '{"a":"☃","b":"😀"}', + }, + // Object keys with numeric strings and sort order (should be lex sorted) + { + input: { '1': 'one', '10': 'ten', '2': 'two' }, + expected: '{"1":"one","10":"ten","2":"two"}', + }, + // Mix of array and object nesting with empty elements + { + input: { a: [], b: {}, c: [{}, [], [{}]] }, + expected: '{"a":[],"b":{},"c":[{},[],[{}]]}', + }, + // Large nested structure with mixed types + { + input: { + z: [5, { x: 10, y: [1, 2, 3], a: { b: 'c' } }], + a: 'test', + m: false, + n: null, + }, + expected: '{"a":"test","m":false,"n":null,"z":[5,{"a":{"b":"c"},"x":10,"y":[1,2,3]}]}', + }, + // Special number values for JSON (NaN, Infinity) should be omitted or converted to null + // This is tricky because canonical JSON doesn't support them; test if expected behavior is null + { + input: { + a: Number.NaN, + b: Number.POSITIVE_INFINITY, + c: Number.NEGATIVE_INFINITY, + d: 1, + }, + expected: '{"a":null,"b":null,"c":null,"d":1}', + }, + // Keys with special characters + { + input: { 'key\nnewLine': 1, 'key"quote': 2, 'key\\backslash': 3 }, + expected: '{"key\\nnewLine":1,"key\\"quote":2,"key\\\\backslash":3}', + }, + // Deeply nested empty objects and arrays + { + input: { a: { b: { c: { d: {} } } }, e: [[[[]]]] }, + expected: '{"a":{"b":{"c":{"d":{}}}},"e":[[[[]]]]}', + }, + // Keys that differ only by case (JSON keys are case-sensitive) + { + input: { a: 1, A: 2 }, + expected: '{"A":2,"a":1}', + }, + // Array of mixed empty values + { + input: { a: [null, '', 0, false, {}, []] }, + expected: '{"a":[null,"",0,false,{},[]]}', + }, + // Very long string with mixed whitespace characters + { + input: { a: ' \t\n\r mixed\t whitespace\n\r ' }, + expected: '{"a":" \\t\\n\\r mixed\\t whitespace\\n\\r "}', + }, + { + input: { 'key\nnewLine': 1, 'key"quote': 2, 'key\\backslash': 3 }, + expected: '{"key\\nnewLine":1,"key\\"quote":2,"key\\\\backslash":3}', + }, + { + input: { 'line\rreturn': 10, 'tab\tkey': 20, 'form\ffeed': 30 }, + expected: '{"form\\ffeed":30,"line\\rreturn":10,"tab\\tkey":20}', + }, + { + input: { + 'quote"inside': 'value', + 'back\\slash': 'test', + 'new\nline': 'data', }, - { - input: { - 'quote"inside': 'value', - 'back\\slash': 'test', - 'new\nline': 'data', + expected: '{"back\\\\slash":"test","new\\nline":"data","quote\\"inside":"value"}', + }, + { + input: { 'mix\'ed"esc\\apes\n': 100 }, + expected: '{"mix\'ed\\"esc\\\\apes\\n":100}', + }, + { + input: { 'normal': 1, 'space key': 2, 'slash/key': 3, 'dot.key': 4 }, + expected: '{"dot.key":4,"normal":1,"slash/key":3,"space key":2}', + }, + { + input: { '\bbackspace': 'bs', '\fformfeed': 'ff', '\rcarriage': 'cr' }, + expected: '{"\\bbackspace":"bs","\\fformfeed":"ff","\\rcarriage":"cr"}', + }, + // In arrays, JSON.stringify converts undefined/function/symbol to null. + { + input: { + a: [ + undefined, + () => { + /* noop */ + }, + Symbol('sym'), + 1, + ], + }, + expected: '{"a":[null,null,null,1]}', + }, + // invalid value should be removed in objects + { + input: { + a: undefined, + b: () => { + /* noop */ }, - expected: - '{"back\\\\slash":"test","new\\nline":"data","quote\\"inside":"value"}', - }, - { - input: { 'mix\'ed"esc\\apes\n': 100 }, - expected: '{"mix\'ed\\"esc\\\\apes\\n":100}', - }, - { - input: { normal: 1, 'space key': 2, 'slash/key': 3, 'dot.key': 4 }, - expected: '{"dot.key":4,"normal":1,"slash/key":3,"space key":2}', - }, - { - input: { '\bbackspace': 'bs', '\fformfeed': 'ff', '\rcarriage': 'cr' }, - expected: '{"\\bbackspace":"bs","\\fformfeed":"ff","\\rcarriage":"cr"}', + c: Symbol('sym'), + d: 1, }, - // In arrays, JSON.stringify converts undefined/function/symbol to null. - { - input: { a: [undefined, () => {}, Symbol('sym'), 1] }, - expected: '{"a":[null,null,null,1]}', - }, - // invalid value should be removed in objects - { - input: { a: undefined, b: () => {}, c: Symbol('sym'), d: 1 }, - expected: '{"d":1}', - }, - // mix those previous two - { - input: { - a: undefined, - b: () => {}, - c: Symbol('sym'), - d: 1, - e: [undefined, () => {}, Symbol('sym'), 2], + expected: '{"d":1}', + }, + // mix those previous two + { + input: { + a: undefined, + b: () => { + /* noop */ }, - expected: '{"d":1,"e":[null,null,null,2]}', - }, - ]; + c: Symbol('sym'), + d: 1, + e: [ + undefined, + () => { + /* noop */ + }, + Symbol('sym'), + 2, + ], + }, + expected: '{"d":1,"e":[null,null,null,2]}', + }, + ]; testCases.forEach(({ input, expected }, index) => { it(`should serialize correctly for test case #${index + 1}`, () => { diff --git a/packages/federation-sdk/.eslintrc.json b/packages/federation-sdk/.eslintrc.json new file mode 100644 index 000000000..be97c53fb --- /dev/null +++ b/packages/federation-sdk/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.eslintrc.json" +} diff --git a/packages/federation-sdk/src/index.ts b/packages/federation-sdk/src/index.ts index 427e1d067..3639cf7ca 100644 --- a/packages/federation-sdk/src/index.ts +++ b/packages/federation-sdk/src/index.ts @@ -1,20 +1,17 @@ import 'reflect-metadata'; import type { EventStagingStore } from '@rocket.chat/federation-core'; -import type { - EventID, - EventStore, - PduForType, -} from '@rocket.chat/federation-room'; -import { Collection } from 'mongodb'; +import type { EventID, EventStore, PduForType } from '@rocket.chat/federation-room'; +import type { Collection } from 'mongodb'; import { container } from 'tsyringe'; + import { StagingAreaListener } from './listeners/staging-area.listener'; -import { Key } from './repositories/key.repository'; -import { Lock } from './repositories/lock.repository'; -import { Room } from './repositories/room.repository'; -import { Server } from './repositories/server.repository'; -import { StateGraphStore } from './repositories/state-graph.repository'; -import { Upload } from './repositories/upload.repository'; +import type { Key } from './repositories/key.repository'; +import type { Lock } from './repositories/lock.repository'; +import type { Room } from './repositories/room.repository'; +import type { Server } from './repositories/server.repository'; +import type { StateGraphStore } from './repositories/state-graph.repository'; +import type { Upload } from './repositories/upload.repository'; import { FederationSDK } from './sdk'; import { DatabaseConnectionService } from './services/database-connection.service'; import { EventEmitterService } from './services/event-emitter.service'; @@ -33,14 +30,7 @@ export type { UserID, RoomID, } from '@rocket.chat/federation-room'; -export type { - EventStore, - FileMessageType, - PresenceState, - FileMessageContent, - MessageType, - Membership, -} from '@rocket.chat/federation-core'; +export type { EventStore, FileMessageType, PresenceState, FileMessageContent, MessageType, Membership } from '@rocket.chat/federation-core'; export { generateEd25519RandomSecretKey } from '@rocket.chat/federation-crypto'; export type { @@ -56,17 +46,10 @@ export type { // Utility exports export { getErrorMessage } from './utils/get-error-message'; export { USERNAME_REGEX, ROOM_ID_REGEX } from './utils/validation-regex'; -export { - eventSchemas, - roomV10Schemas, - type BaseEventType, -} from './utils/event-schemas'; +export { eventSchemas, roomV10Schemas, type BaseEventType } from './utils/event-schemas'; export { errCodes } from './utils/response-codes'; export { NotAllowedError } from './services/invite.service'; -export { - FederationValidationService, - FederationValidationError, -} from './services/federation-validation.service'; +export { FederationValidationService, FederationValidationError } from './services/federation-validation.service'; export type HomeserverEventSignatures = { 'homeserver.ping': { @@ -136,12 +119,7 @@ export type HomeserverEventSignatures = { }; }; -export { - roomIdSchema, - userIdSchema, - eventIdSchema, - extractDomainFromId, -} from '@rocket.chat/federation-room'; +export { roomIdSchema, userIdSchema, eventIdSchema, extractDomainFromId } from '@rocket.chat/federation-room'; export async function init({ dbConfig, @@ -159,9 +137,7 @@ export async function init({ }); container.register>('EventStagingCollection', { - useValue: db.collection( - 'rocketchat_federation_events_staging', - ), + useValue: db.collection('rocketchat_federation_events_staging'), }); container.register>('KeyCollection', { @@ -185,9 +161,7 @@ export async function init({ }); container.register>('StateGraphCollection', { - useValue: db.collection( - 'rocketchat_federation_state_graphs', - ), + useValue: db.collection('rocketchat_federation_state_graphs'), }); // this is required to initialize the listener and register the queue handler container.resolve(StagingAreaListener); diff --git a/packages/federation-sdk/src/listeners/staging-area.listener.ts b/packages/federation-sdk/src/listeners/staging-area.listener.ts index 8327cd33d..e5feda433 100644 --- a/packages/federation-sdk/src/listeners/staging-area.listener.ts +++ b/packages/federation-sdk/src/listeners/staging-area.listener.ts @@ -1,17 +1,15 @@ import { createLogger } from '@rocket.chat/federation-core'; -import { RoomID } from '@rocket.chat/federation-room'; +import type { RoomID } from '@rocket.chat/federation-room'; import { singleton } from 'tsyringe'; -import { StagingAreaQueue } from '../queues/staging-area.queue'; -import { StagingAreaService } from '../services/staging-area.service'; + +import type { StagingAreaQueue } from '../queues/staging-area.queue'; +import type { StagingAreaService } from '../services/staging-area.service'; @singleton() export class StagingAreaListener { private readonly logger = createLogger('StagingAreaListener'); - constructor( - private readonly stagingAreaQueue: StagingAreaQueue, - private readonly stagingAreaService: StagingAreaService, - ) { + constructor(private readonly stagingAreaQueue: StagingAreaQueue, private readonly stagingAreaService: StagingAreaService) { this.stagingAreaQueue.registerHandler(this.handleQueueItem.bind(this)); } diff --git a/packages/federation-sdk/src/queues/base.queue.ts b/packages/federation-sdk/src/queues/base.queue.ts index 80f3e71bc..edac795d4 100644 --- a/packages/federation-sdk/src/queues/base.queue.ts +++ b/packages/federation-sdk/src/queues/base.queue.ts @@ -4,7 +4,9 @@ export type QueueHandler = (item: T) => Promise | void; export abstract class BaseQueue { protected queue: T[] = []; + protected processing = false; + protected handler?: QueueHandler; registerHandler(handler: QueueHandler) { @@ -30,6 +32,7 @@ export abstract class BaseQueue { continue; } + // eslint-disable-next-line no-await-in-loop await this.handler(item); } catch (err) { logger.error({ diff --git a/packages/federation-sdk/src/queues/staging-area.queue.ts b/packages/federation-sdk/src/queues/staging-area.queue.ts index e63fbe764..7c44e5313 100644 --- a/packages/federation-sdk/src/queues/staging-area.queue.ts +++ b/packages/federation-sdk/src/queues/staging-area.queue.ts @@ -1,4 +1,4 @@ -import { RoomID } from '@rocket.chat/federation-room'; +import type { RoomID } from '@rocket.chat/federation-room'; import 'reflect-metadata'; import { singleton } from 'tsyringe'; @@ -7,7 +7,9 @@ type QueueHandler = (roomId: RoomID) => Promise; @singleton() export class StagingAreaQueue { private queue: RoomID[] = []; + private handlers: QueueHandler[] = []; + private processing = false; enqueue(roomId: RoomID): void { @@ -32,6 +34,7 @@ export class StagingAreaQueue { if (!roomId) continue; for (const handler of this.handlers) { + // eslint-disable-next-line no-await-in-loop await handler(roomId); } } diff --git a/packages/federation-sdk/src/repositories/event-staging.repository.ts b/packages/federation-sdk/src/repositories/event-staging.repository.ts index dd5f3fdf0..4ccaa9731 100644 --- a/packages/federation-sdk/src/repositories/event-staging.repository.ts +++ b/packages/federation-sdk/src/repositories/event-staging.repository.ts @@ -1,6 +1,6 @@ import { generateId } from '@rocket.chat/federation-core'; import type { EventStagingStore } from '@rocket.chat/federation-core'; -import { type EventID, Pdu, RoomID } from '@rocket.chat/federation-room'; +import type { Pdu, RoomID, EventID } from '@rocket.chat/federation-room'; import type { Collection, DeleteResult, UpdateResult } from 'mongodb'; import { inject, singleton } from 'tsyringe'; @@ -11,19 +11,14 @@ export class EventStagingRepository { private readonly collection: Collection, ) { this.collection.createIndex({ - roomId: 1, - got: 1, + 'roomId': 1, + 'got': 1, 'event.depth': 1, - createdAt: 1, + 'createdAt': 1, }); } - async create( - eventId: EventID, - origin: string, - event: Pdu, - from: 'join' | 'transaction' = 'transaction', - ): Promise { + async create(eventId: EventID, origin: string, event: Pdu, from: 'join' | 'transaction' = 'transaction'): Promise { // We use an upsert here to handle the case where we see the same event // from the same server multiple times. return this.collection.updateOne( @@ -57,7 +52,7 @@ export class EventStagingRepository { }, }, { - sort: { got: 1, 'event.depth': 1, createdAt: 1 }, + sort: { 'got': 1, 'event.depth': 1, 'createdAt': 1 }, upsert: false, returnDocument: 'before', }, diff --git a/packages/federation-sdk/src/repositories/event.repository.ts b/packages/federation-sdk/src/repositories/event.repository.ts index a9bdb55a3..5ac37209c 100644 --- a/packages/federation-sdk/src/repositories/event.repository.ts +++ b/packages/federation-sdk/src/repositories/event.repository.ts @@ -1,22 +1,7 @@ import { generateId } from '@rocket.chat/federation-core'; import type { EventStore } from '@rocket.chat/federation-core'; -import { - type EventID, - Pdu, - PduForType, - PduType, - RejectCode, - RoomID, - StateID, -} from '@rocket.chat/federation-room'; -import type { - Collection, - FindCursor, - FindOptions, - InsertOneResult, - UpdateResult, - WithId, -} from 'mongodb'; +import type { Pdu, PduForType, PduType, RejectCode, RoomID, StateID, EventID } from '@rocket.chat/federation-room'; +import type { Collection, FindCursor, FindOptions, InsertOneResult, UpdateResult, WithId } from 'mongodb'; import { inject, singleton } from 'tsyringe'; @singleton() @@ -29,21 +14,15 @@ export class EventRepository { async findById(eventId: EventID): Promise { return this.collection.findOne({ _id: eventId }); } - async findByIdAndType( - eventId: EventID, - type: PduType, - ): Promise> | null> { + + async findByIdAndType(eventId: EventID, type: PduType): Promise> | null> { return this.collection.findOne({ - _id: eventId, + '_id': eventId, 'event.type': type, }) as unknown as EventStore> | null; } - findAuthEvents( - eventType: string, - roomId: string, - senderId: string, - ): FindCursor { + findAuthEvents(eventType: string, roomId: string, senderId: string): FindCursor { const baseQueries = { create: { query: { 'event.room_id': roomId, 'event.type': 'm.room.create' }, @@ -77,11 +56,7 @@ export class EventRepository { default: // for all other events (known and unknown), we need to fetch the create, // power levels, and membership events for proper authorization - queries = [ - baseQueries.create, - baseQueries.powerLevels, - baseQueries.membership, - ]; + queries = [baseQueries.create, baseQueries.powerLevels, baseQueries.membership]; break; } @@ -89,10 +64,7 @@ export class EventRepository { } findByRoomId(roomId: string): FindCursor { - return this.collection.find( - { 'event.room_id': roomId }, - { sort: { 'event.depth': 1 } }, - ); + return this.collection.find({ 'event.room_id': roomId }, { sort: { 'event.depth': 1 } }); } async redactEvent(eventId: EventID, redactedEvent: Pdu): Promise { @@ -115,18 +87,14 @@ export class EventRepository { return id; } - public async findPowerLevelsEventByRoomId( - roomId: string, - ): Promise> | null> { + public async findPowerLevelsEventByRoomId(roomId: string): Promise> | null> { return this.collection.findOne({ 'event.room_id': roomId, 'event.type': 'm.room.power_levels', }) as unknown as EventStore> | null; } - public async findAllJoinedMembersEventsByRoomId( - roomId: string, - ): Promise>[]> { + public async findAllJoinedMembersEventsByRoomId(roomId: string): Promise>[]> { return this.collection .find({ 'event.room_id': roomId, @@ -136,15 +104,12 @@ export class EventRepository { .toArray() as Promise>[]>; } - async findLatestEventByRoomIdBeforeTimestampWithAssociatedState( - roomId: string, - timestamp: number, - ): Promise { + async findLatestEventByRoomIdBeforeTimestampWithAssociatedState(roomId: string, timestamp: number): Promise { return this.collection.findOne( { 'event.room_id': roomId, 'event.origin_server_ts': { $lt: timestamp }, // events before passed timestamp - stateId: { $ne: '' as StateID }, + 'stateId': { $ne: '' as StateID }, }, { sort: { @@ -154,10 +119,7 @@ export class EventRepository { ); } - findEventsByRoomIdAfterTimestamp( - roomId: string, - timestamp: number, - ): FindCursor { + findEventsByRoomIdAfterTimestamp(roomId: string, timestamp: number): FindCursor { return this.collection .find({ 'event.room_id': roomId, @@ -178,18 +140,16 @@ export class EventRepository { return this.collection .find( { - nextEventId: '', + 'nextEventId': '', 'event.room_id': roomId, - rejectCode: { $exists: false }, + 'rejectCode': { $exists: false }, }, - { sort: { 'event.depth': 1, createdAt: 1 } }, + { sort: { 'event.depth': 1, 'createdAt': 1 } }, ) .toArray(); } - findMembershipEventsFromDirectMessageRooms( - users: string[], - ): FindCursor>> { + findMembershipEventsFromDirectMessageRooms(users: string[]): FindCursor>> { return this.collection.find({ 'event.type': 'm.room.member', 'event.state_key': { $in: users }, @@ -206,74 +166,54 @@ export class EventRepository { }); } - findByIds( - eventIds: EventID[], - ): FindCursor>>> { + findByIds(eventIds: EventID[]): FindCursor>>> { return this.collection.find( { _id: { $in: eventIds }, }, - { sort: { 'event.depth': -1, createdAt: -1 } }, + { sort: { 'event.depth': -1, 'createdAt': -1 } }, ) as FindCursor>>>; } - findByIdsOrderedDescending( - eventIds: EventID[], - ): FindCursor>>> { + findByIdsOrderedDescending(eventIds: EventID[]): FindCursor>>> { return this.collection.find( { _id: { $in: eventIds }, }, - { sort: { 'event.depth': -1, createdAt: -1 } }, + { sort: { 'event.depth': -1, 'createdAt': -1 } }, ) as FindCursor>>>; } - findByRoomIdAndTypes( - roomId: string, - eventTypes: string[], - ): FindCursor { + findByRoomIdAndTypes(roomId: string, eventTypes: string[]): FindCursor { return this.collection.find({ 'event.room_id': roomId, 'event.type': { $in: eventTypes }, }); } - async setMissingDependencies( - eventId: EventID, - missingDependencies: EventStore['missing_dependencies'], - ): Promise { - await this.collection.updateOne( - { _id: eventId }, - { $set: { missing_dependencies: missingDependencies } }, - ); + async setMissingDependencies(eventId: EventID, missingDependencies: EventStore['missing_dependencies']): Promise { + await this.collection.updateOne({ _id: eventId }, { $set: { missing_dependencies: missingDependencies } }); } findFromNonPublicRooms(eventIds: string[]): FindCursor { return this.collection.find({ - eventId: { $in: eventIds }, + 'eventId': { $in: eventIds }, 'event.content.join_rule': { $ne: 'public' }, }); } - async findByRoomIdAndType( - roomId: string, - eventType: T, - ): Promise> | null> { + async findByRoomIdAndType(roomId: string, eventType: T): Promise> | null> { return this.collection.findOne({ 'event.room_id': roomId, 'event.type': eventType, }) as unknown as EventStore> | null; } - findByRoomIdExcludingEventIds( - roomId: string, - eventIdsToExclude: EventID[], - limit: number, - ): FindCursor { + findByRoomIdExcludingEventIds(roomId: string, eventIdsToExclude: EventID[], limit: number): FindCursor { return this.collection.find( { 'event.room_id': roomId, - _id: { $nin: eventIdsToExclude }, + '_id': { $nin: eventIdsToExclude }, }, { limit, @@ -281,10 +221,7 @@ export class EventRepository { ); } - async findInviteEventsByRoomIdAndUserId( - roomId: string, - userId: string, - ): Promise { + async findInviteEventsByRoomIdAndUserId(roomId: string, userId: string): Promise { const result = this.collection.find( { 'event.room_id': roomId, @@ -298,22 +235,16 @@ export class EventRepository { } async findLatestFromRoomId(roomId: string): Promise { - return this.collection.findOne( - { 'event.room_id': roomId }, - { sort: { 'event.depth': -1 } }, - ); + return this.collection.findOne({ 'event.room_id': roomId }, { sort: { 'event.depth': -1 } }); } - findEventsByIdsWithDepth( - roomId: string, - eventIds: EventID[], - ): FindCursor>> { + findEventsByIdsWithDepth(roomId: string, eventIds: EventID[]): FindCursor>> { return this.collection.find( { 'event.room_id': roomId, - _id: { $in: eventIds }, + '_id': { $in: eventIds }, }, - { projection: { _id: 1, 'event.depth': 1 } }, + { projection: { '_id': 1, 'event.depth': 1 } }, ); } @@ -329,7 +260,7 @@ export class EventRepository { { 'event.room_id': roomId, 'event.depth': { $gte: minDepth, $lte: maxDepth }, - event_id: { $nin: excludeEventIds }, + 'event_id': { $nin: excludeEventIds }, }, { projection: { event: 1 }, @@ -339,13 +270,10 @@ export class EventRepository { .limit(limit); } - async findNewestEventForBackfill( - roomId: string, - eventIds: EventID[], - ): Promise { + async findNewestEventForBackfill(roomId: string, eventIds: EventID[]): Promise { return this.collection.findOne( { - _id: { $in: eventIds }, + '_id': { $in: eventIds }, 'event.room_id': roomId, }, { @@ -357,16 +285,11 @@ export class EventRepository { ); } - findEventsForBackfill( - roomId: string, - depth: number, - originServerTs: number, - limit: number, - ) { + findEventsForBackfill(roomId: string, depth: number, originServerTs: number, limit: number) { return this.collection .find({ 'event.room_id': roomId, - $or: [ + '$or': [ { 'event.depth': { $lt: depth } }, { 'event.depth': depth, @@ -383,12 +306,7 @@ export class EventRepository { // new ones // ------------------- - insertOrUpdateEventWithStateId( - eventId: EventID, - event: Pdu, - stateId: StateID, - partial = false, - ): Promise { + insertOrUpdateEventWithStateId(eventId: EventID, event: Pdu, stateId: StateID, partial = false): Promise { return this.collection.updateOne( { _id: eventId }, { @@ -406,11 +324,7 @@ export class EventRepository { ); } - insertOutlierEvent( - eventId: EventID, - event: Pdu, - origin: string, - ): Promise> { + insertOutlierEvent(eventId: EventID, event: Pdu, origin: string): Promise> { return this.collection.insertOne({ _id: eventId, event, @@ -423,10 +337,7 @@ export class EventRepository { }); } - async updateNextEventReferences( - newEventId: EventID, - previousEventIds: EventID[], - ): Promise { + async updateNextEventReferences(newEventId: EventID, previousEventIds: EventID[]): Promise { return this.collection.updateMany( { _id: { $in: previousEventIds }, nextEventId: '' as EventID }, { $set: { nextEventId: newEventId } }, @@ -434,10 +345,7 @@ export class EventRepository { } async findStateIdByEventId(eventId: EventID): Promise { - const result = await this.collection.findOne>( - { _id: eventId }, - { projection: { stateId: 1 } }, - ); + const result = await this.collection.findOne>({ _id: eventId }, { projection: { stateId: 1 } }); return result?.stateId; } @@ -485,9 +393,6 @@ export class EventRepository { } findPartialsByRoomId(roomId: RoomID) { - return this.collection.find( - { 'event.room_id': roomId, partial: true }, - { sort: { 'event.depth': 1, createdAt: 1 } }, - ); + return this.collection.find({ 'event.room_id': roomId, 'partial': true }, { sort: { 'event.depth': 1, 'createdAt': 1 } }); } } diff --git a/packages/federation-sdk/src/repositories/key.repository.ts b/packages/federation-sdk/src/repositories/key.repository.ts index 1ef3cd295..42aa41ffc 100644 --- a/packages/federation-sdk/src/repositories/key.repository.ts +++ b/packages/federation-sdk/src/repositories/key.repository.ts @@ -1,4 +1,4 @@ -import { Collection } from 'mongodb'; +import type { Collection } from 'mongodb'; import { inject, singleton } from 'tsyringe'; export type Key = { @@ -10,14 +10,9 @@ export type Key = { @singleton() export class KeyRepository { - constructor( - @inject('KeyCollection') private readonly collection: Collection, - ) {} + constructor(@inject('KeyCollection') private readonly collection: Collection) {} - async getValidPublicKeyFromLocal( - origin: string, - keyId: string, - ): Promise { + async getValidPublicKeyFromLocal(origin: string, keyId: string): Promise { const key = await this.collection.findOne({ origin, key_id: keyId, @@ -27,12 +22,7 @@ export class KeyRepository { return key?.public_key; } - async storePublicKey( - origin: string, - keyId: string, - publicKey: string, - validUntil?: Date, - ): Promise { + async storePublicKey(origin: string, keyId: string, publicKey: string, validUntil?: Date): Promise { await this.collection.updateOne( { origin, key_id: keyId }, { diff --git a/packages/federation-sdk/src/repositories/lock.repository.ts b/packages/federation-sdk/src/repositories/lock.repository.ts index 4fe294377..6ada5a6b6 100644 --- a/packages/federation-sdk/src/repositories/lock.repository.ts +++ b/packages/federation-sdk/src/repositories/lock.repository.ts @@ -1,4 +1,4 @@ -import { Collection } from 'mongodb'; +import type { Collection } from 'mongodb'; import { inject, singleton } from 'tsyringe'; export type Lock = { @@ -9,9 +9,7 @@ export type Lock = { @singleton() export class LockRepository { - constructor( - @inject('LockCollection') private readonly collection: Collection, - ) { + constructor(@inject('LockCollection') private readonly collection: Collection) { // TODO define proper way of creating indexes in repositories this.collection.createIndex({ roomId: 1 }, { unique: true }); } @@ -57,9 +55,6 @@ export class LockRepository { } async updateLockTimestamp(roomId: string, instanceId: string): Promise { - await this.collection.updateOne( - { roomId, instanceId }, - { $set: { lockedAt: new Date() } }, - ); + await this.collection.updateOne({ roomId, instanceId }, { $set: { lockedAt: new Date() } }); } } diff --git a/packages/federation-sdk/src/repositories/room.repository.ts b/packages/federation-sdk/src/repositories/room.repository.ts index 1abe06920..bb72c5d1d 100644 --- a/packages/federation-sdk/src/repositories/room.repository.ts +++ b/packages/federation-sdk/src/repositories/room.repository.ts @@ -1,6 +1,6 @@ import type { EventBase } from '@rocket.chat/federation-core'; import type { EventID } from '@rocket.chat/federation-room'; -import { Collection } from 'mongodb'; +import type { Collection } from 'mongodb'; import { inject, singleton } from 'tsyringe'; export type Room = { @@ -18,9 +18,7 @@ export type Room = { @singleton() export class RoomRepository { - constructor( - @inject('RoomCollection') private readonly collection: Collection, - ) {} + constructor(@inject('RoomCollection') private readonly collection: Collection) {} async upsert(roomId: string, state: EventBase[]) { await this.collection.findOneAndUpdate( @@ -35,10 +33,7 @@ export class RoomRepository { ); } - async insert( - roomId: string, - props: { name?: string; canonicalAlias?: string; alias?: string }, - ): Promise { + async insert(roomId: string, props: { name?: string; canonicalAlias?: string; alias?: string }): Promise { await this.collection.insertOne({ _id: roomId, room: { @@ -52,28 +47,19 @@ export class RoomRepository { } async getRoomVersion(roomId: string): Promise { - const room = await this.collection.findOne( - { _id: roomId }, - { projection: { version: 1 } }, - ); + const room = await this.collection.findOne({ _id: roomId }, { projection: { version: 1 } }); return room?.room.version || null; } async updateRoomName(roomId: string, name: string): Promise { - await this.collection.updateOne( - { room_id: roomId }, - { $set: { name: name } }, - { upsert: false }, - ); + await this.collection.updateOne({ room_id: roomId }, { $set: { name } }, { upsert: false }); } + public async findOneById(roomId: string): Promise { return this.collection.findOne({ _id: roomId }); } - async markRoomAsDeleted( - roomId: string, - tombstoneEventId: string, - ): Promise { + async markRoomAsDeleted(roomId: string, tombstoneEventId: string): Promise { await this.collection.updateOne( { _id: roomId }, { diff --git a/packages/federation-sdk/src/repositories/server.repository.ts b/packages/federation-sdk/src/repositories/server.repository.ts index 57defef91..7f9d85133 100644 --- a/packages/federation-sdk/src/repositories/server.repository.ts +++ b/packages/federation-sdk/src/repositories/server.repository.ts @@ -1,4 +1,4 @@ -import { Collection } from 'mongodb'; +import type { Collection } from 'mongodb'; import { inject, singleton } from 'tsyringe'; export type Server = { @@ -13,24 +13,14 @@ export type Server = { @singleton() export class ServerRepository { - constructor( - @inject('ServerCollection') private readonly collection: Collection, - ) {} + constructor(@inject('ServerCollection') private readonly collection: Collection) {} - async getValidPublicKeyFromLocal( - origin: string, - key: string, - ): Promise { + async getValidPublicKeyFromLocal(origin: string, key: string): Promise { const server = await this.collection.findOne({ name: origin }); return server?.keys?.[key]?.key; } - async storePublicKey( - origin: string, - key: string, - value: string, - validUntil: number, - ): Promise { + async storePublicKey(origin: string, key: string, value: string, validUntil: number): Promise { await this.collection.findOneAndUpdate( { name: origin }, { diff --git a/packages/federation-sdk/src/repositories/state-graph.repository.ts b/packages/federation-sdk/src/repositories/state-graph.repository.ts index 3e0bf36fc..2862403b1 100644 --- a/packages/federation-sdk/src/repositories/state-graph.repository.ts +++ b/packages/federation-sdk/src/repositories/state-graph.repository.ts @@ -32,9 +32,7 @@ export class StateGraphRepository { private readonly collection: Collection, ) {} - private async _buildPreviousStateMapById( - stateId: StateID, - ): Promise> { + private async _buildPreviousStateMapById(stateId: StateID): Promise> { const result = await this.collection .aggregate<{ stateMap: Record }>([ { $match: { _id: stateId } }, @@ -75,14 +73,10 @@ export class StateGraphRepository { return new Map(); } - return new Map( - Object.entries(result[0].stateMap) as [StateMapKey, EventID][], - ); + return new Map(Object.entries(result[0].stateMap) as [StateMapKey, EventID][]); } - async buildPreviousStateMapById( - stateId: StateID, - ): Promise | null> { + async buildPreviousStateMapById(stateId: StateID): Promise | null> { const current = await this.collection.findOne({ _id: stateId }); if (!current) { return null; @@ -91,9 +85,7 @@ export class StateGraphRepository { return this._buildPreviousStateMapById(stateId); } - async buildStateMapById( - stateId: StateID, - ): Promise | null> { + async buildStateMapById(stateId: StateID): Promise | null> { const current = await this.collection.findOne({ _id: stateId }); if (!current) { return null; @@ -104,10 +96,7 @@ export class StateGraphRepository { return null; } - stateMap.set( - getStateMapKey({ type: current.type, state_key: current.stateKey }), - current.eventId, - ); + stateMap.set(getStateMapKey({ type: current.type, state_key: current.stateKey }), current.eventId); return stateMap; } @@ -129,13 +118,11 @@ export class StateGraphRepository { const [create, ...rest] = sorted; if (!create) { - throw new Error( - 'StateGraphReposiory: no create event in state snapshot to be saved', - ); + throw new Error('StateGraphReposiory: no create event in state snapshot to be saved'); } let previousStateId = await this.createDelta(create, '' as StateID); - for (const event of rest) { + for await (const event of rest) { previousStateId = await this.createDelta(event, previousStateId); } @@ -143,10 +130,7 @@ export class StateGraphRepository { } async findChainIdByStateId(stateId: StateID) { - const doc = await this.collection.findOne( - { _id: stateId }, - { projection: { chainId: 1 } }, - ); + const doc = await this.collection.findOne({ _id: stateId }, { projection: { chainId: 1 } }); if (!doc) { throw new Error(`No chain id for existing state id ${stateId}`); } @@ -162,10 +146,7 @@ export class StateGraphRepository { return this.collection.findOne({ previousNode: stateId }); } - async createDelta( - event: PersistentEventBase, - previousStateId: StateID, - ): Promise { + async createDelta(event: PersistentEventBase, previousStateId: StateID): Promise { const stateId = new ObjectId().toString() as StateID; const previousDelta = await this.findOneById(previousStateId); @@ -205,10 +186,7 @@ export class StateGraphRepository { } async findLatestByStateIds(stateIds: StateID[]) { - return this.collection.findOne( - { _id: { $in: stateIds } }, - { sort: { depth: -1 } }, - ); + return this.collection.findOne({ _id: { $in: stateIds } }, { sort: { depth: -1 } }); } findByEventIds(eventIds: EventID[]) { @@ -216,9 +194,6 @@ export class StateGraphRepository { } findLatestByChainIdAndEventIds(chainId: string, eventIds: EventID[]) { - return this.collection.findOne( - { chainId, eventId: { $in: eventIds } }, - { sort: { depth: -1 } }, - ); + return this.collection.findOne({ chainId, eventId: { $in: eventIds } }, { sort: { depth: -1 } }); } } diff --git a/packages/federation-sdk/src/repositories/upload.repository.ts b/packages/federation-sdk/src/repositories/upload.repository.ts index d67ea7458..c7057ad87 100644 --- a/packages/federation-sdk/src/repositories/upload.repository.ts +++ b/packages/federation-sdk/src/repositories/upload.repository.ts @@ -1,5 +1,5 @@ -import { RoomID } from '@rocket.chat/federation-room'; -import { Collection } from 'mongodb'; +import type { RoomID } from '@rocket.chat/federation-room'; +import type { Collection } from 'mongodb'; import { inject, singleton } from 'tsyringe'; export type Upload = { @@ -14,9 +14,7 @@ export type Upload = { @singleton() export class UploadRepository { - constructor( - @inject('UploadCollection') private readonly collection: Collection, - ) {} + constructor(@inject('UploadCollection') private readonly collection: Collection) {} async findByMediaId(mediaId: string): Promise { return this.collection.findOne({ diff --git a/packages/federation-sdk/src/sdk.ts b/packages/federation-sdk/src/sdk.ts index f42c4db5f..44e2c399e 100644 --- a/packages/federation-sdk/src/sdk.ts +++ b/packages/federation-sdk/src/sdk.ts @@ -2,23 +2,23 @@ import type { EventStore } from '@rocket.chat/federation-core'; import type { PduForType, PduType, UserID } from '@rocket.chat/federation-room'; import { singleton } from 'tsyringe'; -import { AppConfig, ConfigService } from './services/config.service'; -import { EduService } from './services/edu.service'; -import { EventAuthorizationService } from './services/event-authorization.service'; -import { EventEmitterService } from './services/event-emitter.service'; -import { EventService } from './services/event.service'; -import { FederationRequestService } from './services/federation-request.service'; -import { FederationValidationService } from './services/federation-validation.service'; -import { FederationService } from './services/federation.service'; -import { InviteService } from './services/invite.service'; -import { MediaService } from './services/media.service'; -import { MessageService } from './services/message.service'; -import { ProfilesService } from './services/profiles.service'; -import { RoomService } from './services/room.service'; -import { SendJoinService } from './services/send-join.service'; -import { ServerService } from './services/server.service'; -import { StateService } from './services/state.service'; -import { WellKnownService } from './services/well-known.service'; +import type { AppConfig, ConfigService } from './services/config.service'; +import type { EduService } from './services/edu.service'; +import type { EventAuthorizationService } from './services/event-authorization.service'; +import type { EventEmitterService } from './services/event-emitter.service'; +import type { EventService } from './services/event.service'; +import type { FederationRequestService } from './services/federation-request.service'; +import type { FederationValidationService } from './services/federation-validation.service'; +import type { FederationService } from './services/federation.service'; +import type { InviteService } from './services/invite.service'; +import type { MediaService } from './services/media.service'; +import type { MessageService } from './services/message.service'; +import type { ProfilesService } from './services/profiles.service'; +import type { RoomService } from './services/room.service'; +import type { SendJoinService } from './services/send-join.service'; +import type { ServerService } from './services/server.service'; +import type { StateService } from './services/state.service'; +import type { WellKnownService } from './services/well-known.service'; // create a federation sdk class to export @singleton() @@ -46,19 +46,11 @@ export class FederationSDK { /** * @deprecated use createDirectMessage instead */ - createDirectMessageRoom( - ...args: Parameters - ) { + createDirectMessageRoom(...args: Parameters) { return this.roomService.createDirectMessageRoom(...args); } - async createDirectMessage({ - creatorUserId, - members, - }: { - creatorUserId: UserID; - members: UserID[]; - }) { + async createDirectMessage({ creatorUserId, members }: { creatorUserId: UserID; members: UserID[] }) { return this.roomService.createDirectMessage({ creatorUserId, members, @@ -69,15 +61,11 @@ export class FederationSDK { return this.roomService.createRoom(...args); } - inviteUserToRoom( - ...args: Parameters - ) { + inviteUserToRoom(...args: Parameters) { return this.inviteService.inviteUserToRoom(...args); } - sendFileMessage( - ...args: Parameters - ) { + sendFileMessage(...args: Parameters) { return this.messageService.sendFileMessage(...args); } @@ -135,21 +123,15 @@ export class FederationSDK { return this.roomService.setRoomTopic(...args); } - setPowerLevelForUser( - ...args: Parameters - ) { + setPowerLevelForUser(...args: Parameters) { return this.roomService.setPowerLevelForUser(...args); } - sendTypingNotification( - ...args: Parameters - ) { + sendTypingNotification(...args: Parameters) { return this.eduService.sendTypingNotification(...args); } - getSignedServerKey( - ...args: Parameters - ) { + getSignedServerKey(...args: Parameters) { return this.serverService.getSignedServerKey(...args); } @@ -161,11 +143,7 @@ export class FederationSDK { return this.inviteService.processInvite(...args); } - verifyRequestSignature( - ...args: Parameters< - typeof this.eventAuthorizationService.verifyRequestSignature - > - ) { + verifyRequestSignature(...args: Parameters) { return this.eventAuthorizationService.verifyRequestSignature(...args); } @@ -177,15 +155,11 @@ export class FederationSDK { return this.roomService.rejectInvite(...args); } - getLatestRoomState2( - ...args: Parameters - ) { + getLatestRoomState2(...args: Parameters) { return this.stateService.getLatestRoomState2(...args); } - downloadFromRemoteServer( - ...args: Parameters - ) { + downloadFromRemoteServer(...args: Parameters) { return this.mediaService.downloadFromRemoteServer(...args); } @@ -193,9 +167,7 @@ export class FederationSDK { return this.profilesService.queryProfile(...args); } - getAllPublicRoomIdsAndNames( - ...args: Parameters - ) { + getAllPublicRoomIdsAndNames(...args: Parameters) { return this.stateService.getAllPublicRoomIdsAndNames(...args); } @@ -203,9 +175,7 @@ export class FederationSDK { return this.sendJoinService.sendJoin(...args); } - processIncomingTransaction( - ...args: Parameters - ) { + processIncomingTransaction(...args: Parameters) { return this.eventService.processIncomingTransaction(...args); } @@ -217,47 +187,31 @@ export class FederationSDK { return this.eventService.getState(...args); } - getBackfillEvents( - ...args: Parameters - ) { + getBackfillEvents(...args: Parameters) { return this.eventService.getBackfillEvents(...args); } - canAccessResource( - ...args: Parameters - ) { + canAccessResource(...args: Parameters) { return this.eventAuthorizationService.canAccessResource(...args); } - getWellKnownHostData( - ...args: Parameters - ) { + getWellKnownHostData(...args: Parameters) { return this.wellKnownService.getWellKnownHostData(...args); } - validateOutboundUser( - ...args: Parameters< - typeof this.federationValidationService.validateOutboundUser - > - ) { + validateOutboundUser(...args: Parameters) { return this.federationValidationService.validateOutboundUser(...args); } - updateUserPowerLevel( - ...args: Parameters - ) { + updateUserPowerLevel(...args: Parameters) { return this.roomService.updateUserPowerLevel(...args); } - findStateAtEvent( - ...args: Parameters - ) { + findStateAtEvent(...args: Parameters) { return this.stateService.findStateAtEvent(...args); } - getLatestRoomState( - ...args: Parameters - ) { + getLatestRoomState(...args: Parameters) { return this.stateService.getLatestRoomState(...args); } @@ -265,9 +219,7 @@ export class FederationSDK { return this.stateService.handlePdu(...args); } - markRoomAsTombstone( - ...args: Parameters - ) { + markRoomAsTombstone(...args: Parameters) { return this.roomService.markRoomAsTombstone(...args); } @@ -275,34 +227,19 @@ export class FederationSDK { return this.stateService.getAllRoomIds(...args); } - makeSignedRequest( - ...args: Parameters - ) { + makeSignedRequest(...args: Parameters) { return this.federationRequestService.makeSignedRequest(...args); } - queryProfileRemote({ - homeserverUrl, - userId, - }: { homeserverUrl: string; userId: string }) { - return this.federationRequestService.get( - homeserverUrl, - '/_matrix/federation/v1/query/profile', - { user_id: userId }, - ); + queryProfileRemote({ homeserverUrl, userId }: { homeserverUrl: string; userId: string }) { + return this.federationRequestService.get(homeserverUrl, '/_matrix/federation/v1/query/profile', { user_id: userId }); } - buildEvent( - ...args: Parameters> - ) { + buildEvent(...args: Parameters>) { return this.stateService.buildEvent(...args); } - sendEventToAllServersInRoom( - ...args: Parameters< - typeof this.federationService.sendEventToAllServersInRoom - > - ) { + sendEventToAllServersInRoom(...args: Parameters) { return this.federationService.sendEventToAllServersInRoom(...args); } @@ -310,9 +247,7 @@ export class FederationSDK { return this.profilesService.makeJoin(...args); } - getMissingEvents( - ...args: Parameters - ) { + getMissingEvents(...args: Parameters) { return this.eventService.getMissingEvents(...args); } @@ -328,9 +263,7 @@ export class FederationSDK { return this.profilesService.queryKeys(...args); } - sendPresenceUpdateToRooms( - ...args: Parameters - ) { + sendPresenceUpdateToRooms(...args: Parameters) { return this.eduService.sendPresenceUpdateToRooms(...args); } } diff --git a/packages/federation-sdk/src/server-discovery/_multi-error.ts b/packages/federation-sdk/src/server-discovery/_multi-error.ts index 819df811d..8099f4838 100644 --- a/packages/federation-sdk/src/server-discovery/_multi-error.ts +++ b/packages/federation-sdk/src/server-discovery/_multi-error.ts @@ -1,9 +1,8 @@ export class MultiError extends Error { private _finalMessage = ''; + append(message: string, error: Error) { - this._finalMessage += message - ? `\n${message}: ${error.message}` - : error.message; + this._finalMessage += message ? `\n${message}: ${error.message}` : error.message; } concat(other: MultiError) { diff --git a/packages/federation-sdk/src/server-discovery/_resolver.ts b/packages/federation-sdk/src/server-discovery/_resolver.ts index f131fb9ce..018bdad07 100644 --- a/packages/federation-sdk/src/server-discovery/_resolver.ts +++ b/packages/federation-sdk/src/server-discovery/_resolver.ts @@ -9,9 +9,7 @@ class _Resolver extends Resolver { super(); if (process.env.HOMESERVER_CONFIG_DNS_SERVERS) { - const servers = process.env.HOMESERVER_CONFIG_DNS_SERVERS.split(',').map( - (s) => s.trim(), - ); + const servers = process.env.HOMESERVER_CONFIG_DNS_SERVERS.split(',').map((s) => s.trim()); this.setServers(servers); } @@ -20,11 +18,7 @@ class _Resolver extends Resolver { // for systems with no v6 support this still lets allow a fallback to v4 first. without needing to add system level dns filter. // as long as the name has an a record, should be able to communicate. const order = process.env.HOMESERVER_CONFIG_DNS_LOOKUP_ORDER; - if ( - order === 'ipv4first' || - order === 'ipv6first' || - order === 'verbatim' - ) { + if (order === 'ipv4first' || order === 'ipv6first' || order === 'verbatim') { this.lookupOrder = order; } } diff --git a/packages/federation-sdk/src/server-discovery/discovery.spec.ts b/packages/federation-sdk/src/server-discovery/discovery.spec.ts index 4dddbd058..246cdcf64 100644 --- a/packages/federation-sdk/src/server-discovery/discovery.spec.ts +++ b/packages/federation-sdk/src/server-discovery/discovery.spec.ts @@ -1,6 +1,10 @@ import { describe, expect, it, mock } from 'bun:test'; + import sinon from 'sinon'; +import { _URL } from './_url'; +import { getHomeserverFinalAddress } from './discovery'; + const stubs = { fetch: sinon.stub(), @@ -18,9 +22,6 @@ await mock.module('./_resolver', () => ({ }, })); -import { _URL } from './_url'; -import { getHomeserverFinalAddress } from './discovery'; - const mockFetch = stubs.fetch as unknown as typeof fetch; // const originalFetch = globalThis.fetch; globalThis.fetch = mockFetch; @@ -64,18 +65,12 @@ function spec_1__2(): [INPUT[], OUTPUT[]] { function spec_2__1(): [INPUT[], OUTPUT[]] { stubs.resolveHostname.resolves('11.0.0.1'); - return [ - ['example.com:45'], - [['https://11.0.0.1:45' as const, { Host: 'example.com:45' }]], - ]; + return [['example.com:45'], [['https://11.0.0.1:45' as const, { Host: 'example.com:45' }]]]; } function spec_2__2(): [INPUT[], OUTPUT[]] { stubs.resolveHostname.resolves('[::1]'); - return [ - ['example_spec_2__2.com:45'], - [['https://[::1]:45' as const, { Host: 'example_spec_2__2.com:45' }]], - ]; + return [['example_spec_2__2.com:45'], [['https://[::1]:45' as const, { Host: 'example_spec_2__2.com:45' }]]]; } // wellknown @@ -134,10 +129,7 @@ function spec_3_2(): [INPUT[], OUTPUT[]] { json: () => Promise.resolve({ 'm.server': 'example2_spec_3_2.com:45' }), // delegatedPort is present }); - return [ - inputs, - [['https://[::1]:45' as const, { Host: 'example2_spec_3_2.com:45' }]], - ]; + return [inputs, [['https://[::1]:45' as const, { Host: 'example2_spec_3_2.com:45' }]]]; } /* If is not an IP literal and no is present, an SRV record is looked up for _matrix-fed._tcp.. This may result in another hostname (to be resolved using AAAA or A records) and port. Requests should be made to the resolved IP address and port with a Host header containing the . The target server must present a valid certificate for .*/ @@ -153,10 +145,7 @@ function spec_3_3__1(): [INPUT[], OUTPUT[]] { stubs.resolveSrv.resolves([{ name: '::1', port: 45 }]); - return [ - inputs, - [['https://[::1]:45' as const, { Host: 'example2_spec_3_3__1.com' }]], - ]; + return [inputs, [['https://[::1]:45' as const, { Host: 'example2_spec_3_3__1.com' }]]]; } function spec_3_3__2(): [INPUT[], OUTPUT[]] { @@ -176,10 +165,7 @@ function spec_3_3__2(): [INPUT[], OUTPUT[]] { stubs.resolveSrv.resolves([{ name: 'example3_spec_3_3__2.com', port: 45 }]); // another hostname // now should do another resolveHostname - return [ - inputs, - [['https://[::1]:45' as const, { Host: 'example2_spec_3_3__2.com' }]], - ]; + return [inputs, [['https://[::1]:45' as const, { Host: 'example2_spec_3_3__2.com' }]]]; } /* If the /.well-known request returned an error response, and no SRV records were found, an IP address is resolved using CNAME, AAAA and A records. Requests are made to the resolved IP address using port 8448 and a Host header containing the . The target server must present a valid certificate for . */ @@ -196,10 +182,7 @@ function spec_3_4__1(): [INPUT[], OUTPUT[]] { // srv no stubs.resolveSrv.resolves([]); - return [ - inputs, - [['https://11.0.0.1:8448' as const, { Host: 'example_spec_3_4__1.com' }]], - ]; + return [inputs, [['https://11.0.0.1:8448' as const, { Host: 'example_spec_3_4__1.com' }]]]; } async function runTest(inputs: INPUT[], outputs: OUTPUT[]) { @@ -207,6 +190,7 @@ async function runTest(inputs: INPUT[], outputs: OUTPUT[]) { const input = inputs[i]; const output = outputs[i]; + // eslint-disable-next-line no-await-in-loop const [address, headers] = await getHomeserverFinalAddress(input); expect(address).toBe(output[0]); diff --git a/packages/federation-sdk/src/server-discovery/discovery.ts b/packages/federation-sdk/src/server-discovery/discovery.ts index 1d9ba1476..5419cf499 100644 --- a/packages/federation-sdk/src/server-discovery/discovery.ts +++ b/packages/federation-sdk/src/server-discovery/discovery.ts @@ -1,5 +1,7 @@ import { isIPv4, isIPv6 } from 'node:net'; + import memoize from 'memoize'; + import { MultiError } from './_multi-error'; import { resolver } from './_resolver'; import { _URL } from './_url'; @@ -19,8 +21,7 @@ type IP4or6WithPortString = `${IP4or6String}:${PortString | number}`; // | 'http' // | 'https'}://${AddressWithPortString}`; -type IP4or6WithPortAndProtocolString = - `${'http' | 'https'}://${IP4or6WithPortString}`; +type IP4or6WithPortAndProtocolString = `${'http' | 'https'}://${IP4or6WithPortString}`; type HostHeaders = { Host: AddressString | AddressWithPortString | IP4or6WithPortString; @@ -44,10 +45,7 @@ function fix6(addr: string): `[${string}]` { return /^\[.+\]$/.test(addr) ? (addr as `[${string}]`) : `[${addr}]`; } -export async function resolveHostname( - hostname: string, - resolveCname: boolean, -): Promise { +export async function resolveHostname(hostname: string, resolveCname: boolean): Promise { const errors = new MultiError(); // in order as in spec @@ -155,8 +153,7 @@ async function getHomeserverFinalAddressInternal( }); try { - const [addr, hostHeaders] = - await fromSRVResolutionWithBasicFallback(hostname); + const [addr, hostHeaders] = await fromSRVResolutionWithBasicFallback(hostname); logger?.debug({ msg: 'result from SRV resolution', addr, hostHeaders }); return [`https://${addr}` as const, hostHeaders]; @@ -174,28 +171,16 @@ async function getHomeserverFinalAddressInternal( } } -export const getHomeserverFinalAddress = memoize( - getHomeserverFinalAddressInternal, - { maxAge: SERVER_DISCOVERY_CACHE_MAX_AGE }, -); +export const getHomeserverFinalAddress = memoize(getHomeserverFinalAddressInternal, { maxAge: SERVER_DISCOVERY_CACHE_MAX_AGE }); type WellKnownResponse = { 'm.server': string; }; // error must be caught and handled by the caller -async function fromWellKnownDelegation( - host: string, -): Promise<[IP4or6WithPortAndProtocolString, HostHeaders]> { - const isWellKnownResponse = ( - response: unknown, - ): response is WellKnownResponse => { - return ( - typeof response === 'object' && - response !== null && - 'm.server' in response && - typeof response['m.server'] === 'string' - ); +async function fromWellKnownDelegation(host: string): Promise<[IP4or6WithPortAndProtocolString, HostHeaders]> { + const isWellKnownResponse = (response: unknown): response is WellKnownResponse => { + return typeof response === 'object' && response !== null && 'm.server' in response && typeof response['m.server'] === 'string'; }; // SPEC: 3. If the hostname is not an IP literal, a regular HTTPS request is made to https:///.well-known/matrix/server, @@ -240,8 +225,7 @@ async function fromWellKnownDelegation( if (url.isIP()) { // compiler should take care of this redundant reassignment const delegatedIp = delegatedHostname; - const finalAddress = - `https://${delegatedIp}:${delegatedPort || DEFAULT_PORT}` as const; + const finalAddress = `https://${delegatedIp}:${delegatedPort || DEFAULT_PORT}` as const; return [ finalAddress, { @@ -264,28 +248,22 @@ async function fromWellKnownDelegation( } // SPEC: 3.3. If is not an IP literal and no is present, an SRV record is looked up for _matrix-fed._tcp.. This may result in another hostname (to be resolved using AAAA or A records) and port. Requests should be made to the resolved IP address and port with a Host header containing the . The target server must present a valid certificate for . - const [addr, hostHeaders] = - await fromSRVResolutionWithBasicFallback(delegatedHostname); + const [addr, hostHeaders] = await fromSRVResolutionWithBasicFallback(delegatedHostname); return [`https://${addr}` as const, hostHeaders]; } // SPEC: If the /.well-known request resulted in an error response, a server is found by resolving an SRV record for _matrix-fed._tcp.. This may result in a hostname (to be resolved using AAAA or A records) and port. Requests are made to the resolved IP address and port, with a Host header of . The target server must present a valid certificate for . -async function fromSRVDelegation( - hostname: string, -): Promise<[IP4or6WithPortString, HostHeaders]> { - const _do = async ( - name: string, - ): Promise> | undefined> => { +async function fromSRVDelegation(hostname: string): Promise<[IP4or6WithPortString, HostHeaders]> { + const _do = async (name: string): Promise> | undefined> => { const srvs = await resolver.resolveSrv(name); - for (const srv of srvs) { + for await (const srv of srvs) { const _is4 = isIPv4(srv.name); const _is6 = isIPv6(srv.name); if (_is4 || _is6) { // use as is - const finalAddress = - `${_is6 ? fix6(srv.name) : srv.name}:${srv.port}` as const; + const finalAddress = `${_is6 ? fix6(srv.name) : srv.name}:${srv.port}` as const; return [finalAddress, { Host: hostname }]; } @@ -316,9 +294,7 @@ async function fromSRVDelegation( throw new Error(`no srv address found for ${hostname}`); } -async function fromSRVResolutionWithBasicFallback( - hostname: AddressString, -): Promise<[IP4or6WithPortString, HostHeaders]> { +async function fromSRVResolutionWithBasicFallback(hostname: AddressString): Promise<[IP4or6WithPortString, HostHeaders]> { // SPEC: 6. If the /.well-known request returned an error response, and no SRV records were found, an IP address is resolved using CNAME, AAAA and A records. Requests are made to the resolved IP address using port 8448 and a Host header containing the . The target server must present a valid certificate for . try { return await fromSRVDelegation(hostname); diff --git a/packages/federation-sdk/src/services/config.service.ts b/packages/federation-sdk/src/services/config.service.ts index 398813287..8283500c3 100644 --- a/packages/federation-sdk/src/services/config.service.ts +++ b/packages/federation-sdk/src/services/config.service.ts @@ -1,11 +1,6 @@ -import { - SigningKey, - createLogger, - generateKeyPairsFromString, - toUnpaddedBase64, -} from '@rocket.chat/federation-core'; +import type { SigningKey } from '@rocket.chat/federation-core'; +import { createLogger, generateKeyPairsFromString, toUnpaddedBase64 } from '@rocket.chat/federation-core'; import { singleton } from 'tsyringe'; - import { z } from 'zod'; export interface AppConfig { @@ -45,29 +40,17 @@ export const AppConfigSchema = z.object({ port: z.number().int().min(1).max(65535, 'Port must be between 1 and 65535'), version: z.string().min(1, 'Server version is required'), matrixDomain: z.string().min(1, 'Matrix domain is required'), - keyRefreshInterval: z - .number() - .int() - .min(1, 'Key refresh interval must be at least 1'), + keyRefreshInterval: z.number().int().min(1, 'Key refresh interval must be at least 1'), signingKey: z.string().optional(), timeout: z.number().optional(), signingKeyPath: z.string(), media: z.object({ - maxFileSize: z - .number() - .int() - .min(1, 'Max file size must be at least 1 byte'), + maxFileSize: z.number().int().min(1, 'Max file size must be at least 1 byte'), allowedMimeTypes: z.array(z.string()), enableThumbnails: z.boolean(), rateLimits: z.object({ - uploadPerMinute: z - .number() - .int() - .min(1, 'Upload rate limit must be at least 1'), - downloadPerMinute: z - .number() - .int() - .min(1, 'Download rate limit must be at least 1'), + uploadPerMinute: z.number().int().min(1, 'Upload rate limit must be at least 1'), + downloadPerMinute: z.number().int().min(1, 'Download rate limit must be at least 1'), }), }), invite: z.object({ @@ -78,24 +61,16 @@ export const AppConfigSchema = z.object({ processTyping: z.boolean(), processPresence: z.boolean(), }), - networkCheckTimeoutMs: z - .number() - .int() - .min(1000, 'Network check timeout must be at least 1000ms') - .default(5000) - .optional(), - userCheckTimeoutMs: z - .number() - .int() - .min(1000, 'User check timeout must be at least 1000ms') - .default(10000) - .optional(), + networkCheckTimeoutMs: z.number().int().min(1000, 'Network check timeout must be at least 1000ms').default(5000).optional(), + userCheckTimeoutMs: z.number().int().min(1000, 'User check timeout must be at least 1000ms').default(10000).optional(), }); @singleton() export class ConfigService { private config: AppConfig = {} as AppConfig; + private logger = createLogger('ConfigService'); + private serverKeys: SigningKey[] = []; setConfig(values: AppConfig) { @@ -111,9 +86,7 @@ export class ConfigService { msg: 'Configuration validation failed:', err: error, }); - throw new Error( - `Invalid configuration: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`, - ); + throw new Error(`Invalid configuration: ${error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`); } throw error; } @@ -142,9 +115,7 @@ export class ConfigService { } if (!this.serverKeys.length) { - const signingKey = await generateKeyPairsFromString( - this.config.signingKey, - ); + const signingKey = await generateKeyPairsFromString(this.config.signingKey); this.serverKeys = [signingKey]; } diff --git a/packages/federation-sdk/src/services/database-connection.service.ts b/packages/federation-sdk/src/services/database-connection.service.ts index b0e905c3a..f0e4c3d08 100644 --- a/packages/federation-sdk/src/services/database-connection.service.ts +++ b/packages/federation-sdk/src/services/database-connection.service.ts @@ -1,16 +1,18 @@ import { createLogger } from '@rocket.chat/federation-core'; -import { Db, MongoClient, type MongoClientOptions } from 'mongodb'; +import { MongoClient } from 'mongodb'; +import type { Db, MongoClientOptions } from 'mongodb'; export class DatabaseConnectionService { private client: MongoClient | null = null; + private db: Db | null = null; + private connectionPromise: Promise | null = null; + private readonly logger = createLogger('DatabaseConnectionService'); constructor(private readonly config: { uri: string; poolSize: number }) { - this.connect().catch((err) => - this.logger.error({ msg: 'Initial database connection failed', err }), - ); + this.connect().catch((err) => this.logger.error({ msg: 'Initial database connection failed', err })); } async getDb(): Promise { @@ -45,11 +47,9 @@ export class DatabaseConnectionService { this.client = new MongoClient(dbConfig.uri, options); this.client.connect(); - const dbName = this.client.options.dbName; + const { dbName } = this.client.options; if (!dbName) { - throw new Error( - "Can't get database name from MongoDB connection string", - ); + throw new Error("Can't get database name from MongoDB connection string"); } this.db = this.client.db(dbName); diff --git a/packages/federation-sdk/src/services/edu.service.ts b/packages/federation-sdk/src/services/edu.service.ts index 013cefb97..901488625 100644 --- a/packages/federation-sdk/src/services/edu.service.ts +++ b/packages/federation-sdk/src/services/edu.service.ts @@ -1,14 +1,11 @@ import type { PresenceUpdate } from '@rocket.chat/federation-core'; -import { - createPresenceEDU, - createTypingEDU, -} from '@rocket.chat/federation-core'; -import { createLogger } from '@rocket.chat/federation-core'; -import { RoomID } from '@rocket.chat/federation-room'; +import { createPresenceEDU, createTypingEDU, createLogger } from '@rocket.chat/federation-core'; +import type { RoomID } from '@rocket.chat/federation-room'; import { singleton } from 'tsyringe'; -import { ConfigService } from './config.service'; -import { FederationService } from './federation.service'; -import { StateService } from './state.service'; + +import type { ConfigService } from './config.service'; +import type { FederationService } from './federation.service'; +import type { StateService } from './state.service'; @singleton() export class EduService { @@ -20,27 +17,19 @@ export class EduService { private readonly stateService: StateService, ) {} - async sendTypingNotification( - roomId: RoomID, - userId: string, - typing: boolean, - ): Promise { + async sendTypingNotification(roomId: RoomID, userId: string, typing: boolean): Promise { try { const origin = this.configService.serverName; const typingEDU = createTypingEDU(roomId, userId, typing, origin); - this.logger.debug( - `Sending typing notification for room ${roomId}: ${userId} (typing: ${typing}) to all servers in room`, - ); + this.logger.debug(`Sending typing notification for room ${roomId}: ${userId} (typing: ${typing}) to all servers in room`); const servers = await this.stateService.getServersInRoom(roomId); const uniqueServers = servers.filter((server) => server !== origin); await this.federationService.sendEDUToServers([typingEDU], uniqueServers); - this.logger.debug( - `Sent typing notification to ${uniqueServers.length} unique servers for room ${roomId}`, - ); + this.logger.debug(`Sent typing notification to ${uniqueServers.length} unique servers for room ${roomId}`); } catch (error) { this.logger.error({ msg: 'Failed to send typing notification', @@ -50,20 +39,15 @@ export class EduService { } } - async sendPresenceUpdateToRooms( - presenceUpdates: PresenceUpdate[], - roomIds: RoomID[], - ): Promise { + async sendPresenceUpdateToRooms(presenceUpdates: PresenceUpdate[], roomIds: RoomID[]): Promise { try { const origin = this.configService.serverName; const presenceEDU = createPresenceEDU(presenceUpdates, origin); - this.logger.debug( - `Sending presence updates for ${presenceUpdates.length} users to all servers in rooms: ${roomIds.join(', ')}`, - ); + this.logger.debug(`Sending presence updates for ${presenceUpdates.length} users to all servers in rooms: ${roomIds.join(', ')}`); const uniqueServers = new Set(); - for (const roomId of roomIds) { + for await (const roomId of roomIds) { const servers = await this.stateService.getServersInRoom(roomId); for (const server of servers) { if (server !== origin) { @@ -72,14 +56,9 @@ export class EduService { } } - await this.federationService.sendEDUToServers( - [presenceEDU], - Array.from(uniqueServers), - ); + await this.federationService.sendEDUToServers([presenceEDU], Array.from(uniqueServers)); - this.logger.debug( - `Sent presence updates to ${uniqueServers.size} unique servers for ${roomIds.length} rooms`, - ); + this.logger.debug(`Sent presence updates to ${uniqueServers.size} unique servers for ${roomIds.length} rooms`); } catch (error) { this.logger.error({ msg: 'Failed to send presence update to rooms', diff --git a/packages/federation-sdk/src/services/event-authorization.service.ts b/packages/federation-sdk/src/services/event-authorization.service.ts index e73f3081f..147d9e127 100644 --- a/packages/federation-sdk/src/services/event-authorization.service.ts +++ b/packages/federation-sdk/src/services/event-authorization.service.ts @@ -1,21 +1,12 @@ -import { - createLogger, - extractSignaturesFromHeader, - generateId, - validateAuthorizationHeader, -} from '@rocket.chat/federation-core'; -import type { - EventID, - Pdu, - PersistentEventBase, - RoomID, -} from '@rocket.chat/federation-room'; +import { createLogger, extractSignaturesFromHeader, generateId, validateAuthorizationHeader } from '@rocket.chat/federation-core'; +import type { EventID, Pdu, PersistentEventBase, RoomID } from '@rocket.chat/federation-room'; import { delay, inject, singleton } from 'tsyringe'; + +import type { ConfigService } from './config.service'; +import type { EventService } from './event.service'; +import type { ServerService } from './server.service'; +import type { StateService } from './state.service'; import { UploadRepository } from '../repositories/upload.repository'; -import { ConfigService } from './config.service'; -import { EventService } from './event.service'; -import { ServerService } from './server.service'; -import { StateService } from './state.service'; export class AclDeniedError extends Error { constructor(serverName: string, roomId: string) { @@ -38,9 +29,7 @@ export class EventAuthorizationService { ) {} async authorizeEvent(event: Pdu, authEvents: Pdu[]): Promise { - this.logger.debug( - `Authorizing event ${generateId(event)} of type ${event.type}`, - ); + this.logger.debug(`Authorizing event ${generateId(event)} of type ${event.type}`); // Simple implementation - would need proper auth rules based on Matrix spec // https://spec.matrix.org/v1.7/server-server-api/#checks-performed-on-receipt-of-a-pdu @@ -52,9 +41,7 @@ export class EventAuthorizationService { // Check sender is allowed to send this type of event const senderAllowed = this.checkSenderAllowed(event, authEvents); if (!senderAllowed) { - this.logger.warn( - `Sender ${event.sender} not allowed to send ${event.type}`, - ); + this.logger.warn(`Sender ${event.sender} not allowed to send ${event.type}`); return false; } @@ -90,9 +77,7 @@ export class EventAuthorizationService { private checkSenderAllowed(event: Pdu, authEvents: Pdu[]): boolean { // Find power levels - const powerLevelsEvent = authEvents.find( - (e) => e.type === 'm.room.power_levels', - ); + const powerLevelsEvent = authEvents.find((e) => e.type === 'm.room.power_levels'); if (!powerLevelsEvent) { // No power levels - only allow room creator? const createEvent = authEvents.find((e) => e.type === 'm.room.create'); @@ -136,15 +121,9 @@ export class EventAuthorizationService { } try { - const { origin, destination, key, signature } = - extractSignaturesFromHeader(authorizationHeader); - - if ( - !origin || - !key || - !signature || - (destination && destination !== this.configService.serverName) - ) { + const { origin, destination, key, signature } = extractSignaturesFromHeader(authorizationHeader); + + if (!origin || !key || !signature || (destination && destination !== this.configService.serverName)) { return; } @@ -160,15 +139,7 @@ export class EventAuthorizationService { } const actualDestination = destination || this.configService.serverName; - const isValid = await validateAuthorizationHeader( - origin, - publicKey, - actualDestination, - method, - uri, - signature, - body, - ); + const isValid = await validateAuthorizationHeader(origin, publicKey, actualDestination, method, uri, signature, body); if (!isValid) { this.logger.warn(`Invalid signature from ${origin}`); return; @@ -180,7 +151,6 @@ export class EventAuthorizationService { msg: 'Error verifying request signature', err: error, }); - return; } } @@ -211,24 +181,15 @@ export class EventAuthorizationService { } // as per Matrix spec: https://spec.matrix.org/v1.15/client-server-api/#mroomserver_acl - async checkServerAcl( - aclEvent: PersistentEventBase | undefined, - serverName: string, - ): Promise { + async checkServerAcl(aclEvent: PersistentEventBase | undefined, serverName: string): Promise { if (!aclEvent || !aclEvent.isServerAclEvent()) { return true; } const serverAclContent = aclEvent.getContent(); - const { - allow = [], - deny = [], - allow_ip_literals = true, - } = serverAclContent; - - const isIpLiteral = - /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?$/.test(serverName) || - /^\[.*\](:\d+)?$/.test(serverName); // IPv6 + const { allow = [], deny = [], allow_ip_literals = true } = serverAclContent; + + const isIpLiteral = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?$/.test(serverName) || /^\[.*\](:\d+)?$/.test(serverName); // IPv6 if (isIpLiteral && !allow_ip_literals) { this.logger.debug(`Server ${serverName} denied: IP literals not allowed`); return false; @@ -236,9 +197,7 @@ export class EventAuthorizationService { for (const pattern of deny) { if (this.matchesServerPattern(serverName, pattern)) { - this.logger.debug( - `Server ${serverName} matches deny pattern: ${pattern}`, - ); + this.logger.debug(`Server ${serverName} matches deny pattern: ${pattern}`); return false; } } @@ -252,9 +211,7 @@ export class EventAuthorizationService { for (const pattern of allow) { if (this.matchesServerPattern(serverName, pattern)) { - this.logger.debug( - `Server ${serverName} matches allow pattern: ${pattern}`, - ); + this.logger.debug(`Server ${serverName} matches allow pattern: ${pattern}`); return true; } } @@ -278,10 +235,7 @@ export class EventAuthorizationService { } } - async serverHasAccessToResource( - roomId: RoomID, - serverName: string, - ): Promise { + async serverHasAccessToResource(roomId: RoomID, serverName: string): Promise { const state = await this.stateService.getLatestRoomState(roomId); if (!state) { this.logger.debug(`Room ${roomId} not found`); @@ -291,9 +245,7 @@ export class EventAuthorizationService { const aclEvent = state.get('m.room.server_acl:'); const isServerAllowed = await this.checkServerAcl(aclEvent, serverName); if (!isServerAllowed) { - this.logger.warn( - `Server ${serverName} is denied by room ACL for room ${roomId}`, - ); + this.logger.warn(`Server ${serverName} is denied by room ACL for room ${roomId}`); return false; } @@ -306,7 +258,7 @@ export class EventAuthorizationService { for (const [key, event] of state.entries()) { if (key.startsWith('m.room.member:') && event?.isMembershipEvent()) { const membership = event.getContent()?.membership; - const stateKey = event.stateKey; + const { stateKey } = event; if (!membership || !stateKey || !stateKey.includes(':')) { continue; @@ -315,9 +267,7 @@ export class EventAuthorizationService { if (membership === 'invite') { const invitedUserServer = stateKey.split(':').pop(); if (invitedUserServer === serverName) { - this.logger.debug( - `Server ${serverName} has pending invites in room, allowing access`, - ); + this.logger.debug(`Server ${serverName} has pending invites in room, allowing access`); return true; } } @@ -325,20 +275,12 @@ export class EventAuthorizationService { } const historyVisibilityEvent = state.get('m.room.history_visibility:'); - if ( - historyVisibilityEvent?.isHistoryVisibilityEvent() && - historyVisibilityEvent.getContent().history_visibility === - 'world_readable' - ) { - this.logger.debug( - `Room ${roomId} is world_readable, allowing ${serverName}`, - ); + if (historyVisibilityEvent?.isHistoryVisibilityEvent() && historyVisibilityEvent.getContent().history_visibility === 'world_readable') { + this.logger.debug(`Room ${roomId} is world_readable, allowing ${serverName}`); return true; } - this.logger.debug( - `Server ${serverName} not authorized: not in room and room not world_readable`, - ); + this.logger.debug(`Server ${serverName} not authorized: not in room and room not world_readable`); return false; } @@ -462,12 +404,7 @@ export class EventAuthorizationService { } > { try { - const signatureResult = await this.verifyRequestSignature( - method, - uri, - authorizationHeader, - body, - ); + const signatureResult = await this.verifyRequestSignature(method, uri, authorizationHeader, body); if (!signatureResult) { return { authorized: false, diff --git a/packages/federation-sdk/src/services/event-emitter.service.ts b/packages/federation-sdk/src/services/event-emitter.service.ts index af480e440..3077ec803 100644 --- a/packages/federation-sdk/src/services/event-emitter.service.ts +++ b/packages/federation-sdk/src/services/event-emitter.service.ts @@ -1,24 +1,16 @@ -import { - AsyncDispatcher, - type EventHandlerOf, - type EventOf, - logger, -} from '@rocket.chat/federation-core'; +import { Emitter } from '@rocket.chat/emitter'; +import { AsyncDispatcher, type EventHandlerOf, type EventOf, logger } from '@rocket.chat/federation-core'; import { singleton } from 'tsyringe'; -import { Emitter } from '@rocket.chat/emitter'; import type { HomeserverEventSignatures } from '..'; @singleton() export class EventEmitterService { - private emitter: AsyncDispatcher = - new AsyncDispatcher(); + private emitter: AsyncDispatcher = new AsyncDispatcher(); public async emit( event: K, - ...[data]: EventOf extends void - ? [undefined?] - : [EventOf] + ...[data]: EventOf extends void ? [undefined?] : [EventOf] ): Promise { await this.emitter.emit(event, ...([data] as any)); logger.debug({ msg: `Event emitted: ${event}`, event, data }); @@ -38,10 +30,7 @@ export class EventEmitterService { return this.emitter.once(event, handler); } - public off( - event: K, - handler: EventHandlerOf, - ): void { + public off(event: K, handler: EventHandlerOf): void { this.emitter.off(event, handler); } } diff --git a/packages/federation-sdk/src/services/event-fetcher.service.ts b/packages/federation-sdk/src/services/event-fetcher.service.ts index acc65799d..eb9f66557 100644 --- a/packages/federation-sdk/src/services/event-fetcher.service.ts +++ b/packages/federation-sdk/src/services/event-fetcher.service.ts @@ -1,11 +1,10 @@ -import { isFederationEventWithPDUs } from '@rocket.chat/federation-core'; -import { createLogger } from '@rocket.chat/federation-core'; -import { generateId } from '@rocket.chat/federation-core'; -import { EventID, Pdu } from '@rocket.chat/federation-room'; +import { isFederationEventWithPDUs, createLogger, generateId } from '@rocket.chat/federation-core'; +import type { EventID, Pdu } from '@rocket.chat/federation-room'; import { delay, inject, singleton } from 'tsyringe'; + +import type { ConfigService } from './config.service'; +import type { FederationService } from './federation.service'; import { EventRepository } from '../repositories/event.repository'; -import { ConfigService } from './config.service'; -import { FederationService } from './federation.service'; export interface FetchedEvents { events: { eventId: string; event: Pdu }[]; @@ -23,11 +22,7 @@ export class EventFetcherService { private readonly configService: ConfigService, ) {} - public async fetchEventsByIds( - eventIds: EventID[], - roomId: string, - originServer: string, - ): Promise { + public async fetchEventsByIds(eventIds: EventID[], roomId: string, originServer: string): Promise { this.logger.debug(`Fetching ${eventIds.length} events for room ${roomId}`); if (!eventIds || eventIds.length === 0) { @@ -55,17 +50,12 @@ export class EventFetcherService { } // For events we don't have locally, try federation - const missingEventIds = eventIds.filter( - (id) => !localEvents.some((e) => e.eventId === id), - ); + const missingEventIds = eventIds.filter((id) => !localEvents.some((e) => e.eventId === id)); if (missingEventIds.length > 0) { this.logger.debug( `Fetching ${missingEventIds.length} missing events from federation ${Array.from(missingEventIds).join(', ')} ${originServer}`, ); - const federationEvents = await this.fetchEventsFromFederation( - missingEventIds, - originServer, - ); + const federationEvents = await this.fetchEventsFromFederation(missingEventIds, originServer); const federationEventsWithIds = federationEvents.map((e) => ({ eventId: generateId(e), @@ -74,9 +64,7 @@ export class EventFetcherService { return { events: [...localEvents, ...federationEventsWithIds], - missingEventIds: missingEventIds.filter( - (id) => !federationEventsWithIds.some((e) => e.eventId === id), - ), + missingEventIds: missingEventIds.filter((id) => !federationEventsWithIds.some((e) => e.eventId === id)), }; } @@ -86,27 +74,20 @@ export class EventFetcherService { }; } - async fetchEventsFromFederation( - eventIds: string[], - targetServerName: string, - ): Promise { + async fetchEventsFromFederation(eventIds: string[], targetServerName: string): Promise { const eventsToReturn: Pdu[] = []; try { // TODO: Improve batch event requests to avoid too many parallel requests const chunks = this.chunkArray(eventIds, 10); - for (const chunk of chunks) { + for await (const chunk of chunks) { if (targetServerName === this.configService.serverName) { this.logger.info(`Skipping request to self: ${targetServerName}`); return []; } - const federationResponses = await Promise.all( - chunk.map((id) => - this.federationService.getEvent(targetServerName, id), - ), - ); + const federationResponses = await Promise.all(chunk.map((id) => this.federationService.getEvent(targetServerName, id))); for (const response of federationResponses) { // The Matrix spec defines that federation responses may contain PDUs field diff --git a/packages/federation-sdk/src/services/event.service.ts b/packages/federation-sdk/src/services/event.service.ts index 587d7965a..cd337c435 100644 --- a/packages/federation-sdk/src/services/event.service.ts +++ b/packages/federation-sdk/src/services/event.service.ts @@ -4,38 +4,24 @@ import type { PresenceEDU, RoomPowerLevelsEvent, TypingEDU, + RedactionEvent, + EventStore, } from '@rocket.chat/federation-core'; -import { isPresenceEDU, isTypingEDU } from '@rocket.chat/federation-core'; -import type { RedactionEvent } from '@rocket.chat/federation-core'; -import { generateId } from '@rocket.chat/federation-core'; -import type { EventStore } from '@rocket.chat/federation-core'; -import { pruneEventDict } from '@rocket.chat/federation-core'; - -import { checkSignAndHashes } from '@rocket.chat/federation-core'; -import { createLogger } from '@rocket.chat/federation-core'; -import { - type EventID, - type Pdu, - type PduForType, - type PduType, - PersistentEventFactory, - RoomID, - RoomState, - RoomVersion, - type State, - getAuthChain, -} from '@rocket.chat/federation-room'; +import { isPresenceEDU, isTypingEDU, generateId, pruneEventDict, checkSignAndHashes, createLogger } from '@rocket.chat/federation-core'; +import { PersistentEventFactory, RoomState, getAuthChain } from '@rocket.chat/federation-room'; +import type { RoomID, RoomVersion, EventID, Pdu, PduForType, PduType, State } from '@rocket.chat/federation-room'; import { delay, inject, singleton } from 'tsyringe'; import type { z } from 'zod'; -import { StagingAreaQueue } from '../queues/staging-area.queue'; + +import type { ConfigService } from './config.service'; +import type { EventEmitterService } from './event-emitter.service'; +import type { ServerService } from './server.service'; +import type { StateService } from './state.service'; +import type { StagingAreaQueue } from '../queues/staging-area.queue'; import { EventStagingRepository } from '../repositories/event-staging.repository'; import { EventRepository } from '../repositories/event.repository'; import { LockRepository } from '../repositories/lock.repository'; import { eventSchemas } from '../utils/event-schemas'; -import { ConfigService } from './config.service'; -import { EventEmitterService } from './event-emitter.service'; -import { ServerService } from './server.service'; -import type { StateService } from './state.service'; export interface AuthEventParams { roomId: string; @@ -51,6 +37,7 @@ export class EventService { constructor( private readonly configService: ConfigService, private readonly stagingAreaQueue: StagingAreaQueue, + // eslint-disable-next-line @typescript-eslint/no-var-requires @inject(delay(() => require('./state.service').StateService)) private readonly stateService: StateService, private readonly serverService: ServerService, @@ -63,20 +50,14 @@ export class EventService { private readonly lockRepository: LockRepository, ) {} - async getEventById>>( - eventId: EventID, - type?: T, - ): Promise

{ + async getEventById>>(eventId: EventID, type?: T): Promise

{ if (type) { - return (this.eventRepository.findByIdAndType(eventId, type) ?? - null) as Promise

; + return (this.eventRepository.findByIdAndType(eventId, type) ?? null) as Promise

; } return (this.eventRepository.findById(eventId) ?? null) as Promise

; } - async checkIfEventsExists( - eventIds: EventID[], - ): Promise<{ missing: EventID[]; found: EventID[] }> { + async checkIfEventsExists(eventIds: EventID[]): Promise<{ missing: EventID[]; found: EventID[] }> { // TODO, return only the IDs, not the full events const eventsCursor = this.eventRepository.findByIds(eventIds); const events = await eventsCursor.toArray(); @@ -97,9 +78,7 @@ export class EventService { ); } - async getLeastDepthEventForRoom( - roomId: string, - ): Promise { + async getLeastDepthEventForRoom(roomId: string): Promise { return this.eventStagingRepository.getLeastDepthEventForRoom(roomId); } @@ -110,15 +89,7 @@ export class EventService { await this.eventStagingRepository.removeByEventId(event._id); } - async processIncomingTransaction({ - origin, - pdus, - edus, - }: { - origin: string; - pdus: Pdu[]; - edus?: BaseEDU[]; - }): Promise { + async processIncomingTransaction({ origin, pdus, edus }: { origin: string; pdus: Pdu[]; edus?: BaseEDU[] }): Promise { if (!Array.isArray(pdus)) { throw new Error('pdus must be an array'); } @@ -143,10 +114,7 @@ export class EventService { this.currentTransactions.add(origin); // process both PDU and EDU in "parallel" to no block EDUs due to heavy PDU operations - await Promise.all([ - this.processIncomingPDUs(origin, pdus), - edus && this.processIncomingEDUs(edus), - ]); + await Promise.all([this.processIncomingPDUs(origin, pdus), edus && this.processIncomingEDUs(edus)]); } finally { this.currentTransactions.delete(origin); } @@ -195,18 +163,13 @@ export class EventService { const roomVersion = await getRoomVersion(event.room_id); - const pdu = PersistentEventFactory.createFromRawEvent( - event, - roomVersion, - ); + const pdu = PersistentEventFactory.createFromRawEvent(event, roomVersion); - const eventId = pdu.eventId; + const { eventId } = pdu; const existing = await this.eventRepository.findById(eventId); if (existing) { - this.logger.info( - `Ignoring received event ${eventId} which we have already seen`, - ); + this.logger.info(`Ignoring received event ${eventId} which we have already seen`); // TODO we may need to check if an event is an outlier and re-process it continue; @@ -216,10 +179,7 @@ export class EventService { await this.eventStagingRepository.create(eventId, origin, event); // acquire a lock for processing the event - const lock = await this.lockRepository.getLock( - roomId, - this.configService.instanceId, - ); + const lock = await this.lockRepository.getLock(roomId, this.configService.instanceId); if (!lock) { this.logger.debug(`Couldn't acquire a lock for room ${roomId}`); continue; @@ -241,11 +201,7 @@ export class EventService { throw new Error('M_UNKNOWN_ROOM_VERSION'); } - if ( - event.type === 'm.room.member' && - event.content.membership === 'invite' && - 'third_party_invite' in event.content - ) { + if (event.type === 'm.room.member' && event.content.membership === 'invite' && 'third_party_invite' in event.content) { throw new Error('Third party invites are not supported'); } @@ -289,7 +245,7 @@ export class EventService { private async processIncomingEDUs(edus: BaseEDU[]): Promise { this.logger.debug(`Processing ${edus.length} incoming EDUs`); - for (const edu of edus) { + for await (const edu of edus) { try { await this.processEDU(edu); } catch (error) { @@ -312,15 +268,10 @@ export class EventService { } if (isPresenceEDU(edu)) { await this.processPresenceEDU(edu, origin); - return; } - return; } - private async processTypingEDU( - typingEDU: TypingEDU, - origin?: string, - ): Promise { + private async processTypingEDU(typingEDU: TypingEDU, origin?: string): Promise { const config = this.configService.getConfig('edu'); if (!config.processTyping) { return; @@ -329,15 +280,11 @@ export class EventService { const { room_id, user_id, typing } = typingEDU.content; if (!room_id || !user_id || typeof typing !== 'boolean') { - this.logger.warn( - 'Invalid typing EDU content, missing room_id, user_id, or typing', - ); + this.logger.warn('Invalid typing EDU content, missing room_id, user_id, or typing'); return; } - this.logger.debug( - `Processing typing notification for room ${room_id}: ${user_id} (typing: ${typing})`, - ); + this.logger.debug(`Processing typing notification for room ${room_id}: ${user_id} (typing: ${typing})`); await this.eventEmitterService.emit('homeserver.matrix.typing', { room_id, @@ -347,10 +294,7 @@ export class EventService { }); } - private async processPresenceEDU( - presenceEDU: PresenceEDU, - origin?: string, - ): Promise { + private async processPresenceEDU(presenceEDU: PresenceEDU, origin?: string): Promise { const config = this.configService.getConfig('edu'); if (!config.processPresence) { return; @@ -363,19 +307,15 @@ export class EventService { return; } - for (const presenceUpdate of push) { + for await (const presenceUpdate of push) { if (!presenceUpdate.user_id || !presenceUpdate.presence) { - this.logger.warn( - 'Invalid presence update, missing user_id or presence', - ); + this.logger.warn('Invalid presence update, missing user_id or presence'); continue; } this.logger.debug( `Processing presence update for ${presenceUpdate.user_id}: ${presenceUpdate.presence}${ - presenceUpdate.last_active_ago !== undefined - ? ` (${presenceUpdate.last_active_ago}ms ago)` - : '' + presenceUpdate.last_active_ago !== undefined ? ` (${presenceUpdate.last_active_ago}ms ago)` : '' }`, ); @@ -392,11 +332,7 @@ export class EventService { const errors: string[] = []; if (event.type !== 'm.room.create') { - if ( - !event.prev_events || - !Array.isArray(event.prev_events) || - event.prev_events.length === 0 - ) { + if (!event.prev_events || !Array.isArray(event.prev_events) || event.prev_events.length === 0) { errors.push('Event must reference previous events (prev_events)'); } @@ -419,9 +355,7 @@ export class EventService { const senderDomain = this.extractDomain(event.sender); if (roomDomain !== senderDomain) { - errors.push( - `Room ID domain (${roomDomain}) does not match sender domain (${senderDomain})`, - ); + errors.push(`Room ID domain (${roomDomain}) does not match sender domain (${senderDomain})`); } } @@ -432,26 +366,9 @@ export class EventService { if (!event.content || !event.content.room_version) { errors.push('Create event must specify a room_version'); } else { - const validRoomVersions = [ - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - '10', - '11', - ]; - if ( - typeof event.content.room_version !== 'string' || - !validRoomVersions.includes(event.content.room_version) - ) { - errors.push( - `Unsupported room version: ${event.content.room_version}`, - ); + const validRoomVersions = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']; + if (typeof event.content.room_version !== 'string' || !validRoomVersions.includes(event.content.room_version)) { + errors.push(`Unsupported room version: ${event.content.room_version}`); } } } @@ -465,10 +382,7 @@ export class EventService { } private async getRoomVersion(event: Pick) { - return ( - this.stateService.getRoomVersion(event.room_id) || - PersistentEventFactory.defaultRoomVersion - ); + return this.stateService.getRoomVersion(event.room_id) || PersistentEventFactory.defaultRoomVersion; } private getEventSchema(roomVersion: string, eventType: string): z.ZodSchema { @@ -479,9 +393,7 @@ export class EventService { const schema = versionSchemas[eventType] || versionSchemas.default; if (!schema) { - throw new Error( - `No schema available for event type ${eventType} in room version ${roomVersion}`, - ); + throw new Error(`No schema available for event type ${eventType} in room version ${roomVersion}`); } return schema; @@ -492,10 +404,7 @@ export class EventService { } async getCreateEventForRoom(roomId: string): Promise { - const createEvent = await this.eventRepository.findByRoomIdAndType( - roomId, - 'm.room.create', - ); + const createEvent = await this.eventRepository.findByRoomIdAndType(roomId, 'm.room.create'); return createEvent?.event ?? null; } @@ -514,22 +423,14 @@ export class EventService { const maxDepth = Math.min(...latestEventsData); const events = await this.eventRepository - .findEventsByRoomAndDepth( - roomId, - minDepth, - maxDepth, - [...earliestEventsId, ...latestEventsId], - limit, - ) + .findEventsByRoomAndDepth(roomId, minDepth, maxDepth, [...earliestEventsId, ...latestEventsId], limit) .map((e) => e.event) .toArray(); return { events }; } - async getEventsByIds( - eventIds: EventID[], - ): Promise<{ _id: EventID; event: Pdu }[]> { + async getEventsByIds(eventIds: EventID[]): Promise<{ _id: EventID; event: Pdu }[]> { if (!eventIds || eventIds.length === 0) { return []; } @@ -547,21 +448,11 @@ export class EventService { * Find an invite event for a specific user in a specific room */ findInviteEvent(roomId: string, userId: string): Promise { - return this.eventRepository.findInviteEventsByRoomIdAndUserId( - roomId, - userId, - ); + return this.eventRepository.findInviteEventsByRoomIdAndUserId(roomId, userId); } - async getAuthEventIds( - eventType: PduType, - params: AuthEventParams, - ): Promise { - const authEventsCursor = this.eventRepository.findAuthEvents( - eventType, - params.roomId, - params.senderId, - ); + async getAuthEventIds(eventType: PduType, params: AuthEventParams): Promise { + const authEventsCursor = this.eventRepository.findAuthEvents(eventType, params.roomId, params.senderId); const authEvents: EventStore[] = []; for await (const storeEvent of authEventsCursor) { @@ -583,9 +474,7 @@ export class EventService { ) { authEvents.push(storeEvent); } else { - this.logger.warn( - `EventStore with id ${storeEvent._id} has an unrecognized event type: ${storeEvent.event?.type}`, - ); + this.logger.warn(`EventStore with id ${storeEvent._id} has an unrecognized event type: ${storeEvent.event?.type}`); } } @@ -595,17 +484,13 @@ export class EventService { async processRedaction(redactionEvent: RedactionEvent): Promise { const eventIdToRedact = redactionEvent.redacts; if (!eventIdToRedact) { - this.logger.error( - `[REDACTION] Event is missing 'redacts' field: ${generateId(redactionEvent)}`, - ); + this.logger.error(`[REDACTION] Event is missing 'redacts' field: ${generateId(redactionEvent)}`); return; } const eventToRedact = await this.eventRepository.findById(eventIdToRedact); if (!eventToRedact) { - this.logger.warn( - `[REDACTION] Event to redact ${eventIdToRedact} not found`, - ); + this.logger.warn(`[REDACTION] Event to redact ${eventIdToRedact} not found`); return; } @@ -649,24 +534,15 @@ export class EventService { this.logger.info(`Successfully redacted event ${eventIdToRedact}`); } - async checkUserPermission( - powerLevelsEventId: EventID, - userId: string, - actionType: PduType, - ): Promise { - const powerLevelsEvent = - await this.eventRepository.findById(powerLevelsEventId); + async checkUserPermission(powerLevelsEventId: EventID, userId: string, actionType: PduType): Promise { + const powerLevelsEvent = await this.eventRepository.findById(powerLevelsEventId); if (!powerLevelsEvent) { this.logger.warn(`Power levels event ${powerLevelsEventId} not found`); return false; } - const powerLevelsContent = powerLevelsEvent.event - .content as RoomPowerLevelsEvent['content']; - const userPowerLevel = - powerLevelsContent.users?.[userId] ?? - powerLevelsContent.users_default ?? - 0; + const powerLevelsContent = powerLevelsEvent.event.content as RoomPowerLevelsEvent['content']; + const userPowerLevel = powerLevelsContent.users?.[userId] ?? powerLevelsContent.users_default ?? 0; let requiredPowerLevel = powerLevelsContent.events?.[actionType]; if (requiredPowerLevel === undefined) { @@ -693,10 +569,7 @@ export class EventService { // not we try to process one room at a time for await (const roomId of rooms) { - const lock = await this.lockRepository.getLock( - roomId, - this.configService.instanceId, - ); + const lock = await this.lockRepository.getLock(roomId, this.configService.instanceId); if (!lock) { this.logger.debug(`Couldn't acquire a lock for room ${roomId}`); continue; @@ -713,10 +586,7 @@ export class EventService { } } - async getStateIds( - roomId: string, - eventId: EventID, - ): Promise<{ pdu_ids: string[]; auth_chain_ids: string[] }> { + async getStateIds(roomId: string, eventId: EventID): Promise<{ pdu_ids: string[]; auth_chain_ids: string[] }> { try { // Ensure the event exists and belongs to the requested room const event = await this.stateService.getEvent(eventId); @@ -736,7 +606,7 @@ export class EventService { const store = this.stateService._getStore(event.version); // Extract state event IDs and collect auth chain IDs - for (const [, event] of state.entries()) { + for await (const [, event] of state.entries()) { // Get the complete auth chain for this event try { const authChain = await getAuthChain(event, store); @@ -778,10 +648,8 @@ export class EventService { throw new Error('M_NOT_FOUND'); } - let state: State; - // Get state at a specific event - state = await this.stateService.getStateBeforeEvent(event); + const state = await this.stateService.getStateBeforeEvent(event); const pdus: Record[] = []; const authChainIds = new Set(); @@ -795,7 +663,7 @@ export class EventService { // Get the event store const store = this.stateService._getStore(roomVersion); // Extract state event objects and collect auth chain IDs - for (const [, event] of state.entries()) { + for await (const [, event] of state.entries()) { // PersistentEventBase has an event getter that contains the actual event data pdus.push(event.event); @@ -830,7 +698,7 @@ export class EventService { } return { - pdus: pdus, + pdus, auth_chain: authChain, }; } catch (error) { @@ -862,21 +730,13 @@ export class EventService { try { const parsedLimit = Math.min(Math.max(1, limit), 100); - const newestRef = await this.eventRepository.findNewestEventForBackfill( - roomId, - eventIds, - ); + const newestRef = await this.eventRepository.findNewestEventForBackfill(roomId, eventIds); if (!newestRef) { throw new Error('No newest event found'); } const events = await this.eventRepository - .findEventsForBackfill( - roomId, - newestRef.event.depth, - newestRef.event.origin_server_ts, - parsedLimit, - ) + .findEventsForBackfill(roomId, newestRef.event.depth, newestRef.event.origin_server_ts, parsedLimit) .toArray(); const pdus = events.map((eventStore) => eventStore.event); @@ -905,12 +765,10 @@ export class EventService { switch (true) { case event.event.type === 'm.room.create': - { - await this.eventEmitterService.emit('homeserver.matrix.room.create', { - event_id: eventId, - event: event.event, - }); - } + await this.eventEmitterService.emit('homeserver.matrix.room.create', { + event_id: eventId, + event: event.event, + }); break; case event.event.type === 'm.room.message': await this.eventEmitterService.emit('homeserver.matrix.message', { @@ -966,23 +824,17 @@ export class EventService { break; } case event.event.type === 'm.room.server_acl': { - await this.eventEmitterService.emit( - 'homeserver.matrix.room.server_acl', - { - event_id: eventId, - event: event.event, - }, - ); + await this.eventEmitterService.emit('homeserver.matrix.room.server_acl', { + event_id: eventId, + event: event.event, + }); break; } case event.event.type === 'm.room.power_levels': { - await this.eventEmitterService.emit( - 'homeserver.matrix.room.power_levels', - { - event_id: eventId, - event: event.event, - }, - ); + await this.eventEmitterService.emit('homeserver.matrix.room.power_levels', { + event_id: eventId, + event: event.event, + }); const getRole = (powerLevel: number) => { if (powerLevel === 100) { return 'owner'; @@ -1000,9 +852,7 @@ export class EventService { } // at this point we potentially have the new power level event - const oldRoomState = new RoomState( - await this.stateService.getStateBeforeEvent(plEvent), - ); + const oldRoomState = new RoomState(await this.stateService.getStateBeforeEvent(plEvent)); const oldPowerLevels = oldRoomState.powerLevels?.users; @@ -1017,7 +867,7 @@ export class EventService { break; } - for (const userId of Object.keys(oldPowerLevels)) { + for await (const userId of Object.keys(oldPowerLevels)) { if (userId === owner) { continue; } @@ -1036,19 +886,14 @@ export class EventService { if (!oldPowerLevels) { this.logger.debug('No current power levels, setting new ones'); // no existing, set the new ones - for (const [userId, power] of Object.entries(changedUserPowers)) { - this.logger.debug( - `Setting power level for ${userId} to ${power}`, - ); - await this.eventEmitterService.emit( - 'homeserver.matrix.room.role', - { - sender_id: event.event.sender, - user_id: userId, - room_id: roomId, - role: getRole(power), - }, - ); + for await (const [userId, power] of Object.entries(changedUserPowers)) { + this.logger.debug(`Setting power level for ${userId} to ${power}`); + await this.eventEmitterService.emit('homeserver.matrix.room.role', { + sender_id: event.event.sender, + user_id: userId, + room_id: roomId, + role: getRole(power), + }); } break; @@ -1057,9 +902,7 @@ export class EventService { const usersInOldPowerLevelEvent = Object.keys(oldPowerLevels); const usersInNewPowerLevelEvent = Object.keys(changedUserPowers); - const setOrUnsetPowerLevels = new Set( - usersInNewPowerLevelEvent, - ).difference(new Set(usersInOldPowerLevelEvent)); + const setOrUnsetPowerLevels = new Set(usersInNewPowerLevelEvent).difference(new Set(usersInOldPowerLevelEvent)); this.logger.debug( { @@ -1069,11 +912,9 @@ export class EventService { ); // for the difference only new power level content matters - for (const userId of setOrUnsetPowerLevels) { + for await (const userId of setOrUnsetPowerLevels) { const newPowerLevel = changedUserPowers[userId]; // if unset, it's 0, if set, it's the power level - this.logger.debug( - `Emitting event for ${userId} with new power level ${newPowerLevel ?? 0}`, - ); + this.logger.debug(`Emitting event for ${userId} with new power level ${newPowerLevel ?? 0}`); await this.eventEmitterService.emit('homeserver.matrix.room.role', { sender_id: event.event.sender, user_id: userId, @@ -1085,7 +926,7 @@ export class EventService { this.logger.debug('Emitting events for changed user powers'); // now use the new content - for (const [userId, power] of Object.entries(changedUserPowers)) { + for await (const [userId, power] of Object.entries(changedUserPowers)) { if ( power === oldPowerLevels[userId] || // no change setOrUnsetPowerLevels.has(userId) // already handled @@ -1093,9 +934,7 @@ export class EventService { continue; } - this.logger.debug( - `Emitting event for ${userId} with power level ${power}`, - ); + this.logger.debug(`Emitting event for ${userId} with power level ${power}`); await this.eventEmitterService.emit('homeserver.matrix.room.role', { sender_id: event.event.sender, @@ -1109,9 +948,7 @@ export class EventService { break; } default: - this.logger.warn( - `Unknown event type: ${event.event.type} for emitterService for now`, - ); + this.logger.warn(`Unknown event type: ${event.event.type} for emitterService for now`); break; } } diff --git a/packages/federation-sdk/src/services/federation-request.service.spec.ts b/packages/federation-sdk/src/services/federation-request.service.spec.ts index 83a056402..172a1498a 100644 --- a/packages/federation-sdk/src/services/federation-request.service.spec.ts +++ b/packages/federation-sdk/src/services/federation-request.service.spec.ts @@ -1,16 +1,9 @@ -import { - afterAll, - afterEach, - beforeEach, - describe, - expect, - it, - mock, - spyOn, -} from 'bun:test'; +import { afterAll, afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test'; + import * as core from '@rocket.chat/federation-core'; import * as nacl from 'tweetnacl'; -import { ConfigService } from './config.service'; + +import type { ConfigService } from './config.service'; import { FederationRequestService } from './federation-request.service'; describe('FederationRequestService', async () => { @@ -33,9 +26,7 @@ describe('FederationRequestService', async () => { }, ]; - const { getHomeserverFinalAddress } = await import( - '../server-discovery/discovery' - ); + const { getHomeserverFinalAddress } = await import('../server-discovery/discovery'); const { fetch: originalFetch } = await import('@rocket.chat/federation-core'); @@ -76,8 +67,7 @@ describe('FederationRequestService', async () => { }, }; - const mockAuthHeaders = - 'X-Matrix origin="example.com",destination="target.example.com",key="ed25519:1",sig="xyz123"'; + const mockAuthHeaders = 'X-Matrix origin="example.com",destination="target.example.com",key="ed25519:1",sig="xyz123"'; beforeEach(() => { spyOn(nacl.sign.keyPair, 'fromSecretKey').mockReturnValue(mockKeyPair); @@ -86,9 +76,7 @@ describe('FederationRequestService', async () => { spyOn(core, 'extractURIfromURL').mockReturnValue('/test/path?query=value'); spyOn(core, 'authorizationHeaders').mockResolvedValue(mockAuthHeaders); spyOn(core, 'signJson').mockResolvedValue(mockSignedJson); - spyOn(core, 'computeAndMergeHash').mockImplementation( - (obj: unknown) => obj, - ); + spyOn(core, 'computeAndMergeHash').mockImplementation((obj: unknown) => obj); configService = { getConfig: (key: string) => { @@ -195,9 +183,7 @@ describe('FederationRequestService', async () => { }); expect(fetchSpy).toHaveBeenCalledWith( - new URL( - 'https://target.example.com/test/path?param1=value1¶m2=value2', - ), + new URL('https://target.example.com/test/path?param1=value1¶m2=value2'), expect.any(Object), ); @@ -216,7 +202,11 @@ describe('FederationRequestService', async () => { multipart: async () => null, } as Response; }, - { preconnect: () => {} }, + { + preconnect: () => { + /* noop */ + }, + }, ) as typeof fetch; try { @@ -227,9 +217,7 @@ describe('FederationRequestService', async () => { }); } catch (error: unknown) { if (error instanceof Error) { - expect(error.message).toContain( - 'Federation request failed: 404 Not Found', - ); + expect(error.message).toContain('Federation request failed: 404 Not Found'); } else { throw error; } @@ -242,12 +230,15 @@ describe('FederationRequestService', async () => { return { ok: false, status: 400, - text: async () => - '{"error":"Bad Request","code":"M_INVALID_PARAM"}', + text: async () => '{"error":"Bad Request","code":"M_INVALID_PARAM"}', multipart: async () => null, } as Response; }, - { preconnect: () => {} }, + { + preconnect: () => { + /* noop */ + }, + }, ) as typeof fetch; try { @@ -258,9 +249,7 @@ describe('FederationRequestService', async () => { }); } catch (error: unknown) { if (error instanceof Error) { - expect(error.message).toContain( - 'Federation request failed: 400 {"error":"Bad Request","code":"M_INVALID_PARAM"}', - ); + expect(error.message).toContain('Federation request failed: 400 {"error":"Bad Request","code":"M_INVALID_PARAM"}'); } else { throw error; } @@ -272,7 +261,11 @@ describe('FederationRequestService', async () => { async () => { throw new Error('Network Error'); }, - { preconnect: () => {} }, + { + preconnect: () => { + /* noop */ + }, + }, ) as typeof fetch; try { @@ -293,10 +286,7 @@ describe('FederationRequestService', async () => { describe('convenience methods', () => { it('should call makeSignedRequest with correct parameters for GET', async () => { - const makeSignedRequestSpy = spyOn( - service, - 'makeSignedRequest', - ).mockResolvedValue({ + const makeSignedRequestSpy = spyOn(service, 'makeSignedRequest').mockResolvedValue({ ok: true, status: 200, json: async () => ({ result: 'success' }), @@ -317,10 +307,7 @@ describe('FederationRequestService', async () => { }); it('should call makeSignedRequest with correct parameters for POST', async () => { - const makeSignedRequestSpy = spyOn( - service, - 'makeSignedRequest', - ).mockResolvedValue({ + const makeSignedRequestSpy = spyOn(service, 'makeSignedRequest').mockResolvedValue({ ok: true, status: 200, json: async () => ({ result: 'success' }), @@ -343,10 +330,7 @@ describe('FederationRequestService', async () => { }); it('should call makeSignedRequest with correct parameters for PUT', async () => { - const makeSignedRequestSpy = spyOn( - service, - 'makeSignedRequest', - ).mockResolvedValue({ + const makeSignedRequestSpy = spyOn(service, 'makeSignedRequest').mockResolvedValue({ ok: true, status: 200, json: async () => ({ result: 'success' }), @@ -370,20 +354,13 @@ describe('FederationRequestService', async () => { describe('requestBinaryData', () => { it('should call makeSignedRequest for binary data without query params', async () => { const mockBuffer = Buffer.from('binary content'); - const makeSignedRequestSpy = spyOn( - service, - 'makeSignedRequest', - ).mockResolvedValue({ + const makeSignedRequestSpy = spyOn(service, 'makeSignedRequest').mockResolvedValue({ ok: true, status: 200, multipart: async () => ({ content: mockBuffer }), }); - const result = await service.requestBinaryData( - 'GET', - 'target.example.com', - '/media/download', - ); + const result = await service.requestBinaryData('GET', 'target.example.com', '/media/download'); expect(makeSignedRequestSpy).toHaveBeenCalledWith({ method: 'GET', @@ -396,21 +373,13 @@ describe('FederationRequestService', async () => { it('should call makeSignedRequest for binary data with query params', async () => { const mockBuffer = Buffer.from('binary content'); - const makeSignedRequestSpy = spyOn( - service, - 'makeSignedRequest', - ).mockResolvedValue({ + const makeSignedRequestSpy = spyOn(service, 'makeSignedRequest').mockResolvedValue({ ok: true, status: 200, multipart: async () => ({ content: mockBuffer }), }); - const result = await service.requestBinaryData( - 'GET', - 'target.example.com', - '/media/download', - { width: '100', height: '100' }, - ); + const result = await service.requestBinaryData('GET', 'target.example.com', '/media/download', { width: '100', height: '100' }); expect(makeSignedRequestSpy).toHaveBeenCalledWith({ method: 'GET', diff --git a/packages/federation-sdk/src/services/federation-request.service.ts b/packages/federation-sdk/src/services/federation-request.service.ts index 41b4b24a6..323ec0669 100644 --- a/packages/federation-sdk/src/services/federation-request.service.ts +++ b/packages/federation-sdk/src/services/federation-request.service.ts @@ -1,8 +1,4 @@ -import type { - FetchResponse, - MultipartResult, - SigningKey, -} from '@rocket.chat/federation-core'; +import type { FetchResponse, MultipartResult, SigningKey } from '@rocket.chat/federation-core'; import { EncryptionValidAlgorithm, authorizationHeaders, @@ -14,8 +10,9 @@ import { } from '@rocket.chat/federation-core'; import { singleton } from 'tsyringe'; import * as nacl from 'tweetnacl'; + +import type { ConfigService } from './config.service'; import { getHomeserverFinalAddress } from '../server-discovery/discovery'; -import { ConfigService } from './config.service'; interface SignedRequest { method: string; @@ -27,10 +24,7 @@ interface SignedRequest { type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; export class FederationRequestError extends Error { - constructor( - readonly response: FetchResponse, - errorText: string, - ) { + constructor(readonly response: FetchResponse, errorText: string) { let errorDetail = errorText; try { errorDetail = JSON.stringify(JSON.parse(errorText || '')); @@ -54,13 +48,7 @@ export class FederationRequestService { constructor(private readonly configService: ConfigService) {} - async makeSignedRequest({ - method, - domain, - uri, - body, - queryString, - }: SignedRequest): Promise> { + async makeSignedRequest({ method, domain, uri, body, queryString }: SignedRequest): Promise> { const serverName = this.configService.getConfig('serverName'); const signingKeyBase64 = await this.configService.getSigningKeyBase64(); const signingKeyId = await this.configService.getSigningKeyId(); @@ -72,14 +60,10 @@ export class FederationRequestService { version: signingKeyId.split(':')[1] || '1', privateKey: keyPair.secretKey, publicKey: keyPair.publicKey, - sign: async (data: Uint8Array) => - nacl.sign.detached(data, keyPair.secretKey), + sign: async (data: Uint8Array) => nacl.sign.detached(data, keyPair.secretKey), }; - const [address, discoveryHeaders] = await getHomeserverFinalAddress( - domain, - this.logger, - ); + const [address, discoveryHeaders] = await getHomeserverFinalAddress(domain, this.logger); const url = new URL(`${address}${uri}`); if (queryString) { @@ -90,21 +74,10 @@ export class FederationRequestService { let signedBody: Record | undefined; if (body) { - signedBody = await signJson( - body.hashes ? body : computeAndMergeHash({ ...body, signatures: {} }), - signingKey, - serverName, - ); + signedBody = await signJson(body.hashes ? body : computeAndMergeHash({ ...body, signatures: {} }), signingKey, serverName); } - const auth = await authorizationHeaders( - serverName, - signingKey, - domain, - method, - extractURIfromURL(url), - signedBody, - ); + const auth = await authorizationHeaders(serverName, signingKey, domain, method, extractURIfromURL(url), signedBody); const headers = { Authorization: auth, @@ -147,9 +120,7 @@ export class FederationRequestService { let queryString = ''; if (targetServer === this.configService.getConfig('serverName')) { - throw new SelfServerFetchError( - 'Cannot make federation request to self server', - ); + throw new SelfServerFetchError('Cannot make federation request to self server'); } if (queryParams) { @@ -177,35 +148,15 @@ export class FederationRequestService { ).json(); } - async get( - targetServer: string, - endpoint: string, - queryParams?: Record, - ): Promise { - return this.request( - 'GET', - targetServer, - endpoint, - undefined, - queryParams, - ); + async get(targetServer: string, endpoint: string, queryParams?: Record): Promise { + return this.request('GET', targetServer, endpoint, undefined, queryParams); } - async put( - targetServer: string, - endpoint: string, - body: Record, - queryParams?: Record, - ): Promise { + async put(targetServer: string, endpoint: string, body: Record, queryParams?: Record): Promise { return this.request('PUT', targetServer, endpoint, body, queryParams); } - async post( - targetServer: string, - endpoint: string, - body: Record, - queryParams?: Record, - ): Promise { + async post(targetServer: string, endpoint: string, body: Record, queryParams?: Record): Promise { return this.request('POST', targetServer, endpoint, body, queryParams); } @@ -216,17 +167,13 @@ export class FederationRequestService { queryParams?: Record, ): Promise { if (targetServer === this.configService.getConfig('serverName')) { - throw new SelfServerFetchError( - 'Cannot make federation request to self server', - ); + throw new SelfServerFetchError('Cannot make federation request to self server'); } const response = await this.makeSignedRequest({ method, domain: targetServer, uri: endpoint, - queryString: queryParams - ? new URLSearchParams(queryParams).toString() - : '', + queryString: queryParams ? new URLSearchParams(queryParams).toString() : '', }); return response.multipart(); diff --git a/packages/federation-sdk/src/services/federation-validation.service.ts b/packages/federation-sdk/src/services/federation-validation.service.ts index 3aaa56c1c..98cc84b38 100644 --- a/packages/federation-sdk/src/services/federation-validation.service.ts +++ b/packages/federation-sdk/src/services/federation-validation.service.ts @@ -1,19 +1,17 @@ import type { RoomID, UserID } from '@rocket.chat/federation-room'; import { extractDomainFromId } from '@rocket.chat/federation-room'; import { singleton } from 'tsyringe'; + +import type { ConfigService } from './config.service'; +import type { EventAuthorizationService } from './event-authorization.service'; +import type { FederationRequestService } from './federation-request.service'; +import type { StateService } from './state.service'; import { FederationEndpoints } from '../specs/federation-api'; -import { ConfigService } from './config.service'; -import { EventAuthorizationService } from './event-authorization.service'; -import { FederationRequestService } from './federation-request.service'; -import { StateService } from './state.service'; export class FederationValidationError extends Error { public error: string; - constructor( - public code: 'POLICY_DENIED' | 'CONNECTION_FAILED' | 'USER_NOT_FOUND', - public userMessage: string, - ) { + constructor(public code: 'POLICY_DENIED' | 'CONNECTION_FAILED' | 'USER_NOT_FOUND', public userMessage: string) { super(userMessage); this.name = 'FederationValidationError'; this.error = `federation-${code.toLowerCase().replace(/_/g, '-')}`; @@ -49,10 +47,7 @@ export class FederationValidationService { return; } - const isAllowed = await this.eventAuthorizationService.checkServerAcl( - aclEvent, - domain, - ); + const isAllowed = await this.eventAuthorizationService.checkServerAcl(aclEvent, domain); if (!isAllowed) { throw new FederationValidationError( 'POLICY_DENIED', @@ -67,8 +62,7 @@ export class FederationValidationService { return; } - const timeoutMs = - this.configService.getConfig('networkCheckTimeoutMs') || 5000; + const timeoutMs = this.configService.getConfig('networkCheckTimeoutMs') || 5000; try { const versionPromise = this.federationRequestService.get<{ @@ -85,8 +79,7 @@ export class FederationValidationService { } private async checkUserExists(userId: UserID, domain: string): Promise { - const timeoutMs = - this.configService.getConfig('userCheckTimeoutMs') || 10000; + const timeoutMs = this.configService.getConfig('userCheckTimeoutMs') || 10000; try { const uri = FederationEndpoints.queryProfile(userId); @@ -106,10 +99,7 @@ export class FederationValidationService { } } - private async withTimeout( - promise: Promise, - timeoutMs: number, - ): Promise { + private async withTimeout(promise: Promise, timeoutMs: number): Promise { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error(`Operation timed out after ${timeoutMs}ms`)); diff --git a/packages/federation-sdk/src/services/federation.service.ts b/packages/federation-sdk/src/services/federation.service.ts index fc7e7f860..a9a51bd26 100644 --- a/packages/federation-sdk/src/services/federation.service.ts +++ b/packages/federation-sdk/src/services/federation.service.ts @@ -1,14 +1,12 @@ -import type { EventBase } from '@rocket.chat/federation-core'; -import type { BaseEDU } from '@rocket.chat/federation-core'; +import type { EventBase, BaseEDU } from '@rocket.chat/federation-core'; import { createLogger } from '@rocket.chat/federation-core'; -import { - EventID, - Pdu, - PersistentEventBase, - PersistentEventFactory, - extractDomainFromId, -} from '@rocket.chat/federation-room'; +import type { EventID, Pdu, PersistentEventBase } from '@rocket.chat/federation-room'; +import { PersistentEventFactory, extractDomainFromId } from '@rocket.chat/federation-room'; import { singleton } from 'tsyringe'; + +import type { ConfigService } from './config.service'; +import type { FederationRequestService } from './federation-request.service'; +import type { StateService } from './state.service'; import { FederationEndpoints, type MakeJoinResponse, @@ -17,9 +15,6 @@ import { type Transaction, type Version, } from '../specs/federation-api'; -import { ConfigService } from './config.service'; -import { FederationRequestService } from './federation-request.service'; -import { StateService } from './state.service'; @singleton() export class FederationService { @@ -34,12 +29,7 @@ export class FederationService { /** * Get a make_join template for a room and user */ - async makeJoin( - domain: string, - roomId: string, - userId: string, - version?: string, - ): Promise { + async makeJoin(domain: string, roomId: string, userId: string, version?: string): Promise { try { const uri = FederationEndpoints.makeJoin(roomId, userId); const queryParams: Record = {}; @@ -50,11 +40,7 @@ export class FederationService { queryParams.ver = PersistentEventFactory.supportedRoomVersions; } - return await this.requestService.get( - domain, - uri, - queryParams, - ); + return await this.requestService.get(domain, uri, queryParams); } catch (error: any) { this.logger.error({ msg: 'makeJoin failed', err: error }); throw error; @@ -64,45 +50,28 @@ export class FederationService { /** * Send a join event to a remote server */ - async sendJoin( - joinEvent: PersistentEventBase, - omitMembers = false, - ): Promise { + async sendJoin(joinEvent: PersistentEventBase, omitMembers = false): Promise { try { - const event = joinEvent.event; + const { event } = joinEvent; - const uri = FederationEndpoints.sendJoinV2( - joinEvent.roomId, - joinEvent.eventId, - ); + const uri = FederationEndpoints.sendJoinV2(joinEvent.roomId, joinEvent.eventId); const queryParams = omitMembers ? { omit_members: 'true' } : undefined; const residentServer = joinEvent.roomId.split(':').pop(); if (!residentServer) { this.logger.debug({ msg: 'invalid room_id', event: joinEvent.event }); - throw new Error( - `invalid room_id ${joinEvent.roomId}, no server_name part`, - ); + throw new Error(`invalid room_id ${joinEvent.roomId}, no server_name part`); } - return await this.requestService.put( - residentServer, - uri, - event, - queryParams, - ); + return await this.requestService.put(residentServer, uri, event, queryParams); } catch (error: any) { this.logger.error({ msg: 'sendJoin failed', err: error }); throw error; } } - async makeLeave( - domain: string, - roomId: string, - userId: string, - ): Promise<{ event: Pdu; room_version: string }> { + async makeLeave(domain: string, roomId: string, userId: string): Promise<{ event: Pdu; room_version: string }> { try { const uri = FederationEndpoints.makeLeave(roomId, userId); return await this.requestService.get<{ @@ -117,25 +86,16 @@ export class FederationService { async sendLeave(leaveEvent: PersistentEventBase): Promise { try { - const uri = FederationEndpoints.sendLeave( - leaveEvent.roomId, - leaveEvent.eventId, - ); + const uri = FederationEndpoints.sendLeave(leaveEvent.roomId, leaveEvent.eventId); const residentServer = leaveEvent.roomId.split(':').pop(); if (!residentServer) { this.logger.debug({ msg: 'invalid room_id', event: leaveEvent.event }); - throw new Error( - `invalid room_id ${leaveEvent.roomId}, no server_name part`, - ); + throw new Error(`invalid room_id ${leaveEvent.roomId}, no server_name part`); } - await this.requestService.put( - residentServer, - uri, - leaveEvent.event, - ); + await this.requestService.put(residentServer, uri, leaveEvent.event); } catch (error: any) { this.logger.error({ msg: 'sendLeave failed', err: error }); throw error; @@ -145,19 +105,12 @@ export class FederationService { /** * Send a transaction to a remote server */ - async sendTransaction( - domain: string, - transaction: Transaction, - ): Promise { + async sendTransaction(domain: string, transaction: Transaction): Promise { try { const txnId = Date.now().toString(); const uri = FederationEndpoints.sendTransaction(txnId); - return await this.requestService.put( - domain, - uri, - transaction, - ); + return await this.requestService.put(domain, uri, transaction); } catch (error: any) { this.logger.error({ msg: 'sendTransaction failed', err: error }); throw error; @@ -167,10 +120,7 @@ export class FederationService { /** * Send an event to a remote server */ - async sendEvent( - domain: string, - event: T, - ): Promise { + async sendEvent(domain: string, event: T): Promise { try { const transaction: Transaction = { origin: this.configService.serverName, @@ -226,11 +176,7 @@ export class FederationService { /** * Get state for a room from remote server */ - async getState( - domain: string, - roomId: string, - eventId: string, - ): Promise { + async getState(domain: string, roomId: string, eventId: string): Promise { try { const uri = FederationEndpoints.getState(roomId); const queryParams = { event_id: eventId }; @@ -260,10 +206,7 @@ export class FederationService { */ async getVersion(domain: string): Promise { try { - return await this.requestService.get( - domain, - FederationEndpoints.version, - ); + return await this.requestService.get(domain, FederationEndpoints.version); } catch (error: any) { this.logger.error({ msg: 'getVersion failed', err: error }); throw error; @@ -272,32 +215,23 @@ export class FederationService { // invite user from another homeserver to our homeserver async inviteUser(inviteEvent: PersistentEventBase, roomVersion: string) { - const uri = FederationEndpoints.inviteV2( - inviteEvent.roomId, - inviteEvent.eventId, - ); + const uri = FederationEndpoints.inviteV2(inviteEvent.roomId, inviteEvent.eventId); if (!inviteEvent.stateKey) { this.logger.debug({ msg: 'invalid state_key', event: inviteEvent.event }); - throw new Error( - 'failed to send invite request, invite has invalid state_key', - ); + throw new Error('failed to send invite request, invite has invalid state_key'); } const residentServer = inviteEvent.stateKey.split(':').pop(); if (!residentServer) { - throw new Error( - `invalid state_key ${inviteEvent.stateKey}, no domain found, failed to send invite`, - ); + throw new Error(`invalid state_key ${inviteEvent.stateKey}, no domain found, failed to send invite`); } - return await this.requestService.put(residentServer, uri, { + return this.requestService.put(residentServer, uri, { event: inviteEvent.event, room_version: roomVersion, - invite_room_state: await this.stateService.getStrippedRoomState( - inviteEvent.roomId, - ), + invite_room_state: await this.stateService.getStrippedRoomState(inviteEvent.roomId), }); } @@ -312,11 +246,9 @@ export class FederationService { } } - for (const server of servers) { + for await (const server of servers) { if (server === event.origin) { - this.logger.info( - `Skipping transaction to event origin: ${event.origin}`, - ); + this.logger.info(`Skipping transaction to event origin: ${event.origin}`); continue; } @@ -354,7 +286,7 @@ export class FederationService { async sendEDUToServers(edus: BaseEDU[], servers: string[]): Promise { // Process servers sequentially to avoid concurrent transactions per Matrix spec - for (const server of servers) { + for await (const server of servers) { if (server === this.configService.serverName) { this.logger.info(`Skipping EDU to local server: ${server}`); continue; @@ -368,7 +300,7 @@ export class FederationService { batches.push(edus.slice(i, i + maxEDUsPerTransaction)); } - for (const batch of batches) { + for await (const batch of batches) { const txn: Transaction = { origin: this.configService.serverName, origin_server_ts: Date.now(), diff --git a/packages/federation-sdk/src/services/invite.service.ts b/packages/federation-sdk/src/services/invite.service.ts index 056dd0a82..35d254d2f 100644 --- a/packages/federation-sdk/src/services/invite.service.ts +++ b/packages/federation-sdk/src/services/invite.service.ts @@ -1,22 +1,16 @@ import { createLogger } from '@rocket.chat/federation-core'; -import { - EventID, - PduForType, - PersistentEventBase, - PersistentEventFactory, - RoomID, - RoomVersion, - UserID, - extractDomainFromId, -} from '@rocket.chat/federation-room'; +import type { EventID, PduForType, PersistentEventBase, RoomID, RoomVersion, UserID } from '@rocket.chat/federation-room'; +import { PersistentEventFactory, extractDomainFromId } from '@rocket.chat/federation-room'; import { delay, inject, singleton } from 'tsyringe'; -import { EventRepository } from '../repositories/event.repository'; -import { ConfigService } from './config.service'; -import { EventAuthorizationService } from './event-authorization.service'; -import { EventEmitterService } from './event-emitter.service'; + +import type { ConfigService } from './config.service'; +import type { EventAuthorizationService } from './event-authorization.service'; +import type { EventEmitterService } from './event-emitter.service'; import { FederationValidationService } from './federation-validation.service'; -import { FederationService } from './federation.service'; -import { StateService } from './state.service'; +import type { FederationService } from './federation.service'; +import type { StateService } from './state.service'; +import { EventRepository } from '../repositories/event.repository'; + export class NotAllowedError extends Error { constructor(message: string) { super(message); @@ -55,15 +49,13 @@ export class InviteService { }> { this.logger.debug(`Inviting ${userId} to room ${roomId}`); - const stateService = this.stateService; - const federationService = this.federationService; + const { stateService } = this; + const { federationService } = this; const roomVersion = await this.stateService.getRoomVersion(roomId); // Extract displayname from userId for direct messages - const displayname = isDirectMessage - ? userId.split(':').shift()?.slice(1) - : undefined; + const displayname = isDirectMessage ? userId.split(':').shift()?.slice(1) : undefined; const inviteEvent = await stateService.buildEvent<'m.room.member'>( { @@ -72,7 +64,7 @@ export class InviteService { membership: 'invite', ...(isDirectMessage && { is_direct: true, - displayname: displayname, + displayname, }), }, room_id: roomId, @@ -81,7 +73,7 @@ export class InviteService { depth: 0, prev_events: [], origin_server_ts: Date.now(), - sender: sender, + sender, }, roomVersion, @@ -91,9 +83,7 @@ export class InviteService { const invitedServer = extractDomainFromId(inviteEvent.stateKey ?? ''); if (!invitedServer) { - throw new Error( - `invalid state_key ${inviteEvent.stateKey}, no server_name part`, - ); + throw new Error(`invalid state_key ${inviteEvent.stateKey}, no server_name part`); } // if user invited belongs to our server @@ -106,43 +96,26 @@ export class InviteService { return { event_id: inviteEvent.eventId, - event: PersistentEventFactory.createFromRawEvent( - inviteEvent.event, - roomVersion, - ), + event: PersistentEventFactory.createFromRawEvent(inviteEvent.event, roomVersion), room_id: roomId, }; } - await this.federationValidationService.validateOutboundInvite( - userId, - roomId, - ); + await this.federationValidationService.validateOutboundInvite(userId, roomId); // get signed invite event - const inviteResponse = await federationService.inviteUser( - inviteEvent, - roomVersion, - ); + const inviteResponse = await federationService.inviteUser(inviteEvent, roomVersion); // try to save // can only invite if already part of the room - await stateService.handlePdu( - PersistentEventFactory.createFromRawEvent( - inviteResponse.event, - roomVersion, - ), - ); + await stateService.handlePdu(PersistentEventFactory.createFromRawEvent(inviteResponse.event, roomVersion)); // let everyone know void federationService.sendEventToAllServersInRoom(inviteEvent); return { event_id: inviteEvent.eventId, - event: PersistentEventFactory.createFromRawEvent( - inviteEvent.event, - roomVersion, - ), + event: PersistentEventFactory.createFromRawEvent(inviteEvent.event, roomVersion), room_id: roomId, }; } @@ -159,25 +132,16 @@ export class InviteService { >[], ): Promise { const isRoomNonPrivate = strippedStateEvents.some( - (stateEvent) => - stateEvent.type === 'm.room.join_rules' && - stateEvent.content.join_rule === 'public', + (stateEvent) => stateEvent.type === 'm.room.join_rules' && stateEvent.content.join_rule === 'public', ); - const isRoomEncrypted = strippedStateEvents.some( - (stateEvent) => stateEvent.type === 'm.room.encryption', - ); + const isRoomEncrypted = strippedStateEvents.some((stateEvent) => stateEvent.type === 'm.room.encryption'); - const { allowedEncryptedRooms, allowedNonPrivateRooms } = - this.configService.getConfig('invite'); + const { allowedEncryptedRooms, allowedNonPrivateRooms } = this.configService.getConfig('invite'); - const shouldRejectInvite = - (!allowedEncryptedRooms && isRoomEncrypted) || - (!allowedNonPrivateRooms && isRoomNonPrivate); + const shouldRejectInvite = (!allowedEncryptedRooms && isRoomEncrypted) || (!allowedNonPrivateRooms && isRoomNonPrivate); if (shouldRejectInvite) { - throw new NotAllowedError( - `Could not process invite due to room being ${isRoomEncrypted ? 'encrypted' : 'public'}`, - ); + throw new NotAllowedError(`Could not process invite due to room being ${isRoomEncrypted ? 'encrypted' : 'public'}`); } } @@ -197,11 +161,7 @@ export class InviteService { ): Promise> { await this.shouldProcessInvite(strippedStateEvents); - const inviteEvent = - PersistentEventFactory.createFromRawEvent<'m.room.member'>( - event, - roomVersion, - ); + const inviteEvent = PersistentEventFactory.createFromRawEvent<'m.room.member'>(event, roomVersion); if (inviteEvent.eventId !== eventId) { throw new Error(`Invalid eventId ${eventId}`); @@ -210,10 +170,7 @@ export class InviteService { const { residentServer } = inviteEvent; if (residentServer === this.configService.serverName) { - await this.eventAuthorizationService.checkAclForInvite( - event.room_id, - residentServer, - ); + await this.eventAuthorizationService.checkAclForInvite(event.room_id, residentServer); await this.stateService.handlePdu(inviteEvent); @@ -227,9 +184,7 @@ export class InviteService { const invitedServer = extractDomainFromId(event.state_key); if (!invitedServer) { - throw new Error( - `invalid state_key ${event.state_key}, no server_name part`, - ); + throw new Error(`invalid state_key ${event.state_key}, no server_name part`); } if (invitedServer !== this.configService.serverName) { throw new Error( @@ -245,19 +200,12 @@ export class InviteService { // check if we are already in the room, if so we can handlePdu because we have the state and should save // the invite in the state as well - const createEvent = await this.eventRepository.findByRoomIdAndType( - event.room_id, - 'm.room.create', - ); + const createEvent = await this.eventRepository.findByRoomIdAndType(event.room_id, 'm.room.create'); if (createEvent) { await this.stateService.handlePdu(inviteEvent); } else { // otherwise we save as outlier only so we can deal with it later - await this.eventRepository.insertOutlierEvent( - inviteEvent.eventId, - inviteEvent.event, - residentServer, - ); + await this.eventRepository.insertOutlierEvent(inviteEvent.eventId, inviteEvent.event, residentServer); } await this.emitterService.emit('homeserver.matrix.membership', { diff --git a/packages/federation-sdk/src/services/media.service.ts b/packages/federation-sdk/src/services/media.service.ts index dc9ff04ef..7384c918a 100644 --- a/packages/federation-sdk/src/services/media.service.ts +++ b/packages/federation-sdk/src/services/media.service.ts @@ -1,41 +1,30 @@ import { createLogger } from '@rocket.chat/federation-core'; import { singleton } from 'tsyringe'; -import { ConfigService } from './config.service'; -import { FederationRequestService } from './federation-request.service'; + +import type { ConfigService } from './config.service'; +import type { FederationRequestService } from './federation-request.service'; @singleton() export class MediaService { private readonly logger = createLogger('MediaService'); - constructor( - private readonly configService: ConfigService, - private readonly federationRequest: FederationRequestService, - ) {} + constructor(private readonly configService: ConfigService, private readonly federationRequest: FederationRequestService) {} - async downloadFromRemoteServer( - serverName: string, - mediaId: string, - ): Promise { + async downloadFromRemoteServer(serverName: string, mediaId: string): Promise { const endpoints = [ `/_matrix/federation/v1/media/download/${mediaId}`, `/_matrix/media/v3/download/${serverName}/${mediaId}`, `/_matrix/media/r0/download/${serverName}/${mediaId}`, ]; - for (const endpoint of endpoints) { + for await (const endpoint of endpoints) { try { // TODO: Stream remote file downloads instead of buffering the entire file in memory. - const response = await this.federationRequest.requestBinaryData( - 'GET', - serverName, - endpoint, - ); + const response = await this.federationRequest.requestBinaryData('GET', serverName, endpoint); return response.content; } catch (err) { - this.logger.debug( - `Endpoint ${endpoint} failed: ${err instanceof Error ? err.message : String(err)}`, - ); + this.logger.debug(`Endpoint ${endpoint} failed: ${err instanceof Error ? err.message : String(err)}`); } } diff --git a/packages/federation-sdk/src/services/message.service.ts b/packages/federation-sdk/src/services/message.service.ts index c51c40e87..11dae5be3 100644 --- a/packages/federation-sdk/src/services/message.service.ts +++ b/packages/federation-sdk/src/services/message.service.ts @@ -1,16 +1,11 @@ -import { ForbiddenError } from '@rocket.chat/federation-core'; -import { createLogger } from '@rocket.chat/federation-core'; -import { - type EventID, - type PersistentEventBase, - RoomID, - UserID, -} from '@rocket.chat/federation-room'; +import { ForbiddenError, createLogger } from '@rocket.chat/federation-core'; +import type { RoomID, UserID, EventID, PersistentEventBase } from '@rocket.chat/federation-room'; import { singleton } from 'tsyringe'; -import { EventService } from './event.service'; -import { FederationService } from './federation.service'; -import { RoomService } from './room.service'; -import { StateService } from './state.service'; + +import type { EventService } from './event.service'; +import type { FederationService } from './federation.service'; +import type { RoomService } from './room.service'; +import type { StateService } from './state.service'; type Reply = | { @@ -60,33 +55,23 @@ export class MessageService { ) {} private buildReplyContent(reply: Reply) { - if ( - 'replyToEventId' in reply && - reply?.replyToEventId && - 'threadEventId' in reply && - reply?.threadEventId - ) { + if ('replyToEventId' in reply && reply?.replyToEventId && 'threadEventId' in reply && reply?.threadEventId) { return { 'm.relates_to': { ...(!reply.showInMainChat && { rel_type: 'm.thread' as const }), - is_falling_back: false, - event_id: reply.threadEventId, + 'is_falling_back': false, + 'event_id': reply.threadEventId, 'm.in_reply_to': { event_id: reply.replyToEventId }, }, } as const; } - if ( - 'threadEventId' in reply && - reply?.threadEventId && - 'latestThreadEventId' in reply && - reply?.latestThreadEventId - ) { + if ('threadEventId' in reply && reply?.threadEventId && 'latestThreadEventId' in reply && reply?.latestThreadEventId) { return { 'm.relates_to': { ...(!reply.showInMainChat && { rel_type: 'm.thread' as const }), - event_id: reply.threadEventId, - is_falling_back: true, + 'event_id': reply.threadEventId, + 'is_falling_back': true, 'm.in_reply_to': { event_id: reply.latestThreadEventId }, }, } as const; @@ -112,9 +97,7 @@ export class MessageService { ): Promise { const roomVersion = await this.stateService.getRoomVersion(roomId); if (!roomVersion) { - throw new Error( - `Room version not found for room ${roomId} white trying to send message`, - ); + throw new Error(`Room version not found for room ${roomId} white trying to send message`); } const event = await this.stateService.buildEvent<'m.room.message'>( @@ -160,33 +143,18 @@ export class MessageService { ): Promise { const roomVersion = await this.stateService.getRoomVersion(roomId); if (!roomVersion) { - throw new Error( - `Room version not found for room ${roomId} white trying to send message`, - ); + throw new Error(`Room version not found for room ${roomId} white trying to send message`); } - return this.sendMessage( - roomId, - rawMessage, - formattedMessage, - senderUserId, - { - replyToEventId: eventToReplyTo, - }, - ); + return this.sendMessage(roomId, rawMessage, formattedMessage, senderUserId, { + replyToEventId: eventToReplyTo, + }); } - async sendFileMessage( - roomId: RoomID, - content: FileMessageContent, - senderUserId: UserID, - reply?: Reply, - ): Promise { + async sendFileMessage(roomId: RoomID, content: FileMessageContent, senderUserId: UserID, reply?: Reply): Promise { const roomVersion = await this.stateService.getRoomVersion(roomId); if (!roomVersion) { - throw new Error( - `Room version not found for room ${roomId} while trying to send file message`, - ); + throw new Error(`Room version not found for room ${roomId} while trying to send file message`); } const event = await this.stateService.buildEvent<'m.room.message'>( @@ -229,21 +197,13 @@ export class MessageService { ): Promise { const roomVersion = await this.stateService.getRoomVersion(roomId); if (!roomVersion) { - throw new Error( - `Room version not found for room ${roomId} while trying to send thread message`, - ); + throw new Error(`Room version not found for room ${roomId} while trying to send thread message`); } - return this.sendMessage( - roomId, - rawMessage, - formattedMessage, - senderUserId, - { - threadEventId: threadRootEventId, - latestThreadEventId: latestThreadEventId ?? threadRootEventId, - }, - ); + return this.sendMessage(roomId, rawMessage, formattedMessage, senderUserId, { + threadEventId: threadRootEventId, + latestThreadEventId: latestThreadEventId ?? threadRootEventId, + }); } /** @@ -259,37 +219,20 @@ export class MessageService { ): Promise { const roomVersion = await this.stateService.getRoomVersion(roomId); if (!roomVersion) { - throw new Error( - `Room version not found for room ${roomId} while trying to send thread message`, - ); + throw new Error(`Room version not found for room ${roomId} while trying to send thread message`); } - return this.sendMessage( - roomId, - rawMessage, - formattedMessage, - senderUserId, - { - threadEventId: threadRootEventId, - replyToEventId: eventToReplyTo, - }, - ); + return this.sendMessage(roomId, rawMessage, formattedMessage, senderUserId, { + threadEventId: threadRootEventId, + replyToEventId: eventToReplyTo, + }); } - async sendReaction( - roomId: RoomID, - eventId: EventID, - emoji: string, - senderUserId: UserID, - ): Promise { + async sendReaction(roomId: RoomID, eventId: EventID, emoji: string, senderUserId: UserID): Promise { const isTombstoned = await this.roomService.isRoomTombstoned(roomId); if (isTombstoned) { - this.logger.warn( - `Attempted to react to a message in a tombstoned room: ${roomId}`, - ); - throw new ForbiddenError( - 'Cannot react to a message in a tombstoned room', - ); + this.logger.warn(`Attempted to react to a message in a tombstoned room: ${roomId}`); + throw new ForbiddenError('Cannot react to a message in a tombstoned room'); } const roomInfo = await this.stateService.getRoomInformation(roomId); @@ -321,31 +264,25 @@ export class MessageService { return reactionEvent.eventId; } - async unsetReaction( - roomId: RoomID, - eventIdReactedTo: EventID, - _emoji: string, - senderUserId: UserID, - ): Promise { + async unsetReaction(roomId: RoomID, eventIdReactedTo: EventID, _emoji: string, senderUserId: UserID): Promise { const roomInfo = await this.stateService.getRoomInformation(roomId); - const redactionEvent = - await this.stateService.buildEvent<'m.room.redaction'>( - { - type: 'm.room.redaction', - content: { - reason: 'Unsetting reaction', - }, - redacts: eventIdReactedTo, - room_id: roomId, - auth_events: [], - depth: 0, - prev_events: [], - origin_server_ts: Date.now(), - sender: senderUserId, + const redactionEvent = await this.stateService.buildEvent<'m.room.redaction'>( + { + type: 'm.room.redaction', + content: { + reason: 'Unsetting reaction', }, - roomInfo.room_version, - ); + redacts: eventIdReactedTo, + room_id: roomId, + auth_events: [], + depth: 0, + prev_events: [], + origin_server_ts: Date.now(), + sender: senderUserId, + }, + roomInfo.room_version, + ); await this.stateService.handlePdu(redactionEvent); @@ -367,10 +304,10 @@ export class MessageService { { type: 'm.room.message', content: { - msgtype: 'm.text', - body: rawMessage, - format: 'org.matrix.custom.html', - formatted_body: formattedMessage, + 'msgtype': 'm.text', + 'body': rawMessage, + 'format': 'org.matrix.custom.html', + 'formatted_body': formattedMessage, 'm.new_content': { msgtype: 'm.text', body: rawMessage, @@ -399,15 +336,10 @@ export class MessageService { return redactionEvent.eventId; } - async redactMessage( - roomId: RoomID, - eventIdToRedact: EventID, - ): Promise { + async redactMessage(roomId: RoomID, eventIdToRedact: EventID): Promise { const isTombstoned = await this.roomService.isRoomTombstoned(roomId); if (isTombstoned) { - this.logger.warn( - `Attempted to delete a message in a tombstoned room: ${roomId}`, - ); + this.logger.warn(`Attempted to delete a message in a tombstoned room: ${roomId}`); throw new ForbiddenError('Cannot delete a message in a tombstoned room'); } @@ -418,23 +350,22 @@ export class MessageService { throw new Error(`Sender user ID not found for event ${eventIdToRedact}`); } - const redactionEvent = - await this.stateService.buildEvent<'m.room.redaction'>( - { - type: 'm.room.redaction', - content: { - reason: `Deleting message: ${eventIdToRedact}`, - }, - redacts: eventIdToRedact, - room_id: roomId, - auth_events: [], - depth: 0, - prev_events: [], - origin_server_ts: Date.now(), - sender: senderUserId.event.sender, + const redactionEvent = await this.stateService.buildEvent<'m.room.redaction'>( + { + type: 'm.room.redaction', + content: { + reason: `Deleting message: ${eventIdToRedact}`, }, - roomInfo.room_version, - ); + redacts: eventIdToRedact, + room_id: roomId, + auth_events: [], + depth: 0, + prev_events: [], + origin_server_ts: Date.now(), + sender: senderUserId.event.sender, + }, + roomInfo.room_version, + ); await this.stateService.handlePdu(redactionEvent); diff --git a/packages/federation-sdk/src/services/missing-event.service.ts b/packages/federation-sdk/src/services/missing-event.service.ts index 0a4fd7d43..789222607 100644 --- a/packages/federation-sdk/src/services/missing-event.service.ts +++ b/packages/federation-sdk/src/services/missing-event.service.ts @@ -1,10 +1,10 @@ import { createLogger } from '@rocket.chat/federation-core'; - -import { EventID } from '@rocket.chat/federation-room'; +import type { EventID } from '@rocket.chat/federation-room'; import { singleton } from 'tsyringe'; -import { EventFetcherService } from './event-fetcher.service'; -import { EventService } from './event.service'; -import { StateService } from './state.service'; + +import type { EventFetcherService } from './event-fetcher.service'; +import type { EventService } from './event.service'; +import type { StateService } from './state.service'; type MissingEventType = { eventId: EventID; @@ -27,26 +27,18 @@ export class MissingEventService { const exists = await this.eventService.getEventById(eventId); if (exists) { - this.logger.debug( - `Event ${eventId} already exists in database (staged or processed), marking as fetched`, - ); + this.logger.debug(`Event ${eventId} already exists in database (staged or processed), marking as fetched`); return true; } try { - const fetchedEvents = await this.eventFetcherService.fetchEventsByIds( - [eventId], - roomId, - origin, - ); + const fetchedEvents = await this.eventFetcherService.fetchEventsByIds([eventId], roomId, origin); if (fetchedEvents.events.length === 0) { - this.logger.warn( - `Failed to fetch missing event ${eventId} from ${origin}`, - ); + this.logger.warn(`Failed to fetch missing event ${eventId} from ${origin}`); return false; } - for (const { event, eventId } of fetchedEvents.events) { + for await (const { event, eventId } of fetchedEvents.events) { this.logger.debug(`Persisting fetched missing event ${eventId}`); // TODO is there anything else we need to do with missing dependencies from received event? diff --git a/packages/federation-sdk/src/services/profiles.service.ts b/packages/federation-sdk/src/services/profiles.service.ts index ac9fa965c..ca136b5b4 100644 --- a/packages/federation-sdk/src/services/profiles.service.ts +++ b/packages/federation-sdk/src/services/profiles.service.ts @@ -1,22 +1,14 @@ -import { ConfigService } from './config.service'; - -import { - EventID, - Pdu, - PduForType, - RoomID, - RoomVersion, - UserID, -} from '@rocket.chat/federation-room'; +import type { EventID, PduForType, RoomID, RoomVersion, UserID } from '@rocket.chat/federation-room'; +import { Pdu } from '@rocket.chat/federation-room'; import { singleton } from 'tsyringe'; -import { StateService } from './state.service'; + +import type { ConfigService } from './config.service'; +import type { StateService } from './state.service'; @singleton() export class ProfilesService { - constructor( - private readonly configService: ConfigService, - private readonly stateService: StateService, - ) {} + constructor(private readonly configService: ConfigService, private readonly stateService: StateService) {} + async queryProfile(userId: string): Promise<{ avatar_url: string; displayname: string; @@ -27,9 +19,7 @@ export class ProfilesService { }; } - async queryKeys( - deviceKeys: Record, - ): Promise<{ device_keys: Record }> { + async queryKeys(deviceKeys: Record): Promise<{ device_keys: Record }> { const keys = Object.keys(deviceKeys).reduce( (v, cur) => { v[cur] = 'unknown_key'; @@ -68,7 +58,7 @@ export class ProfilesService { event: PduForType<'m.room.member'> & { origin: string }; room_version: RoomVersion; }> { - const stateService = this.stateService; + const { stateService } = this; const roomInformation = await stateService.getRoomInformation(roomId); const roomVersion = roomInformation.room_version; @@ -77,11 +67,7 @@ export class ProfilesService { throw new Error(`Unsupported room version: ${roomVersion}`); } - if ( - !(await this.stateService.getLatestRoomState2(roomId)).isUserInvited( - userId, - ) - ) { + if (!(await this.stateService.getLatestRoomState2(roomId)).isUserInvited(userId)) { throw new Error(`User ${userId} is not invited`); } @@ -109,10 +95,7 @@ export class ProfilesService { }; } - async eventAuth( - _roomId: RoomID, - _eventId: EventID, - ): Promise<{ auth_chain: Record[] }> { + async eventAuth(_roomId: RoomID, _eventId: EventID): Promise<{ auth_chain: Record[] }> { return { auth_chain: [], }; diff --git a/packages/federation-sdk/src/services/room.service.spec.ts b/packages/federation-sdk/src/services/room.service.spec.ts index 8b94f2e85..247829880 100644 --- a/packages/federation-sdk/src/services/room.service.spec.ts +++ b/packages/federation-sdk/src/services/room.service.spec.ts @@ -1,15 +1,18 @@ import { beforeAll, describe, expect, it } from 'bun:test'; -import * as room from '@rocket.chat/federation-room'; -import { + +import type { PduJoinRuleEventContent, PduPowerLevelsEventContent, PersistentEventBase, RoomVersion, StateMapKey, + UserID, } from '@rocket.chat/federation-room'; import { container } from 'tsyringe'; + import { FederationValidationService, federationSDK, init } from '..'; -import { AppConfig, ConfigService } from './config.service'; +import type { AppConfig } from './config.service'; +import { ConfigService } from './config.service'; import { RoomService } from './room.service'; import { StateService } from './state.service'; @@ -21,9 +24,7 @@ describe('RoomService', async () => { beforeAll(() => { const databaseConfig = { - uri: - process.env.MONGO_URI || - 'mongodb://localhost:27017?directConnection=true', + uri: process.env.MONGO_URI || 'mongodb://localhost:27017?directConnection=true', name: 'matrix_test', poolSize: 100, }; @@ -59,7 +60,7 @@ describe('RoomService', async () => { const roomService = container.resolve(RoomService); const createRoom = async ( - username: room.UserID, + username: UserID, joinRule: PduJoinRuleEventContent['join_rule'], { users = {}, @@ -72,21 +73,14 @@ describe('RoomService', async () => { events: {}, }, ) => { - const result = await federationSDK.createRoom( - username, - 'Test Room', - joinRule, - { - users, - events, - }, - ); + const result = await federationSDK.createRoom(username, 'Test Room', joinRule, { + users, + events, + }); const state = await stateService.getLatestRoomState(result.room_id); - const memberKey = [...state.keys()].find((stateKey) => - stateKey.includes('m.room.member:'), - ) as `m.room.member:${string}`; + const memberKey = [...state.keys()].find((stateKey) => stateKey.includes('m.room.member:')) as `m.room.member:${string}`; const roomCreateEvent = state.get('m.room.create:'); const joinRuleEvent = state.get('m.room.join_rules:'); @@ -94,13 +88,7 @@ describe('RoomService', async () => { const creatorMembershipEvent = state.get(memberKey); const roomNameEvent = state.get('m.room.name:'); - if ( - !roomCreateEvent || - !joinRuleEvent || - !powerLevelEvent || - !creatorMembershipEvent || - !roomNameEvent - ) { + if (!roomCreateEvent || !joinRuleEvent || !powerLevelEvent || !creatorMembershipEvent || !roomNameEvent) { throw new Error('Event not found'); } @@ -121,35 +109,22 @@ describe('RoomService', async () => { describe('createRoom', () => { it('should create room correctly', async () => { - const username = '@alice:example.com' as room.UserID; - const { - roomCreateEvent, - roomNameEvent, - joinRuleEvent, - powerLevelEvent, - creatorMembershipEvent, - } = await createRoom(username, 'public'); + const username = '@alice:example.com' as UserID; + const { roomCreateEvent, roomNameEvent, joinRuleEvent, powerLevelEvent, creatorMembershipEvent } = await createRoom( + username, + 'public', + ); - const roomId = roomCreateEvent.roomId; + const { roomId } = roomCreateEvent; const state = await stateService.getLatestRoomState(roomId); // check each event - expect( - state.get(roomCreateEvent.getUniqueStateIdentifier()), - ).toHaveProperty('eventId', roomCreateEvent.eventId); - expect( - state.get(roomNameEvent.getUniqueStateIdentifier()), - ).toHaveProperty('eventId', roomNameEvent.eventId); - expect( - state.get(joinRuleEvent.getUniqueStateIdentifier()), - ).toHaveProperty('eventId', joinRuleEvent.eventId); - expect( - state.get(powerLevelEvent.getUniqueStateIdentifier()), - ).toHaveProperty('eventId', powerLevelEvent.eventId); - expect( - state.get(creatorMembershipEvent.getUniqueStateIdentifier()), - ).toHaveProperty('eventId', creatorMembershipEvent.eventId); + expect(state.get(roomCreateEvent.getUniqueStateIdentifier())).toHaveProperty('eventId', roomCreateEvent.eventId); + expect(state.get(roomNameEvent.getUniqueStateIdentifier())).toHaveProperty('eventId', roomNameEvent.eventId); + expect(state.get(joinRuleEvent.getUniqueStateIdentifier())).toHaveProperty('eventId', joinRuleEvent.eventId); + expect(state.get(powerLevelEvent.getUniqueStateIdentifier())).toHaveProperty('eventId', powerLevelEvent.eventId); + expect(state.get(creatorMembershipEvent.getUniqueStateIdentifier())).toHaveProperty('eventId', creatorMembershipEvent.eventId); expect([...state.keys()]).toEqual( expect.arrayContaining([ @@ -164,7 +139,7 @@ describe('RoomService', async () => { }); it('should always keep the owner as 100', async () => { - const username = '@alice:example.com' as room.UserID; + const username = '@alice:example.com' as UserID; const { powerLevelEvent } = await createRoom(username, 'public', { users: { '@alice:example.com': 10 }, }); @@ -173,7 +148,7 @@ describe('RoomService', async () => { }); it('should accept custom event powers', async () => { - const username = '@alice:example.com' as room.UserID; + const username = '@alice:example.com' as UserID; const { powerLevelEvent } = await createRoom(username, 'public', { events: { 'rc.room.name': 10 }, }); @@ -181,35 +156,28 @@ describe('RoomService', async () => { }); it.skip('should create direct message room correctly', async () => { - const username = '@alice:example.com' as room.UserID; - const targetUsername = '@bob:example.com' as room.UserID; + const username = '@alice:example.com' as UserID; + const targetUsername = '@bob:example.com' as UserID; - const roomId = await federationSDK.createDirectMessageRoom( - username, - targetUsername, - ); + const roomId = await federationSDK.createDirectMessageRoom(username, targetUsername); const state = await stateService.getLatestRoomState(roomId); expect(state.size).toBe(2); expect([...state.keys()]).toEqual( - expect.arrayContaining([ - 'm.room.create:', - `m.room.member:${targetUsername}`, - `m.room.member:${username}`, - ]), + expect.arrayContaining(['m.room.create:', `m.room.member:${targetUsername}`, `m.room.member:${username}`]), ); }); }); describe('joinUser', () => { it('should join user to room correctly', async () => { - const username = '@alice:example.com' as room.UserID; - const secondaryUsername = '@bob:example.com' as room.UserID; + const username = '@alice:example.com' as UserID; + const secondaryUsername = '@bob:example.com' as UserID; const { roomCreateEvent } = await createRoom(username, 'public'); - const roomId = roomCreateEvent.roomId; + const { roomId } = roomCreateEvent; const initialState = await stateService.getLatestRoomState(roomId); @@ -231,33 +199,25 @@ describe('RoomService', async () => { const state = await stateService.getLatestRoomState(roomId); expect([...state.keys()]).toEqual( - expect.arrayContaining([ - 'm.room.create:', - `m.room.member:${username}`, - `m.room.member:${secondaryUsername}`, - ]), + expect.arrayContaining(['m.room.create:', `m.room.member:${username}`, `m.room.member:${secondaryUsername}`]), ); }); }); describe('leaveRoom', () => { it('should leave user from room correctly', async () => { - const username = '@alice:example.com' as room.UserID; + const username = '@alice:example.com' as UserID; const { roomCreateEvent } = await createRoom(username, 'public'); - const roomId = roomCreateEvent.roomId; + const { roomId } = roomCreateEvent; await federationSDK.leaveRoom(roomId, username); const state = await stateService.getLatestRoomState(roomId); - expect([...state.keys()]).toEqual( - expect.arrayContaining(['m.room.create:', `m.room.member:${username}`]), - ); + expect([...state.keys()]).toEqual(expect.arrayContaining(['m.room.create:', `m.room.member:${username}`])); - const userState = state.get( - `m.room.member:${username}`, - ) as PersistentEventBase; + const userState = state.get(`m.room.member:${username}`) as PersistentEventBase; expect(userState?.getContent().membership).toBe('leave'); }); @@ -265,27 +225,21 @@ describe('RoomService', async () => { describe('kickUser', () => { it('should ban user to room correctly', async () => { - const username = '@alice:example.com' as room.UserID; - const secondaryUsername = '@bob:example.com' as room.UserID; + const username = '@alice:example.com' as UserID; + const secondaryUsername = '@bob:example.com' as UserID; const { roomCreateEvent } = await createRoom(username, 'public'); - const roomId = roomCreateEvent.roomId; + const { roomId } = roomCreateEvent; await federationSDK.kickUser(roomId, secondaryUsername, username); const state = await stateService.getLatestRoomState(roomId); expect([...state.keys()]).toEqual( - expect.arrayContaining([ - 'm.room.create:', - `m.room.member:${username}`, - `m.room.member:${secondaryUsername}`, - ]), + expect.arrayContaining(['m.room.create:', `m.room.member:${username}`, `m.room.member:${secondaryUsername}`]), ); - const secondaryState = state.get( - `m.room.member:${secondaryUsername}`, - ) as PersistentEventBase; + const secondaryState = state.get(`m.room.member:${secondaryUsername}`) as PersistentEventBase; expect(secondaryState?.getContent().membership).toBe('leave'); }); @@ -293,26 +247,20 @@ describe('RoomService', async () => { describe('banUser', () => { it('should ban user to room correctly', async () => { - const username = '@alice:example.com' as room.UserID; - const secondaryUsername = '@bob:example.com' as room.UserID; + const username = '@alice:example.com' as UserID; + const secondaryUsername = '@bob:example.com' as UserID; const { roomCreateEvent } = await createRoom(username, 'public'); - const roomId = roomCreateEvent.roomId; + const { roomId } = roomCreateEvent; await federationSDK.banUser(roomId, secondaryUsername, username); const state = await stateService.getLatestRoomState(roomId); expect([...state.keys()]).toEqual( - expect.arrayContaining([ - 'm.room.create:', - `m.room.member:${username}`, - `m.room.member:${secondaryUsername}`, - ]), + expect.arrayContaining(['m.room.create:', `m.room.member:${username}`, `m.room.member:${secondaryUsername}`]), ); - const secondaryState = state.get( - `m.room.member:${secondaryUsername}`, - ) as PersistentEventBase; + const secondaryState = state.get(`m.room.member:${secondaryUsername}`) as PersistentEventBase; expect(secondaryState?.getContent().membership).toBe('ban'); }); @@ -320,11 +268,11 @@ describe('RoomService', async () => { describe('updateUserPowerLevel', () => { it('should update user power level correctly', async () => { - const username = '@alice:example.com' as room.UserID; - const secondaryUsername = '@bob:example.com' as room.UserID; + const username = '@alice:example.com' as UserID; + const secondaryUsername = '@bob:example.com' as UserID; const { roomCreateEvent } = await createRoom(username, 'public'); - const roomId = roomCreateEvent.roomId; + const { roomId } = roomCreateEvent; await roomService.joinUser(roomId, username, secondaryUsername); @@ -336,12 +284,7 @@ describe('RoomService', async () => { [username]: 100, }); - await federationSDK.updateUserPowerLevel( - roomId, - secondaryUsername, - 50, - username, - ); + await federationSDK.updateUserPowerLevel(roomId, secondaryUsername, 50, username); expect( await stateService.getLatestRoomState(roomId).then((state) => { @@ -356,68 +299,59 @@ describe('RoomService', async () => { describe('acceptInvite', () => { it('should accept invite and join user to room correctly', async () => { - const username = '@alice:example.com' as room.UserID; - const invitedUsername = '@bob:example.com' as room.UserID; + const username = '@alice:example.com' as UserID; + const invitedUsername = '@bob:example.com' as UserID; const { roomCreateEvent } = await createRoom(username, 'invite'); - const roomId = roomCreateEvent.roomId; + const { roomId } = roomCreateEvent; await federationSDK.inviteUserToRoom(invitedUsername, roomId, username); const stateBeforeAccept = await stateService.getLatestRoomState(roomId); - const inviteMemberEvent = stateBeforeAccept.get( - `m.room.member:${invitedUsername}`, - ) as PersistentEventBase | undefined; + const inviteMemberEvent = stateBeforeAccept.get(`m.room.member:${invitedUsername}`) as + | PersistentEventBase + | undefined; expect(inviteMemberEvent?.getContent().membership).toBe('invite'); await federationSDK.acceptInvite(roomId, invitedUsername); const stateAfterAccept = await stateService.getLatestRoomState(roomId); - const joinMemberEvent = stateAfterAccept.get( - `m.room.member:${invitedUsername}`, - ) as PersistentEventBase; + const joinMemberEvent = stateAfterAccept.get(`m.room.member:${invitedUsername}`) as PersistentEventBase; expect(joinMemberEvent.getContent().membership).toBe('join'); expect([...stateAfterAccept.keys()]).toEqual( - expect.arrayContaining([ - 'm.room.create:', - `m.room.member:${username}`, - `m.room.member:${invitedUsername}`, - ]), + expect.arrayContaining(['m.room.create:', `m.room.member:${username}`, `m.room.member:${invitedUsername}`]), ); }); }); describe('rejectInvite', () => { it('should reject invite and leave room correctly', async () => { - const username = '@alice:example.com' as room.UserID; - const invitedUsername = '@bob:example.com' as room.UserID; + const username = '@alice:example.com' as UserID; + const invitedUsername = '@bob:example.com' as UserID; const { roomCreateEvent } = await createRoom(username, 'invite'); - const roomId = roomCreateEvent.roomId; + const { roomId } = roomCreateEvent; await federationSDK.inviteUserToRoom(invitedUsername, roomId, username); const stateBeforeReject = await stateService.getLatestRoomState(roomId); - const inviteMemberEvent = stateBeforeReject.get( - `m.room.member:${invitedUsername}`, - ) as PersistentEventBase | undefined; + const inviteMemberEvent = stateBeforeReject.get(`m.room.member:${invitedUsername}`) as + | PersistentEventBase + | undefined; expect(inviteMemberEvent?.getContent().membership).toBe('invite'); await federationSDK.rejectInvite(roomId, invitedUsername); const stateAfterReject = await stateService.getLatestRoomState(roomId); - const leaveMemberEvent = stateAfterReject.get( - `m.room.member:${invitedUsername}`, - ) as PersistentEventBase; + const leaveMemberEvent = stateAfterReject.get(`m.room.member:${invitedUsername}`) as PersistentEventBase< + RoomVersion, + 'm.room.member' + >; expect(leaveMemberEvent.getContent().membership).toBe('leave'); expect([...stateAfterReject.keys()]).toEqual( - expect.arrayContaining([ - 'm.room.create:', - `m.room.member:${username}`, - `m.room.member:${invitedUsername}`, - ]), + expect.arrayContaining(['m.room.create:', `m.room.member:${username}`, `m.room.member:${invitedUsername}`]), ); }); }); diff --git a/packages/federation-sdk/src/services/room.service.ts b/packages/federation-sdk/src/services/room.service.ts index cdc320669..f4d0160e3 100644 --- a/packages/federation-sdk/src/services/room.service.ts +++ b/packages/federation-sdk/src/services/room.service.ts @@ -1,54 +1,37 @@ -import { - EventBase, - EventStore, - RoomPowerLevelsEvent, - SignedEvent, - TombstoneAuthEvents, - createLogger, - roomPowerLevelsEvent, -} from '@rocket.chat/federation-core'; -import { delay, inject, singleton } from 'tsyringe'; - -import { - ForbiddenError, - HttpException, - HttpStatus, -} from '@rocket.chat/federation-core'; - -import { logger } from '@rocket.chat/federation-core'; -import { - type EventID, +import type { EventBase, EventStore, RoomPowerLevelsEvent, SignedEvent, TombstoneAuthEvents } from '@rocket.chat/federation-core'; +import { createLogger, roomPowerLevelsEvent, ForbiddenError, HttpException, HttpStatus, logger } from '@rocket.chat/federation-core'; +import { PersistentEventFactory, extractDomainFromId } from '@rocket.chat/federation-room'; +import type { PduForType, PduJoinRuleEventContent, PduType, PersistentEventBase, - PersistentEventFactory, EventStore as RoomEventStore, RoomID, RoomVersion, UserID, - extractDomainFromId, + EventID, } from '@rocket.chat/federation-room'; +import { delay, inject, singleton } from 'tsyringe'; + +import type { ConfigService } from './config.service'; +import { EventAuthorizationService } from './event-authorization.service'; +import type { EventEmitterService } from './event-emitter.service'; +import type { EventFetcherService } from './event-fetcher.service'; +import type { EventService } from './event.service'; +import type { FederationValidationService } from './federation-validation.service'; +import type { FederationService } from './federation.service'; +import type { InviteService } from './invite.service'; +import type { StateService } from './state.service'; +import { RoomInfoNotReadyError, UnknownRoomError } from './state.service'; import { EventStagingRepository } from '../repositories/event-staging.repository'; import { EventRepository } from '../repositories/event.repository'; import { RoomRepository } from '../repositories/room.repository'; -import { ConfigService } from './config.service'; -import { EventAuthorizationService } from './event-authorization.service'; -import { EventEmitterService } from './event-emitter.service'; -import { EventFetcherService } from './event-fetcher.service'; -import { EventService } from './event.service'; -import { FederationValidationService } from './federation-validation.service'; -import { FederationService } from './federation.service'; -import { InviteService } from './invite.service'; -import { - RoomInfoNotReadyError, - StateService, - UnknownRoomError, -} from './state.service'; @singleton() export class RoomService { private readonly logger = createLogger('RoomService'); + constructor( private readonly eventService: EventService, private readonly configService: ConfigService, @@ -73,41 +56,29 @@ export class RoomService { targetUserId: string, newPowerLevel: number, ): void { - const senderPower = - currentPowerLevelsContent.users?.[senderId] ?? - currentPowerLevelsContent.users_default; + const senderPower = currentPowerLevelsContent.users?.[senderId] ?? currentPowerLevelsContent.users_default; // 1. Check if sender can modify m.room.power_levels event itself const requiredLevelToModifyEvent = - currentPowerLevelsContent.events?.['m.room.power_levels'] ?? - currentPowerLevelsContent.state_default ?? - 100; + currentPowerLevelsContent.events?.['m.room.power_levels'] ?? currentPowerLevelsContent.state_default ?? 100; if (senderPower < requiredLevelToModifyEvent) { logger.warn( `Sender ${senderId} (power ${senderPower}) lacks global permission (needs ${requiredLevelToModifyEvent}) to modify power levels event.`, ); - throw new HttpException( - "You don't have permission to change power levels events.", - HttpStatus.FORBIDDEN, - ); + throw new HttpException("You don't have permission to change power levels events.", HttpStatus.FORBIDDEN); } // 2. Specific checks when changing another user's power level if (senderId !== targetUserId) { - const targetUserCurrentPower = - currentPowerLevelsContent.users?.[targetUserId] ?? - currentPowerLevelsContent.users_default; + const targetUserCurrentPower = currentPowerLevelsContent.users?.[targetUserId] ?? currentPowerLevelsContent.users_default; // Rule: Cannot set another user's power level higher than one's own. if (newPowerLevel > senderPower) { logger.warn( `Sender ${senderId} (power ${senderPower}) cannot set user ${targetUserId}'s power to ${newPowerLevel} (higher than own).`, ); - throw new HttpException( - "You cannot set another user's power level higher than your own.", - HttpStatus.FORBIDDEN, - ); + throw new HttpException("You cannot set another user's power level higher than your own.", HttpStatus.FORBIDDEN); } // Rule: Cannot change power level of a user whose current power is >= sender's power. @@ -123,75 +94,39 @@ export class RoomService { } } - private validateKickPermission( - currentPowerLevelsContent: RoomPowerLevelsEvent['content'], - senderId: string, - kickedUserId: string, - ): void { - const senderPower = - currentPowerLevelsContent.users?.[senderId] ?? - currentPowerLevelsContent.users_default ?? - 0; - const kickedUserPower = - currentPowerLevelsContent.users?.[kickedUserId] ?? - currentPowerLevelsContent.users_default ?? - 0; + private validateKickPermission(currentPowerLevelsContent: RoomPowerLevelsEvent['content'], senderId: string, kickedUserId: string): void { + const senderPower = currentPowerLevelsContent.users?.[senderId] ?? currentPowerLevelsContent.users_default ?? 0; + const kickedUserPower = currentPowerLevelsContent.users?.[kickedUserId] ?? currentPowerLevelsContent.users_default ?? 0; const kickLevel = currentPowerLevelsContent.kick ?? 50; // Default kick level if not specified if (senderPower < kickLevel) { - logger.warn( - `Sender ${senderId} (power ${senderPower}) does not meet required power level (${kickLevel}) to kick users.`, - ); - throw new HttpException( - "You don't have permission to kick users from this room.", - HttpStatus.FORBIDDEN, - ); + logger.warn(`Sender ${senderId} (power ${senderPower}) does not meet required power level (${kickLevel}) to kick users.`); + throw new HttpException("You don't have permission to kick users from this room.", HttpStatus.FORBIDDEN); } if (kickedUserPower >= senderPower) { logger.warn( `Sender ${senderId} (power ${senderPower}) cannot kick user ${kickedUserId} (power ${kickedUserPower}) who has equal or greater power.`, ); - throw new HttpException( - 'You cannot kick a user with power greater than or equal to your own.', - HttpStatus.FORBIDDEN, - ); + throw new HttpException('You cannot kick a user with power greater than or equal to your own.', HttpStatus.FORBIDDEN); } } - private validateBanPermission( - currentPowerLevelsContent: RoomPowerLevelsEvent['content'], - senderId: string, - bannedUserId: string, - ): void { - const senderPower = - currentPowerLevelsContent.users?.[senderId] ?? - currentPowerLevelsContent.users_default ?? - 0; - const bannedUserPower = - currentPowerLevelsContent.users?.[bannedUserId] ?? - currentPowerLevelsContent.users_default ?? - 0; + private validateBanPermission(currentPowerLevelsContent: RoomPowerLevelsEvent['content'], senderId: string, bannedUserId: string): void { + const senderPower = currentPowerLevelsContent.users?.[senderId] ?? currentPowerLevelsContent.users_default ?? 0; + const bannedUserPower = currentPowerLevelsContent.users?.[bannedUserId] ?? currentPowerLevelsContent.users_default ?? 0; const banLevel = currentPowerLevelsContent.ban ?? 50; // Default ban level if not specified if (senderPower < banLevel) { - logger.warn( - `Sender ${senderId} (power ${senderPower}) does not meet required power level (${banLevel}) to ban users.`, - ); - throw new HttpException( - "You don't have permission to ban users from this room.", - HttpStatus.FORBIDDEN, - ); + logger.warn(`Sender ${senderId} (power ${senderPower}) does not meet required power level (${banLevel}) to ban users.`); + throw new HttpException("You don't have permission to ban users from this room.", HttpStatus.FORBIDDEN); } if (bannedUserPower >= senderPower) { logger.warn( `Sender ${senderId} (power ${senderPower}) cannot ban user ${bannedUserId} (power ${bannedUserPower}) who has equal or greater power.`, ); - throw new HttpException( - 'You cannot ban a user with power greater than or equal to your own.', - HttpStatus.FORBIDDEN, - ); + throw new HttpException('You cannot ban a user with power greater than or equal to your own.', HttpStatus.FORBIDDEN); } } @@ -205,17 +140,13 @@ export class RoomService { } // Find power levels - const powerLevelsEvent = state.find( - (event) => event.type === 'm.room.power_levels', - ); + const powerLevelsEvent = state.find((event) => event.type === 'm.room.power_levels'); if (powerLevelsEvent) { logger.info(`Found power levels event for room ${roomId}`); } // Count member events - const memberEvents = state.filter( - (event) => event.type === 'm.room.member', - ); + const memberEvents = state.filter((event) => event.type === 'm.room.member'); logger.info(`Room ${roomId} has ${memberEvents.length} member events`); try { @@ -242,43 +173,37 @@ export class RoomService { events: {}, }, ) { - logger.debug( - `Creating room for ${username} with ${name} join_rule: ${joinRule}`, - ); + logger.debug(`Creating room for ${username} with ${name} join_rule: ${joinRule}`); - const roomCreateEvent = PersistentEventFactory.newCreateEvent( - username, - PersistentEventFactory.defaultRoomVersion, - ); + const roomCreateEvent = PersistentEventFactory.newCreateEvent(username, PersistentEventFactory.defaultRoomVersion); - const stateService = this.stateService; + const { stateService } = this; await stateService.signEvent(roomCreateEvent); await stateService.handlePdu(roomCreateEvent); - const creatorMembershipEvent = - await stateService.buildEvent<'m.room.member'>( - { - type: 'm.room.member', - content: { membership: 'join' }, - room_id: roomCreateEvent.roomId, - state_key: username, - auth_events: [], - depth: 0, - prev_events: [], - origin_server_ts: Date.now(), - sender: username, - }, - PersistentEventFactory.defaultRoomVersion, - ); + const creatorMembershipEvent = await stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + content: { membership: 'join' }, + room_id: roomCreateEvent.roomId, + state_key: username, + auth_events: [], + depth: 0, + prev_events: [], + origin_server_ts: Date.now(), + sender: username, + }, + PersistentEventFactory.defaultRoomVersion, + ); await stateService.handlePdu(creatorMembershipEvent); const roomNameEvent = await stateService.buildEvent<'m.room.name'>( { type: 'm.room.name', - content: { name: name }, + content: { name }, room_id: roomCreateEvent.roomId, state_key: '', auth_events: [], @@ -292,36 +217,35 @@ export class RoomService { await stateService.handlePdu(roomNameEvent); - const powerLevelEvent = - await stateService.buildEvent<'m.room.power_levels'>( - { - type: 'm.room.power_levels', - content: { - users: { - ...powers.users, - [username]: 100, - }, - users_default: 0, - events: { - ...powers.events, - }, - events_default: 0, - state_default: 50, - ban: 50, - kick: 50, - redact: 50, - invite: 50, + const powerLevelEvent = await stateService.buildEvent<'m.room.power_levels'>( + { + type: 'm.room.power_levels', + content: { + users: { + ...powers.users, + [username]: 100, }, - room_id: roomCreateEvent.roomId, - state_key: '', - auth_events: [], - depth: 0, - prev_events: [], - origin_server_ts: Date.now(), - sender: username, + users_default: 0, + events: { + ...powers.events, + }, + events_default: 0, + state_default: 50, + ban: 50, + kick: 50, + redact: 50, + invite: 50, }, - PersistentEventFactory.defaultRoomVersion, - ); + room_id: roomCreateEvent.roomId, + state_key: '', + auth_events: [], + depth: 0, + prev_events: [], + origin_server_ts: Date.now(), + sender: username, + }, + PersistentEventFactory.defaultRoomVersion, + ); await stateService.handlePdu(powerLevelEvent); @@ -342,24 +266,23 @@ export class RoomService { await stateService.handlePdu(joinRuleEvent); - const canonicalAliasEvent = - await stateService.buildEvent<'m.room.canonical_alias'>( - { - type: 'm.room.canonical_alias', - content: { - alias: `#${name}:${this.configService.serverName}`, - alt_aliases: [], - }, - room_id: roomCreateEvent.roomId, - state_key: '', - auth_events: [], - depth: 0, - prev_events: [], - origin_server_ts: Date.now(), - sender: username, + const canonicalAliasEvent = await stateService.buildEvent<'m.room.canonical_alias'>( + { + type: 'm.room.canonical_alias', + content: { + alias: `#${name}:${this.configService.serverName}`, + alt_aliases: [], }, - PersistentEventFactory.defaultRoomVersion, - ); + room_id: roomCreateEvent.roomId, + state_key: '', + auth_events: [], + depth: 0, + prev_events: [], + origin_server_ts: Date.now(), + sender: username, + }, + PersistentEventFactory.defaultRoomVersion, + ); await stateService.handlePdu(canonicalAliasEvent); @@ -370,15 +293,13 @@ export class RoomService { } async updateRoomName(roomId: RoomID, name: string, senderId: UserID) { - logger.info( - `Updating room name for ${roomId} to \"${name}\" by ${senderId}`, - ); + logger.info(`Updating room name for ${roomId} to \"${name}\" by ${senderId}`); const roomversion = await this.stateService.getRoomVersion(roomId); if (!roomversion) { throw new Error('Room version not found'); } - const stateService = this.stateService; + const { stateService } = this; const roomNameEvent = await stateService.buildEvent<'m.room.name'>( { @@ -418,7 +339,7 @@ export class RoomService { depth: 0, prev_events: [], origin_server_ts: Date.now(), - sender: sender, + sender, }, roomVersion, ); @@ -433,95 +354,51 @@ export class RoomService { type: E, extra?: (event: EventStore>) => boolean, ): EventStore> | undefined { - const event = authEventIds.find( - (e) => - e.event.type === type && - (!extra || extra(e as unknown as EventStore>)), - ); + const event = authEventIds.find((e) => e.event.type === type && (!extra || extra(e as unknown as EventStore>))); return event as unknown as EventStore> | undefined; } - async updateUserPowerLevel( - roomId: RoomID, - userId: UserID, - powerLevel: number, - senderId: UserID, - ): Promise { - logger.info( - `Updating power level for user ${userId} in room ${roomId} to ${powerLevel} by ${senderId}`, - ); + async updateUserPowerLevel(roomId: RoomID, userId: UserID, powerLevel: number, senderId: UserID): Promise { + logger.info(`Updating power level for user ${userId} in room ${roomId} to ${powerLevel} by ${senderId}`); - const authEventIds = await this.eventService.getAuthEventIds( - 'm.room.power_levels', - { roomId, senderId }, - ); + const authEventIds = await this.eventService.getAuthEventIds('m.room.power_levels', { roomId, senderId }); - const powerLevelsAuthResult = this.getEventByType( - authEventIds, - 'm.room.power_levels', - ); + const powerLevelsAuthResult = this.getEventByType(authEventIds, 'm.room.power_levels'); const currentPowerLevelsEvent = - powerLevelsAuthResult?._id && - (await this.eventService.getEventById( - powerLevelsAuthResult._id, - 'm.room.power_levels', - )); + powerLevelsAuthResult?._id && (await this.eventService.getEventById(powerLevelsAuthResult._id, 'm.room.power_levels')); if (!currentPowerLevelsEvent) { logger.error(`No m.room.power_levels event found for room ${roomId}`); - throw new HttpException( - 'Room power levels not found, cannot update.', - HttpStatus.NOT_FOUND, - ); + throw new HttpException('Room power levels not found, cannot update.', HttpStatus.NOT_FOUND); } - this.validatePowerLevelChange( - currentPowerLevelsEvent.event.content, - senderId, - userId, - powerLevel, - ); + this.validatePowerLevelChange(currentPowerLevelsEvent.event.content, senderId, userId, powerLevel); const createAuthResult = this.getEventByType(authEventIds, 'm.room.create'); - const memberAuthResult = this.getEventByType( - authEventIds, - 'm.room.member', - (e) => e.event.state_key === senderId, - ); + const memberAuthResult = this.getEventByType(authEventIds, 'm.room.member', (e) => e.event.state_key === senderId); // Ensure critical auth events were found if (!createAuthResult || !powerLevelsAuthResult || !memberAuthResult) { logger.error( - `Critical auth events missing for power level update. Create: ${ - createAuthResult?._id ?? 'missing' - }, PowerLevels: ${powerLevelsAuthResult?._id ?? 'missing'}, Member: ${ - memberAuthResult?._id ?? 'missing' - }`, - ); - throw new HttpException( - 'Internal server error: Missing auth events for power level update.', - HttpStatus.INTERNAL_SERVER_ERROR, + `Critical auth events missing for power level update. Create: ${createAuthResult?._id ?? 'missing'}, PowerLevels: ${ + powerLevelsAuthResult?._id ?? 'missing' + }, Member: ${memberAuthResult?._id ?? 'missing'}`, ); + throw new HttpException('Internal server error: Missing auth events for power level update.', HttpStatus.INTERNAL_SERVER_ERROR); } const lastEventStore = await this.eventService.getLastEventForRoom(roomId); if (!lastEventStore) { logger.error(`No last event found for room ${roomId}`); - throw new HttpException( - 'Room has no history, cannot update power levels', - HttpStatus.BAD_REQUEST, - ); + throw new HttpException('Room has no history, cannot update power levels', HttpStatus.BAD_REQUEST); } - const serverName = this.configService.serverName; + const { serverName } = this.configService; if (!serverName) { logger.error('Server name is not configured. Cannot set event origin.'); - throw new HttpException( - 'Server configuration error for event origin.', - HttpStatus.INTERNAL_SERVER_ERROR, - ); + throw new HttpException('Server configuration error for event origin.', HttpStatus.INTERNAL_SERVER_ERROR); } const eventToSign = roomPowerLevelsEvent({ @@ -561,9 +438,7 @@ export class RoomService { await this.stateService.handlePdu(event); - logger.info( - `Successfully created and stored m.room.power_levels event ${event.eventId} for room ${roomId}`, - ); + logger.info(`Successfully created and stored m.room.power_levels event ${event.eventId} for room ${roomId}`); void this.federationService.sendEventToAllServersInRoom(event); @@ -602,30 +477,20 @@ export class RoomService { }; } - async sendLeave( - roomId: RoomID, - eventId: EventID, - event: PduForType<'m.room.member'>, - ): Promise { + async sendLeave(roomId: RoomID, eventId: EventID, event: PduForType<'m.room.member'>): Promise { const roomVersion = await this.stateService.getRoomVersion(roomId); if (!roomVersion) { throw new Error('Room version not found while sending leave'); } - const leaveEvent = - PersistentEventFactory.createFromRawEvent<'m.room.member'>( - event, - roomVersion, - ); + const leaveEvent = PersistentEventFactory.createFromRawEvent<'m.room.member'>(event, roomVersion); if (leaveEvent.eventId !== eventId) { throw new Error(`Invalid eventId ${eventId}`); } const residentServer = extractDomainFromId(roomId); if (residentServer !== this.configService.serverName) { - throw new Error( - `Server ${residentServer} is not the resident server for room ${roomId}`, - ); + throw new Error(`Server ${residentServer} is not the resident server for room ${roomId}`); } await this.stateService.handlePdu(leaveEvent); @@ -646,119 +511,62 @@ export class RoomService { // TODO we should actually use the room state to validate the power levels - const authEventIds = await this.eventService.getAuthEventIds( - 'm.room.member', - { roomId, senderId }, - ); + const authEventIds = await this.eventService.getAuthEventIds('m.room.member', { roomId, senderId }); // For a leave event, the user must have permission to send m.room.member events. // This is typically covered by them being a member, but power levels might restrict it. - const powerLevelsEventId = this.getEventByType( - authEventIds, - 'm.room.power_levels', - )?._id; + const powerLevelsEventId = this.getEventByType(authEventIds, 'm.room.power_levels')?._id; if (!powerLevelsEventId) { - logger.warn( - `No power_levels event found for room ${roomId}, cannot verify permission to leave.`, - ); - throw new HttpException( - 'Cannot verify permission to leave room.', - HttpStatus.FORBIDDEN, - ); + logger.warn(`No power_levels event found for room ${roomId}, cannot verify permission to leave.`); + throw new HttpException('Cannot verify permission to leave room.', HttpStatus.FORBIDDEN); } - const canLeaveRoom = await this.eventService.checkUserPermission( - powerLevelsEventId, - senderId, - 'm.room.member', - ); + const canLeaveRoom = await this.eventService.checkUserPermission(powerLevelsEventId, senderId, 'm.room.member'); if (!canLeaveRoom) { - logger.warn( - `User ${senderId} does not have permission to send m.room.member events in ${roomId} (i.e., to leave).`, - ); - throw new HttpException( - "You don't have permission to leave this room.", - HttpStatus.FORBIDDEN, - ); + logger.warn(`User ${senderId} does not have permission to send m.room.member events in ${roomId} (i.e., to leave).`); + throw new HttpException("You don't have permission to leave this room.", HttpStatus.FORBIDDEN); } const makeLeaveResponse = await this.makeLeave(roomId, senderId); - const leaveEvent = PersistentEventFactory.createFromRawEvent( - makeLeaveResponse.event, - makeLeaveResponse.room_version, - ); + const leaveEvent = PersistentEventFactory.createFromRawEvent(makeLeaveResponse.event, makeLeaveResponse.room_version); await this.stateService.handlePdu(leaveEvent); - logger.info( - `Successfully created and stored m.room.member (leave) event ${leaveEvent.eventId} for user ${senderId} in room ${roomId}`, - ); + logger.info(`Successfully created and stored m.room.member (leave) event ${leaveEvent.eventId} for user ${senderId} in room ${roomId}`); void this.federationService.sendEventToAllServersInRoom(leaveEvent); return leaveEvent.eventId; } - async kickUser( - roomId: RoomID, - kickedUserId: UserID, - senderId: UserID, - reason?: string, - ): Promise { - logger.info( - `User ${senderId} kicking user ${kickedUserId} from room ${roomId}. Reason: ${ - reason || 'No reason specified' - }`, - ); + async kickUser(roomId: RoomID, kickedUserId: UserID, senderId: UserID, reason?: string): Promise { + logger.info(`User ${senderId} kicking user ${kickedUserId} from room ${roomId}. Reason: ${reason || 'No reason specified'}`); const roomInfo = await this.stateService.getRoomInformation(roomId); - const authEventIdsForPowerLevels = await this.eventService.getAuthEventIds( - 'm.room.power_levels', - { roomId, senderId }, - ); - const powerLevelsEventId = this.getEventByType( - authEventIdsForPowerLevels, - 'm.room.power_levels', - )?._id; + const authEventIdsForPowerLevels = await this.eventService.getAuthEventIds('m.room.power_levels', { roomId, senderId }); + const powerLevelsEventId = this.getEventByType(authEventIdsForPowerLevels, 'm.room.power_levels')?._id; if (!powerLevelsEventId) { - logger.warn( - `No power_levels event found for room ${roomId}, cannot verify permission to kick.`, - ); - throw new HttpException( - 'Cannot verify permission to kick user.', - HttpStatus.FORBIDDEN, - ); + logger.warn(`No power_levels event found for room ${roomId}, cannot verify permission to kick.`); + throw new HttpException('Cannot verify permission to kick user.', HttpStatus.FORBIDDEN); } - const powerLevelsEvent = await this.eventService.getEventById( - powerLevelsEventId, - 'm.room.power_levels', - ); + const powerLevelsEvent = await this.eventService.getEventById(powerLevelsEventId, 'm.room.power_levels'); if (!powerLevelsEvent) { - logger.error( - `Power levels event ${powerLevelsEventId} not found despite ID being retrieved.`, - ); - throw new HttpException( - 'Internal server error: Power levels event data missing.', - HttpStatus.INTERNAL_SERVER_ERROR, - ); + logger.error(`Power levels event ${powerLevelsEventId} not found despite ID being retrieved.`); + throw new HttpException('Internal server error: Power levels event data missing.', HttpStatus.INTERNAL_SERVER_ERROR); } - this.validateKickPermission( - powerLevelsEvent.event.content, - senderId, - kickedUserId, - ); + this.validateKickPermission(powerLevelsEvent.event.content, senderId, kickedUserId); const kickEvent = await this.stateService.buildEvent<'m.room.member'>( { type: 'm.room.member', content: { membership: 'leave', - reason: reason, + reason, }, room_id: roomId, state_key: kickedUserId, @@ -816,9 +624,7 @@ export class RoomService { await this.stateService.handlePdu(memberEvent); if (memberEvent.rejected) { - throw new Error( - `Member profile update rejected: ${memberEvent.rejectReason}`, - ); + throw new Error(`Member profile update rejected: ${memberEvent.rejectReason}`); } void this.federationService.sendEventToAllServersInRoom(memberEvent); @@ -826,65 +632,33 @@ export class RoomService { return memberEvent; } - async banUser( - roomId: RoomID, - bannedUserId: UserID, - senderId: UserID, - reason?: string, - ): Promise { - logger.info( - `User ${senderId} banning user ${bannedUserId} from room ${roomId}. Reason: ${ - reason || 'No reason specified' - }`, - ); + async banUser(roomId: RoomID, bannedUserId: UserID, senderId: UserID, reason?: string): Promise { + logger.info(`User ${senderId} banning user ${bannedUserId} from room ${roomId}. Reason: ${reason || 'No reason specified'}`); const roomInfo = await this.stateService.getRoomInformation(roomId); - const authEventIdsForPowerLevels = await this.eventService.getAuthEventIds( - 'm.room.power_levels', - { roomId, senderId }, - ); + const authEventIdsForPowerLevels = await this.eventService.getAuthEventIds('m.room.power_levels', { roomId, senderId }); - const powerLevelsEventId = this.getEventByType( - authEventIdsForPowerLevels, - 'm.room.power_levels', - )?._id; + const powerLevelsEventId = this.getEventByType(authEventIdsForPowerLevels, 'm.room.power_levels')?._id; if (!powerLevelsEventId) { - logger.warn( - `No power_levels event found for room ${roomId}, cannot verify permission to ban.`, - ); - throw new HttpException( - 'Cannot verify permission to ban user.', - HttpStatus.FORBIDDEN, - ); + logger.warn(`No power_levels event found for room ${roomId}, cannot verify permission to ban.`); + throw new HttpException('Cannot verify permission to ban user.', HttpStatus.FORBIDDEN); } - const powerLevelsEvent = await this.eventService.getEventById( - powerLevelsEventId, - 'm.room.power_levels', - ); + const powerLevelsEvent = await this.eventService.getEventById(powerLevelsEventId, 'm.room.power_levels'); if (!powerLevelsEvent) { - logger.error( - `Power levels event ${powerLevelsEventId} not found despite ID being retrieved.`, - ); - throw new HttpException( - 'Internal server error: Power levels event data missing.', - HttpStatus.INTERNAL_SERVER_ERROR, - ); + logger.error(`Power levels event ${powerLevelsEventId} not found despite ID being retrieved.`); + throw new HttpException('Internal server error: Power levels event data missing.', HttpStatus.INTERNAL_SERVER_ERROR); } - this.validateBanPermission( - powerLevelsEvent.event.content, - senderId, - bannedUserId, - ); + this.validateBanPermission(powerLevelsEvent.event.content, senderId, bannedUserId); const banEvent = await this.stateService.buildEvent<'m.room.member'>( { type: 'm.room.member', content: { membership: 'ban', - reason: reason, + reason, }, room_id: roomId, state_key: bannedUserId, @@ -899,9 +673,7 @@ export class RoomService { await this.stateService.handlePdu(banEvent); - logger.info( - `Successfully created and stored m.room.member (ban) event ${banEvent.eventId} for user ${bannedUserId} in room ${roomId}`, - ); + logger.info(`Successfully created and stored m.room.member (ban) event ${banEvent.eventId} for user ${bannedUserId} in room ${roomId}`); void this.federationService.sendEventToAllServersInRoom(banEvent); @@ -911,9 +683,9 @@ export class RoomService { // if local room, add the user to the room if allowed. // if remote room, run through the join process async joinUser(roomId: RoomID, userId: UserID) { - const configService = this.configService; - const stateService = this.stateService; - const federationService = this.federationService; + const { configService } = this; + const { stateService } = this; + const { federationService } = this; // where the room is hosted at const residentServer = extractDomainFromId(roomId); @@ -926,9 +698,7 @@ export class RoomService { const createEvent = room.get('m.room.create:'); if (!createEvent) { - throw new Error( - 'Room create event not found when trying to join a room', - ); + throw new Error('Room create event not found when trying to join a room'); } const membershipEvent = await stateService.buildEvent<'m.room.member'>( @@ -974,21 +744,13 @@ export class RoomService { // ^ have the template for the join event now - const joinEvent = PersistentEventFactory.createFromRawEvent( - makeJoinResponse.event, - makeJoinResponse.room_version, - ); + const joinEvent = PersistentEventFactory.createFromRawEvent(makeJoinResponse.event, makeJoinResponse.room_version); // const signedJoinEvent = await stateService.signEvent(joinEvent); // TODO: sign the event here vvv // currently makeSignedRequest does the signing - const { - state, - auth_chain: authChain, - event, - servers_in_room: serversInRoom = [], - } = await federationService.sendJoin(joinEvent); + const { state, auth_chain: authChain, event, servers_in_room: serversInRoom = [] } = await federationService.sendJoin(joinEvent); // TODO: validate hash and sig (item 2) @@ -1001,37 +763,23 @@ export class RoomService { } catch (error) { if (error instanceof UnknownRoomError) { // if already in room, skip this, walk join event to fill the state - this.logger.info( - { roomId }, - 'room not found, processing initial state', - ); + this.logger.info({ roomId }, 'room not found, processing initial state'); await stateService.processInitialState(state, authChain); } } if (await stateService.isRoomStatePartial(roomId)) { - this.logger.info( - { roomId }, - 'received incomplete graph of state from send_join, completing state before processing join', - ); + this.logger.info({ roomId }, 'received incomplete graph of state from send_join, completing state before processing join'); const partialEvents = await stateService.getPartialEvents(roomId); - this.logger.info( - { roomId, partialEventIds: partialEvents.map((e) => e.eventId) }, - 'events with incomplete states', - ); + this.logger.info({ roomId, partialEventIds: partialEvents.map((e) => e.eventId) }, 'events with incomplete states'); - for (const event of partialEvents) { + for await (const event of partialEvents) { this.logger.info({ roomId, eventId: event.eventId }, 'walking branch'); const missingBranchEvents = ( - await this._fetchFullBranch( - event.getPreviousEventIds(), - residentServer, - serversInRoom, - { event }, - ) + await this._fetchFullBranch(event.getPreviousEventIds(), residentServer, serversInRoom, { event }) ).sort((e1, e2) => { if (e1.depth !== e2.depth) { return e1.depth - e2.depth; @@ -1044,7 +792,7 @@ export class RoomService { return e1.eventId.localeCompare(e2.eventId); }); - for (const missingEvent of missingBranchEvents) { + for await (const missingEvent of missingBranchEvents) { this.logger.info( { eventId: event.eventId, @@ -1055,12 +803,7 @@ export class RoomService { 'processing state at event', ); await stateService._resolveStateAtEvent(missingEvent); - void this.eventStagingRepository.create( - missingEvent.eventId, - residentServer, - missingEvent.event, - 'join', - ); + void this.eventStagingRepository.create(missingEvent.eventId, residentServer, missingEvent.event, 'join'); } // if partial, join event will also be partial @@ -1068,21 +811,13 @@ export class RoomService { } if (await stateService.isRoomStatePartial(roomId)) { - throw new Error( - `${roomId} still in partial state after processing all branches`, - ); + throw new Error(`${roomId} still in partial state after processing all branches`); } } else { - this.logger.info( - { roomId }, - 'received complete graph of state from send_join, nothing to do', - ); + this.logger.info({ roomId }, 'received complete graph of state from send_join, nothing to do'); } - const joinEventFinal = PersistentEventFactory.createFromRawEvent( - event, - makeJoinResponse.room_version, - ); + const joinEventFinal = PersistentEventFactory.createFromRawEvent(event, makeJoinResponse.room_version); // FIXME: this should be here, but since using join event to walk and repopulate missing message, there is no gurantee the check will pass @@ -1111,44 +846,29 @@ export class RoomService { }); // try to persist the join event now, should succeed with state in place - void this.eventService.processIncomingPDUs( - residentServer || joinEventFinal.origin, - [...state, joinEventFinal.event], - ); + void this.eventService.processIncomingPDUs(residentServer || joinEventFinal.origin, [...state, joinEventFinal.event]); return joinEventFinal.eventId; } async acceptInvite(roomId: RoomID, userId: UserID) { - const inviteEventStore = await this.eventService.findInviteEvent( - roomId, - userId, - ); + const inviteEventStore = await this.eventService.findInviteEvent(roomId, userId); if (!inviteEventStore) { - throw new Error( - `Invite event not found for user ${userId} in room ${roomId}`, - ); + throw new Error(`Invite event not found for user ${userId} in room ${roomId}`); } return this.joinUser(roomId, userId); } async rejectInvite(roomId: RoomID, userId: UserID): Promise { - const inviteEventStore = await this.eventService.findInviteEvent( - roomId, - userId, - ); + const inviteEventStore = await this.eventService.findInviteEvent(roomId, userId); if (!inviteEventStore) { - throw new Error( - `Invite event not found for user ${userId} in room ${roomId}`, - ); + throw new Error(`Invite event not found for user ${userId} in room ${roomId}`); } const invitingServer = extractDomainFromId(inviteEventStore.event.sender); if (!invitingServer) { - throw new Error( - `Invalid sender in invite event: ${inviteEventStore.event.sender}`, - ); + throw new Error(`Invalid sender in invite event: ${inviteEventStore.event.sender}`); } // if inviting server is our own, we can handle the leave event ourselves @@ -1160,14 +880,9 @@ export class RoomService { // important to note that on rejections from remote servers, we might not have the room state yet, // that's why we handle it manually instead of calling this.leaveRoom - const { event: leaveTemplate, room_version } = - await this.federationService.makeLeave(invitingServer, roomId, userId); + const { event: leaveTemplate, room_version } = await this.federationService.makeLeave(invitingServer, roomId, userId); - const leaveEvent = - PersistentEventFactory.createFromRawEvent<'m.room.member'>( - leaveTemplate, - room_version, - ); + const leaveEvent = PersistentEventFactory.createFromRawEvent<'m.room.member'>(leaveTemplate, room_version); await this.stateService.signEvent(leaveEvent); @@ -1190,18 +905,13 @@ export class RoomService { } const previousEvents = [] as PersistentEventBase[]; - const roomId = context.event.roomId; + const { roomId } = context.event; const roomVersion = context.event.version; const store = this.stateService._getStore(roomVersion); let missing = [] as EventID[]; - const { events, missing: stillMissing } = await this._fetchMissingEvents( - eventIds, - roomVersion, - store, - residentServer, - ); + const { events, missing: stillMissing } = await this._fetchMissingEvents(eventIds, roomVersion, store, residentServer); missing = stillMissing; previousEvents.push(...events); @@ -1237,8 +947,10 @@ export class RoomService { throw new Error(); } - for (let i = 0; i < serverList.length && missing.length > 0; i++) { - const askingServer = serverList[i]; + for await (const askingServer of serverList) { + if (missing.length === 0) { + break; + } this.logger.warn({ msg: 'attempting to fetch events from participating server', @@ -1248,12 +960,7 @@ export class RoomService { missing, }); - const { events, missing: stillMissing } = await this._fetchMissingEvents( - missing, - roomVersion, - store, - askingServer, - ); + const { events, missing: stillMissing } = await this._fetchMissingEvents(missing, roomVersion, store, askingServer); missing = stillMissing; previousEvents.push(...events); @@ -1283,12 +990,7 @@ export class RoomService { return previousEvents; } - private async _fetchMissingEvents( - eventIds: EventID[], - roomVersion: RoomVersion, - store: RoomEventStore, - askedServerName: string, - ) { + private async _fetchMissingEvents(eventIds: EventID[], roomVersion: RoomVersion, store: RoomEventStore, askedServerName: string) { const seenEvents = await store.getEvents(eventIds); if (seenEvents.length === eventIds.length) { @@ -1300,10 +1002,7 @@ export class RoomService { .values() .toArray(); - const remotePdus = await this.eventFetcherService.fetchEventsFromFederation( - needsFetching, - askedServerName, - ); + const remotePdus = await this.eventFetcherService.fetchEventsFromFederation(needsFetching, askedServerName); const cache = new Map(); for (const pdu of remotePdus) { @@ -1323,7 +1022,7 @@ export class RoomService { replacementRoomId?: RoomID, ): Promise>> { logger.debug(`Marking room ${roomId} as tombstone by ${sender}`); - const serverName = this.configService.serverName; + const { serverName } = this.configService; const room = await this.roomRepository.findOneById(roomId); if (!room) { @@ -1338,49 +1037,34 @@ export class RoomService { throw new HttpException('Invalid sender', HttpStatus.BAD_REQUEST); } - const powerLevelsEvent = - await this.eventRepository.findPowerLevelsEventByRoomId(roomId); + const powerLevelsEvent = await this.eventRepository.findPowerLevelsEventByRoomId(roomId); if (!powerLevelsEvent) { - throw new HttpException( - 'Cannot delete room without power levels', - HttpStatus.FORBIDDEN, - ); + throw new HttpException('Cannot delete room without power levels', HttpStatus.FORBIDDEN); } this.validatePowerLevelForTombstone(powerLevelsEvent.event, sender); - const authEvents = await this.eventService.getAuthEventIds( - 'm.room.message', - { - roomId, - senderId: sender, - }, - ); + const authEvents = await this.eventService.getAuthEventIds('m.room.message', { + roomId, + senderId: sender, + }); const latestEvent = await this.eventService.getLastEventForRoom(roomId); const currentDepth = latestEvent?.event?.depth ?? 0; const depth = currentDepth + 1; const authEventsMap: TombstoneAuthEvents = { - 'm.room.create': authEvents.find( - (event) => event.event.type === 'm.room.create', - )?._id, - 'm.room.power_levels': authEvents.find( - (event) => event.event.type === 'm.room.power_levels', - )?._id, - 'm.room.member': authEvents.find( - (event) => event.event.type === 'm.room.member', - )?._id, + 'm.room.create': authEvents.find((event) => event.event.type === 'm.room.create')?._id, + 'm.room.power_levels': authEvents.find((event) => event.event.type === 'm.room.power_levels')?._id, + 'm.room.member': authEvents.find((event) => event.event.type === 'm.room.member')?._id, }; const prevEvents = latestEvent ? [latestEvent._id] : []; - const authEventsArray = Object.values(authEventsMap).filter( - (event) => event !== undefined, - ) as EventID[]; + const authEventsArray = Object.values(authEventsMap).filter((event) => event !== undefined) as EventID[]; const event = await this.stateService.buildEvent<'m.room.tombstone'>( { room_id: roomId, - sender: sender, + sender, content: { body: reason, replacement_room: replacementRoomId, @@ -1418,14 +1102,11 @@ export class RoomService { try { const room = await this.roomRepository.findOneById(roomId); if (room?.room.deleted) { - logger.debug( - `Room ${roomId} is marked as deleted in the room repository`, - ); + logger.debug(`Room ${roomId} is marked as deleted in the room repository`); return true; } - const tombstoneEvents = - this.eventRepository.findTombstoneEventsByRoomId(roomId); + const tombstoneEvents = this.eventRepository.findTombstoneEventsByRoomId(roomId); return (await tombstoneEvents.toArray()).length > 0; } catch (error) { logger.error({ @@ -1437,39 +1118,23 @@ export class RoomService { } } - private validatePowerLevelForTombstone( - powerLevels: PduForType<'m.room.power_levels'>, - sender: UserID, - ): void { - const userPowerLevel = - powerLevels.content.users?.[sender] ?? - powerLevels.content.users_default ?? - 0; + private validatePowerLevelForTombstone(powerLevels: PduForType<'m.room.power_levels'>, sender: UserID): void { + const userPowerLevel = powerLevels.content.users?.[sender] ?? powerLevels.content.users_default ?? 0; const requiredPowerLevel = powerLevels.content.state_default ?? 50; if (userPowerLevel < requiredPowerLevel) { - throw new HttpException( - 'Insufficient power level to delete room', - HttpStatus.FORBIDDEN, - ); + throw new HttpException('Insufficient power level to delete room', HttpStatus.FORBIDDEN); } } - async setPowerLevelForUser( - roomId: RoomID, - sender: UserID, - userId: UserID, - powerLevel: number, - ) { + async setPowerLevelForUser(roomId: RoomID, sender: UserID, userId: UserID, powerLevel: number) { const state = await this.stateService.getLatestRoomState2(roomId); const existing = state.powerLevels; if (!existing) { // TODO we should have one always for ours - throw new Error( - 'Power levels event not found while setting power level for user', - ); + throw new Error('Power levels event not found while setting power level for user'); } const clone = structuredClone(existing); @@ -1490,7 +1155,7 @@ export class RoomService { depth: 0, prev_events: [], origin_server_ts: Date.now(), - sender: sender, + sender, }, state.version, ); @@ -1500,183 +1165,159 @@ export class RoomService { void this.federationService.sendEventToAllServersInRoom(event); } - async createDirectMessage({ - creatorUserId, - members, - }: { - creatorUserId: UserID; - members: UserID[]; - }) { - const roomCreateEvent = - PersistentEventFactory.newCreateEvent(creatorUserId); + async createDirectMessage({ creatorUserId, members }: { creatorUserId: UserID; members: UserID[] }) { + const roomCreateEvent = PersistentEventFactory.newCreateEvent(creatorUserId); await this.stateService.signEvent(roomCreateEvent); await this.stateService.handlePdu(roomCreateEvent); - const creatorMembershipEvent = - await this.stateService.buildEvent<'m.room.member'>( - { - type: 'm.room.member', - auth_events: [], - prev_events: [], - sender: creatorUserId, - content: { - membership: 'join', - is_direct: true, - displayname: creatorUserId.split(':').shift()?.slice(1), - }, - depth: 2, - room_id: roomCreateEvent.roomId, - state_key: creatorUserId, - origin_server_ts: Date.now(), + const creatorMembershipEvent = await this.stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + auth_events: [], + prev_events: [], + sender: creatorUserId, + content: { + membership: 'join', + is_direct: true, + displayname: creatorUserId.split(':').shift()?.slice(1), }, - roomCreateEvent.version, - ); + depth: 2, + room_id: roomCreateEvent.roomId, + state_key: creatorUserId, + origin_server_ts: Date.now(), + }, + roomCreateEvent.version, + ); await this.stateService.handlePdu(creatorMembershipEvent); - const powerLevelsEvent = - await this.stateService.buildEvent<'m.room.power_levels'>( - { - type: 'm.room.power_levels', - auth_events: [], - prev_events: [], - content: { - users: { - [creatorUserId]: 100, - ...(members.length === 1 ? { [members[0]]: 100 } : {}), // 1:1 DM both get 100 power level - }, - users_default: 0, - events: { - 'm.room.name': 50, - 'm.room.power_levels': 100, - 'm.room.history_visibility': 100, - 'm.room.canonical_alias': 50, - 'm.room.avatar': 50, - 'm.room.tombstone': 100, - 'm.room.server_acl': 100, - 'm.room.encryption': 100, - }, - events_default: 0, - state_default: 50, - ban: 50, - kick: 50, - redact: 50, - invite: 0, - // historical: 100, + const powerLevelsEvent = await this.stateService.buildEvent<'m.room.power_levels'>( + { + type: 'm.room.power_levels', + auth_events: [], + prev_events: [], + content: { + users: { + [creatorUserId]: 100, + ...(members.length === 1 ? { [members[0]]: 100 } : {}), // 1:1 DM both get 100 power level }, - room_id: roomCreateEvent.roomId, - state_key: '', - depth: 3, - origin_server_ts: Date.now(), - sender: creatorUserId, + users_default: 0, + events: { + 'm.room.name': 50, + 'm.room.power_levels': 100, + 'm.room.history_visibility': 100, + 'm.room.canonical_alias': 50, + 'm.room.avatar': 50, + 'm.room.tombstone': 100, + 'm.room.server_acl': 100, + 'm.room.encryption': 100, + }, + events_default: 0, + state_default: 50, + ban: 50, + kick: 50, + redact: 50, + invite: 0, + // historical: 100, }, - roomCreateEvent.version, - ); + room_id: roomCreateEvent.roomId, + state_key: '', + depth: 3, + origin_server_ts: Date.now(), + sender: creatorUserId, + }, + roomCreateEvent.version, + ); await this.stateService.handlePdu(powerLevelsEvent); - const joinRulesEvent = - await this.stateService.buildEvent<'m.room.join_rules'>( - { - type: 'm.room.join_rules', - auth_events: [], - prev_events: [], - content: { join_rule: 'invite' }, - room_id: roomCreateEvent.roomId, - state_key: '', - depth: 4, - origin_server_ts: Date.now(), - sender: creatorUserId, - }, - roomCreateEvent.version, - ); + const joinRulesEvent = await this.stateService.buildEvent<'m.room.join_rules'>( + { + type: 'm.room.join_rules', + auth_events: [], + prev_events: [], + content: { join_rule: 'invite' }, + room_id: roomCreateEvent.roomId, + state_key: '', + depth: 4, + origin_server_ts: Date.now(), + sender: creatorUserId, + }, + roomCreateEvent.version, + ); await this.stateService.handlePdu(joinRulesEvent); - const historyVisibilityEvent = - await this.stateService.buildEvent<'m.room.history_visibility'>( - { - type: 'm.room.history_visibility', - content: { history_visibility: 'shared' }, - room_id: roomCreateEvent.roomId, - state_key: '', - auth_events: [], - depth: 5, - prev_events: [], - origin_server_ts: Date.now(), - sender: creatorUserId, - }, - roomCreateEvent.version, - ); + const historyVisibilityEvent = await this.stateService.buildEvent<'m.room.history_visibility'>( + { + type: 'm.room.history_visibility', + content: { history_visibility: 'shared' }, + room_id: roomCreateEvent.roomId, + state_key: '', + auth_events: [], + depth: 5, + prev_events: [], + origin_server_ts: Date.now(), + sender: creatorUserId, + }, + roomCreateEvent.version, + ); await this.stateService.handlePdu(historyVisibilityEvent); - const guestAccessEvent = - await this.stateService.buildEvent<'m.room.guest_access'>( + const guestAccessEvent = await this.stateService.buildEvent<'m.room.guest_access'>( + { + type: 'm.room.guest_access', + content: { guest_access: 'forbidden' }, // synapse uses 'can_join' for DMs + room_id: roomCreateEvent.roomId, + state_key: '', + auth_events: [], + depth: 6, + prev_events: [], + origin_server_ts: Date.now(), + sender: creatorUserId, + }, + roomCreateEvent.version, + ); + + await this.stateService.handlePdu(guestAccessEvent); + + let memberDepthCounter = 7; + + for await (const member of members) { + const targetMembershipEvent = await this.stateService.buildEvent<'m.room.member'>( { - type: 'm.room.guest_access', - content: { guest_access: 'forbidden' }, // synapse uses 'can_join' for DMs - room_id: roomCreateEvent.roomId, - state_key: '', + type: 'm.room.member', auth_events: [], - depth: 6, prev_events: [], + content: { + membership: 'invite', + displayname: member.split(':').shift()?.slice(1), + ...(members.length === 1 && { is_direct: true }), // synapse don't send is_direct on invites for group DMs + }, + room_id: roomCreateEvent.roomId, + state_key: member, + depth: memberDepthCounter++, origin_server_ts: Date.now(), sender: creatorUserId, }, roomCreateEvent.version, ); - await this.stateService.handlePdu(guestAccessEvent); - - let memberDepthCounter = 7; - - for await (const member of members) { - const targetMembershipEvent = - await this.stateService.buildEvent<'m.room.member'>( - { - type: 'm.room.member', - auth_events: [], - prev_events: [], - content: { - membership: 'invite', - displayname: member.split(':').shift()?.slice(1), - ...(members.length === 1 && { is_direct: true }), // synapse don't send is_direct on invites for group DMs - }, - room_id: roomCreateEvent.roomId, - state_key: member, - depth: memberDepthCounter++, - origin_server_ts: Date.now(), - sender: creatorUserId, - }, - roomCreateEvent.version, - ); - const targetServerName = extractDomainFromId(member); if (targetServerName !== this.configService.serverName) { // TODO this may not be the best place to do this validation - await this.federationValidationService.validateOutboundInvite( - member, - roomCreateEvent.roomId, - ); + await this.federationValidationService.validateOutboundInvite(member, roomCreateEvent.roomId); // get signed invite event - const inviteResponse = await this.federationService.inviteUser( - targetMembershipEvent, - roomCreateEvent.version, - ); + const inviteResponse = await this.federationService.inviteUser(targetMembershipEvent, roomCreateEvent.version); // try to save // can only invite if already part of the room - await this.stateService.handlePdu( - PersistentEventFactory.createFromRawEvent( - inviteResponse.event, - roomCreateEvent.version, - ), - ); + await this.stateService.handlePdu(PersistentEventFactory.createFromRawEvent(inviteResponse.event, roomCreateEvent.version)); // void this.federationService.sendEventToAllServersInRoom( // targetMembershipEvent, @@ -1692,18 +1333,10 @@ export class RoomService { /** * @deprecated Use createDirectMessage instead */ - async createDirectMessageRoom( - creatorUserId: UserID, - targetUserId: UserID, - ): Promise { - logger.debug( - `Creating direct message room between ${creatorUserId} and ${targetUserId}`, - ); + async createDirectMessageRoom(creatorUserId: UserID, targetUserId: UserID): Promise { + logger.debug(`Creating direct message room between ${creatorUserId} and ${targetUserId}`); - const existingRoomId = await this.findExistingDirectMessageRoom( - creatorUserId, - targetUserId, - ); + const existingRoomId = await this.findExistingDirectMessageRoom(creatorUserId, targetUserId); if (existingRoomId) { logger.debug(`Found existing DM room ${existingRoomId} between users`); return existingRoomId; @@ -1713,12 +1346,9 @@ export class RoomService { const localServerName = this.configService.serverName; const isExternalUser = targetServerName !== localServerName; - const stateService = this.stateService; + const { stateService } = this; - const roomCreateEvent = PersistentEventFactory.newCreateEvent( - creatorUserId, - PersistentEventFactory.defaultRoomVersion, - ); + const roomCreateEvent = PersistentEventFactory.newCreateEvent(creatorUserId, PersistentEventFactory.defaultRoomVersion); await stateService.signEvent(roomCreateEvent); @@ -1727,56 +1357,54 @@ export class RoomService { // Extract displayname from userId for direct messages const creatorDisplayname = creatorUserId.split(':').shift()?.slice(1); - const creatorMembershipEvent = - await stateService.buildEvent<'m.room.member'>( - { - type: 'm.room.member', - content: { - membership: 'join', - is_direct: true, - displayname: creatorDisplayname, - }, - room_id: roomCreateEvent.roomId, - state_key: creatorUserId, - auth_events: [], - depth: 0, - prev_events: [], - origin_server_ts: Date.now(), - sender: creatorUserId, + const creatorMembershipEvent = await stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + content: { + membership: 'join', + is_direct: true, + displayname: creatorDisplayname, }, - PersistentEventFactory.defaultRoomVersion, - ); + room_id: roomCreateEvent.roomId, + state_key: creatorUserId, + auth_events: [], + depth: 0, + prev_events: [], + origin_server_ts: Date.now(), + sender: creatorUserId, + }, + PersistentEventFactory.defaultRoomVersion, + ); await stateService.handlePdu(creatorMembershipEvent); - const powerLevelsEvent = - await stateService.buildEvent<'m.room.power_levels'>( - { - type: 'm.room.power_levels', - content: { - users: { - [creatorUserId]: 50, - [targetUserId]: 50, - }, - users_default: 0, - events: {}, - events_default: 0, - state_default: 50, - ban: 50, - kick: 50, - redact: 50, - invite: 50, + const powerLevelsEvent = await stateService.buildEvent<'m.room.power_levels'>( + { + type: 'm.room.power_levels', + content: { + users: { + [creatorUserId]: 50, + [targetUserId]: 50, }, - room_id: roomCreateEvent.roomId, - state_key: '', - auth_events: [], - depth: 0, - prev_events: [], - origin_server_ts: Date.now(), - sender: creatorUserId, + users_default: 0, + events: {}, + events_default: 0, + state_default: 50, + ban: 50, + kick: 50, + redact: 50, + invite: 50, }, - PersistentEventFactory.defaultRoomVersion, - ); + room_id: roomCreateEvent.roomId, + state_key: '', + auth_events: [], + depth: 0, + prev_events: [], + origin_server_ts: Date.now(), + sender: creatorUserId, + }, + PersistentEventFactory.defaultRoomVersion, + ); await stateService.handlePdu(powerLevelsEvent); @@ -1797,47 +1425,42 @@ export class RoomService { await stateService.handlePdu(joinRulesEvent); - const historyVisibilityEvent = - await stateService.buildEvent<'m.room.history_visibility'>( - { - type: 'm.room.history_visibility', - content: { history_visibility: 'shared' }, - room_id: roomCreateEvent.roomId, - state_key: '', - auth_events: [], - depth: 0, - prev_events: [], - origin_server_ts: Date.now(), - sender: creatorUserId, - }, - PersistentEventFactory.defaultRoomVersion, - ); + const historyVisibilityEvent = await stateService.buildEvent<'m.room.history_visibility'>( + { + type: 'm.room.history_visibility', + content: { history_visibility: 'shared' }, + room_id: roomCreateEvent.roomId, + state_key: '', + auth_events: [], + depth: 0, + prev_events: [], + origin_server_ts: Date.now(), + sender: creatorUserId, + }, + PersistentEventFactory.defaultRoomVersion, + ); await stateService.handlePdu(historyVisibilityEvent); - const guestAccessEvent = - await stateService.buildEvent<'m.room.guest_access'>( - { - type: 'm.room.guest_access', - content: { guest_access: 'forbidden' }, - room_id: roomCreateEvent.roomId, - state_key: '', - auth_events: [], - depth: 0, - prev_events: [], - origin_server_ts: Date.now(), - sender: creatorUserId, - }, - PersistentEventFactory.defaultRoomVersion, - ); + const guestAccessEvent = await stateService.buildEvent<'m.room.guest_access'>( + { + type: 'm.room.guest_access', + content: { guest_access: 'forbidden' }, + room_id: roomCreateEvent.roomId, + state_key: '', + auth_events: [], + depth: 0, + prev_events: [], + origin_server_ts: Date.now(), + sender: creatorUserId, + }, + PersistentEventFactory.defaultRoomVersion, + ); await stateService.handlePdu(guestAccessEvent); if (isExternalUser) { - await this.federationValidationService.validateOutboundInvite( - targetUserId, - roomCreateEvent.roomId, - ); + await this.federationValidationService.validateOutboundInvite(targetUserId, roomCreateEvent.roomId); await this.inviteService.inviteUserToRoom( targetUserId, @@ -1849,25 +1472,24 @@ export class RoomService { // Extract displayname from userId for direct messages const displayname = targetUserId.split(':').shift()?.slice(1); - const targetMembershipEvent = - await stateService.buildEvent<'m.room.member'>( - { - type: 'm.room.member', - content: { - membership: 'join', - is_direct: true, - displayname: displayname, - }, - room_id: roomCreateEvent.roomId, - state_key: targetUserId, - auth_events: [], - depth: 0, - prev_events: [], - origin_server_ts: Date.now(), - sender: creatorUserId, + const targetMembershipEvent = await stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + content: { + membership: 'join', + is_direct: true, + displayname, }, - PersistentEventFactory.defaultRoomVersion, - ); + room_id: roomCreateEvent.roomId, + state_key: targetUserId, + auth_events: [], + depth: 0, + prev_events: [], + origin_server_ts: Date.now(), + sender: creatorUserId, + }, + PersistentEventFactory.defaultRoomVersion, + ); await stateService.handlePdu(targetMembershipEvent); } @@ -1875,14 +1497,9 @@ export class RoomService { return roomCreateEvent.roomId; } - private async findExistingDirectMessageRoom( - userId1: UserID, - userId2: UserID, - ): Promise { + private async findExistingDirectMessageRoom(userId1: UserID, userId2: UserID): Promise { try { - const membershipEvents = await this.eventRepository - .findMembershipEventsFromDirectMessageRooms([userId1, userId2]) - .toArray(); + const membershipEvents = await this.eventRepository.findMembershipEventsFromDirectMessageRooms([userId1, userId2]).toArray(); const roomMemberCounts = new Map>(); @@ -1901,25 +1518,12 @@ export class RoomService { } } - for (const [roomId, members] of roomMemberCounts) { - if ( - members.size === 2 && - members.has(userId1) && - members.has(userId2) - ) { - const currentMembers = - await this.eventRepository.findAllJoinedMembersEventsByRoomId( - roomId, - ); - const currentUserIds = currentMembers - .map((m) => m.event.state_key) - .filter(Boolean); - - if ( - currentUserIds.length === 2 && - currentUserIds.includes(userId1) && - currentUserIds.includes(userId2) - ) { + for await (const [roomId, members] of roomMemberCounts) { + if (members.size === 2 && members.has(userId1) && members.has(userId2)) { + const currentMembers = await this.eventRepository.findAllJoinedMembersEventsByRoomId(roomId); + const currentUserIds = currentMembers.map((m) => m.event.state_key).filter(Boolean); + + if (currentUserIds.length === 2 && currentUserIds.includes(userId1) && currentUserIds.includes(userId2)) { return roomId; } } diff --git a/packages/federation-sdk/src/services/send-join.service.ts b/packages/federation-sdk/src/services/send-join.service.ts index c739a92ae..05b9f36d1 100644 --- a/packages/federation-sdk/src/services/send-join.service.ts +++ b/packages/federation-sdk/src/services/send-join.service.ts @@ -1,15 +1,12 @@ -import { - type EventID, - PduForType, - RoomID, - getAuthChain, -} from '@rocket.chat/federation-room'; +import { getAuthChain } from '@rocket.chat/federation-room'; +import type { PduForType, RoomID, EventID } from '@rocket.chat/federation-room'; import { singleton } from 'tsyringe'; -import { ConfigService } from './config.service'; -import { EventEmitterService } from './event-emitter.service'; -import { EventService } from './event.service'; -import { FederationService } from './federation.service'; -import { StateService } from './state.service'; + +import type { ConfigService } from './config.service'; +import type { EventEmitterService } from './event-emitter.service'; +import type { EventService } from './event.service'; +import type { FederationService } from './federation.service'; +import type { StateService } from './state.service'; @singleton() export class SendJoinService { @@ -21,12 +18,8 @@ export class SendJoinService { private readonly federationService: FederationService, ) {} - async sendJoin( - roomId: RoomID, - eventId: EventID, - event: PduForType<'m.room.member'>, - ) { - const stateService = this.stateService; + async sendJoin(roomId: RoomID, eventId: EventID, event: PduForType<'m.room.member'>) { + const { stateService } = this; const roomVersion = await stateService.getRoomVersion(roomId); @@ -34,10 +27,7 @@ export class SendJoinService { throw new Error('Room version not found'); } - const joinEvent = await this.stateService.buildEvent<'m.room.member'>( - event, - roomVersion, - ); + const joinEvent = await this.stateService.buildEvent<'m.room.member'>(event, roomVersion); // now check the calculated id if it matches what is passed in param if (joinEvent.eventId !== eventId) { @@ -53,17 +43,14 @@ export class SendJoinService { // accepted? allow other servers to start processing already void this.federationService.sendEventToAllServersInRoom(joinEvent); - const configService = this.configService; + const { configService } = this; const origin = configService.serverName; const authChain = []; - for (const event of state.values()) { - const authEvents = await getAuthChain( - event, - stateService._getStore(roomVersion), - ); + for await (const event of state.values()) { + const authEvents = await getAuthChain(event, stateService._getStore(roomVersion)); authChain.push(...authEvents); } diff --git a/packages/federation-sdk/src/services/server.service.ts b/packages/federation-sdk/src/services/server.service.ts index 83f91ba86..56d9c4a30 100644 --- a/packages/federation-sdk/src/services/server.service.ts +++ b/packages/federation-sdk/src/services/server.service.ts @@ -1,12 +1,8 @@ -import { - type SigningKey, - getPublicKeyFromRemoteServer, - signJson, - toUnpaddedBase64, -} from '@rocket.chat/federation-core'; +import { type SigningKey, getPublicKeyFromRemoteServer, signJson, toUnpaddedBase64 } from '@rocket.chat/federation-core'; import { delay, inject, singleton } from 'tsyringe'; + +import type { ConfigService } from './config.service'; import { ServerRepository } from '../repositories/server.repository'; -import { ConfigService } from './config.service'; @singleton() export class ServerService { @@ -16,19 +12,11 @@ export class ServerService { private configService: ConfigService, ) {} - async getValidPublicKeyFromLocal( - origin: string, - key: string, - ): Promise { - return await this.serverRepository.getValidPublicKeyFromLocal(origin, key); + async getValidPublicKeyFromLocal(origin: string, key: string): Promise { + return this.serverRepository.getValidPublicKeyFromLocal(origin, key); } - async storePublicKey( - origin: string, - key: string, - value: string, - validUntil: number, - ): Promise { + async storePublicKey(origin: string, key: string, value: string, validUntil: number): Promise { await this.serverRepository.storePublicKey(origin, key, value, validUntil); } @@ -37,18 +25,12 @@ export class ServerService { return this.configService.getPublicSigningKeyBase64(); } - const localPublicKey = - await this.serverRepository.getValidPublicKeyFromLocal(origin, key); + const localPublicKey = await this.serverRepository.getValidPublicKeyFromLocal(origin, key); if (localPublicKey) { return localPublicKey; } - const { key: remotePublicKey, validUntil } = - await getPublicKeyFromRemoteServer( - origin, - this.configService.serverName, - key, - ); + const { key: remotePublicKey, validUntil } = await getPublicKeyFromRemoteServer(origin, this.configService.serverName, key); if (!remotePublicKey) { throw new Error('Could not get public key from remote server'); @@ -79,12 +61,8 @@ export class ServerService { }; let signedResponse = baseResponse; - for (const key of signingKeys) { - signedResponse = await signJson( - signedResponse, - key, - this.configService.serverName, - ); + for await (const key of signingKeys) { + signedResponse = await signJson(signedResponse, key, this.configService.serverName); } return signedResponse; diff --git a/packages/federation-sdk/src/services/staging-area.service.ts b/packages/federation-sdk/src/services/staging-area.service.ts index 841976f12..61ed6b07c 100644 --- a/packages/federation-sdk/src/services/staging-area.service.ts +++ b/packages/federation-sdk/src/services/staging-area.service.ts @@ -1,30 +1,18 @@ -import type { - EventBase, - EventStagingStore, - Membership, -} from '@rocket.chat/federation-core'; -import { singleton } from 'tsyringe'; - -import { - MessageType, - createLogger, - isRedactedEvent, -} from '@rocket.chat/federation-core'; -import { - PduPowerLevelsEventContent, - PersistentEventFactory, - RoomState, -} from '@rocket.chat/federation-room'; +import type { EventBase, EventStagingStore, Membership } from '@rocket.chat/federation-core'; +import { MessageType, createLogger, isRedactedEvent } from '@rocket.chat/federation-core'; +import { PduPowerLevelsEventContent, PersistentEventFactory, RoomState } from '@rocket.chat/federation-room'; import type { Pdu, RoomID, RoomVersion } from '@rocket.chat/federation-room'; -import { EventAuthorizationService } from './event-authorization.service'; -import { EventEmitterService } from './event-emitter.service'; -import { EventService } from './event.service'; +import { singleton } from 'tsyringe'; -import { LockRepository } from '../repositories/lock.repository'; -import { ConfigService } from './config.service'; -import { FederationService } from './federation.service'; -import { MissingEventService } from './missing-event.service'; -import { PartialStateResolutionError, StateService } from './state.service'; +import type { ConfigService } from './config.service'; +import type { EventAuthorizationService } from './event-authorization.service'; +import type { EventEmitterService } from './event-emitter.service'; +import type { EventService } from './event.service'; +import type { FederationService } from './federation.service'; +import type { MissingEventService } from './missing-event.service'; +import type { StateService } from './state.service'; +import { PartialStateResolutionError } from './state.service'; +import type { LockRepository } from '../repositories/lock.repository'; const MAX_EVENT_RETRY = ((maxRetry?: string) => { @@ -76,9 +64,7 @@ export class StagingAreaService { async processEventForRoom(roomId: RoomID) { const roomIdToRoomVersion = new Map(); const getRoomVersion = async (roomId: RoomID) => { - const version = - roomIdToRoomVersion.get(roomId) ?? - (await this.stateService.getRoomVersion(roomId)); + const version = roomIdToRoomVersion.get(roomId) ?? (await this.stateService.getRoomVersion(roomId)); roomIdToRoomVersion.set(roomId, version); return version; }; @@ -91,6 +77,7 @@ export class StagingAreaService { let event: EventStagingStore | null = null; do { + // eslint-disable-next-line no-await-in-loop event = await this.eventService.getLeastDepthEventForRoom(roomId); if (!event) { this.logger.debug({ msg: 'No staged event found for room', roomId }); @@ -98,9 +85,8 @@ export class StagingAreaService { } if (event.got > MAX_EVENT_RETRY) { - this.logger.warn( - `Event ${event._id} has been tried ${MAX_EVENT_RETRY} times, removing from staging area`, - ); + this.logger.warn(`Event ${event._id} has been tried ${MAX_EVENT_RETRY} times, removing from staging area`); + // eslint-disable-next-line no-await-in-loop await this.eventService.markEventAsUnstaged(event); continue; } @@ -109,12 +95,11 @@ export class StagingAreaService { // if we got an event, we need to update the lock's timestamp to avoid it being timed out // and acquired by another instance while we're processing a batch of events for this room - await this.lockRepository.updateLockTimestamp( - roomId, - this.configService.instanceId, - ); + // eslint-disable-next-line no-await-in-loop + await this.lockRepository.updateLockTimestamp(roomId, this.configService.instanceId); try { + // eslint-disable-next-line no-await-in-loop const addedMissing = await this.processDependencyStage(event); if (addedMissing) { // if we added missing events, we postpone the processing of this event @@ -122,11 +107,14 @@ export class StagingAreaService { throw new MissingEventsError('Added missing events'); } + // eslint-disable-next-line no-await-in-loop await this.stateService.handlePdu(await toEventBase(event.event)); + // eslint-disable-next-line no-await-in-loop await this.eventService.notify({ eventId: event._id, event: event.event, }); + // eslint-disable-next-line no-await-in-loop await this.eventService.markEventAsUnstaged(event); // TODO add missing logic from synapse: Prune the event queue if it's getting large. @@ -159,45 +147,30 @@ export class StagingAreaService { } while (event); // release the lock after processing - await this.lockRepository.releaseLock( - roomId, - this.configService.instanceId, - ); + await this.lockRepository.releaseLock(roomId, this.configService.instanceId); } private async processDependencyStage(event: EventStagingStore) { const eventId = event._id; - const [authEvents, prevEvents] = this.extractEventsFromIncomingPDU( - event.event, - ); + const [authEvents, prevEvents] = this.extractEventsFromIncomingPDU(event.event); const eventIds = [...authEvents, ...prevEvents]; - this.logger.debug( - `Checking dependencies for event ${eventId}: ${eventIds.length} references`, - ); + this.logger.debug(`Checking dependencies for event ${eventId}: ${eventIds.length} references`); - const { missing } = await this.eventService.checkIfEventsExists( - eventIds.flat(), - ); + const { missing } = await this.eventService.checkIfEventsExists(eventIds.flat()); if (missing.length === 0) { return false; } - this.logger.debug( - `Missing ${missing.length} events for ${eventId}: ${missing}`, - ); + this.logger.debug(`Missing ${missing.length} events for ${eventId}: ${missing}`); - const latestEvent = await this.eventService.getLastEventForRoom( - event.event.room_id, - ); + const latestEvent = await this.eventService.getLastEventForRoom(event.event.room_id); let addedMissing = false; if (latestEvent) { - this.logger.debug( - `Fetching missing events between ${latestEvent._id} and ${eventId} for room ${event.event.room_id}`, - ); + this.logger.debug(`Fetching missing events between ${latestEvent._id} and ${eventId} for room ${event.event.room_id}`); const missingEvents = await this.federationService.getMissingEvents( event.origin, @@ -208,22 +181,15 @@ export class StagingAreaService { 0, ); - this.logger.debug( - `Persisting ${missingEvents.events.length} fetched missing events`, - ); + this.logger.debug(`Persisting ${missingEvents.events.length} fetched missing events`); - await this.eventService.processIncomingPDUs( - event.origin, - missingEvents.events, - ); + await this.eventService.processIncomingPDUs(event.origin, missingEvents.events); addedMissing = missingEvents.events.length > 0; } else { const found = await Promise.all( missing.map((missingId) => { - this.logger.debug( - `Adding missing event ${missingId} to missing events service`, - ); + this.logger.debug(`Adding missing event ${missingId} to missing events service`); return this.missingEventsService.fetchMissingEvent({ eventId: missingId, diff --git a/packages/federation-sdk/src/services/state.service.spec.ts b/packages/federation-sdk/src/services/state.service.spec.ts index d3d8b3326..f74fbf7f3 100644 --- a/packages/federation-sdk/src/services/state.service.spec.ts +++ b/packages/federation-sdk/src/services/state.service.spec.ts @@ -1,28 +1,27 @@ import { beforeEach, describe, expect, it, spyOn, test } from 'bun:test'; + import { type EventStore } from '@rocket.chat/federation-core'; -import type { State } from '@rocket.chat/federation-room'; -import * as room from '@rocket.chat/federation-room'; -import { +import type { + State, EventID, PduCreateEventContent, PduJoinRuleEventContent, PduPowerLevelsEventContent, PduRoomNameEventContent, PersistentEventBase, - PersistentEventFactory, - RejectCodes, RoomVersion, } from '@rocket.chat/federation-room'; +import * as room from '@rocket.chat/federation-room'; +import { PersistentEventFactory, RejectCodes } from '@rocket.chat/federation-room'; import { type WithId } from 'mongodb'; -import { EventRepository } from '../repositories/event.repository'; -import { - StateGraphRepository, - StateGraphStore, -} from '../repositories/state-graph.repository'; + import { type ConfigService } from './config.service'; import { DatabaseConnectionService } from './database-connection.service'; -import { EventService } from './event.service'; +import type { EventService } from './event.service'; import { StateService } from './state.service'; +import { EventRepository } from '../repositories/event.repository'; +import { StateGraphRepository } from '../repositories/state-graph.repository'; +import type { StateGraphStore } from '../repositories/state-graph.repository'; function getDefaultFields() { return { @@ -33,10 +32,7 @@ function getDefaultFields() { }; } -function copyEventAndTransform( - e: T, - fn: (obj: PersistentEventBase['event']) => typeof obj, -): T { +function copyEventAndTransform(e: T, fn: (obj: PersistentEventBase['event']) => typeof obj): T { const { event } = e; return PersistentEventFactory.createFromRawEvent(fn(event), e.version) as T; @@ -63,31 +59,22 @@ function stripPreviousAndAuthEvents(e: T): T { function compareStates(state1: State, state2: typeof state1) { // convert to an object with eventy.eventId - const s1 = Object.entries(Object.fromEntries(state1.entries())).reduce( - (acc, [key, event]) => { - acc[key] = event.eventId; - return acc; - }, - {} as Record, - ); + const s1 = Object.entries(Object.fromEntries(state1.entries())).reduce((acc, [key, event]) => { + acc[key] = event.eventId; + return acc; + }, {} as Record); - const s2 = Object.entries(Object.fromEntries(state2.entries())).reduce( - (acc, [key, event]) => { - acc[key] = event.eventId; - return acc; - }, - {} as Record, - ); + const s2 = Object.entries(Object.fromEntries(state2.entries())).reduce((acc, [key, event]) => { + acc[key] = event.eventId; + return acc; + }, {} as Record); expect(s1).toEqual(s2); } let stateService: StateService; -async function copyDepth< - F extends PersistentEventBase, - T extends PersistentEventBase, ->(from: F, to: T): Promise { +async function copyDepth(from: F, to: T): Promise { const store = stateService._getStore(from.version); const previousEvents = await store.getEvents(from.getPreviousEventIds()); @@ -117,44 +104,34 @@ describe('StateService', async () => { } const databaseConfig = { - uri: - process.env.MONGO_URI || - 'mongodb://localhost:27017?directConnection=true', + uri: process.env.MONGO_URI || 'mongodb://localhost:27017?directConnection=true', name: 'matrix_test', poolSize: 100, }; const configServiceInstance = { - getSigningKey: async () => {}, + getSigningKey: async () => { + /* noop */ + }, serverName: 'example.com', } as unknown as ConfigService; const database = new DatabaseConnectionService(databaseConfig); - const eventCollection = (await database.getDb()).collection< - WithId - >('events_test'); - const stateGraphCollection = ( - await database.getDb() - ).collection('state_graph_test'); + const eventCollection = (await database.getDb()).collection>('events_test'); + const stateGraphCollection = (await database.getDb()).collection('state_graph_test'); beforeEach(async () => { - await Promise.all([ - eventCollection.deleteMany({}), - stateGraphCollection.deleteMany({}), - ]); + await Promise.all([eventCollection.deleteMany({}), stateGraphCollection.deleteMany({})]); }); const eventRepository = new EventRepository(eventCollection); const stateGraphRepository = new StateGraphRepository(stateGraphCollection); // TODO: use IStateService - stateService = new StateService( - stateGraphRepository, - eventRepository, - configServiceInstance, - { notify: () => Promise.resolve() } as unknown as EventService, - ); + stateService = new StateService(stateGraphRepository, eventRepository, configServiceInstance, { + notify: () => Promise.resolve(), + } as unknown as EventService); const createRoom = async ( joinRule: PduJoinRuleEventContent['join_rule'], @@ -164,27 +141,22 @@ describe('StateService', async () => { const username = '@alice:example.com'; const name = 'Test Room'; - const roomCreateEvent = PersistentEventFactory.newCreateEvent( - username as room.UserID, - PersistentEventFactory.defaultRoomVersion, - ); + const roomCreateEvent = PersistentEventFactory.newCreateEvent(username as room.UserID, PersistentEventFactory.defaultRoomVersion); await stateService.handlePdu(roomCreateEvent); - const roomVersion: RoomVersion = - roomCreateEvent.getContent().room_version; + const roomVersion: RoomVersion = roomCreateEvent.getContent().room_version; - const creatorMembershipEvent = - await stateService.buildEvent<'m.room.member'>( - { - type: 'm.room.member', - room_id: roomCreateEvent.roomId, - sender: username as room.UserID, - state_key: username as room.UserID, - content: { membership: 'join' }, - ...getDefaultFields(), - }, - roomVersion, - ); + const creatorMembershipEvent = await stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + room_id: roomCreateEvent.roomId, + sender: username as room.UserID, + state_key: username as room.UserID, + content: { membership: 'join' }, + ...getDefaultFields(), + }, + roomVersion, + ); await stateService.handlePdu(creatorMembershipEvent); @@ -202,33 +174,32 @@ describe('StateService', async () => { await stateService.handlePdu(roomNameEvent); - const powerLevelEvent = - await stateService.buildEvent<'m.room.power_levels'>( - { - type: 'm.room.power_levels', - room_id: roomCreateEvent.roomId, - sender: username as room.UserID, - state_key: '', - content: { - users: { - [username]: 100, - ...userPowers, - }, - users_default: 0, - events: { - ...eventsPowers, - }, - events_default: 0, - state_default: 50, - ban: 50, - kick: 50, - redact: 50, - invite: 50, + const powerLevelEvent = await stateService.buildEvent<'m.room.power_levels'>( + { + type: 'm.room.power_levels', + room_id: roomCreateEvent.roomId, + sender: username as room.UserID, + state_key: '', + content: { + users: { + [username]: 100, + ...userPowers, }, - ...getDefaultFields(), + users_default: 0, + events: { + ...eventsPowers, + }, + events_default: 0, + state_default: 50, + ban: 50, + kick: 50, + redact: 50, + invite: 50, }, - roomVersion, - ); + ...getDefaultFields(), + }, + roomVersion, + ); await stateService.handlePdu(powerLevelEvent); @@ -255,9 +226,7 @@ describe('StateService', async () => { }; }; - const getStore = ( - cache: Map, - ): room.EventStore => ({ + const getStore = (cache: Map): room.EventStore => ({ getEvents: (eventIds: EventID[]) => { return Promise.resolve(eventIds.map((eid) => cache.get(eid)!)); }, @@ -268,28 +237,24 @@ describe('StateService', async () => { const username = '@alice:anotherserver.com' as room.UserID; const name = 'Test Partial State Room'; - const roomCreateEvent = PersistentEventFactory.newCreateEvent( - username as room.UserID, - PersistentEventFactory.defaultRoomVersion, - ); + const roomCreateEvent = PersistentEventFactory.newCreateEvent(username as room.UserID, PersistentEventFactory.defaultRoomVersion); const roomVersion = roomCreateEvent.version; - const creatorMembershipEvent = - await stateService.buildEvent<'m.room.member'>( - { - type: 'm.room.member', - room_id: roomCreateEvent.roomId, - sender: username as room.UserID, - state_key: username as room.UserID, - content: { membership: 'join' }, - ...getDefaultFields(), - prev_events: [roomCreateEvent.eventId], - auth_events: [roomCreateEvent.eventId], - depth: 1, - }, - roomVersion, - ); + const creatorMembershipEvent = await stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + room_id: roomCreateEvent.roomId, + sender: username as room.UserID, + state_key: username as room.UserID, + content: { membership: 'join' }, + ...getDefaultFields(), + prev_events: [roomCreateEvent.eventId], + auth_events: [roomCreateEvent.eventId], + depth: 1, + }, + roomVersion, + ); // insert a random message event to make the tree incomplete const messageEvent = await stateService.buildEvent<'m.room.message'>( @@ -300,10 +265,7 @@ describe('StateService', async () => { type: 'm.room.message', ...getDefaultFields(), prev_events: [creatorMembershipEvent.eventId], - auth_events: [ - creatorMembershipEvent.eventId, - roomCreateEvent.eventId, - ], + auth_events: [creatorMembershipEvent.eventId, roomCreateEvent.eventId], depth: 2, }, roomVersion, @@ -318,10 +280,7 @@ describe('StateService', async () => { type: 'm.room.name', ...getDefaultFields(), prev_events: [messageEvent.eventId], - auth_events: [ - creatorMembershipEvent.eventId, - roomCreateEvent.eventId, - ], + auth_events: [creatorMembershipEvent.eventId, roomCreateEvent.eventId], depth: 3, }, roomVersion, @@ -336,46 +295,38 @@ describe('StateService', async () => { state_key: '', ...getDefaultFields(), prev_events: [roomNameEvent.eventId], - auth_events: [ - roomCreateEvent.eventId, - creatorMembershipEvent.eventId, - ], + auth_events: [roomCreateEvent.eventId, creatorMembershipEvent.eventId], depth: 4, }, roomVersion, ); - const powerLevelEvent = - await stateService.buildEvent<'m.room.power_levels'>( - { - type: 'm.room.power_levels', - room_id: roomCreateEvent.roomId, - sender: username as room.UserID, - state_key: '', - content: { - users: { - [username]: 100, - }, - users_default: 0, - events: {}, - events_default: 0, - state_default: 50, - ban: 50, - kick: 50, - redact: 50, - invite: 50, + const powerLevelEvent = await stateService.buildEvent<'m.room.power_levels'>( + { + type: 'm.room.power_levels', + room_id: roomCreateEvent.roomId, + sender: username as room.UserID, + state_key: '', + content: { + users: { + [username]: 100, }, - ...getDefaultFields(), - prev_events: [joinRuleEvent.eventId], - auth_events: [ - roomCreateEvent.eventId, - creatorMembershipEvent.eventId, - joinRuleEvent.eventId, - ], - depth: 5, + users_default: 0, + events: {}, + events_default: 0, + state_default: 50, + ban: 50, + kick: 50, + redact: 50, + invite: 50, }, - roomVersion, - ); + ...getDefaultFields(), + prev_events: [joinRuleEvent.eventId], + auth_events: [roomCreateEvent.eventId, creatorMembershipEvent.eventId, joinRuleEvent.eventId], + depth: 5, + }, + roomVersion, + ); const ourUserJoinEvent = await stateService.buildEvent<'m.room.member'>( { @@ -386,11 +337,7 @@ describe('StateService', async () => { content: { membership: 'join' }, ...getDefaultFields(), prev_events: [powerLevelEvent.eventId], - auth_events: [ - roomCreateEvent.eventId, - powerLevelEvent.eventId, - joinRuleEvent.eventId, - ], + auth_events: [roomCreateEvent.eventId, powerLevelEvent.eventId, joinRuleEvent.eventId], depth: 6, }, roomVersion, @@ -412,15 +359,13 @@ describe('StateService', async () => { const store = getStore(map); - for (const event of Object.values(state)) { + for await (const event of Object.values(state)) { for (const eventId of await room.getAuthChain(event, store)) { authChainSet.add(eventId); } } - const authChain = Array.from(authChainSet.values()).map( - (eid) => map.get(eid)!, - ); + const authChain = Array.from(authChainSet.values()).map((eid) => map.get(eid)!); return { state, @@ -433,28 +378,24 @@ describe('StateService', async () => { const username = '@alice:anotherserver.com' as room.UserID; const name = 'Test Partial State Room'; - const roomCreateEvent = PersistentEventFactory.newCreateEvent( - username as room.UserID, - PersistentEventFactory.defaultRoomVersion, - ); + const roomCreateEvent = PersistentEventFactory.newCreateEvent(username as room.UserID, PersistentEventFactory.defaultRoomVersion); const roomVersion = roomCreateEvent.version; - const creatorMembershipEvent = - await stateService.buildEvent<'m.room.member'>( - { - type: 'm.room.member', - room_id: roomCreateEvent.roomId, - sender: username as room.UserID, - state_key: username as room.UserID, - content: { membership: 'join' }, - ...getDefaultFields(), - prev_events: [roomCreateEvent.eventId], - auth_events: [roomCreateEvent.eventId], - depth: 1, - }, - roomVersion, - ); + const creatorMembershipEvent = await stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + room_id: roomCreateEvent.roomId, + sender: username as room.UserID, + state_key: username as room.UserID, + content: { membership: 'join' }, + ...getDefaultFields(), + prev_events: [roomCreateEvent.eventId], + auth_events: [roomCreateEvent.eventId], + depth: 1, + }, + roomVersion, + ); // insert a random message event to make the tree incomplete const messageEvent = await stateService.buildEvent<'m.room.message'>( @@ -465,10 +406,7 @@ describe('StateService', async () => { type: 'm.room.message', ...getDefaultFields(), prev_events: [creatorMembershipEvent.eventId], - auth_events: [ - creatorMembershipEvent.eventId, - roomCreateEvent.eventId, - ], + auth_events: [creatorMembershipEvent.eventId, roomCreateEvent.eventId], depth: 2, }, roomVersion, @@ -484,10 +422,7 @@ describe('StateService', async () => { type: 'm.room.name', ...getDefaultFields(), prev_events: [messageEvent.eventId], - auth_events: [ - creatorMembershipEvent.eventId, - roomCreateEvent.eventId, - ], + auth_events: [creatorMembershipEvent.eventId, roomCreateEvent.eventId], depth: 3, }, roomVersion, @@ -502,10 +437,7 @@ describe('StateService', async () => { type: 'm.room.message', ...getDefaultFields(), prev_events: [roomNameEvent.eventId], - auth_events: [ - creatorMembershipEvent.eventId, - roomCreateEvent.eventId, - ], + auth_events: [creatorMembershipEvent.eventId, roomCreateEvent.eventId], depth: 4, }, roomVersion, @@ -520,46 +452,38 @@ describe('StateService', async () => { state_key: '', ...getDefaultFields(), prev_events: [messageEvent2.eventId], - auth_events: [ - roomCreateEvent.eventId, - creatorMembershipEvent.eventId, - ], + auth_events: [roomCreateEvent.eventId, creatorMembershipEvent.eventId], depth: 5, }, roomVersion, ); - const powerLevelEvent = - await stateService.buildEvent<'m.room.power_levels'>( - { - type: 'm.room.power_levels', - room_id: roomCreateEvent.roomId, - sender: username as room.UserID, - state_key: '', - content: { - users: { - [username]: 100, - }, - users_default: 0, - events: {}, - events_default: 0, - state_default: 50, - ban: 50, - kick: 50, - redact: 50, - invite: 50, + const powerLevelEvent = await stateService.buildEvent<'m.room.power_levels'>( + { + type: 'm.room.power_levels', + room_id: roomCreateEvent.roomId, + sender: username as room.UserID, + state_key: '', + content: { + users: { + [username]: 100, }, - ...getDefaultFields(), - prev_events: [joinRuleEvent.eventId], - auth_events: [ - roomCreateEvent.eventId, - creatorMembershipEvent.eventId, - joinRuleEvent.eventId, - ], - depth: 6, + users_default: 0, + events: {}, + events_default: 0, + state_default: 50, + ban: 50, + kick: 50, + redact: 50, + invite: 50, }, - roomVersion, - ); + ...getDefaultFields(), + prev_events: [joinRuleEvent.eventId], + auth_events: [roomCreateEvent.eventId, creatorMembershipEvent.eventId, joinRuleEvent.eventId], + depth: 6, + }, + roomVersion, + ); const ourUserJoinEvent = await stateService.buildEvent<'m.room.member'>( { @@ -570,11 +494,7 @@ describe('StateService', async () => { content: { membership: 'join' }, ...getDefaultFields(), prev_events: [powerLevelEvent.eventId], - auth_events: [ - roomCreateEvent.eventId, - powerLevelEvent.eventId, - joinRuleEvent.eventId, - ], + auth_events: [roomCreateEvent.eventId, powerLevelEvent.eventId, joinRuleEvent.eventId], depth: 7, }, roomVersion, @@ -597,15 +517,13 @@ describe('StateService', async () => { const store = getStore(map); - for (const event of Object.values(state)) { + for await (const event of Object.values(state)) { for (const eventId of await room.getAuthChain(event, store)) { authChainSet.add(eventId); } } - const authChain = Array.from(authChainSet.values()).map( - (eid) => map.get(eid)!, - ); + const authChain = Array.from(authChainSet.values()).map((eid) => map.get(eid)!); return { state, @@ -618,28 +536,24 @@ describe('StateService', async () => { const creator = '@alice:anotherserver.com' as room.UserID; const name = 'Test Partial State Room'; - const roomCreateEvent = PersistentEventFactory.newCreateEvent( - creator as room.UserID, - PersistentEventFactory.defaultRoomVersion, - ); + const roomCreateEvent = PersistentEventFactory.newCreateEvent(creator as room.UserID, PersistentEventFactory.defaultRoomVersion); const roomVersion = roomCreateEvent.version; - const creatorMembershipEvent = - await stateService.buildEvent<'m.room.member'>( - { - type: 'm.room.member', - room_id: roomCreateEvent.roomId, - sender: creator as room.UserID, - state_key: creator as room.UserID, - content: { membership: 'join' }, - ...getDefaultFields(), - prev_events: [roomCreateEvent.eventId], - auth_events: [roomCreateEvent.eventId], - depth: 1, - }, - roomVersion, - ); + const creatorMembershipEvent = await stateService.buildEvent<'m.room.member'>( + { + type: 'm.room.member', + room_id: roomCreateEvent.roomId, + sender: creator as room.UserID, + state_key: creator as room.UserID, + content: { membership: 'join' }, + ...getDefaultFields(), + prev_events: [roomCreateEvent.eventId], + auth_events: [roomCreateEvent.eventId], + depth: 1, + }, + roomVersion, + ); // insert a random message event to make the tree incomplete const messageEvent = await stateService.buildEvent<'m.room.message'>( @@ -650,10 +564,7 @@ describe('StateService', async () => { type: 'm.room.message', ...getDefaultFields(), prev_events: [creatorMembershipEvent.eventId], - auth_events: [ - creatorMembershipEvent.eventId, - roomCreateEvent.eventId, - ], + auth_events: [creatorMembershipEvent.eventId, roomCreateEvent.eventId], depth: 2, }, roomVersion, @@ -669,10 +580,7 @@ describe('StateService', async () => { type: 'm.room.name', ...getDefaultFields(), prev_events: [messageEvent.eventId], - auth_events: [ - creatorMembershipEvent.eventId, - roomCreateEvent.eventId, - ], + auth_events: [creatorMembershipEvent.eventId, roomCreateEvent.eventId], depth: 3, }, roomVersion, @@ -687,10 +595,7 @@ describe('StateService', async () => { type: 'm.room.message', ...getDefaultFields(), prev_events: [roomNameEvent.eventId], - auth_events: [ - creatorMembershipEvent.eventId, - roomCreateEvent.eventId, - ], + auth_events: [creatorMembershipEvent.eventId, roomCreateEvent.eventId], depth: 4, }, roomVersion, @@ -705,46 +610,38 @@ describe('StateService', async () => { state_key: '', ...getDefaultFields(), prev_events: [messageEvent2.eventId], - auth_events: [ - roomCreateEvent.eventId, - creatorMembershipEvent.eventId, - ], + auth_events: [roomCreateEvent.eventId, creatorMembershipEvent.eventId], depth: 5, }, roomVersion, ); - const powerLevelEvent = - await stateService.buildEvent<'m.room.power_levels'>( - { - type: 'm.room.power_levels', - room_id: roomCreateEvent.roomId, - sender: creator as room.UserID, - state_key: '', - content: { - users: { - [creator]: 100, - }, - users_default: 0, - events: {}, - events_default: 0, - state_default: 50, - ban: 50, - kick: 50, - redact: 50, - invite: 50, + const powerLevelEvent = await stateService.buildEvent<'m.room.power_levels'>( + { + type: 'm.room.power_levels', + room_id: roomCreateEvent.roomId, + sender: creator as room.UserID, + state_key: '', + content: { + users: { + [creator]: 100, }, - ...getDefaultFields(), - prev_events: [joinRuleEvent.eventId], - auth_events: [ - roomCreateEvent.eventId, - creatorMembershipEvent.eventId, - joinRuleEvent.eventId, - ], - depth: 6, + users_default: 0, + events: {}, + events_default: 0, + state_default: 50, + ban: 50, + kick: 50, + redact: 50, + invite: 50, }, - roomVersion, - ); + ...getDefaultFields(), + prev_events: [joinRuleEvent.eventId], + auth_events: [roomCreateEvent.eventId, creatorMembershipEvent.eventId, joinRuleEvent.eventId], + depth: 6, + }, + roomVersion, + ); const ourUserInviteEvent = await stateService.buildEvent<'m.room.member'>( { @@ -755,12 +652,7 @@ describe('StateService', async () => { content: { membership: 'invite' }, ...getDefaultFields(), prev_events: [powerLevelEvent.eventId], - auth_events: [ - roomCreateEvent.eventId, - powerLevelEvent.eventId, - joinRuleEvent.eventId, - creatorMembershipEvent.eventId, - ], + auth_events: [roomCreateEvent.eventId, powerLevelEvent.eventId, joinRuleEvent.eventId, creatorMembershipEvent.eventId], depth: 7, }, roomVersion, @@ -775,12 +667,7 @@ describe('StateService', async () => { content: { membership: 'join' }, ...getDefaultFields(), prev_events: [ourUserInviteEvent.eventId], - auth_events: [ - roomCreateEvent.eventId, - powerLevelEvent.eventId, - joinRuleEvent.eventId, - ourUserInviteEvent.eventId, - ], + auth_events: [roomCreateEvent.eventId, powerLevelEvent.eventId, joinRuleEvent.eventId, ourUserInviteEvent.eventId], depth: 8, }, roomVersion, @@ -804,15 +691,13 @@ describe('StateService', async () => { const store = getStore(map); - for (const event of Object.values(state)) { + for await (const event of Object.values(state)) { for (const eventId of await room.getAuthChain(event, store)) { authChainSet.add(eventId); } } - const authChain = Array.from(authChainSet.values()).map( - (eid) => map.get(eid)!, - ); + const authChain = Array.from(authChainSet.values()).map((eid) => map.get(eid)!); return { state, @@ -852,7 +737,7 @@ describe('StateService', async () => { room_id: roomId as room.RoomID, sender: (sender || userId) as room.UserID, state_key: userId as room.UserID, - content: { membership: membership }, + content: { membership }, ...getDefaultFields(), }, roomVersion, @@ -864,39 +749,21 @@ describe('StateService', async () => { }; it('001 should correctly calculate state through linear changes', async () => { - const { - roomCreateEvent, - roomNameEvent, - joinRuleEvent, - powerLevelEvent, - creatorMembershipEvent, - } = await createRoom('public'); + const { roomCreateEvent, roomNameEvent, joinRuleEvent, powerLevelEvent, creatorMembershipEvent } = await createRoom('public'); const stateAtEvent = new Map(); - const roomId = roomCreateEvent.roomId; + const { roomId } = roomCreateEvent; const creator = roomCreateEvent.getContent().creator as room.UserID; const state = await stateService.getLatestRoomState(roomId); // check each event - expect( - state.get(roomCreateEvent.getUniqueStateIdentifier()), - ).toHaveProperty('eventId', roomCreateEvent.eventId); - expect(state.get(roomNameEvent.getUniqueStateIdentifier())).toHaveProperty( - 'eventId', - roomNameEvent.eventId, - ); - expect(state.get(joinRuleEvent.getUniqueStateIdentifier())).toHaveProperty( - 'eventId', - joinRuleEvent.eventId, - ); - expect( - state.get(powerLevelEvent.getUniqueStateIdentifier()), - ).toHaveProperty('eventId', powerLevelEvent.eventId); - expect( - state.get(creatorMembershipEvent.getUniqueStateIdentifier()), - ).toHaveProperty('eventId', creatorMembershipEvent.eventId); + expect(state.get(roomCreateEvent.getUniqueStateIdentifier())).toHaveProperty('eventId', roomCreateEvent.eventId); + expect(state.get(roomNameEvent.getUniqueStateIdentifier())).toHaveProperty('eventId', roomNameEvent.eventId); + expect(state.get(joinRuleEvent.getUniqueStateIdentifier())).toHaveProperty('eventId', joinRuleEvent.eventId); + expect(state.get(powerLevelEvent.getUniqueStateIdentifier())).toHaveProperty('eventId', powerLevelEvent.eventId); + expect(state.get(creatorMembershipEvent.getUniqueStateIdentifier())).toHaveProperty('eventId', creatorMembershipEvent.eventId); expect(state.size).toBe(5); @@ -905,10 +772,7 @@ describe('StateService', async () => { const state2 = await stateService.getLatestRoomState(roomId); expect(state2.size).toBe(6); - expect(state2.get(bobJoinEvent.getUniqueStateIdentifier())).toHaveProperty( - 'eventId', - bobJoinEvent.eventId, - ); + expect(state2.get(bobJoinEvent.getUniqueStateIdentifier())).toHaveProperty('eventId', bobJoinEvent.eventId); stateAtEvent.set(bobJoinEvent.eventId, state2); @@ -916,10 +780,7 @@ describe('StateService', async () => { const state3 = await stateService.getLatestRoomState(roomId); expect(state3.size).toBe(6); // same as before - expect(state3.get(bobLeaveEvent.getUniqueStateIdentifier())).toHaveProperty( - 'eventId', - bobLeaveEvent.eventId, - ); + expect(state3.get(bobLeaveEvent.getUniqueStateIdentifier())).toHaveProperty('eventId', bobLeaveEvent.eventId); stateAtEvent.set(bobLeaveEvent.eventId, state3); @@ -928,16 +789,12 @@ describe('StateService', async () => { const random1JoinEvent = await joinUser(roomId, random1); const state4 = await stateService.getLatestRoomState(roomId); expect(state4.size).toBe(7); - expect( - state4.get(random1JoinEvent.getUniqueStateIdentifier()), - ).toHaveProperty('eventId', random1JoinEvent.eventId); + expect(state4.get(random1JoinEvent.getUniqueStateIdentifier())).toHaveProperty('eventId', random1JoinEvent.eventId); const random2 = '@random2:example.com'; const random2JoinEvent = await joinUser(roomId, random2); const state5 = await stateService.getLatestRoomState(roomId); expect(state5.size).toBe(8); - expect( - state5.get(random2JoinEvent.getUniqueStateIdentifier()), - ).toHaveProperty('eventId', random2JoinEvent.eventId); + expect(state5.get(random2JoinEvent.getUniqueStateIdentifier())).toHaveProperty('eventId', random2JoinEvent.eventId); stateAtEvent.set(random1JoinEvent.eventId, state4); stateAtEvent.set(random2JoinEvent.eventId, state5); @@ -947,8 +804,7 @@ describe('StateService', async () => { const roomNameEvent2 = await stateService.buildEvent<'m.room.name'>( { room_id: roomId, - sender: roomCreateEvent.getContent() - .creator as room.UserID, + sender: roomCreateEvent.getContent().creator as room.UserID, content: { name: newRoomName }, state_key: '', type: 'm.room.name', @@ -959,9 +815,7 @@ describe('StateService', async () => { await stateService.handlePdu(roomNameEvent2); const state6 = await stateService.getLatestRoomState(roomId); expect(state6.size).toBe(8); // same as before, overwriting existing name - expect( - state6.get(roomNameEvent2.getUniqueStateIdentifier()), - ).toHaveProperty('eventId', roomNameEvent2.eventId); + expect(state6.get(roomNameEvent2.getUniqueStateIdentifier())).toHaveProperty('eventId', roomNameEvent2.eventId); stateAtEvent.set(roomNameEvent2.eventId, state6); @@ -970,9 +824,7 @@ describe('StateService', async () => { const banRandom1Event = await banUser(roomId, random3, creator); const state7 = await stateService.getLatestRoomState(roomId); expect(state7.size).toBe(9); - expect( - state7.get(banRandom1Event.getUniqueStateIdentifier()), - ).toHaveProperty('eventId', banRandom1Event.eventId); + expect(state7.get(banRandom1Event.getUniqueStateIdentifier())).toHaveProperty('eventId', banRandom1Event.eventId); stateAtEvent.set(banRandom1Event.eventId, state7); @@ -1018,15 +870,11 @@ describe('StateService', async () => { }); it('01 should return the correct room information for room id', async () => { - expect(stateService.getRoomInformation('abcd')).rejects.toThrowError( - /Create event mapping not found/, - ); + expect(stateService.getRoomInformation('abcd')).rejects.toThrowError(/Create event mapping not found/); const { roomCreateEvent } = await createRoom('public'); - expect( - stateService.getRoomInformation(roomCreateEvent.roomId), - ).resolves.toHaveProperty( + expect(stateService.getRoomInformation(roomCreateEvent.roomId)).resolves.toHaveProperty( 'creator', roomCreateEvent.getContent().creator, ); @@ -1035,14 +883,9 @@ describe('StateService', async () => { it('02 should get the correct room version', async () => { const { roomCreateEvent } = await createRoom('public'); - const roomVersion = await stateService.getRoomVersion( - roomCreateEvent.roomId, - ); + const roomVersion = await stateService.getRoomVersion(roomCreateEvent.roomId); - expect(roomVersion).toBe( - roomCreateEvent.getContent() - .room_version as RoomVersion, - ); + expect(roomVersion).toBe(roomCreateEvent.getContent().room_version as RoomVersion); expect(stateService.getRoomVersion('roomId')).rejects.toThrowError(); }); @@ -1060,11 +903,9 @@ describe('StateService', async () => { // at each event the corresponding user should be in state - for (const event of events) { + for await (const event of events) { const stateAtEvent = await stateService.getStateAtEvent(event); - expect( - stateAtEvent.get(event.getUniqueStateIdentifier())?.getContent(), - ).toHaveProperty('membership', 'join'); + expect(stateAtEvent.get(event.getUniqueStateIdentifier())?.getContent()).toHaveProperty('membership', 'join'); } }); @@ -1080,21 +921,14 @@ describe('StateService', async () => { const joinEvent1 = await joinUser(roomCreateEvent.roomId, newUser); - const state1 = await stateService.getLatestRoomState( - roomCreateEvent.roomId, - ); - expect(state1.get(joinEvent1.getUniqueStateIdentifier())).toHaveProperty( - 'eventId', - joinEvent1.eventId, - ); + const state1 = await stateService.getLatestRoomState(roomCreateEvent.roomId); + expect(state1.get(joinEvent1.getUniqueStateIdentifier())).toHaveProperty('eventId', joinEvent1.eventId); const joinEvent2 = await joinUser(roomCreateEvent.roomId, newUser); expect(joinEvent1.eventId).not.toBe(joinEvent2.eventId); - const state2 = await stateService.getLatestRoomState( - roomCreateEvent.roomId, - ); + const state2 = await stateService.getLatestRoomState(roomCreateEvent.roomId); expect(state2.get(joinEvent2.getUniqueStateIdentifier())).toHaveProperty( 'eventId', joinEvent1.eventId, // same as old eventid @@ -1106,9 +940,7 @@ describe('StateService', async () => { roomCreateEvent: { roomId }, } = await createRoom('public'); expect(roomId).toBeDefined(); - return expect( - stateService.getLatestRoomState2(roomId), - ).resolves.toBeDefined(); + return expect(stateService.getLatestRoomState2(roomId)).resolves.toBeDefined(); }); it('06 should successfully have a user join the room', async () => { @@ -1118,9 +950,7 @@ describe('StateService', async () => { await joinUser(roomCreateEvent.roomId, newUser); - const state = await stateService.getLatestRoomState2( - roomCreateEvent.roomId, - ); + const state = await stateService.getLatestRoomState2(roomCreateEvent.roomId); expect(state.isUserInRoom(newUser)).toBe(true); }); @@ -1132,9 +962,7 @@ describe('StateService', async () => { await leaveUser(roomCreateEvent.roomId, newUser); - const state = await stateService.getLatestRoomState2( - roomCreateEvent.roomId, - ); + const state = await stateService.getLatestRoomState2(roomCreateEvent.roomId); expect(state.getUserMembership(newUser)).toBe('leave'); }); @@ -1163,23 +991,13 @@ describe('StateService', async () => { const { roomCreateEvent } = await createRoom('invite'); const newUser = '@bob:example.com' as room.UserID; - await inviteUser( - roomCreateEvent.roomId, - newUser, - roomCreateEvent.getContent().creator, - ); + await inviteUser(roomCreateEvent.roomId, newUser, roomCreateEvent.getContent().creator); - expect( - ( - await stateService.getLatestRoomState2(roomCreateEvent.roomId) - ).isUserInvited(newUser), - ).toBeTrue(); + expect((await stateService.getLatestRoomState2(roomCreateEvent.roomId)).isUserInvited(newUser)).toBeTrue(); await joinUser(roomCreateEvent.roomId, newUser); - const state = await stateService.getLatestRoomState2( - roomCreateEvent.roomId, - ); + const state = await stateService.getLatestRoomState2(roomCreateEvent.roomId); expect(state.isUserInRoom(newUser)).toBe(true); }); @@ -1189,23 +1007,11 @@ describe('StateService', async () => { // join first await joinUser(roomCreateEvent.roomId, newUser); - expect( - ( - await stateService.getLatestRoomState2(roomCreateEvent.roomId) - ).isUserInRoom(newUser), - ).toBeTrue(); + expect((await stateService.getLatestRoomState2(roomCreateEvent.roomId)).isUserInRoom(newUser)).toBeTrue(); - await banUser( - roomCreateEvent.roomId, - newUser, - roomCreateEvent.getContent().creator, - ); + await banUser(roomCreateEvent.roomId, newUser, roomCreateEvent.getContent().creator); - expect( - ( - await stateService.getLatestRoomState2(roomCreateEvent.roomId) - ).getUserMembership(newUser), - ).toBe('ban'); + expect((await stateService.getLatestRoomState2(roomCreateEvent.roomId)).getUserMembership(newUser)).toBe('ban'); const membershipEventJoin2 = await stateService.buildEvent<'m.room.member'>( { @@ -1231,15 +1037,9 @@ describe('StateService', async () => { const bob = '@bob:example.com' as room.UserID; await joinUser(roomCreateEvent.roomId, bob); // ban bob now - const banBobEvent = await banUser( - roomCreateEvent.roomId, - bob, - roomCreateEvent.getContent().creator, - ); + const banBobEvent = await banUser(roomCreateEvent.roomId, bob, roomCreateEvent.getContent().creator); - const state1 = await stateService.getLatestRoomState2( - roomCreateEvent.roomId, - ); + const state1 = await stateService.getLatestRoomState2(roomCreateEvent.roomId); expect(state1.getUserMembership(bob)).toBe('ban'); // now we try to make bob "leave", but set the depth manually to be before he was banned @@ -1258,18 +1058,11 @@ describe('StateService', async () => { ), ); - const store = stateService._getStore( - roomCreateEvent.getContent() - .room_version as RoomVersion, - ); + const store = stateService._getStore(roomCreateEvent.getContent().room_version as RoomVersion); - const eventsBeforeBobWasBanned = await store.getEvents( - banBobEvent.getPreviousEventIds(), - ); + const eventsBeforeBobWasBanned = await store.getEvents(banBobEvent.getPreviousEventIds()); - const authEventsForBobBan = await store.getEvents( - banBobEvent.getAuthEventIds(), - ); // should be the same for bob + const authEventsForBobBan = await store.getEvents(banBobEvent.getAuthEventIds()); // should be the same for bob bobLeaveEvent.addPrevEvents(eventsBeforeBobWasBanned); @@ -1281,15 +1074,8 @@ describe('StateService', async () => { expect(bobLeaveEvent.rejectCode).toBe(RejectCodes.AuthError); }); it('should reject event if the power level is not high enough', async () => { - const { roomCreateEvent } = await createRoom( - 'public', - {}, - { 'rc.message': 70 }, - ); - const joinEvent = await joinUser( - roomCreateEvent.roomId, - '@bob:example.com', - ); + const { roomCreateEvent } = await createRoom('public', {}, { 'rc.message': 70 }); + const joinEvent = await joinUser(roomCreateEvent.roomId, '@bob:example.com'); const messageEvent = await stateService.buildEvent( { @@ -1303,17 +1089,11 @@ describe('StateService', async () => { roomCreateEvent.getContent().room_version, ); - await expect(() => stateService.handlePdu(messageEvent)).toThrow( - RejectCodes.AuthError, - ); + await expect(() => stateService.handlePdu(messageEvent)).toThrow(RejectCodes.AuthError); }); it('should allow event if the power level is high enough', async () => { - const { roomCreateEvent, creatorMembershipEvent } = await createRoom( - 'public', - {}, - { 'rc.message': 70 }, - ); + const { roomCreateEvent, creatorMembershipEvent } = await createRoom('public', {}, { 'rc.message': 70 }); const messageEvent = await stateService.buildEvent( { @@ -1333,9 +1113,8 @@ describe('StateService', async () => { it('12 should save unknown and custom events to database without throwing errors', async () => { const { roomCreateEvent } = await createRoom('public'); - const roomId = roomCreateEvent.roomId; - const roomVersion = - roomCreateEvent.getContent().room_version; + const { roomId } = roomCreateEvent; + const roomVersion = roomCreateEvent.getContent().room_version; // add a user with power to send events (events_default is 0 by default) const bob = '@bob:example.com' as room.UserID; @@ -1360,9 +1139,7 @@ describe('StateService', async () => { expect(customEvent.rejected).toBeFalsy(); // verify custom event is saved in database - const savedCustomEvent = await eventRepository.findById( - customEvent.eventId, - ); + const savedCustomEvent = await eventRepository.findById(customEvent.eventId); expect(savedCustomEvent).toBeDefined(); // @ts-expect-error - testing unknown event type expect(savedCustomEvent?.event.type).toBe('io.rocketchat.custom'); @@ -1386,9 +1163,7 @@ describe('StateService', async () => { expect(unknownMatrixEvent.rejected).toBeFalsy(); // verify unknown Matrix event is saved in database - const savedUnknownEvent = await eventRepository.findById( - unknownMatrixEvent.eventId, - ); + const savedUnknownEvent = await eventRepository.findById(unknownMatrixEvent.eventId); expect(savedUnknownEvent).toBeDefined(); // @ts-expect-error - testing unknown event type expect(savedUnknownEvent?.event.type).toBe('m.poll.start'); @@ -1411,19 +1186,16 @@ describe('StateService', async () => { expect(anotherCustomEvent.rejected).toBeFalsy(); // verify it's saved - const savedExampleEvent = await eventRepository.findById( - anotherCustomEvent.eventId, - ); + const savedExampleEvent = await eventRepository.findById(anotherCustomEvent.eventId); expect(savedExampleEvent).toBeDefined(); // @ts-expect-error - testing unknown event type expect(savedExampleEvent?.event.type).toBe('com.example.test'); }); it('01#arriving_late should fix state in case of older event arriving late', async () => { - const { roomCreateEvent, powerLevelEvent, roomNameEvent } = - await createRoom('public'); + const { roomCreateEvent, powerLevelEvent, roomNameEvent } = await createRoom('public'); - const roomId = roomCreateEvent.roomId; + const { roomId } = roomCreateEvent; // add a user const bob = '@bob:example.com' as room.UserID; @@ -1435,25 +1207,21 @@ describe('StateService', async () => { powerLevelContent.users[bob] = 50; - const newPowerLevelEvent = - await stateService.buildEvent<'m.room.power_levels'>( - { - type: 'm.room.power_levels', - room_id: roomCreateEvent.roomId, - sender: roomCreateEvent.getContent() - .creator as room.UserID, - state_key: '', - content: powerLevelContent, - ...getDefaultFields(), - }, - PersistentEventFactory.defaultRoomVersion, - ); + const newPowerLevelEvent = await stateService.buildEvent<'m.room.power_levels'>( + { + type: 'm.room.power_levels', + room_id: roomCreateEvent.roomId, + sender: roomCreateEvent.getContent().creator as room.UserID, + state_key: '', + content: powerLevelContent, + ...getDefaultFields(), + }, + PersistentEventFactory.defaultRoomVersion, + ); await stateService.handlePdu(newPowerLevelEvent); - const state1 = await stateService.getLatestRoomState2( - roomCreateEvent.roomId, - ); + const state1 = await stateService.getLatestRoomState2(roomCreateEvent.roomId); expect(state1.powerLevels?.users[bob]).toBe(50); // now we make bob change the room name, this should work @@ -1472,14 +1240,10 @@ describe('StateService', async () => { await stateService.handlePdu(roomNameEventByBob); - expect((await stateService.getLatestRoomState2(roomId)).name).toBe( - newRoomName, - ); + expect((await stateService.getLatestRoomState2(roomId)).name).toBe(newRoomName); // add another delta so both events point to the same state - const state2 = await stateService.getLatestRoomState2( - roomCreateEvent.roomId, - ); + const state2 = await stateService.getLatestRoomState2(roomCreateEvent.roomId); expect(state2.name).toBe(newRoomName); // we now mimick sending a ban event for bob, but before the power level event was sent @@ -1488,30 +1252,22 @@ describe('StateService', async () => { { type: 'm.room.member', room_id: roomCreateEvent.roomId, - sender: roomCreateEvent.getContent() - .creator as room.UserID, + sender: roomCreateEvent.getContent().creator as room.UserID, state_key: bob, content: { membership: 'ban' }, ...getDefaultFields(), }, - roomCreateEvent.getContent() - .room_version as RoomVersion, + roomCreateEvent.getContent().room_version as RoomVersion, ), ); - const store = stateService._getStore( - roomCreateEvent.getContent() - .room_version as RoomVersion, - ); + const store = stateService._getStore(roomCreateEvent.getContent().room_version as RoomVersion); - const eventsBeforePowerLevel = await store.getEvents( - newPowerLevelEvent.getPreviousEventIds(), - ); + const eventsBeforePowerLevel = await store.getEvents(newPowerLevelEvent.getPreviousEventIds()); banBobEvent.addPrevEvents(eventsBeforePowerLevel); - const stateBeforePowerLevelEvent = - await stateService.getStateBeforeEvent(powerLevelEvent); + const stateBeforePowerLevelEvent = await stateService.getStateBeforeEvent(powerLevelEvent); for (const requiredAuthEvent of banBobEvent.getAuthEventStateKeys()) { const authEvent = stateBeforePowerLevelEvent.get(requiredAuthEvent); @@ -1522,15 +1278,11 @@ describe('StateService', async () => { await stateService.handlePdu(banBobEvent); - const state3 = await stateService.getLatestRoomState2( - roomCreateEvent.roomId, - ); + const state3 = await stateService.getLatestRoomState2(roomCreateEvent.roomId); // console.log((stte3 as any).stateMap); - expect(state3.name).toBe( - roomNameEvent.getContent().name, - ); // should set the state to right versions + expect(state3.name).toBe(roomNameEvent.getContent().name); // should set the state to right versions }); it('02#arriving_late should fix state in case of older event arriving late', async () => { @@ -1547,8 +1299,7 @@ describe('StateService', async () => { await stateService.buildEvent<'m.room.join_rules'>( { room_id: roomCreateEvent.roomId, - sender: roomCreateEvent.getContent() - .creator as room.UserID, + sender: roomCreateEvent.getContent().creator as room.UserID, content: { join_rule: 'invite' }, type: 'm.room.join_rules', state_key: '', @@ -1561,17 +1312,12 @@ describe('StateService', async () => { // we will NOT fill or send the event yet. const randomUser1 = '@random1:example.com'; - const randomUserJoinEvent = await joinUser( - roomCreateEvent.roomId, - randomUser1, - ); + const randomUserJoinEvent = await joinUser(roomCreateEvent.roomId, randomUser1); const randomUser2 = '@random2:example.com'; await joinUser(roomCreateEvent.roomId, randomUser2); - const state1 = await stateService.getLatestRoomState2( - roomCreateEvent.roomId, - ); + const state1 = await stateService.getLatestRoomState2(roomCreateEvent.roomId); expect(state1.isUserInRoom(randomUser1)).toBe(true); expect(state1.isUserInRoom(randomUser2)).toBe(true); @@ -1579,16 +1325,12 @@ describe('StateService', async () => { // join rule was changed before randomuser joined const store = stateService._getStore(state1.version); - const previousEventIdsForJoinRule = - randomUserJoinEvent.getPreviousEventIds(); - const previousEventsForJoinRule = await store.getEvents( - previousEventIdsForJoinRule, - ); + const previousEventIdsForJoinRule = randomUserJoinEvent.getPreviousEventIds(); + const previousEventsForJoinRule = await store.getEvents(previousEventIdsForJoinRule); joinRuleInvite.addPrevEvents(previousEventsForJoinRule); // while the join doesn't affect the auth events for joinrule, still doing it this way as an example for the correct way - const stateBeforeRandomUserJoin = - await stateService.getStateBeforeEvent(randomUserJoinEvent); + const stateBeforeRandomUserJoin = await stateService.getStateBeforeEvent(randomUserJoinEvent); for (const requiredAuthEvent of joinRuleInvite.getAuthEventStateKeys()) { const authEvent = stateBeforeRandomUserJoin.get(requiredAuthEvent); if (authEvent) { @@ -1604,9 +1346,7 @@ describe('StateService', async () => { // console.log('state', [..._state2.entries()]); - const state2 = await stateService.getLatestRoomState2( - roomCreateEvent.roomId, - ); + const state2 = await stateService.getLatestRoomState2(roomCreateEvent.roomId); expect(state2.isUserInRoom(randomUser1)).toBe(false); expect(state2.isUserInRoom(randomUser2)).toBe(false); @@ -1713,9 +1453,7 @@ describe('StateService', async () => { const remoteUser = '@alice:remote.com'; await joinUser(roomCreateEvent.roomId, remoteUser); - const servers2 = await stateService.getServersInRoom( - roomCreateEvent.roomId, - ); + const servers2 = await stateService.getServersInRoom(roomCreateEvent.roomId); expect(servers2).toContain('example.com'); expect(servers2).toContain('remote.com'); expect(servers2.length).toBe(2); @@ -1723,18 +1461,14 @@ describe('StateService', async () => { // now leave the remote user await leaveUser(roomCreateEvent.roomId, remoteUser); - const servers3 = await stateService.getServersInRoom( - roomCreateEvent.roomId, - ); + const servers3 = await stateService.getServersInRoom(roomCreateEvent.roomId); expect(servers3).toContain('example.com'); expect(servers3.length).toBe(1); // now add her again await joinUser(roomCreateEvent.roomId, remoteUser); - const servers4 = await stateService.getServersInRoom( - roomCreateEvent.roomId, - ); + const servers4 = await stateService.getServersInRoom(roomCreateEvent.roomId); expect(servers4).toContain('example.com'); expect(servers4).toContain('remote.com'); expect(servers4.length).toBe(2); @@ -1742,7 +1476,7 @@ describe('StateService', async () => { it('should allow previously rejected events through multiple state resolutions', async () => { const { roomCreateEvent } = await createRoom('public'); - const roomId = roomCreateEvent.roomId; + const { roomId } = roomCreateEvent; const roomVersion = roomCreateEvent.getContent().room_version; const creator = roomCreateEvent.getContent().creator as room.UserID; @@ -1801,7 +1535,7 @@ describe('StateService', async () => { it('should consider previously rejected event as part of state if new out of order event allows it', async () => { const { roomCreateEvent } = await createRoom('public'); - const roomId = roomCreateEvent.roomId; + const { roomId } = roomCreateEvent; const creator = roomCreateEvent.getContent().creator as room.UserID; const roomVersion = roomCreateEvent.version; @@ -1833,7 +1567,7 @@ describe('StateService', async () => { const state2 = await stateService.getLatestRoomState2(roomId); expect(state2.isInviteOnly()).toBeTrue(); expect(state2.isUserInRoom(bob)).toBeFalse(); - //.... + // .... const joinRulePublic = await copyDepth( bobJoinEvent, @@ -1865,7 +1599,7 @@ describe('StateService', async () => { [don]: 50, }); - const roomId = roomCreateEvent.roomId; + const { roomId } = roomCreateEvent; const roomVersion = roomCreateEvent.version; const creator = roomCreateEvent.getContent().creator as room.UserID; @@ -1929,10 +1663,7 @@ describe('StateService', async () => { const state2 = await stateService.getStateAtEvent(roomName); // must not be new name - expect(state2.get(roomName.getUniqueStateIdentifier())).toHaveProperty( - 'eventId', - roomNameEvent.eventId, - ); + expect(state2.get(roomName.getUniqueStateIdentifier())).toHaveProperty('eventId', roomNameEvent.eventId); }); // removing bias from own code @@ -1959,8 +1690,7 @@ describe('StateService', async () => { }, signatures: { 'rc1.tunnel.dev.rocket.chat': { - 'ed25519:0': - 'lMv8+0wXgFSGRtHgBNhu8T8xkcCc6SLZfJwcLGjFIgaXAdAxMjx7HiZNv+JuDWl8qEbgdisuTkPzUTmdsgxgDQ', + 'ed25519:0': 'lMv8+0wXgFSGRtHgBNhu8T8xkcCc6SLZfJwcLGjFIgaXAdAxMjx7HiZNv+JuDWl8qEbgdisuTkPzUTmdsgxgDQ', }, }, unsigned: {}, @@ -1975,13 +1705,9 @@ describe('StateService', async () => { }, room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, state_key: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, - auth_events: [ - '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', - ] as EventID[], + auth_events: ['$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI'] as EventID[], depth: 1, - prev_events: [ - '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', - ] as EventID[], + prev_events: ['$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI'] as EventID[], origin_server_ts: 1759757583403, sender: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, hashes: { @@ -1989,8 +1715,7 @@ describe('StateService', async () => { }, signatures: { 'rc1.tunnel.dev.rocket.chat': { - 'ed25519:0': - 'LCl4QANTD9cSDpIrXj3bv7nJxs9x7QA4y6tZl49//C3CJJiGzZUfjGG3dH11RcSD2esTNSYJwQAbKqNGiWe4Ag', + 'ed25519:0': 'LCl4QANTD9cSDpIrXj3bv7nJxs9x7QA4y6tZl49//C3CJJiGzZUfjGG3dH11RcSD2esTNSYJwQAbKqNGiWe4Ag', }, }, unsigned: {}, @@ -2005,14 +1730,9 @@ describe('StateService', async () => { }, room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, state_key: '', - auth_events: [ - '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', - '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', - ] as EventID[], + auth_events: ['$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI'] as EventID[], depth: 2, - prev_events: [ - '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', - ] as EventID[], + prev_events: ['$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U'] as EventID[], origin_server_ts: 1759757583427, sender: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, hashes: { @@ -2020,8 +1740,7 @@ describe('StateService', async () => { }, signatures: { 'rc1.tunnel.dev.rocket.chat': { - 'ed25519:0': - 'GSgPxsTF7VnbtGEGmwhH7F/Ets4R15BJpl1NjWi+SdwkVp7nvcQm/hKUNY803QlBNYur5OcLIi47DkxJ2Cg1Cw', + 'ed25519:0': 'GSgPxsTF7VnbtGEGmwhH7F/Ets4R15BJpl1NjWi+SdwkVp7nvcQm/hKUNY803QlBNYur5OcLIi47DkxJ2Cg1Cw', }, }, unsigned: {}, @@ -2046,14 +1765,9 @@ describe('StateService', async () => { }, room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, state_key: '', - auth_events: [ - '$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', - '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', - ] as EventID[], + auth_events: ['$_YhqI7eEy5XRK2FEtU1QjWAStEVfDhBiKbUmVh_ML_U', '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI'] as EventID[], depth: 3, - prev_events: [ - '$yvGHQAk_VvuInS5WsW3_w-mi5zLSC_ZWz724wSla_z4', - ] as EventID[], + prev_events: ['$yvGHQAk_VvuInS5WsW3_w-mi5zLSC_ZWz724wSla_z4'] as EventID[], origin_server_ts: 1759757583446, sender: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, hashes: { @@ -2061,8 +1775,7 @@ describe('StateService', async () => { }, signatures: { 'rc1.tunnel.dev.rocket.chat': { - 'ed25519:0': - 'Tftd0/LAn8NaEcrGYb5nXp69nbSyVBNqlDuqSfq3XbAOlWSRZIiP7/Zm4RZmrdZ6zZgjvDABD+TrCiRFccxdDg', + 'ed25519:0': 'Tftd0/LAn8NaEcrGYb5nXp69nbSyVBNqlDuqSfq3XbAOlWSRZIiP7/Zm4RZmrdZ6zZgjvDABD+TrCiRFccxdDg', }, }, unsigned: {}, @@ -2083,9 +1796,7 @@ describe('StateService', async () => { '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', ] as EventID[], depth: 4, - prev_events: [ - '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', - ] as EventID[], + prev_events: ['$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM'] as EventID[], origin_server_ts: 1759757583461, sender: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, hashes: { @@ -2093,8 +1804,7 @@ describe('StateService', async () => { }, signatures: { 'rc1.tunnel.dev.rocket.chat': { - 'ed25519:0': - '4yNSSn29xebrwW2LpM+P7qGgHhpNFbmIFrvKQw7sN0qRHNaLIx7mlwiFyfm2P4gdHaiIV+6WyeueH+QtPJejAA', + 'ed25519:0': '4yNSSn29xebrwW2LpM+P7qGgHhpNFbmIFrvKQw7sN0qRHNaLIx7mlwiFyfm2P4gdHaiIV+6WyeueH+QtPJejAA', }, }, unsigned: {}, @@ -2116,9 +1826,7 @@ describe('StateService', async () => { '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', ] as EventID[], depth: 5, - prev_events: [ - '$wWJBjUdHzAds-ZjpgwLQdDKpA3lQQLPkJuQCq-yUHQc', - ] as EventID[], + prev_events: ['$wWJBjUdHzAds-ZjpgwLQdDKpA3lQQLPkJuQCq-yUHQc'] as EventID[], origin_server_ts: 1759757583476, sender: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, hashes: { @@ -2126,8 +1834,7 @@ describe('StateService', async () => { }, signatures: { 'rc1.tunnel.dev.rocket.chat': { - 'ed25519:0': - 'HwCvEEM4l7HQDiHVyoNQOgsPuKR4Q7ZCfaD025XZ+rv9AG2Gu8rTGJ9Bvtq8oKSCgtqvNrPplLdk/KVow7ZXCA', + 'ed25519:0': 'HwCvEEM4l7HQDiHVyoNQOgsPuKR4Q7ZCfaD025XZ+rv9AG2Gu8rTGJ9Bvtq8oKSCgtqvNrPplLdk/KVow7ZXCA', }, }, unsigned: {}, @@ -2149,9 +1856,7 @@ describe('StateService', async () => { '$wWJBjUdHzAds-ZjpgwLQdDKpA3lQQLPkJuQCq-yUHQc', ] as EventID[], depth: 6, - prev_events: [ - '$3Ttw6n2x6EALDnf4Cm5BKtYrIfzlyuE0VMmfXI2j680', - ] as EventID[], + prev_events: ['$3Ttw6n2x6EALDnf4Cm5BKtYrIfzlyuE0VMmfXI2j680'] as EventID[], origin_server_ts: 1759757778902, sender: '@debdut:rc1.tunnel.dev.rocket.chat' as room.UserID, hashes: { @@ -2159,12 +1864,10 @@ describe('StateService', async () => { }, signatures: { 'rc1.tunnel.dev.rocket.chat': { - 'ed25519:0': - 'hoNC177zwdlYHURCAsavTPVYsBJJMINeuVCsbZjFiBFuO4SEaMEmTU/5Ht0KADE/XwHCOeGg4xB9sD9L+yeWDQ', + 'ed25519:0': 'hoNC177zwdlYHURCAsavTPVYsBJJMINeuVCsbZjFiBFuO4SEaMEmTU/5Ht0KADE/XwHCOeGg4xB9sD9L+yeWDQ', }, 'syn1.tunnel.dev.rocket.chat': { - 'ed25519:a_FAET': - 'hpFY8m5g1e4s/0VrV8fEP3hIsvn1sh68ZfXaUrV5wpMSFwAzZaC5wW5UxwHAopUTDNfNRnR1lANti6a9ddUFBw', + 'ed25519:a_FAET': 'hpFY8m5g1e4s/0VrV8fEP3hIsvn1sh68ZfXaUrV5wpMSFwAzZaC5wW5UxwHAopUTDNfNRnR1lANti6a9ddUFBw', }, }, unsigned: { @@ -2240,23 +1943,20 @@ describe('StateService', async () => { content: { membership: 'join', displayname: 'ah', - // @ts-ignore this has been fixed by rodrigo already in zod avatar_url: null as unknown as undefined, }, sender: '@ah:syn1.tunnel.dev.rocket.chat' as room.UserID, room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, origin_server_ts: 1759757909955, depth: 7, - prev_events: [ - '$Ka58p5BSdnjjmCPN22Erj6piAXRHIm9hQjXv1g5DeXw', - ] as EventID[], + prev_events: ['$Ka58p5BSdnjjmCPN22Erj6piAXRHIm9hQjXv1g5DeXw'] as EventID[], auth_events: [ '$gmoi4PDtLFJO_M4zHK0rGm-1zJePpApcfyNnXwSf5zM', '$Ka58p5BSdnjjmCPN22Erj6piAXRHIm9hQjXv1g5DeXw', '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', '$wWJBjUdHzAds-ZjpgwLQdDKpA3lQQLPkJuQCq-yUHQc', ] as EventID[], - // @ts-ignore + // @ts-expect-error --- IGNORE --- origin: 'syn1.tunnel.dev.rocket.chat', unsigned: { age: 2, @@ -2267,12 +1967,10 @@ describe('StateService', async () => { }, signatures: { 'rc1.tunnel.dev.rocket.chat': { - 'ed25519:0': - '5IyCqACOVFJ9h5HGbXWNOwuNpn2hdCRpDWMgmfnH11epIjCCGKNHLftaBPdiOLTSO9RyBixEtplphmgBazWCDQ', + 'ed25519:0': '5IyCqACOVFJ9h5HGbXWNOwuNpn2hdCRpDWMgmfnH11epIjCCGKNHLftaBPdiOLTSO9RyBixEtplphmgBazWCDQ', }, 'syn1.tunnel.dev.rocket.chat': { - 'ed25519:a_FAET': - 'c6SYtgr3UoFu3OfxjF5Da+Sk2VgEBQxK3StPxC0CzXWrVg2AUkJyqL4RfbfAhMnRm3eDRFTHLZ1+WzllFJv6BA', + 'ed25519:a_FAET': 'c6SYtgr3UoFu3OfxjF5Da+Sk2VgEBQxK3StPxC0CzXWrVg2AUkJyqL4RfbfAhMnRm3eDRFTHLZ1+WzllFJv6BA', }, }, } satisfies room.Pdu; @@ -2320,18 +2018,16 @@ describe('StateService', async () => { const message = { type: 'm.room.message', content: { - body: '1', - // @ts-ignore are we missing this ? TODO: + 'body': '1', + // @ts-expect-error --- IGNORE --- 'm.mentions': {}, - msgtype: 'm.text', + 'msgtype': 'm.text', }, sender: '@ah:syn1.tunnel.dev.rocket.chat' as room.UserID, room_id: '!xZbhusWZ:rc1.tunnel.dev.rocket.chat' as room.RoomID, origin_server_ts: 1759760138291, depth: 8, - prev_events: [ - '$-C1Tf8UTaSZPEGcwVAUD1xGnKVS64HG_DxiEQVbIJBg', - ] as EventID[], + prev_events: ['$-C1Tf8UTaSZPEGcwVAUD1xGnKVS64HG_DxiEQVbIJBg'] as EventID[], auth_events: [ '$-C1Tf8UTaSZPEGcwVAUD1xGnKVS64HG_DxiEQVbIJBg', '$N6KEZQ-ClhVa9P4_MgGmtnR32zZk-W-y7IebNjdoKqI', @@ -2346,15 +2042,12 @@ describe('StateService', async () => { }, signatures: { 'syn1.tunnel.dev.rocket.chat': { - 'ed25519:a_FAET': - 'enVzK6E2K5gC11j4+G5Z+8aezriR2/2P2qqWI7/S8Qhs03ON3vkj9owszdN+bPNBklGQC5YMFCKQRf+TXp+eDw', + 'ed25519:a_FAET': 'enVzK6E2K5gC11j4+G5Z+8aezriR2/2P2qqWI7/S8Qhs03ON3vkj9owszdN+bPNBklGQC5YMFCKQRf+TXp+eDw', }, }, } satisfies room.Pdu; await stateService.handlePdu(toEventBase(message)); - const stateAfterMessage = await stateService.getStateBeforeEvent( - toEventBase(message), - ); + const stateAfterMessage = await stateService.getStateBeforeEvent(toEventBase(message)); const newPduIds = Array.from(stateAfterMessage.values()) .map((e) => e.eventId) .sort(); @@ -2368,7 +2061,7 @@ describe('StateService', async () => { } const { roomCreateEvent } = await createRoom('public'); - const roomId = roomCreateEvent.roomId; + const { roomId } = roomCreateEvent; const version = roomCreateEvent.getContent().room_version; await Promise.all(users.map((u) => joinUser(roomId, u))); @@ -2380,7 +2073,7 @@ describe('StateService', async () => { } // all users should also be able to send a message - for (const user of users) { + for await (const user of users) { const message = await stateService.buildEvent<'m.room.message'>( { type: 'm.room.message', @@ -2405,6 +2098,7 @@ describe('StateService', async () => { }; for (let i = 1; i <= partialStateEvents.length; i++) { + // eslint-disable-next-line no-loop-func describe(label('partial states', i), () => { it(label('should not be able to complete the chain', i), async () => { const { state } = await partialStateEvents[i - 1](); @@ -2415,9 +2109,7 @@ describe('StateService', async () => { eventMap.set(event.eventId, event); } - const hasNoPartial = events.every((event) => - event.getPreviousEventIds().every((prev) => eventMap.has(prev)), - ); + const hasNoPartial = events.every((event) => event.getPreviousEventIds().every((prev) => eventMap.has(prev))); expect(hasNoPartial).toBeFalse(); }); @@ -2435,16 +2127,11 @@ describe('StateService', async () => { expect(stateId).toBeString(); - const event = await stateService.getEvent( - state.ourUserJoinEvent.eventId, - ); + const event = await stateService.getEvent(state.ourUserJoinEvent.eventId); expect(event?.isPartial()).toBeTrue(); }); - it(label( - 'should be able to save and detect partial states', - i, - ), async () => { + it(label('should be able to save and detect partial states', i), async () => { const { state, authChain } = await partialStateEvents[i - 1](); const events = Object.values(state); @@ -2453,37 +2140,25 @@ describe('StateService', async () => { authChain.map((e) => e.event), ); - expect( - stateService.isRoomStatePartial(events[0].roomId), - ).resolves.toBeTrue(); + expect(stateService.isRoomStatePartial(events[0].roomId)).resolves.toBeTrue(); }); - it(label( - 'should complete the state as missing events get filled', - i, - ), async () => { - const { state, authChain, missingEvents } = - await partialStateEvents[i - 1](); + it(label('should complete the state as missing events get filled', i), async () => { + const { state, authChain, missingEvents } = await partialStateEvents[i - 1](); await stateService.processInitialState( Object.values(state).map((e) => e.event), authChain.map((e) => e.event), ); - expect( - stateService.isRoomStatePartial(state.roomCreateEvent.roomId), - ).resolves.toBeTrue(); + expect(stateService.isRoomStatePartial(state.roomCreateEvent.roomId)).resolves.toBeTrue(); const eventStore = new Map(); - for (const e of (Object.values(state) as PersistentEventBase[]).concat( - missingEvents, - )) { + for (const e of (Object.values(state) as PersistentEventBase[]).concat(missingEvents)) { eventStore.set(e.eventId, e); } - const eventsToWalk = await stateService.getPartialEvents( - state.creatorMembershipEvent.roomId, - ); + const eventsToWalk = await stateService.getPartialEvents(state.creatorMembershipEvent.roomId); const store = stateService._getStore(state.roomCreateEvent.version); @@ -2493,12 +2168,8 @@ describe('StateService', async () => { const walk = async (event: PersistentEventBase) => { // for each previous event, walk - const previousEventsInStore = await store.getEvents( - event.getPreviousEventIds(), - ); - if ( - previousEventsInStore.length === event.getPreviousEventIds().length - ) { + const previousEventsInStore = await store.getEvents(event.getPreviousEventIds()); + if (previousEventsInStore.length === event.getPreviousEventIds().length) { console.log(`All previous events found in store ${event.eventId}`); // start processing this event now await stateService._resolveStateAtEvent(event); @@ -2507,24 +2178,16 @@ describe('StateService', async () => { const eventIdsToFind = [] as EventID[]; for (const previousEventId of event.getPreviousEventIds()) { - if ( - !previousEventsInStore - .map((p) => p.eventId) - .includes(previousEventId) - ) { + if (!previousEventsInStore.map((p) => p.eventId).includes(previousEventId)) { eventIdsToFind.push(previousEventId); } } console.log(`Events to find ${eventIdsToFind}`); - const previousEvents = (await remoteFetch( - eventIdsToFind, - )) as PersistentEventBase[]; + const previousEvents = (await remoteFetch(eventIdsToFind)) as PersistentEventBase[]; - expect(previousEvents.length).toBe( - event.getPreviousEventIds().length, - ); + expect(previousEvents.length).toBe(event.getPreviousEventIds().length); previousEvents .sort((e1, e2) => { @@ -2540,28 +2203,24 @@ describe('StateService', async () => { }) .reverse(); - for (const previousEvent of previousEvents) { + for await (const previousEvent of previousEvents) { console.log(`Waling ${previousEvent.eventId}`); await walk(previousEvent); } - console.log( - `Finishing saving ${event.eventId}, all [${event.getPreviousEventIds().join(', ')}] events has been saved`, - ); + console.log(`Finishing saving ${event.eventId}, all [${event.getPreviousEventIds().join(', ')}] events has been saved`); // once all previous events have been walked we process this event await stateService._resolveStateAtEvent(event); }; - for (const event of eventsToWalk) { + for await (const event of eventsToWalk) { console.log(`Starting walking ${event.eventId}`); await walk(event).catch(console.error); } // now room should state to not be in partial state - expect( - stateService.isRoomStatePartial(state.roomCreateEvent.roomId), - ).resolves.toBeFalse(); + expect(stateService.isRoomStatePartial(state.roomCreateEvent.roomId)).resolves.toBeFalse(); }); }); } diff --git a/packages/federation-sdk/src/services/state.service.ts b/packages/federation-sdk/src/services/state.service.ts index c1361e524..afadd18e9 100644 --- a/packages/federation-sdk/src/services/state.service.ts +++ b/packages/federation-sdk/src/services/state.service.ts @@ -1,32 +1,36 @@ import { createLogger, signEvent } from '@rocket.chat/federation-core'; import { - type EventID, - type EventStore, + PersistentEventFactory, + RoomState, + StateResolverAuthorizationError, + checkEventAuthWithState, + extractDomainFromId, + resolveStateV2Plus, +} from '@rocket.chat/federation-room'; +import type { Pdu, - type PduContent, PduCreateEventContent, PduForType, - type PduType, PduWithHashesAndSignaturesOptional, PersistentEventBase, - PersistentEventFactory, RejectCode, RoomID, - RoomState, RoomVersion, State, - type StateID, - type StateMapKey, - StateResolverAuthorizationError, - checkEventAuthWithState, - extractDomainFromId, - resolveStateV2Plus, + EventID, + EventStore, + PduContent, + PduType, + StateID, + StateMapKey, } from '@rocket.chat/federation-room'; import { delay, inject, singleton } from 'tsyringe'; + +import type { ConfigService } from './config.service'; +import type { EventService } from './event.service'; import { EventRepository } from '../repositories/event.repository'; import { StateGraphRepository } from '../repositories/state-graph.repository'; -import { ConfigService } from './config.service'; -import type { EventService } from './event.service'; + type StrippedEvent = { content: PduContent; sender: string; @@ -70,12 +74,14 @@ export class RoomInfoNotReadyError extends Error { @singleton() export class StateService { private readonly logger = createLogger('StateService'); + constructor( @inject(delay(() => StateGraphRepository)) private readonly stateRepository: StateGraphRepository, @inject(delay(() => EventRepository)) private readonly eventRepository: EventRepository, private readonly configService: ConfigService, + // eslint-disable-next-line @typescript-eslint/no-var-requires @inject(delay(() => require('./event.service').EventService)) private readonly eventService: EventService, ) {} @@ -84,15 +90,9 @@ export class StateService { // or getCreateEvent. // currently AFAIK mostly is used for just room version async getRoomInformation(roomId: string): Promise { - const { event, stateId } = - (await this.eventRepository.findByRoomIdAndType( - roomId, - 'm.room.create', - )) ?? {}; + const { event, stateId } = (await this.eventRepository.findByRoomIdAndType(roomId, 'm.room.create')) ?? {}; if (event?.type !== 'm.room.create') { - throw new RoomInfoNotReadyError( - 'Create event mapping not found for room information', - ); + throw new RoomInfoNotReadyError('Create event mapping not found for room information'); } if (!stateId) { @@ -103,10 +103,7 @@ export class StateService { } async getRoomVersion(roomId: RoomID): Promise { - const createEvent = await this.eventRepository.findByRoomIdAndType( - roomId, - 'm.room.create', - ); + const createEvent = await this.eventRepository.findByRoomIdAndType(roomId, 'm.room.create'); if (!createEvent) { throw new UnknownRoomError(roomId); } @@ -133,35 +130,23 @@ export class StateService { } private async updateNextEventReferencesWithEvent(event: PersistentEventBase) { - await this.eventRepository.updateNextEventReferences( - event.eventId, - event.getPreviousEventIds(), - ); + await this.eventRepository.updateNextEventReferences(event.eventId, event.getPreviousEventIds()); } // addToRoomGraph does two things // 1. persists the event if not already persisted // 2. marks the event as forward extremity for the room private async addToRoomGraph(event: PersistentEventBase, stateId: StateID) { - await this.eventRepository.insertOrUpdateEventWithStateId( - event.eventId, - event.event, - stateId, - event.isPartial(), - ); + await this.eventRepository.insertOrUpdateEventWithStateId(event.eventId, event.event, stateId, event.isPartial()); await this.updateNextEventReferencesWithEvent(event); } // at event isalways the stateId referenced for that event private async getStateIdAtEvent(event: PersistentEventBase) { - const stateId = await this.eventRepository.findStateIdByEventId( - event.eventId, - ); + const stateId = await this.eventRepository.findStateIdByEventId(event.eventId); if (!stateId) { - throw new Error( - `Event ${event.eventId} not found in db, failed to fidn associated state id`, - ); + throw new Error(`Event ${event.eventId} not found in db, failed to fidn associated state id`); } return stateId; @@ -212,18 +197,10 @@ export class StateService { const roomVersion = await this.getRoomVersion(event.event.room_id); - const pdu = PersistentEventFactory.createFromRawEvent( - event.event, - roomVersion, - event.partial, - ); + const pdu = PersistentEventFactory.createFromRawEvent(event.event, roomVersion, event.partial); if (event.rejectCode !== undefined) { - pdu.reject( - event.rejectCode, - event.rejectDetail?.reason ?? '', - event.rejectDetail?.rejectedBy, - ); + pdu.reject(event.rejectCode, event.rejectDetail?.reason ?? '', event.rejectDetail?.rejectedBy); } return pdu; @@ -233,9 +210,7 @@ export class StateService { const cache = new Map(); return { - getEvents: async ( - eventIds: EventID[], - ): Promise => { + getEvents: async (eventIds: EventID[]): Promise => { if (eventIds.length === 0) { return []; } @@ -257,16 +232,9 @@ export class StateService { const resultEventsCursor = this.eventRepository.findByIds(toFind); const resultEvents = await resultEventsCursor.toArray(); const eventsFromStore = resultEvents.map((event) => { - const e = PersistentEventFactory.createFromRawEvent( - event.event, - roomVersion, - ); + const e = PersistentEventFactory.createFromRawEvent(event.event, roomVersion); if (event.rejectCode) { - e.reject( - event.rejectCode, - event.rejectDetail?.reason ?? '', - event.rejectDetail?.rejectedBy ?? ('' as EventID), - ); + e.reject(event.rejectCode, event.rejectDetail?.reason ?? '', event.rejectDetail?.rejectedBy ?? ('' as EventID)); } cache.set(e.eventId, e); return e; @@ -281,10 +249,7 @@ export class StateService { event: PduWithHashesAndSignaturesOptional>, roomVersion: RoomVersion, ): Promise> { - const instance = PersistentEventFactory.createFromRawEvent( - event, - roomVersion, - ); + const instance = PersistentEventFactory.createFromRawEvent(event, roomVersion); await Promise.all([ instance.event.auth_events.length === 0 && this.addAuthEvents(instance), instance.event.prev_events.length === 0 && this.addPrevEvents(instance), @@ -313,17 +278,12 @@ export class StateService { throw new Error('Room version not found while filling prev events'); } - const prevEvents = await this.eventRepository.findLatestEvents( - event.roomId, - ); + const prevEvents = await this.eventRepository.findLatestEvents(event.roomId); const events = [] as PersistentEventBase[]; for (const prevEvent of prevEvents) { - const e = PersistentEventFactory.createFromRawEvent( - prevEvent.event, - roomVersion, - ); + const e = PersistentEventFactory.createFromRawEvent(prevEvent.event, roomVersion); events.push(e); } @@ -362,10 +322,7 @@ export class StateService { return state; } - private async buildStateFromStateMap( - stateMap: Map, - roomVersion: RoomVersion, - ) { + private async buildStateFromStateMap(stateMap: Map, roomVersion: RoomVersion) { if (stateMap.size === 0) { throw new Error('State map is empty, cannot build state'); } @@ -375,10 +332,7 @@ export class StateService { const events = await this.eventRepository.findByIds(eventIds).toArray(); const state: State = new Map(); for (const event of events) { - const e = PersistentEventFactory.createFromRawEvent( - event.event, - roomVersion, - ); + const e = PersistentEventFactory.createFromRawEvent(event.event, roomVersion); state.set(e.getUniqueStateIdentifier(), e); } return state; @@ -432,19 +386,10 @@ export class StateService { } // handle create separately - const createEvent = PersistentEventFactory.createFromRawEvent( - create, - version, - ); - const stateId = await this.stateRepository.createDelta( - createEvent, - '' as StateID, - ); + const createEvent = PersistentEventFactory.createFromRawEvent(create, version); + const stateId = await this.stateRepository.createDelta(createEvent, '' as StateID); await this.addToRoomGraph(createEvent, stateId); - this.logger.info( - { eventId: createEvent.eventId, roomId: createEvent.roomId, stateId }, - 'create event saved', - ); + this.logger.info({ eventId: createEvent.eventId, roomId: createEvent.roomId, stateId }, 'create event saved'); const getAuthEventStateMap = (e: PersistentEventBase) => { return e.getAuthEventIds().reduce((accum, curr) => { @@ -475,7 +420,7 @@ export class StateService { let previousStateId = stateId; - for (const event of sortedEvents) { + for await (const event of sortedEvents) { const authState = getAuthEventStateMap(event); try { await checkEventAuthWithState(event, authState, store); @@ -493,23 +438,18 @@ export class StateService { // auth events themseleves can be partial at any point event.setPartial( // if some of the previous events are partial this one also needs to be partial - event - .getPreviousEventIds() - .some((id) => { - const event = authChainCache.get(id) || eventCache.get(id); - // event notseen - if (!event) { - return true; - } - - // seen event is also partial - return event.isPartial(); - }), - ); - previousStateId = await this.stateRepository.createDelta( - event, - previousStateId, + event.getPreviousEventIds().some((id) => { + const event = authChainCache.get(id) || eventCache.get(id); + // event notseen + if (!event) { + return true; + } + + // seen event is also partial + return event.isPartial(); + }), ); + previousStateId = await this.stateRepository.createDelta(event, previousStateId); await this.addToRoomGraph(event, previousStateId); await this.eventService.notify(event); @@ -518,9 +458,7 @@ export class StateService { return previousStateId; } - private async _neeedsProcessing

( - event: P, - ): Promise

{ + private async _neeedsProcessing

(event: P): Promise

{ const record = await this.eventRepository.findById(event.eventId); if (record?.partial) { // event is saved and is partial, pass it @@ -528,9 +466,7 @@ export class StateService { return event; } - const previousEvents = await this.eventRepository - .findByIds(event.getPreviousEventIds()) - .toArray(); + const previousEvents = await this.eventRepository.findByIds(event.getPreviousEventIds()).toArray(); if (previousEvents.length !== event.getPreviousEventIds().length) { // if we don't have all the previous events now, this is a partial state event.setPartial(true); @@ -559,17 +495,14 @@ export class StateService { return false; case 1: { const stateId = stateIds.values().toArray().pop(); - const delta = - stateId && (await this.stateRepository.findOneById(stateId)); + const delta = stateId && (await this.stateRepository.findOneById(stateId)); if (!delta) { throw new Error(`No delta found for ${stateId}`); } return delta.partial; } default: { - const deltas = await this.stateRepository.findByStateIds( - stateIds.values().toArray(), - ); + const deltas = await this.stateRepository.findByStateIds(stateIds.values().toArray()); for await (const delta of deltas) { if (delta.partial) { @@ -588,10 +521,7 @@ export class StateService { async handlePdu

(pdu: P): Promise { if (pdu.isCreateEvent()) { this.logger.debug({ eventId: pdu.eventId }, 'handling create event'); - const stateId = await this.stateRepository.createDelta( - pdu, - '' as StateID, - ); + const stateId = await this.stateRepository.createDelta(pdu, '' as StateID); await this.addToRoomGraph(pdu, stateId); @@ -600,17 +530,11 @@ export class StateService { const event = await this._neeedsProcessing(pdu); if (!event) { - this.logger.debug( - { eventId: pdu.eventId }, - 'event saved and not in partial state, skipping processing', - ); + this.logger.debug({ eventId: pdu.eventId }, 'event saved and not in partial state, skipping processing'); return; } - this.logger.debug( - { eventId: event.eventId, ...this.stripEvent(event) }, - 'handling pdu', - ); + this.logger.debug({ eventId: event.eventId, ...this.stripEvent(event) }, 'handling pdu'); if (await this.isRoomStatePartial(event.roomId)) { throw new PartialStateResolutionError(event); @@ -628,14 +552,10 @@ export class StateService { const authEvents = await store.getEvents(event.getAuthEventIds()); try { - await checkEventAuthWithState( - event, - this.buildStateFromEvents(authEvents), - this._getStore(event.version), - ); + await checkEventAuthWithState(event, this.buildStateFromEvents(authEvents), this._getStore(event.version)); } catch (error) { if (error instanceof StateResolverAuthorizationError) { - this.logger.warn({ error: error }, 'event not authorized'); + this.logger.warn({ error }, 'event not authorized'); event.reject(error.code, error.reason, error.rejectedBy); // at this point potentially there is no state for this event, logic same @@ -650,10 +570,7 @@ export class StateService { throw error; } - this.logger.debug( - { eventId: event.eventId }, - 'event authorized against auth events', - ); + this.logger.debug({ eventId: event.eventId }, 'event authorized against auth events'); // 5. Passes authorization rules based on the state before the event and store event, otherwise it is rejected. await this._resolveStateAtEvent(event); // it is the assumption that this point forwards this event WILL have a state associated with it @@ -685,19 +602,14 @@ export class StateService { const roomVersion = event.version; - this.logger.debug( - { eventId: event.eventId }, - 'validating against latest state', - ); + this.logger.debug({ eventId: event.eventId }, 'validating against latest state'); // 6. Passes authorization rules based on the current state of the room, otherwise it is “soft failed”. // we in memory figure out what the state is NOW const state = await this.getLatestRoomState(event.roomId); - if ( - state.get(event.getUniqueStateIdentifier())?.eventId === event.eventId - ) { + if (state.get(event.getUniqueStateIdentifier())?.eventId === event.eventId) { // linear, already accepted return; } @@ -772,9 +684,7 @@ export class StateService { async getStateAtStateId(stateId: StateID, roomVersion: RoomVersion) { const stateMap = await this.stateRepository.buildStateMapById(stateId); if (!stateMap) { - throw new Error( - `getStateAtStateId: no state map found for state id ${stateId}`, - ); + throw new Error(`getStateAtStateId: no state map found for state id ${stateId}`); } return this.buildStateFromStateMap(stateMap, roomVersion); @@ -817,9 +727,7 @@ export class StateService { } private async _isSameChain(stateIds: StateID[]) { - const stateDocs = await this.stateRepository - .findByStateIds(stateIds) - .toArray(); + const stateDocs = await this.stateRepository.findByStateIds(stateIds).toArray(); const chainIds = new Set(); const depths = new Set(); @@ -829,20 +737,13 @@ export class StateService { depths.add(stateDoc.depth); } - return ( - chainIds.size === 1 /* same chain */ && - depths.size === stateIds.length /* no branches */ - ); + return chainIds.size === 1 /* same chain */ && depths.size === stateIds.length /* no branches */; } - private async _resolveState( - stateIds: StateID[], - roomVersion: RoomVersion, - ): Promise { + private async _resolveState(stateIds: StateID[], roomVersion: RoomVersion): Promise { if (await this._isSameChain(stateIds)) { // pick the latest id from the chain, that's the state to use - const latestDelta = - await this.stateRepository.findLatestByStateIds(stateIds); + const latestDelta = await this.stateRepository.findLatestByStateIds(stateIds); if (!latestDelta) { throw new Error(`Failed to find latest state id from list ${stateIds}`); } @@ -852,13 +753,7 @@ export class StateService { return state; } - const stateLists = ( - await Promise.all( - stateIds - .values() - .map((stateId) => this.stateRepository.buildStateMapById(stateId)), - ) - ).reduce( + const stateLists = (await Promise.all(stateIds.values().map((stateId) => this.stateRepository.buildStateMapById(stateId)))).reduce( (accum, curr) => { if (curr) accum.push(curr); return accum; @@ -866,11 +761,7 @@ export class StateService { [] as Map[], ); - const states = await Promise.all( - stateLists.map((eventIdList) => - this.buildStateFromStateMap(eventIdList, roomVersion), - ), - ); + const states = await Promise.all(stateLists.map((eventIdList) => this.buildStateFromStateMap(eventIdList, roomVersion))); const [state1, ...rest] = states; const stateResRequired = [] as typeof states; @@ -885,21 +776,13 @@ export class StateService { } const [first, second] = firstPair; - if ( - !keys.every( - (key) => first.get(key)?.eventId === second.get(key)?.eventId, - ) - ) { + if (!keys.every((key) => first.get(key)?.eventId === second.get(key)?.eventId)) { // need resolution stateResRequired.push(first, second); } for (const [first, second] of paired) { - if ( - !keys.every( - (key) => first.get(key)?.eventId === second.get(key)?.eventId, - ) - ) { + if (!keys.every((key) => first.get(key)?.eventId === second.get(key)?.eventId)) { // need resolution stateResRequired.push(second); } @@ -914,23 +797,16 @@ export class StateService { return state; } - private async _resolveAndSaveState( - stateIds: StateID[], - roomVersion: RoomVersion, - ) { + private async _resolveAndSaveState(stateIds: StateID[], roomVersion: RoomVersion) { const state = await this._resolveState(stateIds, roomVersion); - const stateId = await this.stateRepository.createSnapshot( - state.values().toArray(), - ); + const stateId = await this.stateRepository.createSnapshot(state.values().toArray()); return { stateId, state }; } async _resolveStateAtEvent(event: PersistentEventBase) { - const stateIdList = this.eventRepository.findStateIdsByEventIds( - event.getPreviousEventIds(), - ); + const stateIdList = this.eventRepository.findStateIdsByEventIds(event.getPreviousEventIds()); const stateIds = new Set(); for await (const record of stateIdList) { stateIds.add(record.stateId); @@ -949,31 +825,21 @@ export class StateService { if (event.isPartial()) { // walked over to this, since we have the state at this event, toggle event to be not partial any longer - this.logger.debug( - { eventId: event.eventId }, - 'completing state at event', - ); + this.logger.debug({ eventId: event.eventId }, 'completing state at event'); // previous states by this point should NOT be partial event.setPartial(!event.isPartial()); } // different stateids, may need to run state resolution if (stateIds.size > 1) { - const { stateId, state } = await this._resolveAndSaveState( - stateIds.values().toArray(), - event.version, - ); + const { stateId, state } = await this._resolveAndSaveState(stateIds.values().toArray(), event.version); // save the event with this state // this is more like "state before event" // but for timeline events, it's all the same await this.addToRoomGraph(event, stateId); try { - await checkEventAuthWithState( - event, - state, - this._getStore(event.version), - ); + await checkEventAuthWithState(event, state, this._getStore(event.version)); } catch (error) { if (error instanceof StateResolverAuthorizationError) { event.reject(error.code, error.reason, error.rejectedBy); @@ -1010,9 +876,7 @@ export class StateService { const authEventIdsInEvent = new Set(event.getAuthEventIds()); - const authEventsWeHaveSeen = Array.from(authState.values()).map( - (e) => e.eventId, - ); + const authEventsWeHaveSeen = Array.from(authState.values()).map((e) => e.eventId); if ( authEventIdsInEvent.size !== authEventsWeHaveSeen.length || @@ -1026,11 +890,7 @@ export class StateService { 'auth events differ from event to our state, checking against state', ); try { - await checkEventAuthWithState( - event, - authState, - this._getStore(event.version), - ); + await checkEventAuthWithState(event, authState, this._getStore(event.version)); } catch (error) { if (error instanceof StateResolverAuthorizationError) { this.logger.warn({ error }, 'event not authorized against state'); @@ -1043,10 +903,7 @@ export class StateService { } } - this.logger.debug( - { eventId: event.eventId, stateId }, - "event authorized against event's state", - ); + this.logger.debug({ eventId: event.eventId, stateId }, "event authorized against event's state"); if (event.isTimelineEvent()) { // associate the event with the "previous state" @@ -1061,14 +918,9 @@ export class StateService { } async _mergeDivergentBranches(roomId: RoomID, roomVersion_?: RoomVersion) { - const roomVersion = roomVersion_ - ? roomVersion_ - : await this.getRoomVersion(roomId); + const roomVersion = roomVersion_ || (await this.getRoomVersion(roomId)); const records = await this.eventRepository.findLatestEvents(roomId); - this.logger.debug( - { roomId, events: records.map((r) => r._id) }, - 'current latest events', - ); + this.logger.debug({ roomId, events: records.map((r) => r._id) }, 'current latest events'); const stateIds = new Set(); for (const record of records) { stateIds.add(record.stateId); @@ -1088,10 +940,7 @@ export class StateService { return this.buildStateFromStateMap(stateMap, roomVersion); } - const state = await this._resolveState( - stateIds.values().toArray(), - roomVersion, - ); + const state = await this._resolveState(stateIds.values().toArray(), roomVersion); // if a new event says any of these events to be a previous event, we need the correct state for the event. // can not just update the state each events point to here, we must do it when a new event actually merges these branches @@ -1116,8 +965,7 @@ export class StateService { throw new Error('invalid event type'); } - const stateMap = - await this.stateRepository.buildPreviousStateMapById(stateId); + const stateMap = await this.stateRepository.buildPreviousStateMapById(stateId); if (!stateMap) { throw new Error(`No state found for event ${event.eventId}`); } @@ -1176,13 +1024,7 @@ export class StateService { const roomVersion = await this.getRoomVersion(roomId); return this.eventRepository .findPartialsByRoomId(roomId) - .map((rec) => - PersistentEventFactory.createFromRawEvent( - rec.event, - roomVersion, - rec.partial, - ), - ) + .map((rec) => PersistentEventFactory.createFromRawEvent(rec.event, roomVersion, rec.partial)) .toArray(); } } diff --git a/packages/federation-sdk/src/services/well-known.service.ts b/packages/federation-sdk/src/services/well-known.service.ts index b54182205..fa606c71b 100644 --- a/packages/federation-sdk/src/services/well-known.service.ts +++ b/packages/federation-sdk/src/services/well-known.service.ts @@ -1,5 +1,6 @@ import { singleton } from 'tsyringe'; -import { ConfigService } from './config.service'; + +import type { ConfigService } from './config.service'; @singleton() export class WellKnownService { diff --git a/packages/federation-sdk/src/specs/federation-api.ts b/packages/federation-sdk/src/specs/federation-api.ts index 24a482a83..3c0ffe47d 100644 --- a/packages/federation-sdk/src/specs/federation-api.ts +++ b/packages/federation-sdk/src/specs/federation-api.ts @@ -4,20 +4,13 @@ * See: https://spec.matrix.org/v1.7/server-server-api/ */ -import { - EventPduTypeRoomMember, - PduStateEventSchema, -} from '@rocket.chat/federation-room'; +import { EventPduTypeRoomMember, PduStateEventSchema } from '@rocket.chat/federation-room'; import { z } from 'zod'; // Common types export const EventIdSchema = z.string().regex(/^\$[A-Za-z0-9_-]+$/); -export const RoomIdSchema = z - .string() - .regex(/^![A-Za-z0-9_-]+:[A-Za-z0-9.-]+\.[A-Za-z]+$/); -export const UserIdSchema = z - .string() - .regex(/^@[A-Za-z0-9_=/.+-]+:[A-Za-z0-9.-]+\.[A-Za-z]+$/); +export const RoomIdSchema = z.string().regex(/^![A-Za-z0-9_-]+:[A-Za-z0-9.-]+\.[A-Za-z]+$/); +export const UserIdSchema = z.string().regex(/^@[A-Za-z0-9_=/.+-]+:[A-Za-z0-9.-]+\.[A-Za-z]+$/); export const ServerNameSchema = z.string().regex(/^[A-Za-z0-9.-]+\.[A-Za-z]+$/); // Federation API endpoints @@ -32,39 +25,28 @@ export const FederationEndpoints = { // Querying room state and events getStateIds: (roomId: string) => `/_matrix/federation/v1/state_ids/${roomId}`, getState: (roomId: string) => `/_matrix/federation/v1/state/${roomId}`, - getEvent: (eventId: string) => - `/_matrix/federation/v1/event/${encodeURIComponent(eventId)}`, - getEventAuth: (roomId: string, eventId: string) => - `/_matrix/federation/v1/event_auth/${roomId}/${encodeURIComponent(eventId)}`, + getEvent: (eventId: string) => `/_matrix/federation/v1/event/${encodeURIComponent(eventId)}`, + getEventAuth: (roomId: string, eventId: string) => `/_matrix/federation/v1/event_auth/${roomId}/${encodeURIComponent(eventId)}`, // Room backfill and missing events - getMissingEvents: (roomId: string) => - `/_matrix/federation/v1/get_missing_events/${roomId}`, + getMissingEvents: (roomId: string) => `/_matrix/federation/v1/get_missing_events/${roomId}`, backfill: (roomId: string) => `/_matrix/federation/v1/backfill/${roomId}`, // Joining/inviting and leaving rooms - makeJoin: (roomId: string, userId: string) => - `/_matrix/federation/v1/make_join/${roomId}/${userId}`, - sendJoin: (roomId: string, eventId: string) => - `/_matrix/federation/v1/send_join/${roomId}/${eventId}`, - sendJoinV2: (roomId: string, eventId: string) => - `/_matrix/federation/v2/send_join/${roomId}/${eventId}`, - makeLeave: (roomId: string, userId: string) => - `/_matrix/federation/v1/make_leave/${roomId}/${userId}`, - sendLeave: (roomId: string, eventId: string) => - `/_matrix/federation/v2/send_leave/${roomId}/${eventId}`, - invite: (roomId: string, eventId: string) => - `/_matrix/federation/v1/invite/${roomId}/${eventId}`, - inviteV2: (roomId: string, eventId: string) => - `/_matrix/federation/v2/invite/${roomId}/${eventId}`, + makeJoin: (roomId: string, userId: string) => `/_matrix/federation/v1/make_join/${roomId}/${userId}`, + sendJoin: (roomId: string, eventId: string) => `/_matrix/federation/v1/send_join/${roomId}/${eventId}`, + sendJoinV2: (roomId: string, eventId: string) => `/_matrix/federation/v2/send_join/${roomId}/${eventId}`, + makeLeave: (roomId: string, userId: string) => `/_matrix/federation/v1/make_leave/${roomId}/${userId}`, + sendLeave: (roomId: string, eventId: string) => `/_matrix/federation/v2/send_leave/${roomId}/${eventId}`, + invite: (roomId: string, eventId: string) => `/_matrix/federation/v1/invite/${roomId}/${eventId}`, + inviteV2: (roomId: string, eventId: string) => `/_matrix/federation/v2/invite/${roomId}/${eventId}`, // Sending events sendTransaction: (txnId: string) => `/_matrix/federation/v1/send/${txnId}`, // User and profile data queryProfile: (_userId: string) => '/_matrix/federation/v1/query/profile', - userDevices: (userId: string) => - `/_matrix/federation/v1/user/devices/${userId}`, + userDevices: (userId: string) => `/_matrix/federation/v1/user/devices/${userId}`, // Public room directory publicRooms: '/_matrix/federation/v1/publicRooms', @@ -174,6 +156,4 @@ export type SendJoinEventResponseSchema = z.infer; export type MakeJoinResponse = z.infer; export type SendJoinResponse = z.infer; export type Transaction = z.infer; -export type SendTransactionResponse = z.infer< - typeof SendTransactionResponseSchema ->; +export type SendTransactionResponse = z.infer; diff --git a/packages/federation-sdk/src/utils/event-schemas.ts b/packages/federation-sdk/src/utils/event-schemas.ts index e07cb119d..f604fef39 100644 --- a/packages/federation-sdk/src/utils/event-schemas.ts +++ b/packages/federation-sdk/src/utils/event-schemas.ts @@ -1,8 +1,4 @@ -import { - eventIdSchema, - roomIdSchema, - userIdSchema, -} from '@rocket.chat/federation-room'; +import { eventIdSchema, roomIdSchema, userIdSchema } from '@rocket.chat/federation-room'; import { z } from 'zod'; const baseEventSchema = z.object({ @@ -14,12 +10,8 @@ const baseEventSchema = z.object({ event_id: eventIdSchema.optional(), state_key: userIdSchema.or(z.literal('')).optional(), depth: z.number().int().nonnegative().optional(), - prev_events: z - .array(z.string().or(z.tuple([z.string(), z.string()]))) - .optional(), - auth_events: z - .array(z.string().or(z.tuple([z.string(), z.string()]))) - .optional(), + prev_events: z.array(z.string().or(z.tuple([z.string(), z.string()]))).optional(), + auth_events: z.array(z.string().or(z.tuple([z.string(), z.string()]))).optional(), redacts: eventIdSchema.optional(), hashes: z.record(z.string()).optional(), signatures: z.record(z.record(z.string())).optional(), @@ -120,7 +112,7 @@ const roomV10Schemas = { 'm.room.join_rules': joinRulesEventSchema, 'm.reaction': reactionEventSchema, 'm.room.redaction': redactionEventSchema, - default: baseEventSchema, + 'default': baseEventSchema, }; export const eventSchemas: Record> = { diff --git a/packages/federation-sdk/src/utils/get-error-message.spec.ts b/packages/federation-sdk/src/utils/get-error-message.spec.ts index c24e4ab69..5d6dc3207 100644 --- a/packages/federation-sdk/src/utils/get-error-message.spec.ts +++ b/packages/federation-sdk/src/utils/get-error-message.spec.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'bun:test'; + import { getErrorMessage } from './get-error-message'; describe('getErrorMessage', () => { diff --git a/packages/federation-sdk/src/utils/response-codes.ts b/packages/federation-sdk/src/utils/response-codes.ts index 57b230337..c6ee96f53 100644 --- a/packages/federation-sdk/src/utils/response-codes.ts +++ b/packages/federation-sdk/src/utils/response-codes.ts @@ -1,7 +1,4 @@ -export const errCodes: Record< - string, - { errcode: string; error: string; status: 401 | 403 | 500 } -> = { +export const errCodes: Record = { M_UNAUTHORIZED: { errcode: 'M_UNAUTHORIZED', error: 'Invalid or missing signature', diff --git a/packages/federation-sdk/src/utils/signJson.spec.ts b/packages/federation-sdk/src/utils/signJson.spec.ts index fc52d9eba..17d1d5c5c 100644 --- a/packages/federation-sdk/src/utils/signJson.spec.ts +++ b/packages/federation-sdk/src/utils/signJson.spec.ts @@ -1,4 +1,5 @@ import { describe, expect, it, test } from 'bun:test'; + import { EncryptionValidAlgorithm, generateKeyPairsFromString, @@ -10,93 +11,64 @@ import { describe('verifySignaturesFromRemote', async () => { test('it should verify a valid signature', async () => { const serverName = 'synapse'; - const signature = await generateKeyPairsFromString( - 'ed25519 a_yNbw tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8', - ); + const signature = await generateKeyPairsFromString('ed25519 a_yNbw tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8'); const signed = await signJson({}, signature, serverName); - await verifySignaturesFromRemote( - signed, - serverName, - async () => signature.publicKey, - ); - - expect( - async () => - await verifySignaturesFromRemote( - signed, - serverName, - async () => signature.publicKey, - ), - ).not.toThrow(); + await verifySignaturesFromRemote(signed, serverName, async () => signature.publicKey); + + expect(async () => { + await verifySignaturesFromRemote(signed, serverName, async () => signature.publicKey); + }).not.toThrow(); }); test('it should throw an error if the signature is invalid', async () => { const serverName = 'synapse'; - const signature = await generateKeyPairsFromString( - 'ed25519 a_yNbw tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8', - ); + const signature = await generateKeyPairsFromString('ed25519 a_yNbw tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8'); const signed = await signJson({}, signature, serverName); - expect( - async () => - await verifySignaturesFromRemote(signed, serverName, async () => - Uint8Array.from( - atob('tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8'), - (c) => c.charCodeAt(0), - ), - ), - ).toThrow(); + expect(async () => { + await verifySignaturesFromRemote(signed, serverName, async () => + Uint8Array.from(atob('tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8'), (c) => c.charCodeAt(0)), + ); + }).toThrow(); }); test('it should throw an error if there is no valid protocol version', async () => { const serverName = 'synapse'; - expect( - async () => - await verifySignaturesFromRemote( - { - signatures: { - [serverName]: { - [`${EncryptionValidAlgorithm.ed25519}1:a_yNbw`]: 'invalid', - }, + expect(async () => { + await verifySignaturesFromRemote( + { + signatures: { + [serverName]: { + [`${EncryptionValidAlgorithm.ed25519}1:a_yNbw`]: 'invalid', }, }, - serverName, - async () => - Uint8Array.from( - atob('tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8'), - (c) => c.charCodeAt(0), - ), - ), - ).toThrow( - `Invalid algorithm ${EncryptionValidAlgorithm.ed25519}1 for ${serverName}`, - ); + }, + serverName, + async () => Uint8Array.from(atob('tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8'), (c) => c.charCodeAt(0)), + ); + }).toThrow(`Invalid algorithm ${EncryptionValidAlgorithm.ed25519}1 for ${serverName}`); }); it('it should throw an error if the signature is invalid for the serverName', async () => { const serverName = 'synapse'; - expect( - async () => - await verifySignaturesFromRemote( - { - signatures: { - differentServer: { - [`${EncryptionValidAlgorithm.ed25519}1:a_yNbw`]: 'invalid', - }, + expect(async () => { + await verifySignaturesFromRemote( + { + signatures: { + differentServer: { + [`${EncryptionValidAlgorithm.ed25519}1:a_yNbw`]: 'invalid', }, }, - serverName, - async () => - Uint8Array.from( - atob('tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8'), - (c) => c.charCodeAt(0), - ), - ), - ).toThrow(`Signatures not found for ${serverName}`); + }, + serverName, + async () => Uint8Array.from(atob('tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8'), (c) => c.charCodeAt(0)), + ); + }).toThrow(`Signatures not found for ${serverName}`); }); }); @@ -145,9 +117,7 @@ describe('verifySignaturesFromRemote', async () => { // } test('signJson send_join', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 a_yNbw tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8', - ); + const signature = await generateKeyPairsFromString('ed25519 a_yNbw tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8'); const signed = await signJson( pruneEventDict({ @@ -209,9 +179,7 @@ test('signJson send_join', async () => { // } test('signJson make_join', async () => { - const signature = await generateKeyPairsFromString( - 'ed25519 a_yNbw tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8', - ); + const signature = await generateKeyPairsFromString('ed25519 a_yNbw tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8'); const signed = await signJson( { @@ -220,8 +188,7 @@ test('signJson make_join', async () => { origin: 'synapse2', signatures: { synapse2: { - 'ed25519:a_yNbw': - 'YmiPpEE6QAeZzd8qw/FUXw2kiu0FJsApxnDfnbZmcEY5gFP97srHRqRzLjBn8Rh38nV2ZFTPXOV2Req+7JR1Bw', + 'ed25519:a_yNbw': 'YmiPpEE6QAeZzd8qw/FUXw2kiu0FJsApxnDfnbZmcEY5gFP97srHRqRzLjBn8Rh38nV2ZFTPXOV2Req+7JR1Bw', }, }, uri: '/_matrix/federation/v1/make_join/%21JVkUxGlBLsuOwTBUpN%3Asynapse1/%40rodrigo2%3Asynapse2?ver=1&ver=2&ver=3&ver=4&ver=5&ver=6&ver=7&ver=8&ver=9&ver=10&ver=11&ver=org.matrix.msc3757.10&ver=org.matrix.msc3757.11', diff --git a/packages/homeserver/.eslintrc.json b/packages/homeserver/.eslintrc.json new file mode 100644 index 000000000..be97c53fb --- /dev/null +++ b/packages/homeserver/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.eslintrc.json" +} diff --git a/packages/homeserver/src/controllers/federation/invite.controller.ts b/packages/homeserver/src/controllers/federation/invite.controller.ts index fc6249c89..e72ca265e 100644 --- a/packages/homeserver/src/controllers/federation/invite.controller.ts +++ b/packages/homeserver/src/controllers/federation/invite.controller.ts @@ -1,13 +1,12 @@ -import { EventID, RoomID } from '@rocket.chat/federation-room'; +import type { EventID } from '@rocket.chat/federation-room'; +import { RoomID } from '@rocket.chat/federation-room'; import { NotAllowedError, federationSDK } from '@rocket.chat/federation-sdk'; +import type { Elysia } from 'elysia'; +import { t } from 'elysia'; + import { isAuthenticatedMiddleware } from '@rocket.chat/homeserver/middlewares/isAuthenticated'; -import { Elysia, t } from 'elysia'; -import { - FederationErrorResponseDto, - ProcessInviteParamsDto, - ProcessInviteResponseDto, - RoomVersionDto, -} from '../../dtos'; + +import { FederationErrorResponseDto, ProcessInviteParamsDto, ProcessInviteResponseDto, RoomVersionDto } from '../../dtos'; export const invitePlugin = (app: Elysia) => { return app.use(isAuthenticatedMiddleware()).put( @@ -18,29 +17,20 @@ export const invitePlugin = (app: Elysia) => { } try { - return await federationSDK.processInvite( - body.event, - eventId as EventID, - body.room_version, - body.invite_room_state, - ); + return await federationSDK.processInvite(body.event, eventId as EventID, body.room_version, body.invite_room_state); } catch (error) { if (error instanceof NotAllowedError) { set.status = 403; return { errcode: 'M_FORBIDDEN', - error: - 'This server does not allow joining this type of room based on federation settings.', + error: 'This server does not allow joining this type of room based on federation settings.', }; } set.status = 500; return { errcode: 'M_UNKNOWN', - error: - error instanceof Error - ? error.message - : 'Internal server error while processing request', + error: error instanceof Error ? error.message : 'Internal server error while processing request', }; } }, diff --git a/packages/homeserver/src/controllers/federation/media.controller.ts b/packages/homeserver/src/controllers/federation/media.controller.ts index 49b4f0315..5e5ddaaf9 100644 --- a/packages/homeserver/src/controllers/federation/media.controller.ts +++ b/packages/homeserver/src/controllers/federation/media.controller.ts @@ -1,6 +1,8 @@ +import type { Elysia } from 'elysia'; +import { t } from 'elysia'; + import { canAccessResourceMiddleware } from '@rocket.chat/homeserver/middlewares/canAccessResource'; import { isAuthenticatedMiddleware } from '@rocket.chat/homeserver/middlewares/isAuthenticated'; -import { Elysia, t } from 'elysia'; const ErrorResponseSchema = t.Object({ errcode: t.Literal('M_UNRECOGNIZED'), @@ -127,9 +129,7 @@ export const mediaPlugin = (app: Elysia) => { query: t.Object({ width: t.Optional(t.Number({ minimum: 1, maximum: 800 })), height: t.Optional(t.Number({ minimum: 1, maximum: 600 })), - method: t.Optional( - t.Union([t.Literal('crop'), t.Literal('scale')]), - ), + method: t.Optional(t.Union([t.Literal('crop'), t.Literal('scale')])), allow_remote: t.Optional(t.Boolean()), timeout_ms: t.Optional(t.Number()), }), diff --git a/packages/homeserver/src/controllers/federation/profiles.controller.ts b/packages/homeserver/src/controllers/federation/profiles.controller.ts index d84c1fa48..bba05d60a 100644 --- a/packages/homeserver/src/controllers/federation/profiles.controller.ts +++ b/packages/homeserver/src/controllers/federation/profiles.controller.ts @@ -1,8 +1,11 @@ -import { EventID, RoomID, UserID } from '@rocket.chat/federation-room'; +import type { EventID, RoomID, UserID } from '@rocket.chat/federation-room'; import { federationSDK } from '@rocket.chat/federation-sdk'; +import type { Elysia } from 'elysia'; +import { t } from 'elysia'; + import { canAccessResourceMiddleware } from '@rocket.chat/homeserver/middlewares/canAccessResource'; import { isAuthenticatedMiddleware } from '@rocket.chat/homeserver/middlewares/isAuthenticated'; -import { Elysia, t } from 'elysia'; + import { ErrorResponseDto, EventAuthParamsDto, @@ -26,22 +29,17 @@ export const profilesPlugin = (app: Elysia) => { .group('/_matrix', (app) => app .use(isAuthenticatedMiddleware()) - .get( - '/federation/v1/query/profile', - ({ query: { user_id } }) => - federationSDK.queryProfile(user_id as UserID), - { - query: QueryProfileQueryDto, - response: { - 200: QueryProfileResponseDto, - }, - detail: { - tags: ['Federation'], - summary: 'Query profile', - description: "Query a user's profile", - }, + .get('/federation/v1/query/profile', ({ query: { user_id } }) => federationSDK.queryProfile(user_id as UserID), { + query: QueryProfileQueryDto, + response: { + 200: QueryProfileResponseDto, }, - ) + detail: { + tags: ['Federation'], + summary: 'Query profile', + description: "Query a user's profile", + }, + }) .post( '/federation/v1/user/keys/query', async ({ set }) => { @@ -63,8 +61,7 @@ export const profilesPlugin = (app: Elysia) => { detail: { tags: ['Federation'], summary: 'Query keys', - description: - "Query a user's device keys (E2EE not implemented)", + description: "Query a user's device keys (E2EE not implemented)", }, }, ) @@ -102,9 +99,7 @@ export const profilesPlugin = (app: Elysia) => { // const { ver } = query; - return federationSDK.makeJoin(roomId as RoomID, userId as UserID, [ - '10', - ]); + return federationSDK.makeJoin(roomId as RoomID, userId as UserID, ['10']); }, { params: MakeJoinParamsDto, @@ -147,11 +142,7 @@ export const profilesPlugin = (app: Elysia) => { ) .get( '/_matrix/federation/v1/event_auth/:roomId/:eventId', - ({ params }) => - federationSDK.eventAuth( - params.roomId as RoomID, - params.eventId as EventID, - ), + ({ params }) => federationSDK.eventAuth(params.roomId as RoomID, params.eventId as EventID), { params: EventAuthParamsDto, response: { diff --git a/packages/homeserver/src/controllers/federation/rooms.controller.ts b/packages/homeserver/src/controllers/federation/rooms.controller.ts index dcf5b70f1..5faa52be5 100644 --- a/packages/homeserver/src/controllers/federation/rooms.controller.ts +++ b/packages/homeserver/src/controllers/federation/rooms.controller.ts @@ -1,6 +1,8 @@ import { federationSDK } from '@rocket.chat/federation-sdk'; +import type { Elysia } from 'elysia'; +import { t } from 'elysia'; + import { isAuthenticatedMiddleware } from '@rocket.chat/homeserver/middlewares/isAuthenticated'; -import { Elysia, t } from 'elysia'; export const roomPlugin = (app: Elysia) => { return app @@ -67,9 +69,7 @@ export const roomPlugin = (app: Elysia) => { chunk: publicRooms .filter((r) => { if (filter.generic_search_term) { - return r.name - .toLowerCase() - .includes(filter.generic_search_term.toLowerCase()); + return r.name.toLowerCase().includes(filter.generic_search_term.toLowerCase()); } if (filter.room_types) { diff --git a/packages/homeserver/src/controllers/federation/send-join.controller.ts b/packages/homeserver/src/controllers/federation/send-join.controller.ts index b4d65b084..c83c932c8 100644 --- a/packages/homeserver/src/controllers/federation/send-join.controller.ts +++ b/packages/homeserver/src/controllers/federation/send-join.controller.ts @@ -1,12 +1,11 @@ import type { EventID, RoomID } from '@rocket.chat/federation-room'; import { federationSDK } from '@rocket.chat/federation-sdk'; +import type { Elysia } from 'elysia'; +import { t } from 'elysia'; + import { canAccessResourceMiddleware } from '@rocket.chat/homeserver/middlewares/canAccessResource'; -import { Elysia, t } from 'elysia'; -import { - ErrorResponseDto, - SendJoinEventDto, - SendJoinResponseDto, -} from '../../dtos'; + +import { ErrorResponseDto, SendJoinEventDto, SendJoinResponseDto } from '../../dtos'; export const sendJoinPlugin = (app: Elysia) => { return app.use(canAccessResourceMiddleware('room')).put( @@ -18,11 +17,7 @@ export const sendJoinPlugin = (app: Elysia) => { }) => { const { roomId, eventId } = params; - return federationSDK.sendJoin( - roomId as RoomID, - eventId as EventID, - body as any, - ); + return federationSDK.sendJoin(roomId as RoomID, eventId as EventID, body as any); }, { params: t.Object({ diff --git a/packages/homeserver/src/controllers/federation/state.controller.ts b/packages/homeserver/src/controllers/federation/state.controller.ts index d09df7e3e..7591696f6 100644 --- a/packages/homeserver/src/controllers/federation/state.controller.ts +++ b/packages/homeserver/src/controllers/federation/state.controller.ts @@ -1,7 +1,9 @@ -import { EventID, RoomID } from '@rocket.chat/federation-room'; +import type { EventID, RoomID } from '@rocket.chat/federation-room'; import { federationSDK } from '@rocket.chat/federation-sdk'; +import type { Elysia } from 'elysia'; + import { canAccessResourceMiddleware } from '@rocket.chat/homeserver/middlewares/canAccessResource'; -import { Elysia } from 'elysia'; + import { ErrorResponseDto, GetStateIdsParamsDto, @@ -17,11 +19,7 @@ export const statePlugin = (app: Elysia) => { .use(canAccessResourceMiddleware('room')) .get( '/_matrix/federation/v1/state_ids/:roomId', - ({ params, query }) => - federationSDK.getStateIds( - params.roomId as RoomID, - query.event_id as EventID, - ), + ({ params, query }) => federationSDK.getStateIds(params.roomId as RoomID, query.event_id as EventID), { params: GetStateIdsParamsDto, query: GetStateIdsQueryDto, @@ -38,11 +36,7 @@ export const statePlugin = (app: Elysia) => { ) .get( '/_matrix/federation/v1/state/:roomId', - ({ params, query }) => - federationSDK.getState( - params.roomId as RoomID, - query.event_id as EventID, - ), + ({ params, query }) => federationSDK.getState(params.roomId as RoomID, query.event_id as EventID), { params: GetStateParamsDto, query: GetStateQueryDto, diff --git a/packages/homeserver/src/controllers/federation/transactions.controller.ts b/packages/homeserver/src/controllers/federation/transactions.controller.ts index d1928bbc6..48274fa83 100644 --- a/packages/homeserver/src/controllers/federation/transactions.controller.ts +++ b/packages/homeserver/src/controllers/federation/transactions.controller.ts @@ -1,8 +1,10 @@ -import { EventID, RoomID } from '@rocket.chat/federation-room'; +import type { EventID, RoomID } from '@rocket.chat/federation-room'; import { federationSDK } from '@rocket.chat/federation-sdk'; +import type { Elysia } from 'elysia'; + import { canAccessResourceMiddleware } from '@rocket.chat/homeserver/middlewares/canAccessResource'; import { isAuthenticatedMiddleware } from '@rocket.chat/homeserver/middlewares/isAuthenticated'; -import { Elysia } from 'elysia'; + import { BackfillErrorResponseDto, BackfillParamsDto, @@ -22,7 +24,6 @@ export const transactionsPlugin = (app: Elysia) => { '/_matrix/federation/v1/send/:txnId', async ({ body }) => { // TODO need to validate better the payload - // biome-ignore lint/suspicious/noExplicitAny: await federationSDK.processIncomingTransaction(body as any); return { @@ -50,9 +51,7 @@ export const transactionsPlugin = (app: Elysia) => { async ({ params, set }) => { const serverName = federationSDK.getConfig('serverName'); - const eventData = await federationSDK.getEventById( - params.eventId as EventID, - ); + const eventData = await federationSDK.getEventById(params.eventId as EventID); if (!eventData) { set.status = 404; return { @@ -89,7 +88,7 @@ export const transactionsPlugin = (app: Elysia) => { '/_matrix/federation/v1/backfill/:roomId', async ({ params, query, set }) => { try { - const limit = query.limit; + const { limit } = query; const eventIdParam = query.v; if (!eventIdParam) { set.status = 400; @@ -99,15 +98,9 @@ export const transactionsPlugin = (app: Elysia) => { }; } - const eventIds = Array.isArray(eventIdParam) - ? eventIdParam - : [eventIdParam]; + const eventIds = Array.isArray(eventIdParam) ? eventIdParam : [eventIdParam]; - return federationSDK.getBackfillEvents( - params.roomId as RoomID, - eventIds as EventID[], - limit, - ); + return federationSDK.getBackfillEvents(params.roomId as RoomID, eventIds as EventID[], limit); } catch { set.status = 500; return { @@ -131,8 +124,7 @@ export const transactionsPlugin = (app: Elysia) => { detail: { tags: ['Federation'], summary: 'Backfill room events', - description: - 'Retrieves a sliding-window history of previous PDUs that occurred in the given room', + description: 'Retrieves a sliding-window history of previous PDUs that occurred in the given room', }, }, ); diff --git a/packages/homeserver/src/controllers/federation/versions.controller.ts b/packages/homeserver/src/controllers/federation/versions.controller.ts index acd34d28b..03569ab7e 100644 --- a/packages/homeserver/src/controllers/federation/versions.controller.ts +++ b/packages/homeserver/src/controllers/federation/versions.controller.ts @@ -1,5 +1,6 @@ import { federationSDK } from '@rocket.chat/federation-sdk'; -import { Elysia } from 'elysia'; +import type { Elysia } from 'elysia'; + import { GetVersionsResponseDto } from '../../dtos'; export const versionsPlugin = (app: Elysia) => { @@ -12,7 +13,7 @@ export const versionsPlugin = (app: Elysia) => { return { server: { name: serverName, - version: version, + version, }, }; }, diff --git a/packages/homeserver/src/controllers/internal/direct-message.controller.ts b/packages/homeserver/src/controllers/internal/direct-message.controller.ts index 89145c0bb..93983c3f2 100644 --- a/packages/homeserver/src/controllers/internal/direct-message.controller.ts +++ b/packages/homeserver/src/controllers/internal/direct-message.controller.ts @@ -1,7 +1,9 @@ -import { UserID } from '@rocket.chat/federation-room'; +import type { UserID } from '@rocket.chat/federation-room'; import { federationSDK } from '@rocket.chat/federation-sdk'; -import { Elysia, t } from 'elysia'; +import type { Elysia } from 'elysia'; +import { t } from 'elysia'; import { container } from 'tsyringe'; + import { type ErrorResponse, ErrorResponseDto } from '../../dtos'; // DTOs for direct message operations @@ -14,24 +16,16 @@ export const InternalDirectMessageResponseDto = t.Object({ roomId: t.String(), }); -export type InternalCreateDirectMessageBody = - typeof InternalCreateDirectMessageBodyDto.static; -export type InternalDirectMessageResponse = - typeof InternalDirectMessageResponseDto.static; +export type InternalCreateDirectMessageBody = typeof InternalCreateDirectMessageBodyDto.static; +export type InternalDirectMessageResponse = typeof InternalDirectMessageResponseDto.static; export const internalDirectMessagePlugin = (app: Elysia) => { return app.post( '/internal/direct-messages/create', - async ({ - body, - set, - }): Promise => { + async ({ body, set }): Promise => { const { senderUserId, targetUserId } = body; try { - const roomId = await federationSDK.createDirectMessageRoom( - senderUserId as UserID, - targetUserId as UserID, - ); + const roomId = await federationSDK.createDirectMessageRoom(senderUserId as UserID, targetUserId as UserID); return { roomId, }; diff --git a/packages/homeserver/src/controllers/internal/external-federation-request.controller.ts b/packages/homeserver/src/controllers/internal/external-federation-request.controller.ts index 20c0cef78..452d5967f 100644 --- a/packages/homeserver/src/controllers/internal/external-federation-request.controller.ts +++ b/packages/homeserver/src/controllers/internal/external-federation-request.controller.ts @@ -8,7 +8,8 @@ import { type UserID, } from '@rocket.chat/federation-room'; import { federationSDK } from '@rocket.chat/federation-sdk'; -import { Elysia, t } from 'elysia'; +import type { Elysia } from 'elysia'; +import { t } from 'elysia'; export const internalRequestPlugin = (app: Elysia) => { app.post( @@ -34,12 +35,9 @@ export const internalRequestPlugin = (app: Elysia) => { description: 'the endpoint uri, roomid user id and all', }), body: t.Optional(t.Any({ description: 'the body to send, if any' })), - method: t.Union( - [t.Literal('GET'), t.Literal('POST'), t.Literal('PUT')], - { - description: 'the method to use', - }, - ), + method: t.Union([t.Literal('GET'), t.Literal('POST'), t.Literal('PUT')], { + description: 'the method to use', + }), query: t.Optional( t.Record(t.String(), t.String(), { description: 'query parameters to append to the url', @@ -63,9 +61,7 @@ export const internalRequestPlugin = (app: Elysia) => { roomId: RoomID; sender: UserID; }; - const version = - (query.version as RoomVersion | undefined) || - PersistentEventFactory.defaultRoomVersion; + const version = (query.version as RoomVersion | undefined) || PersistentEventFactory.defaultRoomVersion; switch (eventType) { case 'm.room.member': { const event = await federationSDK.buildEvent<'m.room.member'>( @@ -198,8 +194,7 @@ export const internalRequestPlugin = (app: Elysia) => { detail: { tags: ['Devtools'], summary: 'Event template', - description: - 'Get an event template to fill and send, use /internal/event/send to send it', + description: 'Get an event template to fill and send, use /internal/event/send to send it', }, }, ); @@ -208,13 +203,11 @@ export const internalRequestPlugin = (app: Elysia) => { '/internal/event/send', async ({ body, query }) => { const event = body as Pdu; - const version = - (query.version as RoomVersion | undefined) || - PersistentEventFactory.defaultRoomVersion; + const version = (query.version as RoomVersion | undefined) || PersistentEventFactory.defaultRoomVersion; if (!PersistentEventFactory.isSupportedRoomVersion(version)) { throw new Error(`Room version ${version} is not supported`); } - // @ts-ignore + // @ts-expect-error defining as undefined to force to be recalculated event.hashes = undefined; // force to be recalculated const pdu = PersistentEventFactory.createFromRawEvent(event, version); if (!pdu) { @@ -230,8 +223,7 @@ export const internalRequestPlugin = (app: Elysia) => { detail: { tags: ['Devtools'], summary: 'Send an event', - description: - 'Send an event, you can get templates from /internal/event/template/:eventType/:roomId/:sender', + description: 'Send an event, you can get templates from /internal/event/template/:eventType/:roomId/:sender', }, }, ); diff --git a/packages/homeserver/src/controllers/internal/invite.controller.ts b/packages/homeserver/src/controllers/internal/invite.controller.ts index 197f25e13..6f942d075 100644 --- a/packages/homeserver/src/controllers/internal/invite.controller.ts +++ b/packages/homeserver/src/controllers/internal/invite.controller.ts @@ -1,12 +1,11 @@ -import { - PersistentEventFactory, - RoomID, - UserID, -} from '@rocket.chat/federation-room'; +import type { RoomID, UserID } from '@rocket.chat/federation-room'; +import { PersistentEventFactory } from '@rocket.chat/federation-room'; import { federationSDK } from '@rocket.chat/federation-sdk'; -import { Elysia } from 'elysia'; -import { type ErrorResponse, ErrorResponseDto } from '../../dtos'; +import type { Elysia } from 'elysia'; + import { + type ErrorResponse, + ErrorResponseDto, InternalInviteUserBodyDto, type InternalInviteUserResponse, InternalInviteUserResponseDto, diff --git a/packages/homeserver/src/controllers/internal/message.controller.ts b/packages/homeserver/src/controllers/internal/message.controller.ts index ce88c8aec..c59178703 100644 --- a/packages/homeserver/src/controllers/internal/message.controller.ts +++ b/packages/homeserver/src/controllers/internal/message.controller.ts @@ -1,8 +1,10 @@ -import { EventID, RoomID, UserID } from '@rocket.chat/federation-room'; +import type { EventID, RoomID, UserID } from '@rocket.chat/federation-room'; import { federationSDK } from '@rocket.chat/federation-sdk'; -import { Elysia } from 'elysia'; -import { type ErrorResponse, ErrorResponseDto } from '../../dtos'; +import type { Elysia } from 'elysia'; + import { + type ErrorResponse, + ErrorResponseDto, type InternalMessageResponse, InternalMessageResponseDto, type InternalReactionResponse, @@ -22,11 +24,7 @@ export const internalMessagePlugin = (app: Elysia) => { return app .patch( '/internal/messages/:messageId', - async ({ - params, - body, - set, - }): Promise => { + async ({ params, body, set }): Promise => { const { roomId, message, senderUserId } = body; try { const eventId = await federationSDK.updateMessage( @@ -64,19 +62,10 @@ export const internalMessagePlugin = (app: Elysia) => { ) .post( '/internal/messages/:messageId/reactions', - async ({ - params, - body, - set, - }): Promise => { + async ({ params, body, set }): Promise => { const { roomId, emoji, senderUserId } = body; try { - const eventId = await federationSDK.sendReaction( - roomId as RoomID, - params.messageId as EventID, - emoji, - senderUserId as UserID, - ); + const eventId = await federationSDK.sendReaction(roomId as RoomID, params.messageId as EventID, emoji, senderUserId as UserID); return { event_id: eventId, origin_server_ts: Date.now(), @@ -105,19 +94,10 @@ export const internalMessagePlugin = (app: Elysia) => { ) .delete( '/internal/messages/:messageId/reactions', - async ({ - params, - body, - set, - }): Promise => { + async ({ params, body, set }): Promise => { const { roomId, emoji, senderUserId } = body; try { - const eventId = await federationSDK.unsetReaction( - roomId as RoomID, - params.messageId as EventID, - emoji, - senderUserId as UserID, - ); + const eventId = await federationSDK.unsetReaction(roomId as RoomID, params.messageId as EventID, emoji, senderUserId as UserID); return { event_id: eventId, origin_server_ts: Date.now(), @@ -146,15 +126,9 @@ export const internalMessagePlugin = (app: Elysia) => { ) .delete( '/internal/messages/:messageId', - async ({ - params, - body, - }): Promise => { + async ({ params, body }): Promise => { const { roomId } = body; - const eventId = await federationSDK.redactMessage( - roomId as RoomID, - params.messageId as EventID, - ); + const eventId = await federationSDK.redactMessage(roomId as RoomID, params.messageId as EventID); return { event_id: eventId, diff --git a/packages/homeserver/src/controllers/internal/ping.controller.ts b/packages/homeserver/src/controllers/internal/ping.controller.ts index 580d01e8a..f12343228 100644 --- a/packages/homeserver/src/controllers/internal/ping.controller.ts +++ b/packages/homeserver/src/controllers/internal/ping.controller.ts @@ -1,4 +1,5 @@ -import { Elysia } from 'elysia'; +import type { Elysia } from 'elysia'; + import { InternalPingResponseDto } from '../../dtos/internal/ping.dto'; export const pingPlugin = (app: Elysia) => diff --git a/packages/homeserver/src/controllers/internal/room.controller.ts b/packages/homeserver/src/controllers/internal/room.controller.ts index a91d93a86..a418c25af 100644 --- a/packages/homeserver/src/controllers/internal/room.controller.ts +++ b/packages/homeserver/src/controllers/internal/room.controller.ts @@ -1,19 +1,14 @@ -import { - EventID, - type PduCreateEventContent, - PersistentEventFactory, - RoomID, - UserID, -} from '@rocket.chat/federation-room'; +import { PersistentEventFactory } from '@rocket.chat/federation-room'; +import type { EventID, RoomID, UserID } from '@rocket.chat/federation-room'; import { federationSDK } from '@rocket.chat/federation-sdk'; -import { Elysia, t } from 'elysia'; +import type { Elysia } from 'elysia'; +import { t } from 'elysia'; + import { type ErrorResponse, ErrorResponseDto, RoomIdDto, UsernameDto, -} from '../../dtos'; -import { InternalBanUserBodyDto, InternalBanUserParamsDto, type InternalBanUserResponse, @@ -65,11 +60,7 @@ export const internalRoomPlugin = (app: Elysia) => { ) .put( '/internal/rooms/:roomId/name', - async ({ - params, - body, - set, - }): Promise => { + async ({ params, body, set }): Promise => { const roomIdParse = RoomIdDto.safeParse(params.roomId); const bodyParse = InternalUpdateRoomNameBodyDto.safeParse(body); if (!roomIdParse.success || !bodyParse.success) { @@ -83,11 +74,7 @@ export const internalRoomPlugin = (app: Elysia) => { }; } const { name, senderUserId } = bodyParse.data; - return federationSDK.updateRoomName( - roomIdParse.data as RoomID, - name, - senderUserId as UserID, - ); + return federationSDK.updateRoomName(roomIdParse.data as RoomID, name, senderUserId as UserID); }, { params: InternalUpdateRoomNameParamsDto, @@ -105,19 +92,11 @@ export const internalRoomPlugin = (app: Elysia) => { ) .put( '/internal/rooms/:roomId/permissions/:userId', - async ({ - params, - body, - set, - }): Promise => { + async ({ params, body, set }): Promise => { const roomIdParse = RoomIdDto.safeParse(params.roomId); const userIdParse = UsernameDto.safeParse(params.userId); const bodyParse = InternalUpdateUserPowerLevelBodyDto.safeParse(body); - if ( - !roomIdParse.success || - !userIdParse.success || - !bodyParse.success - ) { + if (!roomIdParse.success || !userIdParse.success || !bodyParse.success) { set.status = 400; return { error: 'Invalid request', @@ -192,11 +171,7 @@ export const internalRoomPlugin = (app: Elysia) => { ) .put( '/internal/rooms/:roomId/leave', - async ({ - params, - body, - set, - }): Promise => { + async ({ params, body, set }): Promise => { const roomIdParse = RoomIdDto.safeParse(params.roomId); const bodyParse = InternalLeaveRoomBodyDto.safeParse(body); if (!roomIdParse.success || !bodyParse.success) { @@ -211,10 +186,7 @@ export const internalRoomPlugin = (app: Elysia) => { } const { senderUserId } = bodyParse.data; try { - const eventId = await federationSDK.leaveRoom( - roomIdParse.data as RoomID, - senderUserId as UserID, - ); + const eventId = await federationSDK.leaveRoom(roomIdParse.data as RoomID, senderUserId as UserID); return { eventId }; } catch (error) { set.status = 500; @@ -240,19 +212,11 @@ export const internalRoomPlugin = (app: Elysia) => { ) .put( '/internal/rooms/:roomId/kick/:memberId', - async ({ - params, - body, - set, - }): Promise => { + async ({ params, body, set }): Promise => { const roomIdParse = RoomIdDto.safeParse(params.roomId); const memberIdParse = UsernameDto.safeParse(params.memberId); const bodyParse = InternalKickUserBodyDto.safeParse(body); - if ( - !roomIdParse.success || - !memberIdParse.success || - !bodyParse.success - ) { + if (!roomIdParse.success || !memberIdParse.success || !bodyParse.success) { set.status = 400; return { error: 'Invalid request', @@ -263,14 +227,9 @@ export const internalRoomPlugin = (app: Elysia) => { }, }; } - const { /*userIdToKick, */ senderUserId, reason } = bodyParse.data; + const { /* userIdToKick, */ senderUserId, reason } = bodyParse.data; try { - const eventId = await federationSDK.kickUser( - params.roomId as RoomID, - params.memberId as UserID, - senderUserId as UserID, - reason, - ); + const eventId = await federationSDK.kickUser(params.roomId as RoomID, params.memberId as UserID, senderUserId as UserID, reason); return { eventId }; } catch (error) { set.status = 500; @@ -296,10 +255,7 @@ export const internalRoomPlugin = (app: Elysia) => { ) .put( '/internal/rooms/:roomId/ban/:userIdToBan', - async ({ - params, - body, - }): Promise => { + async ({ params, body }): Promise => { // const roomIdParse = RoomIdDto.safeParse(params.roomId); // const userIdParse = UsernameDto.safeParse(params.userIdToBan); // const bodyParse = InternalBanUserBodyDto.safeParse(body); @@ -348,21 +304,20 @@ export const internalRoomPlugin = (app: Elysia) => { throw new Error('Room create event not found'); } - const membershipEvent = - PersistentEventFactory.newEvent<'m.room.member'>( - { - type: 'm.room.member', - content: { membership: 'ban' }, - room_id: roomId as RoomID, - state_key: userIdToBan as UserID, - auth_events: [], - depth: 0, - prev_events: [], - origin_server_ts: Date.now(), - sender: senderUserId as UserID, - }, - createEvent.getContent().room_version, - ); + const membershipEvent = PersistentEventFactory.newEvent<'m.room.member'>( + { + type: 'm.room.member', + content: { membership: 'ban' }, + room_id: roomId as RoomID, + state_key: userIdToBan as UserID, + auth_events: [], + depth: 0, + prev_events: [], + origin_server_ts: Date.now(), + sender: senderUserId as UserID, + }, + createEvent.getContent().room_version, + ); const statesNeeded = membershipEvent.getAuthEventStateKeys(); @@ -395,11 +350,7 @@ export const internalRoomPlugin = (app: Elysia) => { ) .put( '/internal/rooms/:roomId/tombstone', - async ({ - params, - body, - set, - }): Promise => { + async ({ params, body, set }): Promise => { const roomIdParse = RoomIdDto.safeParse(params.roomId); const bodyParse = InternalTombstoneRoomBodyDto.safeParse(body); if (!roomIdParse.success || !bodyParse.success) { @@ -451,11 +402,7 @@ export const internalRoomPlugin = (app: Elysia) => { const { roomId, userId } = params; const { sender } = body; - const resp = await federationSDK.inviteUserToRoom( - userId as UserID, - roomId as RoomID, - sender as UserID, - ); + const resp = await federationSDK.inviteUserToRoom(userId as UserID, roomId as RoomID, sender as UserID); return { eventId: resp.event_id, }; diff --git a/packages/homeserver/src/controllers/key/server.controller.ts b/packages/homeserver/src/controllers/key/server.controller.ts index b1877620b..f02e5792d 100644 --- a/packages/homeserver/src/controllers/key/server.controller.ts +++ b/packages/homeserver/src/controllers/key/server.controller.ts @@ -1,5 +1,6 @@ import { federationSDK } from '@rocket.chat/federation-sdk'; import type { Elysia } from 'elysia'; + import { ServerKeyResponseDto } from '../../dtos'; export const serverKeyPlugin = (app: Elysia) => { diff --git a/packages/homeserver/src/controllers/well-known/well-known.controller.ts b/packages/homeserver/src/controllers/well-known/well-known.controller.ts index e9e229e68..a122985f1 100644 --- a/packages/homeserver/src/controllers/well-known/well-known.controller.ts +++ b/packages/homeserver/src/controllers/well-known/well-known.controller.ts @@ -1,5 +1,6 @@ import { federationSDK } from '@rocket.chat/federation-sdk'; -import { Elysia } from 'elysia'; +import type { Elysia } from 'elysia'; + import { WellKnownServerResponseDto } from '../../dtos'; export const wellKnownPlugin = (app: Elysia) => { @@ -7,9 +8,7 @@ export const wellKnownPlugin = (app: Elysia) => { '/.well-known/matrix/server', ({ set }) => { const responseData = federationSDK.getWellKnownHostData(); - const etag = new Bun.CryptoHasher('md5') - .update(JSON.stringify(responseData)) - .digest('hex'); + const etag = new Bun.CryptoHasher('md5').update(JSON.stringify(responseData)).digest('hex'); set.headers.ETag = etag; set.headers['Content-Type'] = 'application/json'; return responseData; diff --git a/packages/homeserver/src/dtos/common/event.dto.ts b/packages/homeserver/src/dtos/common/event.dto.ts index 636df90a5..ef06b58ff 100644 --- a/packages/homeserver/src/dtos/common/event.dto.ts +++ b/packages/homeserver/src/dtos/common/event.dto.ts @@ -1,24 +1,18 @@ import { TSchema, t } from 'elysia'; -import { - DepthDto, - RoomIdDto, - TimestampDto, - UsernameDto, -} from './validation.dto'; + +import { DepthDto, RoomIdDto, TimestampDto, UsernameDto } from './validation.dto'; export const EventHashDto = t.Object({ sha256: t.String({ description: 'SHA256 hash of the event' }), }); -export const EventSignatureDto = t.Record( - t.String(), - t.Record(t.String(), t.String()), - { description: 'Event signatures by server and key ID' }, -); +export const EventSignatureDto = t.Record(t.String(), t.Record(t.String(), t.String()), { + description: 'Event signatures by server and key ID', +}); type TOptional = ReturnType; -function HiddenOptional(schema: T): T { +function hiddenOptional(schema: T): T { return schema; } @@ -32,26 +26,17 @@ export const EventBaseDto = t.Object({ prev_events: t.Array(t.String(), { description: 'Previous events in the room', }), - origin: HiddenOptional(t.Optional(t.String())), + origin: hiddenOptional(t.Optional(t.String())), auth_events: t.Array(t.String(), { description: 'Authorization events' }), hashes: EventHashDto, signatures: EventSignatureDto, - unsigned: t.Optional( - t.Record(t.String(), t.Any(), { description: 'Unsigned data' }), - ), + unsigned: t.Optional(t.Record(t.String(), t.Any(), { description: 'Unsigned data' })), }); export const MembershipEventContentDto = t.Object({ - membership: t.Union( - [ - t.Literal('join'), - t.Literal('leave'), - t.Literal('invite'), - t.Literal('ban'), - t.Literal('knock'), - ], - { description: 'Membership state' }, - ), + membership: t.Union([t.Literal('join'), t.Literal('leave'), t.Literal('invite'), t.Literal('ban'), t.Literal('knock')], { + description: 'Membership state', + }), displayname: t.Optional(t.Union([t.String(), t.Null()])), avatar_url: t.Optional(t.Union([t.String(), t.Null()])), join_authorised_via_users_server: t.Optional(t.Union([t.String(), t.Null()])), diff --git a/packages/homeserver/src/dtos/federation/backfill.dto.ts b/packages/homeserver/src/dtos/federation/backfill.dto.ts index 43d522fdd..40e615c40 100644 --- a/packages/homeserver/src/dtos/federation/backfill.dto.ts +++ b/packages/homeserver/src/dtos/federation/backfill.dto.ts @@ -1,4 +1,5 @@ import { t } from 'elysia'; + import { EventBaseDto } from '../common/event.dto'; export const BackfillParamsDto = t.Object({ diff --git a/packages/homeserver/src/dtos/federation/error.dto.ts b/packages/homeserver/src/dtos/federation/error.dto.ts index 2b9674a1c..10ee6381a 100644 --- a/packages/homeserver/src/dtos/federation/error.dto.ts +++ b/packages/homeserver/src/dtos/federation/error.dto.ts @@ -1,4 +1,4 @@ -import { type Static, t } from 'elysia'; +import { t } from 'elysia'; export const FederationErrorResponseDto = t.Object({ errcode: t.Enum({ @@ -9,7 +9,3 @@ export const FederationErrorResponseDto = t.Object({ }), error: t.String(), }); - -export type FederationErrorResponseDto = Static< - typeof FederationErrorResponseDto ->; diff --git a/packages/homeserver/src/dtos/federation/invite.dto.ts b/packages/homeserver/src/dtos/federation/invite.dto.ts index 5a8b3195d..6bfb53cee 100644 --- a/packages/homeserver/src/dtos/federation/invite.dto.ts +++ b/packages/homeserver/src/dtos/federation/invite.dto.ts @@ -1,4 +1,5 @@ import { type Static, t } from 'elysia'; + import { RoomMemberEventDto } from '../common/event.dto'; import { EventIdDto, RoomIdDto } from '../common/validation.dto'; diff --git a/packages/homeserver/src/dtos/federation/profiles.dto.ts b/packages/homeserver/src/dtos/federation/profiles.dto.ts index 006a6192c..607fbc0b7 100644 --- a/packages/homeserver/src/dtos/federation/profiles.dto.ts +++ b/packages/homeserver/src/dtos/federation/profiles.dto.ts @@ -1,10 +1,6 @@ import { type Static, t } from 'elysia'; -import { - RoomIdDto, - ServerNameDto, - TimestampDto, - UsernameDto, -} from '../common/validation.dto'; + +import { RoomIdDto, ServerNameDto, TimestampDto, UsernameDto } from '../common/validation.dto'; export const QueryProfileQueryDto = t.Object({ user_id: UsernameDto, @@ -37,12 +33,8 @@ export const GetDevicesResponseDto = t.Object({ devices: t.Array( t.Object({ device_id: t.String({ description: 'Device ID' }), - display_name: t.Optional( - t.String({ description: 'Device display name' }), - ), - last_seen_ip: t.Optional( - t.String({ description: 'Last seen IP address' }), - ), + display_name: t.Optional(t.String({ description: 'Device display name' })), + last_seen_ip: t.Optional(t.String({ description: 'Last seen IP address' })), last_seen_ts: t.Optional(TimestampDto), }), { description: 'List of devices for the user' }, @@ -76,13 +68,7 @@ export const MakeJoinQueryDto = t.Object({ ), }); -const MembershipDto = t.Union([ - t.Literal('join'), - t.Literal('leave'), - t.Literal('invite'), - t.Literal('ban'), - t.Literal('knock'), -]); +const MembershipDto = t.Union([t.Literal('join'), t.Literal('leave'), t.Literal('invite'), t.Literal('ban'), t.Literal('knock')]); export const MakeJoinResponseDto = t.Object({ room_version: t.String({ description: 'Room version' }), @@ -96,15 +82,9 @@ export const MakeJoinResponseDto = t.Object({ state_key: UsernameDto, type: t.Literal('m.room.member'), origin_server_ts: TimestampDto, - depth: t.Optional( - t.Number({ description: 'Depth of the event in the DAG' }), - ), - prev_events: t.Optional( - t.Array(t.String(), { description: 'Previous events in the room' }), - ), - auth_events: t.Optional( - t.Array(t.String(), { description: 'Authorization events' }), - ), + depth: t.Optional(t.Number({ description: 'Depth of the event in the DAG' })), + prev_events: t.Optional(t.Array(t.String(), { description: 'Previous events in the room' })), + auth_events: t.Optional(t.Array(t.String(), { description: 'Authorization events' })), hashes: t.Optional( t.Object({ sha256: t.String({ description: 'SHA256 hash of the event' }), @@ -115,9 +95,7 @@ export const MakeJoinResponseDto = t.Object({ description: 'Event signatures by server and key ID', }), ), - unsigned: t.Optional( - t.Record(t.String(), t.Any(), { description: 'Unsigned data' }), - ), + unsigned: t.Optional(t.Record(t.String(), t.Any(), { description: 'Unsigned data' })), }), }); @@ -169,9 +147,7 @@ export type GetDevicesResponse = Static; export type QueryProfileResponse = Static; export type EventAuthResponse = Static; export type EventAuthParams = Static; -export type GetMissingEventsResponse = Static< - typeof GetMissingEventsResponseDto ->; +export type GetMissingEventsResponse = Static; export type GetMissingEventsBody = Static; export type GetMissingEventsParams = Static; export type MakeJoinResponse = Static; diff --git a/packages/homeserver/src/dtos/federation/send-join.dto.ts b/packages/homeserver/src/dtos/federation/send-join.dto.ts index 64f62bfda..9b2472080 100644 --- a/packages/homeserver/src/dtos/federation/send-join.dto.ts +++ b/packages/homeserver/src/dtos/federation/send-join.dto.ts @@ -1,11 +1,7 @@ import { type Static, t } from 'elysia'; + import { EventBaseDto, MembershipEventContentDto } from '../common/event.dto'; -import { - EventIdDto, - RoomIdDto, - ServerNameDto, - UsernameDto, -} from '../common/validation.dto'; +import { EventIdDto, RoomIdDto, ServerNameDto, UsernameDto } from '../common/validation.dto'; export const SendJoinParamsDto = t.Object({ roomId: RoomIdDto, diff --git a/packages/homeserver/src/dtos/federation/state-ids.dto.ts b/packages/homeserver/src/dtos/federation/state-ids.dto.ts index 88fe87970..2e5c8e397 100644 --- a/packages/homeserver/src/dtos/federation/state-ids.dto.ts +++ b/packages/homeserver/src/dtos/federation/state-ids.dto.ts @@ -1,4 +1,5 @@ import { type Static, t } from 'elysia'; + import { RoomIdDto } from '../common/validation.dto'; export const GetStateIdsParamsDto = t.Object({ diff --git a/packages/homeserver/src/dtos/federation/state.dto.ts b/packages/homeserver/src/dtos/federation/state.dto.ts index 21e172d78..62f341a79 100644 --- a/packages/homeserver/src/dtos/federation/state.dto.ts +++ b/packages/homeserver/src/dtos/federation/state.dto.ts @@ -1,4 +1,5 @@ import { type Static, t } from 'elysia'; + import { RoomIdDto } from '../common/validation.dto'; export const GetStateParamsDto = t.Object({ diff --git a/packages/homeserver/src/dtos/federation/transactions.dto.ts b/packages/homeserver/src/dtos/federation/transactions.dto.ts index 54849fe3a..cb610d804 100644 --- a/packages/homeserver/src/dtos/federation/transactions.dto.ts +++ b/packages/homeserver/src/dtos/federation/transactions.dto.ts @@ -1,4 +1,5 @@ import { type Static, t } from 'elysia'; + import { EventBaseDto } from '../common/event.dto'; export const SendTransactionParamsDto = t.Object({ diff --git a/packages/homeserver/src/dtos/internal/invite.dto.ts b/packages/homeserver/src/dtos/internal/invite.dto.ts index 6b2c71fbd..d43fddfba 100644 --- a/packages/homeserver/src/dtos/internal/invite.dto.ts +++ b/packages/homeserver/src/dtos/internal/invite.dto.ts @@ -1,4 +1,5 @@ import { type Static, t } from 'elysia'; + import { RoomIdDto, UsernameDto } from '../common/validation.dto'; export const InternalInviteUserBodyDto = t.Object({ @@ -16,6 +17,4 @@ export const InternalInviteUserResponseDto = t.Object({ }); export type InternalInviteUserBody = Static; -export type InternalInviteUserResponse = Static< - typeof InternalInviteUserResponseDto ->; +export type InternalInviteUserResponse = Static; diff --git a/packages/homeserver/src/dtos/internal/message.dto.ts b/packages/homeserver/src/dtos/internal/message.dto.ts index 683157854..468be7849 100644 --- a/packages/homeserver/src/dtos/internal/message.dto.ts +++ b/packages/homeserver/src/dtos/internal/message.dto.ts @@ -1,9 +1,6 @@ import { type Static, t } from 'elysia'; -import { - RoomIdDto, - ServerNameDto, - UsernameDto, -} from '../common/validation.dto'; + +import { RoomIdDto, ServerNameDto, UsernameDto } from '../common/validation.dto'; export const InternalSendMessageBodyDto = t.Object({ roomId: RoomIdDto, @@ -64,28 +61,12 @@ export const InternalRedactMessageBodyDto = t.Object({ export const InternalRedactMessageResponseDto = InternalMessageResponseDto; export type InternalMessageResponse = Static; -export type InternalReactionResponse = Static< - typeof InternalReactionResponseDto ->; +export type InternalReactionResponse = Static; export type InternalSendMessageBody = Static; -export type InternalUpdateMessageBody = Static< - typeof InternalUpdateMessageBodyDto ->; -export type InternalUpdateMessageParams = Static< - typeof InternalUpdateMessageParamsDto ->; -export type InternalSendReactionBody = Static< - typeof InternalSendReactionBodyDto ->; -export type InternalSendReactionParams = Static< - typeof InternalSendReactionParamsDto ->; -export type InternalRedactMessageBody = Static< - typeof InternalRedactMessageBodyDto ->; -export type InternalRedactMessageParams = Static< - typeof InternalRedactMessageParamsDto ->; -export type InternalRedactMessageResponse = Static< - typeof InternalRedactMessageResponseDto ->; +export type InternalUpdateMessageBody = Static; +export type InternalUpdateMessageParams = Static; +export type InternalSendReactionBody = Static; +export type InternalSendReactionParams = Static; +export type InternalRedactMessageBody = Static; +export type InternalRedactMessageParams = Static; +export type InternalRedactMessageResponse = Static; diff --git a/packages/homeserver/src/dtos/internal/room.dto.ts b/packages/homeserver/src/dtos/internal/room.dto.ts index c2b713d33..5fdcc99f9 100644 --- a/packages/homeserver/src/dtos/internal/room.dto.ts +++ b/packages/homeserver/src/dtos/internal/room.dto.ts @@ -1,9 +1,6 @@ import { type Static, t } from 'elysia'; -import { - RoomIdDto, - ServerNameDto, - UsernameDto, -} from '../common/validation.dto'; + +import { RoomIdDto, ServerNameDto, UsernameDto } from '../common/validation.dto'; export const InternalCreateRoomBodyDto = t.Object({ username: t.String({ @@ -116,48 +113,22 @@ export const InternalTombstoneRoomResponseDto = t.Object({ }); export type InternalCreateRoomBody = Static; -export type InternalCreateRoomResponse = Static< - typeof InternalCreateRoomResponseDto ->; -export type InternalUpdateRoomNameParams = Static< - typeof InternalUpdateRoomNameParamsDto ->; -export type InternalUpdateRoomNameBody = Static< - typeof InternalUpdateRoomNameBodyDto ->; -export type InternalUpdateRoomNameResponse = Static< - typeof InternalRoomEventResponseDto ->; -export type InternalUpdateUserPowerLevelParams = Static< - typeof InternalUpdateUserPowerLevelParamsDto ->; -export type InternalUpdateUserPowerLevelBody = Static< - typeof InternalUpdateUserPowerLevelBodyDto ->; -export type InternalUpdateUserPowerLevelResponse = Static< - typeof InternalRoomEventResponseDto ->; +export type InternalCreateRoomResponse = Static; +export type InternalUpdateRoomNameParams = Static; +export type InternalUpdateRoomNameBody = Static; +export type InternalUpdateRoomNameResponse = Static; +export type InternalUpdateUserPowerLevelParams = Static; +export type InternalUpdateUserPowerLevelBody = Static; +export type InternalUpdateUserPowerLevelResponse = Static; export type InternalLeaveRoomParams = Static; export type InternalLeaveRoomBody = Static; -export type InternalLeaveRoomResponse = Static< - typeof InternalRoomEventResponseDto ->; +export type InternalLeaveRoomResponse = Static; export type InternalKickUserParams = Static; export type InternalKickUserBody = Static; -export type InternalKickUserResponse = Static< - typeof InternalRoomEventResponseDto ->; +export type InternalKickUserResponse = Static; export type InternalBanUserParams = Static; export type InternalBanUserBody = Static; -export type InternalBanUserResponse = Static< - typeof InternalRoomEventResponseDto ->; -export type InternalTombstoneRoomParams = Static< - typeof InternalTombstoneRoomParamsDto ->; -export type InternalTombstoneRoomBody = Static< - typeof InternalTombstoneRoomBodyDto ->; -export type InternalTombstoneRoomResponse = Static< - typeof InternalTombstoneRoomResponseDto ->; +export type InternalBanUserResponse = Static; +export type InternalTombstoneRoomParams = Static; +export type InternalTombstoneRoomBody = Static; +export type InternalTombstoneRoomResponse = Static; diff --git a/packages/homeserver/src/dtos/key/server.dto.ts b/packages/homeserver/src/dtos/key/server.dto.ts index adca62ca4..b158da12b 100644 --- a/packages/homeserver/src/dtos/key/server.dto.ts +++ b/packages/homeserver/src/dtos/key/server.dto.ts @@ -1,4 +1,5 @@ import { type Static, t } from 'elysia'; + import { ServerNameDto, TimestampDto } from '../common/validation.dto'; export const ServerKeyResponseDto = t.Object({ diff --git a/packages/homeserver/src/homeserver.module.ts b/packages/homeserver/src/homeserver.module.ts index 51f93d539..2522d09cb 100644 --- a/packages/homeserver/src/homeserver.module.ts +++ b/packages/homeserver/src/homeserver.module.ts @@ -4,16 +4,12 @@ import crypto from 'node:crypto'; import * as fs from 'node:fs'; import * as path from 'node:path'; -import { - type HomeserverEventSignatures, - federationSDK, - init, -} from '@rocket.chat/federation-sdk'; -import * as dotenv from 'dotenv'; - import { swagger } from '@elysiajs/swagger'; import type { Emitter } from '@rocket.chat/emitter'; +import { type HomeserverEventSignatures, federationSDK, init } from '@rocket.chat/federation-sdk'; +import * as dotenv from 'dotenv'; import Elysia from 'elysia'; + import { invitePlugin } from './controllers/federation/invite.controller'; import { mediaPlugin } from './controllers/federation/media.controller'; import { profilesPlugin } from './controllers/federation/profiles.controller'; @@ -49,17 +45,12 @@ export async function setup() { serverName: process.env.SERVER_NAME || 'rc1', port: Number.parseInt(process.env.SERVER_PORT || '8080', 10), matrixDomain: process.env.MATRIX_DOMAIN || 'rc1', - keyRefreshInterval: Number.parseInt( - process.env.MATRIX_KEY_REFRESH_INTERVAL || '60', - 10, - ), + keyRefreshInterval: Number.parseInt(process.env.MATRIX_KEY_REFRESH_INTERVAL || '60', 10), signingKey: process.env.SIGNING_KEY, signingKeyPath: process.env.CONFIG_FOLDER || './rc1.signing.key', version: process.env.SERVER_VERSION || '1.0', media: { - maxFileSize: process.env.MEDIA_MAX_FILE_SIZE - ? Number.parseInt(process.env.MEDIA_MAX_FILE_SIZE, 10) * 1024 * 1024 - : 100 * 1024 * 1024, + maxFileSize: process.env.MEDIA_MAX_FILE_SIZE ? Number.parseInt(process.env.MEDIA_MAX_FILE_SIZE, 10) * 1024 * 1024 : 100 * 1024 * 1024, allowedMimeTypes: process.env.MEDIA_ALLOWED_MIME_TYPES?.split(',') || [ 'image/jpeg', 'image/png', @@ -73,21 +64,13 @@ export async function setup() { ], enableThumbnails: process.env.MEDIA_ENABLE_THUMBNAILS === 'true' || true, rateLimits: { - uploadPerMinute: Number.parseInt( - process.env.MEDIA_UPLOAD_RATE_LIMIT || '10', - 10, - ), - downloadPerMinute: Number.parseInt( - process.env.MEDIA_DOWNLOAD_RATE_LIMIT || '60', - 10, - ), + uploadPerMinute: Number.parseInt(process.env.MEDIA_UPLOAD_RATE_LIMIT || '10', 10), + downloadPerMinute: Number.parseInt(process.env.MEDIA_DOWNLOAD_RATE_LIMIT || '60', 10), }, }, invite: { - allowedEncryptedRooms: - process.env.INVITE_ALLOWED_ENCRYPTED_ROOMS === 'true', - allowedNonPrivateRooms: - process.env.INVITE_ALLOWED_NON_PRIVATE_ROOMS === 'true', + allowedEncryptedRooms: process.env.INVITE_ALLOWED_ENCRYPTED_ROOMS === 'true', + allowedNonPrivateRooms: process.env.INVITE_ALLOWED_NON_PRIVATE_ROOMS === 'true', }, edu: { processTyping: process.env.EDU_PROCESS_TYPING !== 'false', @@ -99,14 +82,12 @@ export async function setup() { app .use( - // @ts-ignore - Elysia is not typed correctly swagger({ documentation: { info: { title: 'Matrix Homeserver API', version: '1.0.0', - description: - 'Matrix Protocol Implementation - Federation and Internal APIs', + description: 'Matrix Protocol Implementation - Federation and Internal APIs', }, }, }), diff --git a/packages/homeserver/src/middlewares/canAccessResource.ts b/packages/homeserver/src/middlewares/canAccessResource.ts index ccdd9ffe3..fdfdd8ce0 100644 --- a/packages/homeserver/src/middlewares/canAccessResource.ts +++ b/packages/homeserver/src/middlewares/canAccessResource.ts @@ -1,6 +1,6 @@ -import { federationSDK } from '@rocket.chat/federation-sdk'; -import { errCodes } from '@rocket.chat/federation-sdk'; +import { federationSDK, errCodes } from '@rocket.chat/federation-sdk'; import Elysia from 'elysia'; + import { isAuthenticatedMiddleware } from './isAuthenticated'; function extractEntityId( @@ -22,9 +22,7 @@ function extractEntityId( return null; } -export const canAccessResourceMiddleware = ( - entityType: 'event' | 'media' | 'room', -) => { +export const canAccessResourceMiddleware = (entityType: 'event' | 'media' | 'room') => { return new Elysia({ name: 'homeserver/canAccessResource' }) .use(isAuthenticatedMiddleware()) .onBeforeHandle(async ({ params, authenticatedServer, set }) => { @@ -46,11 +44,7 @@ export const canAccessResourceMiddleware = ( }; } - const resourceAccess = await federationSDK.canAccessResource( - entityType, - resourceId, - authenticatedServer, - ); + const resourceAccess = await federationSDK.canAccessResource(entityType, resourceId, authenticatedServer); if (!resourceAccess) { set.status = errCodes.M_FORBIDDEN.status; return { diff --git a/packages/homeserver/src/middlewares/isAuthenticated.ts b/packages/homeserver/src/middlewares/isAuthenticated.ts index 125912618..b7165c563 100644 --- a/packages/homeserver/src/middlewares/isAuthenticated.ts +++ b/packages/homeserver/src/middlewares/isAuthenticated.ts @@ -7,7 +7,7 @@ export const isAuthenticatedMiddleware = () => { }) .derive({ as: 'global' }, async ({ headers, request, set }) => { const authorizationHeader = headers.authorization; - const method = request.method; + const { method } = request; const url = new URL(request.url); const uri = url.pathname + url.search; @@ -30,12 +30,7 @@ export const isAuthenticatedMiddleware = () => { } } - const isValid = await federationSDK.verifyRequestSignature( - authorizationHeader, - method, - uri, - body, - ); + const isValid = await federationSDK.verifyRequestSignature(authorizationHeader, method, uri, body); if (!isValid) { set.status = 401; @@ -59,10 +54,7 @@ export const isAuthenticatedMiddleware = () => { if (!authenticatedServer) { return { errcode: set.status === 500 ? 'M_UNKNOWN' : 'M_UNAUTHORIZED', - error: - set.status === 500 - ? 'Internal server error' - : 'Authentication required', + error: set.status === 500 ? 'Internal server error' : 'Authentication required', }; } }); diff --git a/packages/room/.eslintrc.json b/packages/room/.eslintrc.json new file mode 100644 index 000000000..be97c53fb --- /dev/null +++ b/packages/room/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.eslintrc.json" +} diff --git a/packages/room/src/authorizartion-rules/rules.spec.ts b/packages/room/src/authorizartion-rules/rules.spec.ts index 832386b1a..119944476 100644 --- a/packages/room/src/authorizartion-rules/rules.spec.ts +++ b/packages/room/src/authorizartion-rules/rules.spec.ts @@ -1,13 +1,11 @@ import { afterEach, describe, expect, it } from 'bun:test'; -import { PersistentEventBase } from '../manager/event-wrapper'; + +import { checkEventAuthWithState, checkEventAuthWithoutState } from './rules'; +import type { PersistentEventBase } from '../manager/event-wrapper'; import { PersistentEventFactory } from '../manager/factory'; -import { - type EventStore, - getStateMapKey, -} from '../state_resolution/definitions/definitions'; +import { type EventStore, getStateMapKey } from '../state_resolution/definitions/definitions'; import { type StateMapKey } from '../types/_common'; -import { Pdu, PduContent, type PduType } from '../types/v3-11'; -import { checkEventAuthWithState, checkEventAuthWithoutState } from './rules'; +import type { Pdu, PduContent, type PduType } from '../types/v3-11'; class MockStore implements EventStore { events: Map = new Map(); @@ -27,6 +25,7 @@ class MockStore implements EventStore { class FakeEventCreatorBase { protected _event!: Pdu; + constructor() { this._event = { content: {}, @@ -44,7 +43,7 @@ class FakeEventCreatorBase { } withType(type: PduType | 'test') { - // @ts-ignore breaking due to type and content conflict, doesn't matter here + // @ts-expect-error breaking due to type and content conflict, doesn't matter here this._event.type = type; return this; } @@ -134,12 +133,7 @@ function getInitialEvents( }, }, ) { - const create = new FakeStateEventCreator() - .asRoomCreate() - .withRoomId(roomId) - .withSender(creator) - .withContent({ creator }) - .build(); + const create = new FakeStateEventCreator().asRoomCreate().withRoomId(roomId).withSender(creator).withContent({ creator }).build(); store.events.set(create.eventId, create); @@ -175,9 +169,7 @@ function getInitialEvents( } function getStateMap(events: PersistentEventBase[]) { - return new Map( - events.map((event) => [getStateMapKey(event.event), event]), - ); + return new Map(events.map((event) => [getStateMapKey(event.event), event])); } // numbering tests for easier debugging, see tasks.json's inputs @@ -196,9 +188,7 @@ describe('authorization rules', () => { .withStateKey(creator) .build(); - expect(() => - checkEventAuthWithoutState(randomEvent, [create, join]), - ).not.toThrow(); + expect(() => checkEventAuthWithoutState(randomEvent, [create, join])).not.toThrow(); const joinRules = new FakeStateEventCreator() .asRoomJoinRules() @@ -209,9 +199,7 @@ describe('authorization rules', () => { }) .build(); - expect(() => - checkEventAuthWithoutState(randomEvent, [create, join, joinRules]), - ).toThrow(); + expect(() => checkEventAuthWithoutState(randomEvent, [create, join, joinRules])).toThrow(); }); it('02 should reject create event if has any prev_events', async () => { @@ -253,9 +241,7 @@ describe('authorization rules', () => { .build(); // should be allowed - expect(() => - checkEventAuthWithoutState(randomEvent, [create, join]), - ).not.toThrow(); + expect(() => checkEventAuthWithoutState(randomEvent, [create, join])).not.toThrow(); // random event2 const randomEvent2 = new FakeStateEventCreator() @@ -267,9 +253,7 @@ describe('authorization rules', () => { .build(); // should be rejected - expect(() => - checkEventAuthWithoutState(randomEvent2, [create, join, join]), - ).toThrow(); + expect(() => checkEventAuthWithoutState(randomEvent2, [create, join, join])).toThrow(); }); it('04 should reject events with excess auth events', async () => { @@ -298,18 +282,9 @@ describe('authorization rules', () => { .authedBy(joinRules) // why .build(); - expect(() => - checkEventAuthWithoutState(goodEvent, [create, join, powerLevel]), - ).not.toThrow(); - - expect(() => - checkEventAuthWithoutState(badEvent, [ - create, - join, - powerLevel, - joinRules, - ]), - ).toThrow(); + expect(() => checkEventAuthWithoutState(goodEvent, [create, join, powerLevel])).not.toThrow(); + + expect(() => checkEventAuthWithoutState(badEvent, [create, join, powerLevel, joinRules])).toThrow(); }); it('05 should reject state events from random users before first power level event', async () => { @@ -329,28 +304,14 @@ describe('authorization rules', () => { const currentState = getStateMap([create, join, inviteAlice]); - const randomStateEvent = new FakeStateEventCreator() - .asTest() - .withRoomId(roomId) - .withSender(creator) - .withContent({}) - .build(); + const randomStateEvent = new FakeStateEventCreator().asTest().withRoomId(roomId).withSender(creator).withContent({}).build(); // creator should be able to send any event - expect(() => - checkEventAuthWithState(randomStateEvent, currentState, store), - ).not.toThrow(); + expect(() => checkEventAuthWithState(randomStateEvent, currentState, store)).not.toThrow(); - const randomEventSentByAlice = new FakeStateEventCreator() - .asTest() - .withRoomId(roomId) - .withSender(alice) - .withContent({}) - .build(); + const randomEventSentByAlice = new FakeStateEventCreator().asTest().withRoomId(roomId).withSender(alice).withContent({}).build(); - expect(() => - checkEventAuthWithState(randomEventSentByAlice, currentState, store), - ).toThrow(); + expect(() => checkEventAuthWithState(randomEventSentByAlice, currentState, store)).toThrow(); }); it('06 users below events_default should not be able to send unknown events', async () => { @@ -403,14 +364,12 @@ describe('authorization rules', () => { .withContent({}) .build(); - expect(() => - checkEventAuthWithState(randomEvent2, state, store), - ).not.toThrow(); + expect(() => checkEventAuthWithState(randomEvent2, state, store)).not.toThrow(); }); // TODO: alias rooms it('07 joining rooms', async () => { - const { create, join, /*powerLevel,*/ joinRules } = getInitialEvents({ + const { create, join, /* powerLevel,*/ joinRules } = getInitialEvents({ joinRule: 'public', }); @@ -427,9 +386,7 @@ describe('authorization rules', () => { .build(); // alice should be able to join the room - expect(() => - checkEventAuthWithState(joinAlice, state, store), - ).not.toThrow(); + expect(() => checkEventAuthWithState(joinAlice, state, store)).not.toThrow(); // expect(joinAlice.rejected).toBe(false); // user can not be forced to join the room @@ -441,9 +398,7 @@ describe('authorization rules', () => { .withStateKey(alice) .build(); - expect(() => - checkEventAuthWithState(forceJoinAliceByCreator, state, store), - ).toThrow(); + expect(() => checkEventAuthWithState(forceJoinAliceByCreator, state, store)).toThrow(); // expect(forceJoinAliceByCreator.rejected).toBe(true); // banned should be rejected @@ -457,9 +412,7 @@ describe('authorization rules', () => { const stateWithBan = getStateMap([create, join, joinRules, banAlice]); - expect(() => - checkEventAuthWithState(joinAlice, stateWithBan, store), - ).toThrow(); + expect(() => checkEventAuthWithState(joinAlice, stateWithBan, store)).toThrow(); // expect(joinAlice.rejected).toBe(true); // a user who left should be able to rejoin @@ -473,16 +426,12 @@ describe('authorization rules', () => { const stateWithLeave = getStateMap([create, join, joinRules, leaveAlice]); - expect(() => - checkEventAuthWithState(joinAlice, stateWithLeave, store), - ).not.toThrow(); + expect(() => checkEventAuthWithState(joinAlice, stateWithLeave, store)).not.toThrow(); // a user can send a join if in the room const stateWithAlice = getStateMap([create, join, joinRules, joinAlice]); - expect(() => - checkEventAuthWithState(joinAlice, stateWithAlice, store), - ).not.toThrow(); + expect(() => checkEventAuthWithState(joinAlice, stateWithAlice, store)).not.toThrow(); // a user can accept an invite const inviteAlice = new FakeStateEventCreator() @@ -495,9 +444,7 @@ describe('authorization rules', () => { const stateWithInvite = getStateMap([create, join, joinRules, inviteAlice]); - expect(() => - checkEventAuthWithState(joinAlice, stateWithInvite, store), - ).not.toThrow(); + expect(() => checkEventAuthWithState(joinAlice, stateWithInvite, store)).not.toThrow(); // should not be able to join if private const privateJoinRules = new FakeStateEventCreator() @@ -507,19 +454,13 @@ describe('authorization rules', () => { .withContent({ join_rule: 'private' }) .build(); - const stateWithPrivateJoinRules = getStateMap([ - create, - join, - privateJoinRules, - ]); + const stateWithPrivateJoinRules = getStateMap([create, join, privateJoinRules]); - expect(() => - checkEventAuthWithState(joinAlice, stateWithPrivateJoinRules, store), - ).toThrow(); + expect(() => checkEventAuthWithState(joinAlice, stateWithPrivateJoinRules, store)).toThrow(); }); it('08 test joining an invite only room', async () => { - const { create, join, /*powerLevel,*/ joinRules } = getInitialEvents({ + const { create, join, /* powerLevel,*/ joinRules } = getInitialEvents({ joinRule: 'invite', }); @@ -547,9 +488,7 @@ describe('authorization rules', () => { .withStateKey(alice) .build(); - expect(() => - checkEventAuthWithState(forceJoinAliceByCreator, state, store), - ).toThrow(); + expect(() => checkEventAuthWithState(forceJoinAliceByCreator, state, store)).toThrow(); // banned should be rejected const banAlice = new FakeStateEventCreator() @@ -562,9 +501,7 @@ describe('authorization rules', () => { const stateWithBan = getStateMap([create, join, joinRules, banAlice]); - expect(() => - checkEventAuthWithState(joinAlice, stateWithBan, store), - ).toThrow(); + expect(() => checkEventAuthWithState(joinAlice, stateWithBan, store)).toThrow(); // who left should not be able to rejoin const leaveAlice = new FakeStateEventCreator() @@ -577,16 +514,12 @@ describe('authorization rules', () => { const stateWithLeave = getStateMap([create, join, joinRules, leaveAlice]); - expect(() => - checkEventAuthWithState(joinAlice, stateWithLeave, store), - ).toThrow(); + expect(() => checkEventAuthWithState(joinAlice, stateWithLeave, store)).toThrow(); // in the room === can "join" const stateWithAlice = getStateMap([create, join, joinRules, joinAlice]); - expect(() => - checkEventAuthWithState(joinAlice, stateWithAlice, store), - ).not.toThrow(); + expect(() => checkEventAuthWithState(joinAlice, stateWithAlice, store)).not.toThrow(); // invite should be allowed to be accepted const inviteAlice = new FakeStateEventCreator() @@ -599,9 +532,7 @@ describe('authorization rules', () => { const stateWithInvite = getStateMap([create, join, joinRules, inviteAlice]); - expect(() => - checkEventAuthWithState(joinAlice, stateWithInvite, store), - ).not.toThrow(); + expect(() => checkEventAuthWithState(joinAlice, stateWithInvite, store)).not.toThrow(); }); it('09 should not allow custom event sending if power level is too low', async () => { @@ -634,16 +565,9 @@ describe('authorization rules', () => { // alice should not be able to send custom event if power < events_default (50) const state49 = setAlicePower(49); - const randomEvent = new FakeTimelineEventCreator() - .asTest() - .withRoomId(roomId) - .withSender(alice) - .withContent({}) - .build(); + const randomEvent = new FakeTimelineEventCreator().asTest().withRoomId(roomId).withSender(alice).withContent({}).build(); - expect( - checkEventAuthWithState(randomEvent, state49, store), - ).rejects.toThrow(); + expect(checkEventAuthWithState(randomEvent, state49, store)).rejects.toThrow(); // alice should be able to send custom event if power >= events_default (50) const state50 = setAlicePower(50); @@ -653,27 +577,16 @@ describe('authorization rules', () => { // should not be able to send a message if power < 50 const state49_message = setAlicePower(49); - const messageEvent = new FakeMessageEventCreator() - .withRoomId(roomId) - .withSender(alice) - .withContent({}) - .build(); + const messageEvent = new FakeMessageEventCreator().withRoomId(roomId).withSender(alice).withContent({}).build(); - expect( - checkEventAuthWithState(messageEvent, state49_message, store), - ).rejects.toThrow(); + expect(checkEventAuthWithState(messageEvent, state49_message, store)).rejects.toThrow(); const state51 = setAlicePower(51); await checkEventAuthWithState(messageEvent, state51, store); // setting custom power required for test events - const randomEvent2 = new FakeStateEventCreator() - .asTest() - .withRoomId(roomId) - .withSender(alice) - .withContent({}) - .build(); + const randomEvent2 = new FakeStateEventCreator().asTest().withRoomId(roomId).withSender(alice).withContent({}).build(); const stateX = (() => { const { create, join, powerLevel, joinRules } = getInitialEvents( @@ -695,9 +608,7 @@ describe('authorization rules', () => { return getStateMap([create, join, powerLevel, joinRules, joinAlice]); })(); - expect( - checkEventAuthWithState(randomEvent2, stateX, store), - ).rejects.toThrow(); + expect(checkEventAuthWithState(randomEvent2, stateX, store)).rejects.toThrow(); // setting custom power required for test events const stateY = (() => { @@ -797,13 +708,7 @@ describe('authorization rules', () => { }, ); - const state2 = getStateMap([ - create2, - join2, - powerLevel2, - joinRules2, - joinAlice, - ]); + const state2 = getStateMap([create2, join2, powerLevel2, joinRules2, joinAlice]); const customEventLowPower = new FakeTimelineEventCreator() // @ts-expect-error - testing unknown event type @@ -813,9 +718,7 @@ describe('authorization rules', () => { .withContent({}) .build(); - expect( - checkEventAuthWithState(customEventLowPower, state2, store), - ).rejects.toThrow(); + expect(checkEventAuthWithState(customEventLowPower, state2, store)).rejects.toThrow(); }); it('10 should resolve power events correctly', async () => { @@ -831,9 +734,7 @@ describe('authorization rules', () => { const initialState = getStateMap([create, join, joinRules]); // should allow the first powerlevel - expect(() => - checkEventAuthWithState(existingPowerLevel, initialState, store), - ).not.toThrow(); + expect(() => checkEventAuthWithState(existingPowerLevel, initialState, store)).not.toThrow(); const alice = '@alice:example.com'; @@ -845,13 +746,7 @@ describe('authorization rules', () => { .withStateKey(alice) .build(); - const secondState = getStateMap([ - create, - join, - joinRules, - existingPowerLevel, - joinAlice, - ]); + const secondState = getStateMap([create, join, joinRules, existingPowerLevel, joinAlice]); const newPowerLevel = new FakeStateEventCreator() .asPowerLevel() @@ -866,20 +761,12 @@ describe('authorization rules', () => { }) .build(); - const setPowerLevel = ( - powerLevel: PersistentEventBase, - state: typeof secondState, - ) => { + const setPowerLevel = (powerLevel: PersistentEventBase, state: typeof secondState) => { state.set(powerLevel.getUniqueStateIdentifier(), powerLevel); }; const createPowerLevel = (content: PduContent) => { - return new FakeStateEventCreator() - .asPowerLevel() - .withRoomId(roomId) - .withSender(alice) - .withContent(content) - .build(); + return new FakeStateEventCreator().asPowerLevel().withRoomId(roomId).withSender(alice).withContent(content).build(); }; // should not be able to increase default user power level if doesn't have enough power to begin with @@ -900,9 +787,7 @@ describe('authorization rules', () => { ); // sender is trying to increase their power level without having the required power to do it - expect(() => - checkEventAuthWithState(newPowerLevel, secondState, store), - ).toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel, secondState, store)).toThrow(); const newPowerLevel2 = createPowerLevel({ events: {}, @@ -913,9 +798,7 @@ describe('authorization rules', () => { }); // reducing - expect(() => - checkEventAuthWithState(newPowerLevel2, secondState, store), - ).not.toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel2, secondState, store)).not.toThrow(); // events_default @@ -928,9 +811,7 @@ describe('authorization rules', () => { events_default: 60, }); - expect(() => - checkEventAuthWithState(newPowerLevel3, secondState, store), - ).toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel3, secondState, store)).toThrow(); const newPowerLevel4 = createPowerLevel({ events: {}, @@ -940,9 +821,7 @@ describe('authorization rules', () => { events_default: 40, // should be able to reduce events_default }); - expect(() => - checkEventAuthWithState(newPowerLevel4, secondState, store), - ).not.toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel4, secondState, store)).not.toThrow(); // state_default @@ -954,9 +833,7 @@ describe('authorization rules', () => { state_default: 60, }); - expect(() => - checkEventAuthWithState(newPowerLevel5, secondState, store), - ).toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel5, secondState, store)).toThrow(); const newPowerLevel6 = createPowerLevel({ events: {}, @@ -966,9 +843,7 @@ describe('authorization rules', () => { state_default: 40, }); - expect(() => - checkEventAuthWithState(newPowerLevel6, secondState, store), - ).not.toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel6, secondState, store)).not.toThrow(); const newPowerLevel7 = createPowerLevel({ events: {}, @@ -978,9 +853,7 @@ describe('authorization rules', () => { ban: 60, }); - expect(() => - checkEventAuthWithState(newPowerLevel7, secondState, store), - ).toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel7, secondState, store)).toThrow(); const newPowerLevel8 = createPowerLevel({ events: {}, @@ -990,9 +863,7 @@ describe('authorization rules', () => { ban: 40, }); - expect(() => - checkEventAuthWithState(newPowerLevel8, secondState, store), - ).not.toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel8, secondState, store)).not.toThrow(); // kick const newPowerLevel9 = createPowerLevel({ @@ -1003,9 +874,7 @@ describe('authorization rules', () => { kick: 60, }); - expect(() => - checkEventAuthWithState(newPowerLevel9, secondState, store), - ).toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel9, secondState, store)).toThrow(); const newPowerLevel10 = createPowerLevel({ events: {}, @@ -1015,9 +884,7 @@ describe('authorization rules', () => { kick: 40, }); - expect(() => - checkEventAuthWithState(newPowerLevel10, secondState, store), - ).not.toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel10, secondState, store)).not.toThrow(); // invite const newPowerLevel11 = createPowerLevel({ @@ -1028,9 +895,7 @@ describe('authorization rules', () => { invite: 60, }); - expect(() => - checkEventAuthWithState(newPowerLevel11, secondState, store), - ).toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel11, secondState, store)).toThrow(); const newPowerLevel12 = createPowerLevel({ events: {}, @@ -1040,9 +905,7 @@ describe('authorization rules', () => { invite: 40, }); - expect(() => - checkEventAuthWithState(newPowerLevel12, secondState, store), - ).not.toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel12, secondState, store)).not.toThrow(); // redact const newPowerLevel13 = createPowerLevel({ @@ -1053,9 +916,7 @@ describe('authorization rules', () => { redact: 60, }); - expect(() => - checkEventAuthWithState(newPowerLevel13, secondState, store), - ).toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel13, secondState, store)).toThrow(); const newPowerLevel14 = createPowerLevel({ events: {}, @@ -1065,9 +926,7 @@ describe('authorization rules', () => { redact: 40, }); - expect(() => - checkEventAuthWithState(newPowerLevel14, secondState, store), - ).not.toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel14, secondState, store)).not.toThrow(); // for each event type // if value is added check against users power level if is higher, reject @@ -1091,9 +950,7 @@ describe('authorization rules', () => { }); // 'test' is added with higher power level than alice's current power level, reject - expect(() => - checkEventAuthWithState(newPowerLevel15, secondState, store), - ).toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel15, secondState, store)).toThrow(); // 'test' is added with lower power level than alice's current power level, allow const newPowerLevel16 = createPowerLevel({ @@ -1105,9 +962,7 @@ describe('authorization rules', () => { }, }); - expect(() => - checkEventAuthWithState(newPowerLevel16, secondState, store), - ).not.toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel16, secondState, store)).not.toThrow(); // only allow removal of an event's required power if it is lower than or equal to user's current power setPowerLevel( @@ -1129,9 +984,7 @@ describe('authorization rules', () => { }, }); - expect(() => - checkEventAuthWithState(newPowerLevel17, secondState, store), - ).not.toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel17, secondState, store)).not.toThrow(); setPowerLevel( createPowerLevel({ @@ -1153,9 +1006,7 @@ describe('authorization rules', () => { }); // 'test' is removed with higher power level than alice's current power level, reject - expect(() => - checkEventAuthWithState(newPowerLevel18, secondState, store), - ).toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel18, secondState, store)).toThrow(); const bob = '@bob:example.com'; @@ -1179,9 +1030,7 @@ describe('authorization rules', () => { }, }); - expect(() => - checkEventAuthWithState(newPowerLevel19, secondState, store), - ).toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel19, secondState, store)).toThrow(); // can reduce bob's power level if alice has enough power const newPowerLevel20 = createPowerLevel({ @@ -1192,9 +1041,7 @@ describe('authorization rules', () => { }, }); - expect(() => - checkEventAuthWithState(newPowerLevel20, secondState, store), - ).not.toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel20, secondState, store)).not.toThrow(); // can not remove bob's power level if alice doesn't have enough power setPowerLevel( @@ -1216,9 +1063,7 @@ describe('authorization rules', () => { }, }); - expect(() => - checkEventAuthWithState(newPowerLevel21, secondState, store), - ).toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel21, secondState, store)).toThrow(); // can remove bob's power level if alice has enough power setPowerLevel( @@ -1240,9 +1085,7 @@ describe('authorization rules', () => { }, }); - expect(() => - checkEventAuthWithState(newPowerLevel22, secondState, store), - ).not.toThrow(); + expect(() => checkEventAuthWithState(newPowerLevel22, secondState, store)).not.toThrow(); }); // TODO: restricted rooms diff --git a/packages/room/src/authorizartion-rules/rules.ts b/packages/room/src/authorizartion-rules/rules.ts index 16d84f3bd..e8a449d75 100644 --- a/packages/room/src/authorizartion-rules/rules.ts +++ b/packages/room/src/authorizartion-rules/rules.ts @@ -1,15 +1,12 @@ import assert from 'node:assert'; -import { type PduType } from '../types/v3-11'; +import { RejectCodes, StateResolverAuthorizationError } from './errors'; import type { PersistentEventBase } from '../manager/event-wrapper'; import { PowerLevelEvent } from '../manager/power-level-event-wrapper'; -import { RoomVersion } from '../manager/type'; -import { - type EventStore, - getStateByMapKey, -} from '../state_resolution/definitions/definitions'; +import type { RoomVersion } from '../manager/type'; +import { type EventStore, getStateByMapKey } from '../state_resolution/definitions/definitions'; import { type StateMapKey } from '../types/_common'; -import { RejectCodes, StateResolverAuthorizationError } from './errors'; +import { type PduType } from '../types/v3-11'; // https://spec.matrix.org/v1.12/rooms/v1/#authorization-rules // skip if not any of the specified type of events @@ -27,9 +24,7 @@ function extractDomain(identifier: string) { return identifier.split(':').pop(); } -function isCreateAllowed( - createEvent: PersistentEventBase, -) { +function isCreateAllowed(createEvent: PersistentEventBase) { // If it has any prev_events, reject. if (createEvent.event.prev_events.length > 0) { throw new StateResolverAuthorizationError(RejectCodes.AuthError, { @@ -49,12 +44,7 @@ function isCreateAllowed( const content = createEvent.getContent(); // If content.room_version is assert(verifier(event as V2Pdu, authEvents), "not allowed"present and is not a recognised version, reject. - if ( - content.room_version && - !['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'].includes( - content.room_version, - ) - ) { + if (content.room_version && !['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'].includes(content.room_version)) { throw new StateResolverAuthorizationError(RejectCodes.AuthError, { rejectedEvent: createEvent, reason: `m.room.create event content.room_version is not a recognised version ${content.room_version}`, @@ -71,9 +61,7 @@ function isCreateAllowed( } // TODO: better typing for alias event -function isRoomAliasAllowed( - roomAliasEvent: PersistentEventBase, -): void { +function isRoomAliasAllowed(roomAliasEvent: PersistentEventBase): void { // If event has no state_key, reject. if (!roomAliasEvent.stateKey) { throw new StateResolverAuthorizationError(RejectCodes.AuthError, { @@ -86,12 +74,9 @@ function isRoomAliasAllowed( if (roomAliasEvent.origin !== roomAliasEvent.stateKey) { throw new StateResolverAuthorizationError(RejectCodes.AuthError, { rejectedEvent: roomAliasEvent, - reason: - 'm.room.canonical_alias event sender domain does not match state_key', + reason: 'm.room.canonical_alias event sender domain does not match state_key', }); } - - return; } async function isMembershipChangeAllowed( @@ -100,10 +85,7 @@ async function isMembershipChangeAllowed( store: EventStore, ): Promise { // If there is no state_key property, or no membership property in content, reject. - if ( - !membershipEventToCheck.stateKey || - !membershipEventToCheck.isMembershipEvent() - ) { + if (!membershipEventToCheck.stateKey || !membershipEventToCheck.isMembershipEvent()) { throw new StateResolverAuthorizationError(RejectCodes.AuthError, { rejectedEvent: membershipEventToCheck, reason: 'm.room.member event has no state_key or membership property', @@ -114,7 +96,7 @@ async function isMembershipChangeAllowed( // state_key -> whose state is asked to change // sender information, like does this user have permission? - const sender = membershipEventToCheck.sender; + const { sender } = membershipEventToCheck; const senderMembershipEvent = getStateByMapKey(authEventStateMap, { type: 'm.room.member', state_key: sender, @@ -135,9 +117,7 @@ async function isMembershipChangeAllowed( type: 'm.room.join_rules', }); - const joinRule = joinRuleEvent?.isJoinRuleEvent() - ? joinRuleEvent.getJoinRule() - : undefined; + const joinRule = joinRuleEvent?.isJoinRuleEvent() ? joinRuleEvent.getJoinRule() : undefined; const powerLevelEventInAuthStateMap = getStateByMapKey(authEventStateMap, { type: 'm.room.power_levels', @@ -155,9 +135,7 @@ async function isMembershipChangeAllowed( const content = membershipEventToCheck.getContent(); - const previousEvents = await store.getEvents( - membershipEventToCheck.getPreviousEventIds(), - ); + const previousEvents = await store.getEvents(membershipEventToCheck.getPreviousEventIds()); switch (content.membership) { case 'join': { @@ -174,10 +152,7 @@ async function isMembershipChangeAllowed( if (previousEvents.length === 1) { const [event] = previousEvents; - if ( - event.isCreateEvent() && - event.getContent().creator === membershipEventToCheck.stateKey - ) { + if (event.isCreateEvent() && event.getContent().creator === membershipEventToCheck.stateKey) { return; } } @@ -275,10 +250,7 @@ async function isMembershipChangeAllowed( } // If the sender’s power level is greater than or equal to the invite level, allow. - const senderPowerLevel = powerLevelEvent.getPowerLevelForUser( - sender, - roomCreateEvent, - ); + const senderPowerLevel = powerLevelEvent.getPowerLevelForUser(sender, roomCreateEvent); // The level required to invite a user. Defaults to 0 if unspecified. const inviteLevel = powerLevelEvent.getRequiredPowerForInvite(); @@ -296,10 +268,7 @@ async function isMembershipChangeAllowed( case 'leave': { // If the sender matches state_key, allow if and only if that user’s current membership state is invite or join. - if ( - sender === invitee && - (inviteeMembership === 'invite' || inviteeMembership === 'join') - ) { + if (sender === invitee && (inviteeMembership === 'invite' || inviteeMembership === 'join')) { return; } @@ -313,10 +282,7 @@ async function isMembershipChangeAllowed( } // If the target user’s current membership state is ban, and the sender’s power level is less than the ban level, reject. - const senderPowerLevel = powerLevelEvent.getPowerLevelForUser( - sender, - roomCreateEvent, - ); + const senderPowerLevel = powerLevelEvent.getPowerLevelForUser(sender, roomCreateEvent); // defaults to 50 if not specified const banLevel = powerLevelEvent.getRequiredPowerForBan(); @@ -330,11 +296,7 @@ async function isMembershipChangeAllowed( // If the sender’s power level is greater than or equal to the kick level, and the target user’s power level is less than the sender’s power level, allow. const kickRequiredLevel = powerLevelEvent.getRequiredPowerForKick(); - if ( - senderPowerLevel >= kickRequiredLevel && - powerLevelEvent.getPowerLevelForUser(invitee, roomCreateEvent) < - senderPowerLevel - ) { + if (senderPowerLevel >= kickRequiredLevel && powerLevelEvent.getPowerLevelForUser(invitee, roomCreateEvent) < senderPowerLevel) { return; } @@ -356,17 +318,10 @@ async function isMembershipChangeAllowed( } // If the sender’s power level is greater than or equal to the ban level, and the target user’s power level is less than the sender’s power level, allow. - const senderPowerLevel = powerLevelEvent.getPowerLevelForUser( - sender, - roomCreateEvent, - ); + const senderPowerLevel = powerLevelEvent.getPowerLevelForUser(sender, roomCreateEvent); // defaults to 50 if not specified const banLevel = powerLevelEvent.getRequiredPowerForBan(); - if ( - senderPowerLevel >= banLevel && - powerLevelEvent.getPowerLevelForUser(invitee, roomCreateEvent) < - senderPowerLevel - ) { + if (senderPowerLevel >= banLevel && powerLevelEvent.getPowerLevelForUser(invitee, roomCreateEvent) < senderPowerLevel) { return; } @@ -405,16 +360,11 @@ export function validatePowerLevelEvent( const newPowerLevel = powerLevelEvent; - const senderCurrentPowerLevel = existingPowerLevel.getPowerLevelForUser( - newPowerLevel.sender, - roomCreateEvent, - ); + const senderCurrentPowerLevel = existingPowerLevel.getPowerLevelForUser(newPowerLevel.sender, roomCreateEvent); - const existingUserDefaultPowerLevel = - existingPowerLevel.getPowerLevelUserDefaultValue(); + const existingUserDefaultPowerLevel = existingPowerLevel.getPowerLevelUserDefaultValue(); - const newUserDefaultPowerLevel = - newPowerLevel.getPowerLevelUserDefaultValue(); + const newUserDefaultPowerLevel = newPowerLevel.getPowerLevelUserDefaultValue(); // For each found alteration: @@ -422,86 +372,60 @@ export function validatePowerLevelEvent( // If the new value is greater than the sender’s current power level, reject. if (existingUserDefaultPowerLevel !== newUserDefaultPowerLevel) { - if ( - newUserDefaultPowerLevel && - newUserDefaultPowerLevel > senderCurrentPowerLevel - ) { + if (newUserDefaultPowerLevel && newUserDefaultPowerLevel > senderCurrentPowerLevel) { throw new StateResolverAuthorizationError(RejectCodes.AuthError, { rejectedEvent: powerLevelEvent.toEventBase()!, - reason: - 'new user_default power level is greater than sender power level', + reason: 'new user_default power level is greater than sender power level', rejectedBy: existingPowerLevel.toEventBase(), }); } - if ( - existingUserDefaultPowerLevel && - existingUserDefaultPowerLevel > senderCurrentPowerLevel - ) { + if (existingUserDefaultPowerLevel && existingUserDefaultPowerLevel > senderCurrentPowerLevel) { throw new StateResolverAuthorizationError(RejectCodes.AuthError, { rejectedEvent: powerLevelEvent.toEventBase()!, - reason: - 'existing user_default power level is greater than sender power level', + reason: 'existing user_default power level is greater than sender power level', rejectedBy: existingPowerLevel.toEventBase(), }); } } const newEventsDefaultValue = newPowerLevel.getPowerLevelEventsDefaultValue(); - const existingEventsDefaultValue = - existingPowerLevel.getPowerLevelEventsDefaultValue(); + const existingEventsDefaultValue = existingPowerLevel.getPowerLevelEventsDefaultValue(); if (existingEventsDefaultValue !== newEventsDefaultValue) { - if ( - newEventsDefaultValue && - newEventsDefaultValue > senderCurrentPowerLevel - ) { + if (newEventsDefaultValue && newEventsDefaultValue > senderCurrentPowerLevel) { throw new StateResolverAuthorizationError(RejectCodes.AuthError, { rejectedEvent: powerLevelEvent.toEventBase()!, - reason: - 'new events_default power level is greater than sender power level', + reason: 'new events_default power level is greater than sender power level', rejectedBy: existingPowerLevel.toEventBase(), }); } - if ( - existingEventsDefaultValue && - existingEventsDefaultValue > senderCurrentPowerLevel - ) { + if (existingEventsDefaultValue && existingEventsDefaultValue > senderCurrentPowerLevel) { throw new StateResolverAuthorizationError(RejectCodes.AuthError, { rejectedEvent: powerLevelEvent.toEventBase()!, - reason: - 'existing events_default power level is greater than sender power level', + reason: 'existing events_default power level is greater than sender power level', rejectedBy: existingPowerLevel.toEventBase(), }); } } const newStateDefaultValue = newPowerLevel.getPowerLevelStateDefaultValue(); - const existingStateDefaultValue = - existingPowerLevel.getPowerLevelStateDefaultValue(); + const existingStateDefaultValue = existingPowerLevel.getPowerLevelStateDefaultValue(); if (existingStateDefaultValue !== newStateDefaultValue) { - if ( - newStateDefaultValue && - newStateDefaultValue > senderCurrentPowerLevel - ) { + if (newStateDefaultValue && newStateDefaultValue > senderCurrentPowerLevel) { throw new StateResolverAuthorizationError(RejectCodes.AuthError, { rejectedEvent: powerLevelEvent.toEventBase()!, - reason: - 'new state_default power level is greater than sender power level', + reason: 'new state_default power level is greater than sender power level', rejectedBy: existingPowerLevel.toEventBase(), }); } - if ( - existingStateDefaultValue && - existingStateDefaultValue > senderCurrentPowerLevel - ) { + if (existingStateDefaultValue && existingStateDefaultValue > senderCurrentPowerLevel) { throw new StateResolverAuthorizationError(RejectCodes.AuthError, { rejectedEvent: powerLevelEvent.toEventBase()!, - reason: - 'existing state_default power level is greater than sender power level', + reason: 'existing state_default power level is greater than sender power level', rejectedBy: existingPowerLevel.toEventBase(), }); } @@ -601,23 +525,15 @@ export function validatePowerLevelEvent( // 4. For each entry being changed in, or removed from, the events property: const existingEvents = Object.keys(existingContent?.events ?? {}); for (const eventType of existingEvents) { - const existingPowerLevelValue = existingPowerLevel.getPowerLevelEventsValue( - eventType as PduType, - ); - const newPowerLevelValue = newPowerLevel.getPowerLevelEventsValue( - eventType as PduType, - ); + const existingPowerLevelValue = existingPowerLevel.getPowerLevelEventsValue(eventType as PduType); + const newPowerLevelValue = newPowerLevel.getPowerLevelEventsValue(eventType as PduType); if (!newPowerLevelValue || newPowerLevelValue !== existingPowerLevelValue) { // changed or removed // If the current value is greater than the sender’s current power level, reject. - if ( - existingPowerLevelValue && - existingPowerLevelValue > senderCurrentPowerLevel - ) { + if (existingPowerLevelValue && existingPowerLevelValue > senderCurrentPowerLevel) { throw new StateResolverAuthorizationError(RejectCodes.AuthError, { rejectedEvent: powerLevelEvent.toEventBase()!, - reason: - 'existing power level value is greater than sender power level', + reason: 'existing power level value is greater than sender power level', rejectedBy: existingPowerLevel.toEventBase(), }); } @@ -627,12 +543,8 @@ export function validatePowerLevelEvent( // 5. For each entry being added to, or changed in, the events property: const newEvents = Object.keys(newContent?.events ?? {}); for (const eventType of newEvents) { - const existingPowerLevelValue = existingPowerLevel.getPowerLevelEventsValue( - eventType as PduType, - ); - const newPowerLevelValue = newPowerLevel.getPowerLevelEventsValue( - eventType as PduType, - ); + const existingPowerLevelValue = existingPowerLevel.getPowerLevelEventsValue(eventType as PduType); + const newPowerLevelValue = newPowerLevel.getPowerLevelEventsValue(eventType as PduType); if (!newPowerLevelValue || newPowerLevelValue !== existingPowerLevelValue) { // changed or added // If the new value is greater than the sender’s current power level, reject. @@ -650,23 +562,15 @@ export function validatePowerLevelEvent( const existingUsers = Object.keys(existingContent?.users ?? {}); for (const userId of existingUsers) { - const existingPowerLevelValue = - existingPowerLevel.getPowerLevelUsersValue(userId); + const existingPowerLevelValue = existingPowerLevel.getPowerLevelUsersValue(userId); const newPowerLevelValue = newPowerLevel.getPowerLevelUsersValue(userId); - if ( - userId !== powerLevelEvent.sender && - (!newPowerLevelValue || newPowerLevelValue !== existingPowerLevelValue) - ) { + if (userId !== powerLevelEvent.sender && (!newPowerLevelValue || newPowerLevelValue !== existingPowerLevelValue)) { // changed or removed // If the current value is greater than the sender’s current power level, reject. - if ( - existingPowerLevelValue && - existingPowerLevelValue > senderCurrentPowerLevel - ) { + if (existingPowerLevelValue && existingPowerLevelValue > senderCurrentPowerLevel) { throw new StateResolverAuthorizationError(RejectCodes.AuthError, { rejectedEvent: powerLevelEvent.toEventBase()!, - reason: - 'existing power level value is greater than sender power level', + reason: 'existing power level value is greater than sender power level', rejectedBy: existingPowerLevel.toEventBase(), }); } @@ -676,13 +580,9 @@ export function validatePowerLevelEvent( // 7. For each entry being changed in, or removed from, the users property: const newUsers = Object.keys(newContent?.users ?? {}); for (const userId of newUsers) { - const existingPowerLevelValue = - existingPowerLevel.getPowerLevelUsersValue(userId); + const existingPowerLevelValue = existingPowerLevel.getPowerLevelUsersValue(userId); const newPowerLevelValue = newPowerLevel.getPowerLevelUsersValue(userId); - if ( - !existingPowerLevelValue || - newPowerLevelValue !== existingPowerLevelValue - ) { + if (!existingPowerLevelValue || newPowerLevelValue !== existingPowerLevelValue) { // changed or added // If the new value is greater than the sender’s current power level, reject. if (newPowerLevelValue && newPowerLevelValue > senderCurrentPowerLevel) { @@ -696,10 +596,7 @@ export function validatePowerLevelEvent( } } -export function checkEventAuthWithoutState( - event: PersistentEventBase, - authEvents: PersistentEventBase[], -) { +export function checkEventAuthWithoutState(event: PersistentEventBase, authEvents: PersistentEventBase[]) { if (event.isCreateEvent()) { if (authEvents.length > 0) { throw new StateResolverAuthorizationError(RejectCodes.AuthError, { @@ -721,9 +618,7 @@ export function checkEventAuthWithoutState( */ - const stateKeysNeeded = new Map( - event.getAuthEventStateKeys().map((key) => [key, null]), - ); + const stateKeysNeeded = new Map(event.getAuthEventStateKeys().map((key) => [key, null])); const authEventStateMap = new Map(); @@ -797,10 +692,7 @@ export async function checkEventAuthWithState( } // If the content of the m.room.create event in the room state has the property m.federate set to false, and the sender domain of the event does not match the sender domain of the create event, reject. - if ( - roomCreateEvent.getContent()['m.federate'] === false && - event.origin !== roomCreateEvent.origin - ) { + if (roomCreateEvent.getContent()['m.federate'] === false && event.origin !== roomCreateEvent.origin) { throw new StateResolverAuthorizationError(RejectCodes.AuthError, { rejectedEvent: event, rejectedBy: roomCreateEvent, @@ -830,7 +722,6 @@ export async function checkEventAuthWithState( } // If type is m.room.third_party_invite: - // @ts-ignore the pdu union doesn't have this type TODO: add if (event.type === 'm.room.third_party_invite') { console.warn('third_party_invite not implemented'); throw new StateResolverAuthorizationError(RejectCodes.NotImplemented, { @@ -848,13 +739,9 @@ export async function checkEventAuthWithState( : PowerLevelEvent.fromDefault(); // If the event type’s required power level is greater than the sender’s power level, reject. - const eventRequiredPowerLevel = - powerLevelEvent.getRequiredPowerLevelForEvent(event); + const eventRequiredPowerLevel = powerLevelEvent.getRequiredPowerLevelForEvent(event); - const userPowerLevel = powerLevelEvent.getPowerLevelForUser( - event.sender, - roomCreateEvent, - ); + const userPowerLevel = powerLevelEvent.getPowerLevelForUser(event.sender, roomCreateEvent); if (userPowerLevel < eventRequiredPowerLevel) { throw new StateResolverAuthorizationError(RejectCodes.AuthError, { @@ -874,11 +761,7 @@ export async function checkEventAuthWithState( // If type is m.room.power_levels: if (event.isPowerLevelEvent()) { - return validatePowerLevelEvent( - event.toPowerLevelEvent(), - roomCreateEvent, - state, - ); + return validatePowerLevelEvent(event.toPowerLevelEvent(), roomCreateEvent, state); } // TODO: redaction diff --git a/packages/room/src/index.ts b/packages/room/src/index.ts index 3ce4a334e..da1f7511d 100644 --- a/packages/room/src/index.ts +++ b/packages/room/src/index.ts @@ -1,7 +1,4 @@ -export { - checkEventAuthWithState, - checkEventAuthWithoutState, -} from './authorizartion-rules/rules'; +export { checkEventAuthWithState, checkEventAuthWithoutState } from './authorizartion-rules/rules'; export * from './authorizartion-rules/errors'; export { resolveStateV2Plus } from './state_resolution/definitions/algorithm/v2'; export * from './manager/factory'; diff --git a/packages/room/src/manager/event-wrapper.spec.ts b/packages/room/src/manager/event-wrapper.spec.ts index f2c90baf1..ee03a0c24 100644 --- a/packages/room/src/manager/event-wrapper.spec.ts +++ b/packages/room/src/manager/event-wrapper.spec.ts @@ -1,18 +1,11 @@ -import { PersistentEventFactory } from './factory'; - import { describe, expect, it } from 'bun:test'; -import type { Pdu } from '../types/v3-11'; + +import { PersistentEventFactory } from './factory'; import type { RoomVersion } from './type'; +import type { Pdu } from '../types/v3-11'; -function runTest( - event: Parameters[0], - expected: any, - roomVersion: RoomVersion = '10', -) { - expect( - PersistentEventFactory.createFromRawEvent(event, roomVersion as RoomVersion) - .redactedRawEvent, - ).toEqual(expected); +function runTest(event: Parameters[0], expected: any, roomVersion: RoomVersion = '10') { + expect(PersistentEventFactory.createFromRawEvent(event, roomVersion as RoomVersion).redactedRawEvent).toEqual(expected); } describe('[EventWrapper] Redaction', () => { @@ -229,7 +222,7 @@ describe('[EventWrapper] Redaction', () => { unsigned: {}, }; - // @ts-expect-error + // @ts-expect-error -- ignore -- runTest(a2, b2, '11'); }); @@ -347,7 +340,7 @@ describe('[EventWrapper] Redaction', () => { const a = { type: 'm.room.message', content: { - body: 'foo', + 'body': 'foo', 'm.relates_to': { rel_type: 'rel_type', event_id: '$parent:domain', @@ -369,30 +362,15 @@ describe('[EventWrapper] Redaction', () => { }); it('correctly calculate new depth', () => { - const e1 = PersistentEventFactory.createFromRawEvent( - { depth: 1 } as Pdu, - '10', - ); - const e2 = PersistentEventFactory.createFromRawEvent( - { depth: 1 } as Pdu, - '10', - ).addPrevEvents([e1]); + const e1 = PersistentEventFactory.createFromRawEvent({ depth: 1 } as Pdu, '10'); + const e2 = PersistentEventFactory.createFromRawEvent({ depth: 1 } as Pdu, '10').addPrevEvents([e1]); expect(e2.depth).toBe(2); - const e3 = PersistentEventFactory.createFromRawEvent( - { depth: 5 } as Pdu, - '10', - ); + const e3 = PersistentEventFactory.createFromRawEvent({ depth: 5 } as Pdu, '10'); e2.addPrevEvents([e3]); expect(e2.depth).toBe(6); - const e4 = PersistentEventFactory.createFromRawEvent( - { depth: 4 } as Pdu, - '10', - ); - const e5 = PersistentEventFactory.createFromRawEvent( - { depth: 7 } as Pdu, - '10', - ); + const e4 = PersistentEventFactory.createFromRawEvent({ depth: 4 } as Pdu, '10'); + const e5 = PersistentEventFactory.createFromRawEvent({ depth: 7 } as Pdu, '10'); e2.addPrevEvents([e5, e4]); // intentional out of order expect(e2.depth).toBe(8); diff --git a/packages/room/src/manager/event-wrapper.ts b/packages/room/src/manager/event-wrapper.ts index d24c76542..3292c296e 100644 --- a/packages/room/src/manager/event-wrapper.ts +++ b/packages/room/src/manager/event-wrapper.ts @@ -1,24 +1,13 @@ import crypto from 'node:crypto'; -import { - encodeCanonicalJson, - toUnpaddedBase64, -} from '@rocket.chat/federation-crypto'; -import { type RejectCode, RejectCodes } from '../authorizartion-rules/errors'; -import { - type EventStore, - getStateMapKey, -} from '../state_resolution/definitions/definitions'; -import type { EventID, PduForType, StateMapKey } from '../types/_common'; -import { - Pdu, - PduContent, - type PduJoinRuleEventContent, - type PduMembershipEventContent, - PduType, - Signature, -} from '../types/v3-11'; + +import { encodeCanonicalJson, toUnpaddedBase64 } from '@rocket.chat/federation-crypto'; + import { PowerLevelEvent } from './power-level-event-wrapper'; import { type RoomVersion } from './type'; +import { type RejectCode, RejectCodes } from '../authorizartion-rules/errors'; +import { getStateMapKey } from '../state_resolution/definitions/definitions'; +import type { EventID, PduForType, StateMapKey } from '../types/_common'; +import type { Pdu, PduContent, PduType, Signature, PduJoinRuleEventContent, PduMembershipEventContent } from '../types/v3-11'; export function extractDomainFromId(identifier: string) { const idx = identifier.indexOf(':'); @@ -36,29 +25,20 @@ type MakeOptional = { export type Prettify = { [K in keyof T]: T[K]; -} & {}; +} & object; -export type PduWithHashesAndSignaturesOptional = Prettify< - MakeOptional ->; +export type PduWithHashesAndSignaturesOptional = Prettify>; export const REDACT_ALLOW_ALL_KEYS: unique symbol = Symbol.for('all'); export interface State extends Map { get( key: T, - ): T extends `${infer I}:${string}` - ? I extends PduType - ? PersistentEventBase | undefined - : never - : never; + ): T extends `${infer I}:${string}` ? (I extends PduType ? PersistentEventBase | undefined : never) : never; } // convinient wrapper to manage schema differences when working with same algorithms across different versions -export abstract class PersistentEventBase< - Version extends RoomVersion = RoomVersion, - Type extends PduType = PduType, -> { +export abstract class PersistentEventBase { public rejectCode = ''; public rejectReason = ''; @@ -70,13 +50,10 @@ export abstract class PersistentEventBase< protected rawEvent: PduWithHashesAndSignaturesOptional; private authEventsIds: Set = new Set(); + private prevEventsIds: Set = new Set(); - constructor( - event: PduWithHashesAndSignaturesOptional, - public readonly version: Version, - private partial = false, - ) { + constructor(event: PduWithHashesAndSignaturesOptional, public readonly version: Version, private partial = false) { this.rawEvent = JSON.parse(JSON.stringify(event)); if (this.rawEvent.signatures) { this.signatures = this.rawEvent.signatures; @@ -193,10 +170,7 @@ export abstract class PersistentEventBase< isState() { // spec wise this is the right way to check if an event is a state event - return ( - 'state_key' in this.rawEvent && - typeof this.rawEvent.state_key === 'string' - ); + return 'state_key' in this.rawEvent && typeof this.rawEvent.state_key === 'string'; } isTimelineEvent() { @@ -207,10 +181,7 @@ export abstract class PersistentEventBase< return this.isState() && this.type === 'm.room.topic'; } - isPowerLevelEvent(): this is PersistentEventBase< - Version, - 'm.room.power_levels' - > { + isPowerLevelEvent(): this is PersistentEventBase { return this.isState() && this.type === 'm.room.power_levels'; } @@ -230,24 +201,15 @@ export abstract class PersistentEventBase< return this.isState() && this.type === 'm.room.create'; } - isServerAclEvent(): this is PersistentEventBase< - Version, - 'm.room.server_acl' - > { + isServerAclEvent(): this is PersistentEventBase { return this.isState() && this.type === 'm.room.server_acl'; } - isHistoryVisibilityEvent(): this is PersistentEventBase< - Version, - 'm.room.history_visibility' - > { + isHistoryVisibilityEvent(): this is PersistentEventBase { return this.isState() && this.type === 'm.room.history_visibility'; } - isCanonicalAliasEvent(): this is PersistentEventBase< - Version, - 'm.room.canonical_alias' - > { + isCanonicalAliasEvent(): this is PersistentEventBase { return this.isState() && this.type === 'm.room.canonical_alias'; } @@ -256,8 +218,7 @@ export abstract class PersistentEventBase< } getMembership() { - if (!this.isMembershipEvent()) - throw new Error('Event is not a membership event'); + if (!this.isMembershipEvent()) throw new Error('Event is not a membership event'); return (this.getContent() as PduMembershipEventContent).membership; } @@ -277,17 +238,13 @@ export abstract class PersistentEventBase< // for redaction algorithm abstract getAllowedKeys(): string[]; - abstract getAllowedContentKeys(): Record< - PduType, - string[] | typeof REDACT_ALLOW_ALL_KEYS - >; + abstract getAllowedContentKeys(): Record; private _getRedactedEvent(event: PduWithHashesAndSignaturesOptional) { type KeysExceptContent = Exclude; // it is expected to have everything in this event ready by this point - const topLevelAllowedKeysExceptContent = - this.getAllowedKeys() as KeysExceptContent[]; + const topLevelAllowedKeysExceptContent = this.getAllowedKeys() as KeysExceptContent[]; const dict = {} as Record; @@ -302,9 +259,7 @@ export abstract class PersistentEventBase< let newContent = {} as Partial; // m.room.member allows keys membership, join_authorised_via_users_server. Additionally, it allows the signed key of the third_party_invite key. - const allowedContentKeys = this.getAllowedContentKeys()[this.type] as - | (keyof PduContent)[] - | typeof REDACT_ALLOW_ALL_KEYS; + const allowedContentKeys = this.getAllowedContentKeys()[this.type] as (keyof PduContent)[] | typeof REDACT_ALLOW_ALL_KEYS; if (allowedContentKeys) { if (allowedContentKeys === REDACT_ALLOW_ALL_KEYS) { @@ -353,17 +308,14 @@ export abstract class PersistentEventBase< getReferenceHash() { // SPEC: https://spec.matrix.org/v1.12/server-server-api/#calculating-the-reference-hash-for-an-event // 1. The signatures and unsigned properties are removed from the event, if present. - const redactedEvent = this.redactedEvent; + const { redactedEvent } = this; const { unsigned, signatures, ...toHash } = redactedEvent; // 2. The event is converted into Canonical JSON. const canonicalJson = encodeCanonicalJson(toHash); // 3. A sha256 hash is calculated on the resulting JSON object. - const referenceHash = crypto - .createHash('sha256') - .update(canonicalJson) - .digest(); + const referenceHash = crypto.createHash('sha256').update(canonicalJson).digest(); return referenceHash; } @@ -373,10 +325,7 @@ export abstract class PersistentEventBase< // First, any existing unsigned, signature, and hashes members are removed. The resulting object is then encoded as Canonical JSON, and the JSON is hashed using SHA-256. const { unsigned, signatures, hashes, ...toHash } = rawEvent; // must not use this.event as it can potentially call getContentHash again - return crypto - .createHash('sha256') - .update(encodeCanonicalJson(toHash)) - .digest(); + return crypto.createHash('sha256').update(encodeCanonicalJson(toHash)).digest(); } static getContentHashString(rawEvent: PduWithHashesAndSignaturesOptional) { @@ -416,10 +365,8 @@ export abstract class PersistentEventBase< // If type is m.room.member: if (this.isMembershipEvent()) { - //The target’s current m.room.member event, if any. - authTypes.add( - getStateMapKey({ type: 'm.room.member', state_key: this.stateKey }), - ); + // The target’s current m.room.member event, if any. + authTypes.add(getStateMapKey({ type: 'm.room.member', state_key: this.stateKey })); // If membership is join or invite, the current m.room.join_rules event, if any. const membership = this.getMembership(); @@ -428,18 +375,12 @@ export abstract class PersistentEventBase< } // If membership is invite and content contains a third_party_invite property, the current m.room.third_party_invite event with state_key matching content.third_party_invite.signed.token, if any. - if ( - membership === 'invite' && - this.getContent().third_party_invite - ) { + if (membership === 'invite' && this.getContent().third_party_invite) { throw new Error('third_party_invite not supported'); } // If content.join_authorised_via_users_server is present, and the room version supports restricted rooms, then the m.room.member event with state_key matching content.join_authorised_via_users_server. - if ( - this.getContent() - .join_authorised_via_users_server - ) { + if (this.getContent().join_authorised_via_users_server) { throw new Error('join_authorised_via_users_server not supported'); } } @@ -473,8 +414,7 @@ export abstract class PersistentEventBase< for (const event of events) { this.prevEventsIds.add(event.eventId); } - const deepestDepth = - events.sort((e1, e2) => e1.depth - e2.depth).pop()?.depth ?? 0; + const deepestDepth = events.sort((e1, e2) => e1.depth - e2.depth).pop()?.depth ?? 0; if (this.rawEvent.depth <= deepestDepth) { this.rawEvent.depth = deepestDepth + 1; } @@ -505,4 +445,3 @@ export abstract class PersistentEventBase< }); } } -export type { EventStore }; diff --git a/packages/room/src/manager/factory.ts b/packages/room/src/manager/factory.ts index b78098b69..38ed5f027 100644 --- a/packages/room/src/manager/factory.ts +++ b/packages/room/src/manager/factory.ts @@ -1,17 +1,13 @@ -import { Pdu, type PduCreateEventContent, PduType } from '../types/v3-11'; - -import { PersistentEventV3 } from './v3'; - -import { PduForType, RoomID, UserID, roomIdSchema } from '../types/_common'; -import type { - PduWithHashesAndSignaturesOptional, - PersistentEventBase, -} from './event-wrapper'; +import type { PduWithHashesAndSignaturesOptional, PersistentEventBase } from './event-wrapper'; import type { RoomVersion } from './type'; +import { PersistentEventV11 } from './v11'; +import { PersistentEventV3 } from './v3'; import { PersistentEventV6 } from './v6'; import { PersistentEventV8 } from './v8'; import { PersistentEventV9 } from './v9'; -import { PersistentEventV11 } from './v11'; +import { RoomID, roomIdSchema } from '../types/_common'; +import type { PduForType, UserID } from '../types/_common'; +import type { Pdu, PduType, PduCreateEventContent } from '../types/v3-11'; // Utility function to create a random ID for room creation function createRoomIdPrefix(length: number) { @@ -44,9 +40,7 @@ export class PersistentEventFactory { static defaultRoomVersion = '10' as const; // same as synapse - static isSupportedRoomVersion( - roomVersion: string, - ): roomVersion is RoomVersion { + static isSupportedRoomVersion(roomVersion: string): roomVersion is RoomVersion { return PersistentEventFactory.supportedRoomVersions.includes(roomVersion); } @@ -82,10 +76,7 @@ export class PersistentEventFactory { // create individual events // a m.room.create event, adds the roomId too - static newCreateEvent( - creator: UserID, - roomVersion: RoomVersion = PersistentEventFactory.defaultRoomVersion, - ) { + static newCreateEvent(creator: UserID, roomVersion: RoomVersion = PersistentEventFactory.defaultRoomVersion) { if (!PersistentEventFactory.isSupportedRoomVersion(roomVersion)) { throw new Error(`Room version ${roomVersion} is not supported`); } @@ -111,10 +102,7 @@ export class PersistentEventFactory { depth: 1, }; - return PersistentEventFactory.createFromRawEvent<'m.room.create'>( - eventPartial, - roomVersion, - ); + return PersistentEventFactory.createFromRawEvent<'m.room.create'>(eventPartial, roomVersion); } static newEvent( diff --git a/packages/room/src/manager/power-level-event-wrapper.ts b/packages/room/src/manager/power-level-event-wrapper.ts index 7caeedeff..e4b8f323f 100644 --- a/packages/room/src/manager/power-level-event-wrapper.ts +++ b/packages/room/src/manager/power-level-event-wrapper.ts @@ -1,21 +1,20 @@ +import type { PersistentEventBase } from './event-wrapper'; +import type { RoomVersion } from './type'; import { type PduPowerLevelsEventContent, type PduType } from '../types/v3-11'; -import { PersistentEventBase } from './event-wrapper'; -import { RoomVersion } from './type'; // centralize all power level values here // whether there is an event or not // all defaults and transformations according to diff versions of pdus class PowerLevelEvent< - PowerLevelEventType extends - | PersistentEventBase - | undefined = PersistentEventBase, + PowerLevelEventType extends PersistentEventBase | undefined = PersistentEventBase< + RoomVersion, + 'm.room.power_levels' + >, > { private readonly _content?: PduPowerLevelsEventContent; - static fromEvent( - event: PersistentEventBase, - ) { + static fromEvent(event: PersistentEventBase) { return new PowerLevelEvent(event); } @@ -64,10 +63,7 @@ class PowerLevelEvent< return this._content.redact ?? 50; } - getPowerLevelForUser( - userId: string, - createEvent?: PersistentEventBase, - ) { + getPowerLevelForUser(userId: string, createEvent?: PersistentEventBase) { if (!this._content) { if (createEvent?.sender === userId) { return 100; diff --git a/packages/room/src/manager/room-state.ts b/packages/room/src/manager/room-state.ts index e7107ef85..83794c73e 100644 --- a/packages/room/src/manager/room-state.ts +++ b/packages/room/src/manager/room-state.ts @@ -1,17 +1,12 @@ -import { getStateByMapKey } from '../state_resolution/definitions/definitions'; -import { StateMapKey, UserID } from '../types/_common'; -import { - PduJoinRuleEventContent, - PduMembershipEventContent, -} from '../types/v3-11'; import { type PersistentEventBase } from './event-wrapper'; -import { RoomVersion } from './type'; +import type { RoomVersion } from './type'; +import { getStateByMapKey } from '../state_resolution/definitions/definitions'; +import type { StateMapKey, UserID } from '../types/_common'; +import type { PduJoinRuleEventContent, PduMembershipEventContent } from '../types/v3-11'; // RoomState is an accessor to help with accessing room properties from internal state representation which is essentially a map (see adrs for more information) export class RoomState { - constructor( - private readonly stateMap: Map, - ) {} + constructor(private readonly stateMap: Map) {} // who created the room get creator() { @@ -26,9 +21,7 @@ export class RoomState { return createEvent.getContent().creator; } - getUserMembership( - userId: string, - ): PduMembershipEventContent['membership'] | undefined { + getUserMembership(userId: string): PduMembershipEventContent['membership'] | undefined { const membershipEvent = getStateByMapKey(this.stateMap, { type: 'm.room.member', state_key: userId, @@ -119,7 +112,7 @@ export class RoomState { throw new Error('Room create event not found'); } - const origin = createEvent.origin; + const { origin } = createEvent; if (!origin) { throw new Error('Room create event has no origin'); } diff --git a/packages/room/src/manager/type.ts b/packages/room/src/manager/type.ts index 69e8b7b64..5ec8eb401 100644 --- a/packages/room/src/manager/type.ts +++ b/packages/room/src/manager/type.ts @@ -1,14 +1,5 @@ export type RoomVersion1And2 = '1' | '2'; -export type RoomVersion3To11 = - | '3' - | '4' - | '5' - | '6' - | '7' - | '8' - | '9' - | '10' - | '11'; +export type RoomVersion3To11 = '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | '11'; export type RoomVersion = RoomVersion1And2 | RoomVersion3To11; diff --git a/packages/room/src/manager/v11.ts b/packages/room/src/manager/v11.ts index c351de2bb..ecb9c2f26 100644 --- a/packages/room/src/manager/v11.ts +++ b/packages/room/src/manager/v11.ts @@ -1,10 +1,8 @@ -import { type PduType } from '../types/v3-11'; import { REDACT_ALLOW_ALL_KEYS } from './event-wrapper'; import { PersistentEventV9 } from './v9'; +import { type PduType } from '../types/v3-11'; -export class PersistentEventV11< - Type extends PduType = PduType, -> extends PersistentEventV9 { +export class PersistentEventV11 extends PersistentEventV9 { getAllowedKeys(): string[] { return [ 'event_id', @@ -20,25 +18,13 @@ export class PersistentEventV11< 'origin_server_ts', ]; } - getAllowedContentKeys(): Record< - string, - string[] | typeof REDACT_ALLOW_ALL_KEYS - > { + + getAllowedContentKeys(): Record { return { 'm.room.member': ['membership', 'join_authorised_via_users_server'], 'm.room.create': REDACT_ALLOW_ALL_KEYS, 'm.room.join_rules': ['join_rule', 'allow'], - 'm.room.power_levels': [ - 'ban', - 'events', - 'events_default', - 'invite', - 'kick', - 'redact', - 'state_default', - 'users', - 'users_default', - ], + 'm.room.power_levels': ['ban', 'events', 'events_default', 'invite', 'kick', 'redact', 'state_default', 'users', 'users_default'], 'm.room.history_visibility': ['history_visibility'], 'm.room.redaction': ['redacts'], }; diff --git a/packages/room/src/manager/v3.ts b/packages/room/src/manager/v3.ts index ffc49c73f..00ddf0604 100644 --- a/packages/room/src/manager/v3.ts +++ b/packages/room/src/manager/v3.ts @@ -1,17 +1,13 @@ import { toUnpaddedBase64 } from '@rocket.chat/federation-crypto'; -import type { EventID } from '../types/_common'; -import { PduType } from '../types/v3-11'; -import { - type EventStore, - PersistentEventBase, - REDACT_ALLOW_ALL_KEYS, -} from './event-wrapper'; + +import { PersistentEventBase } from './event-wrapper'; +import type { REDACT_ALLOW_ALL_KEYS } from './event-wrapper'; import type { RoomVersion3To11 } from './type'; +import type { EventID } from '../types/_common'; +import type { PduType } from '../types/v3-11'; // v3 is where it changes first -export class PersistentEventV3< - Type extends PduType = PduType, -> extends PersistentEventBase { +export class PersistentEventV3 extends PersistentEventBase { private _eventId?: EventID; get eventId(): EventID { @@ -23,8 +19,7 @@ export class PersistentEventV3< const referenceHash = this.getReferenceHash(); // The event ID is the reference hash of the event encoded using Unpadded Base64, prefixed with $. A resulting event ID using this approach should look similar to $CD66HAED5npg6074c6pDtLKalHjVfYb2q4Q3LZgrW6o. - this._eventId = - `\$${toUnpaddedBase64(referenceHash, { urlSafe: true })}` as EventID; + this._eventId = `\$${toUnpaddedBase64(referenceHash, { urlSafe: true })}` as EventID; return this._eventId; } @@ -47,24 +42,12 @@ export class PersistentEventV3< ]; } - getAllowedContentKeys(): Record< - string, - string[] | typeof REDACT_ALLOW_ALL_KEYS - > { + getAllowedContentKeys(): Record { return { 'm.room.create': ['creator'], 'm.room.member': ['membership'], 'm.room.join_rules': ['join_rule'], - 'm.room.power_levels': [ - 'users', - 'users_default', - 'events', - 'events_default', - 'state_default', - 'ban', - 'kick', - 'redact', - ], + 'm.room.power_levels': ['users', 'users_default', 'events', 'events_default', 'state_default', 'ban', 'kick', 'redact'], 'm.room.aliases': ['aliases'], 'm.room.history_visibility': ['history_visibility'], }; diff --git a/packages/room/src/manager/v6.ts b/packages/room/src/manager/v6.ts index 49afe3dd4..20c474a9a 100644 --- a/packages/room/src/manager/v6.ts +++ b/packages/room/src/manager/v6.ts @@ -1,13 +1,10 @@ -import { PduType } from '../types/v3-11'; import { PersistentEventV3 } from './v3'; +import type { PduType } from '../types/v3-11'; -export class PersistentEventV6< - Type extends PduType = PduType, -> extends PersistentEventV3 { +export class PersistentEventV6 extends PersistentEventV3 { getAllowedContentKeys() { const resp = super.getAllowedContentKeys(); - // biome-ignore lint/performance/noDelete: delete resp['m.room.aliases']; return resp; diff --git a/packages/room/src/manager/v8.ts b/packages/room/src/manager/v8.ts index c1d1e414b..80600e4cd 100644 --- a/packages/room/src/manager/v8.ts +++ b/packages/room/src/manager/v8.ts @@ -1,9 +1,7 @@ -import { PduType } from '../types/v3-11'; import { PersistentEventV6 } from './v6'; +import type { PduType } from '../types/v3-11'; -export class PersistentEventV8< - Type extends PduType = PduType, -> extends PersistentEventV6 { +export class PersistentEventV8 extends PersistentEventV6 { getAllowedContentKeys() { const resp = super.getAllowedContentKeys(); diff --git a/packages/room/src/manager/v9.spec.ts b/packages/room/src/manager/v9.spec.ts index 6c304a7a1..9592fe92b 100644 --- a/packages/room/src/manager/v9.spec.ts +++ b/packages/room/src/manager/v9.spec.ts @@ -1,7 +1,7 @@ import { expect, test } from 'bun:test'; -import { EventID } from '../types/_common'; import { PersistentEventV9 } from './v9'; +import type { EventID } from '../types/_common'; test('event without origin', async () => { const event = new PersistentEventV9( @@ -36,7 +36,5 @@ test('event without origin', async () => { '10', ); - expect(event.eventId).toBe( - '$iCA3OWE1EGtPVWIyGudgmifuJcIluQw88FuK_gd0FpM' as EventID, - ); + expect(event.eventId).toBe('$iCA3OWE1EGtPVWIyGudgmifuJcIluQw88FuK_gd0FpM' as EventID); }); diff --git a/packages/room/src/manager/v9.ts b/packages/room/src/manager/v9.ts index 971175e06..da6fd05c8 100644 --- a/packages/room/src/manager/v9.ts +++ b/packages/room/src/manager/v9.ts @@ -1,15 +1,11 @@ -import { PduType } from '../types/v3-11'; import { PersistentEventV8 } from './v8'; +import type { PduType } from '../types/v3-11'; -export class PersistentEventV9< - Type extends PduType = PduType, -> extends PersistentEventV8 { +export class PersistentEventV9 extends PersistentEventV8 { getAllowedContentKeys() { const resp = super.getAllowedContentKeys(); - (resp['m.room.member'] as string[]).push( - 'join_authorised_via_users_server', - ); + (resp['m.room.member'] as string[]).push('join_authorised_via_users_server'); return resp; } diff --git a/packages/room/src/state_resolution/definitions/algorithm/v2.spec.ts b/packages/room/src/state_resolution/definitions/algorithm/v2.spec.ts index 7444ed944..a739ac42c 100644 --- a/packages/room/src/state_resolution/definitions/algorithm/v2.spec.ts +++ b/packages/room/src/state_resolution/definitions/algorithm/v2.spec.ts @@ -1,20 +1,15 @@ -import { type EventID, type StateMapKey } from '../../../types/_common'; -import {} from '../../../types/v3-11'; -import { - type EventStore, - _kahnsOrder, - getAuthChainDifference, - mainlineOrdering, -} from '../definitions'; +import { afterEach, describe, expect, it } from 'bun:test'; import { resolveStateV2Plus } from './v2'; - -import { afterEach, describe, expect, it } from 'bun:test'; import type { PersistentEventBase } from '../../../manager/event-wrapper'; import { PersistentEventFactory } from '../../../manager/factory'; +import { type EventID, type StateMapKey } from '../../../types/_common'; +import {} from '../../../types/v3-11'; +import { type EventStore, _kahnsOrder, getAuthChainDifference, mainlineOrdering } from '../definitions'; class MockEventStore implements EventStore { public events: Array = []; + async getEvents(eventIds: string[]): Promise { return this.events.filter((e) => eventIds.includes(e.eventId)); } @@ -49,20 +44,22 @@ let ORIGIN_SERVER_TS = 0; class FakeEvent { node_id: string; + sender: string; + type: string; + state_key: string | null; + content: Record; + room_id: string; + event_dict: any; + _event_id: EventID; - constructor( - id: string, - sender: string, - type: string, - state_key: string | null, - content: Record, - ) { + + constructor(id: string, sender: string, type: string, state_key: string | null, content: Record) { this.node_id = id; this._event_id = `${id}:example.com`; this.sender = sender; @@ -78,14 +75,13 @@ class FakeEvent { throw new Error('event_dict is not set'); } - return PersistentEventFactory.createFromRawEvent(this.event_dict, '11') - .eventId; + return PersistentEventFactory.createFromRawEvent(this.event_dict, '11').eventId; } toEvent(auth_events: string[], prev_events: string[]) { this.event_dict = { - auth_events: auth_events, - prev_events: prev_events, + auth_events, + prev_events, // event_id: this.event_id, sender: this.sender, type: this.type, @@ -95,7 +91,7 @@ class FakeEvent { room_id: this.room_id, } as any; - ORIGIN_SERVER_TS = ORIGIN_SERVER_TS + 1; + ORIGIN_SERVER_TS += 1; if (this.state_key !== null) { this.event_dict.state_key = this.state_key; @@ -115,28 +111,13 @@ const INITIAL_EVENTS = [ join_rule: 'public', }), new FakeEvent('IMB', BOB, 'm.room.member', BOB, MEMBERSHIP_CONTENT_JOIN), - new FakeEvent( - 'IMC', - CHARLIE, - 'm.room.member', - CHARLIE, - MEMBERSHIP_CONTENT_JOIN, - ), + new FakeEvent('IMC', CHARLIE, 'm.room.member', CHARLIE, MEMBERSHIP_CONTENT_JOIN), new FakeEvent('IMZ', ZARA, 'm.room.member', ZARA, MEMBERSHIP_CONTENT_JOIN), new FakeEvent('START', ZARA, 'm.room.message', null, {}), new FakeEvent('END', ZARA, 'm.room.message', null, {}), ]; -const INITIAL_EDGES = [ - 'START', - 'IMZ', - 'IMC', - 'IMB', - 'IJR', - 'IPOWER', - 'IMA', - 'CREATE', -]; +const INITIAL_EDGES = ['START', 'IMZ', 'IMC', 'IMB', 'IJR', 'IPOWER', 'IMA', 'CREATE']; function getGraph(events: FakeEvent[], edges: string[][]) { const graph = new Map>(); @@ -191,10 +172,7 @@ async function runTest(events: FakeEvent[], edges: string[][]) { compareFunc: (a, b) => a.localeCompare(b), }); - const stateAtEventId = new Map< - string, - Map> - >(); + const stateAtEventId = new Map>>(); const [create, ...rest] = sorted; @@ -205,28 +183,21 @@ async function runTest(events: FakeEvent[], edges: string[][]) { eventStore.events.push(createEvent); - stateAtEventId.set( - createEvent.eventId, - new Map([[createEvent.getUniqueStateIdentifier(), createEvent]]), - ); + stateAtEventId.set(createEvent.eventId, new Map([[createEvent.getUniqueStateIdentifier(), createEvent]])); - for (const nodeId of rest) { + for await (const nodeId of rest) { const prevEventsNodeIds = reverseGraph.get(nodeId)!; let stateBefore: Map; if (prevEventsNodeIds.size === 1) { // very next to CREATE - stateBefore = stateAtEventId.get( - fakeEventMap.get(prevEventsNodeIds.values().next().value!)?.event_id, - )!; + stateBefore = stateAtEventId.get(fakeEventMap.get(prevEventsNodeIds.values().next().value!)?.event_id)!; } else { stateBefore = await resolveStateV2Plus( prevEventsNodeIds .values() - .map( - (nodeId) => stateAtEventId.get(fakeEventMap.get(nodeId)?.event_id)!, - ) + .map((nodeId) => stateAtEventId.get(fakeEventMap.get(nodeId)?.event_id)!) .toArray(), eventStore, ); @@ -235,13 +206,10 @@ async function runTest(events: FakeEvent[], edges: string[][]) { // whatever was state before, append current event info to new state const stateAfter = new Map(stateBefore.entries()); - /// get the authEvents for the current event + // / get the authEvents for the current event const authEvents = []; - const authTypes = fakeEventMap - .get(nodeId) - ?.toEvent([], []) - .getAuthEventStateKeys(); + const authTypes = fakeEventMap.get(nodeId)?.toEvent([], []).getAuthEventStateKeys(); for (const type of authTypes) { // get the auth event id from the seen state @@ -311,13 +279,7 @@ describe('Definitions', () => { new FakeEvent('PA', ALICE, 'm.room.power_levels', '', { users: { [ALICE]: 100, [BOB]: 50 }, }), - new FakeEvent( - 'MA', - ALICE, - 'm.room.member', - ALICE, - MEMBERSHIP_CONTENT_JOIN, - ), + new FakeEvent('MA', ALICE, 'm.room.member', ALICE, MEMBERSHIP_CONTENT_JOIN), new FakeEvent('MB', ALICE, 'm.room.member', BOB, MEMBERSHIP_CONTENT_BAN), new FakeEvent('PB', BOB, 'm.room.power_levels', '', { users: { [ALICE]: 100, [BOB]: 50 }, @@ -331,18 +293,9 @@ describe('Definitions', () => { const finalState = await runTest(events, edges); - expect(finalState?.get('m.room.power_levels:')).toHaveProperty( - 'eventId', - 'PA:example.com', - ); - expect(finalState?.get('m.room.member:@bob:example.com')).toHaveProperty( - 'eventId', - 'MB:example.com', - ); - expect(finalState?.get('m.room.member:@alice:example.com')).toHaveProperty( - 'eventId', - 'MA:example.com', - ); + expect(finalState?.get('m.room.power_levels:')).toHaveProperty('eventId', 'PA:example.com'); + expect(finalState?.get('m.room.member:@bob:example.com')).toHaveProperty('eventId', 'MB:example.com'); + expect(finalState?.get('m.room.member:@alice:example.com')).toHaveProperty('eventId', 'MA:example.com'); }); it('02 join rule evasion', async () => { @@ -362,10 +315,7 @@ describe('Definitions', () => { const finalState = await runTest(events, edges); - expect(finalState?.get('m.room.join_rules:')).toHaveProperty( - 'eventId', - 'JR:example.com', - ); + expect(finalState?.get('m.room.join_rules:')).toHaveProperty('eventId', 'JR:example.com'); }); it('offtopic pl', async () => { // FIXME: @@ -388,10 +338,7 @@ describe('Definitions', () => { const finalState = await runTest(events, edges); - expect(finalState?.get('m.room.power_levels:')).toHaveProperty( - 'eventId', - 'PC:example.com', - ); + expect(finalState?.get('m.room.power_levels:')).toHaveProperty('eventId', 'PC:example.com'); }); it('topic basic', async () => { const events = [ @@ -416,14 +363,8 @@ describe('Definitions', () => { const finalState = await runTest(events, edges); - expect(finalState?.get('m.room.topic:')).toHaveProperty( - 'eventId', - 'T2:example.com', - ); - expect(finalState?.get('m.room.power_levels:')).toHaveProperty( - 'eventId', - 'PA2:example.com', - ); + expect(finalState?.get('m.room.topic:')).toHaveProperty('eventId', 'T2:example.com'); + expect(finalState?.get('m.room.power_levels:')).toHaveProperty('eventId', 'PA2:example.com'); }); it('topic reset', async () => { const events = [ @@ -442,18 +383,9 @@ describe('Definitions', () => { const finalState = await runTest(events, edges); - expect(finalState?.get('m.room.topic:')).toHaveProperty( - 'eventId', - 'T1:example.com', - ); - expect(finalState?.get('m.room.member:@bob:example.com')).toHaveProperty( - 'eventId', - 'MB:example.com', - ); - expect(finalState?.get('m.room.power_levels:')).toHaveProperty( - 'eventId', - 'PA:example.com', - ); + expect(finalState?.get('m.room.topic:')).toHaveProperty('eventId', 'T1:example.com'); + expect(finalState?.get('m.room.member:@bob:example.com')).toHaveProperty('eventId', 'MB:example.com'); + expect(finalState?.get('m.room.power_levels:')).toHaveProperty('eventId', 'PA:example.com'); }); it('topic', async () => { @@ -481,14 +413,8 @@ describe('Definitions', () => { const finalState = await runTest(events, edges); - expect(finalState?.get('m.room.topic:')).toHaveProperty( - 'eventId', - 'T4:example.com', - ); - expect(finalState?.get('m.room.power_levels:')).toHaveProperty( - 'eventId', - 'PA2:example.com', - ); + expect(finalState?.get('m.room.topic:')).toHaveProperty('eventId', 'T4:example.com'); + expect(finalState?.get('m.room.power_levels:')).toHaveProperty('eventId', 'PA2:example.com'); }); it('mainline sort', async () => { @@ -516,15 +442,9 @@ describe('Definitions', () => { const finalState = await runTest(events, edges); - expect(finalState?.get('m.room.topic:')).toHaveProperty( - 'eventId', - 'T3:example.com', - ); + expect(finalState?.get('m.room.topic:')).toHaveProperty('eventId', 'T3:example.com'); - expect(finalState?.get('m.room.power_levels:')).toHaveProperty( - 'eventId', - 'PA2:example.com', - ); + expect(finalState?.get('m.room.power_levels:')).toHaveProperty('eventId', 'PA2:example.com'); }); it('successful mainline sort with no existing power level event', async () => { @@ -532,32 +452,17 @@ describe('Definitions', () => { creator: ALICE, }).toEvent([], []); - const aliceMemberEvent = new FakeEvent( - 'IMA2', - ALICE, - 'm.room.member', - ALICE, - MEMBERSHIP_CONTENT_JOIN, - ).toEvent([createEvent.eventId], [createEvent.eventId]); - - const joinRuleEvent = new FakeEvent( - 'IJR2', - ALICE, - 'm.room.join_rules', - '', - { join_rule: 'public' }, - ).toEvent( + const aliceMemberEvent = new FakeEvent('IMA2', ALICE, 'm.room.member', ALICE, MEMBERSHIP_CONTENT_JOIN).toEvent( + [createEvent.eventId], + [createEvent.eventId], + ); + + const joinRuleEvent = new FakeEvent('IJR2', ALICE, 'm.room.join_rules', '', { join_rule: 'public' }).toEvent( [createEvent.eventId, aliceMemberEvent.eventId], [aliceMemberEvent.eventId], ); - const bobJoinEvent = new FakeEvent( - 'IMB2', - BOB, - 'm.room.member', - BOB, - MEMBERSHIP_CONTENT_JOIN, - ).toEvent( + const bobJoinEvent = new FakeEvent('IMB2', BOB, 'm.room.member', BOB, MEMBERSHIP_CONTENT_JOIN).toEvent( [createEvent.eventId, joinRuleEvent.eventId], [joinRuleEvent.eventId], ); @@ -580,11 +485,9 @@ describe('Definitions', () => { return e.eventId; }); - const mainlineSorted = (await mainlineOrdering(events, eventStore)).map( - (e) => { - return e.eventId; - }, - ); + const mainlineSorted = (await mainlineOrdering(events, eventStore)).map((e) => { + return e.eventId; + }); expect(mainlineSorted).toEqual(sortedEvents); }); @@ -705,13 +608,7 @@ describe('Definitions', () => { const events = [ // two memberships // one join rule - new FakeEvent( - 'EB', - EVELYN, - 'm.room.member', - EVELYN, - MEMBERSHIP_CONTENT_JOIN, - ), + new FakeEvent('EB', EVELYN, 'm.room.member', EVELYN, MEMBERSHIP_CONTENT_JOIN), new FakeEvent('JRI', ALICE, 'm.room.join_rules', '', { join_rule: 'invite', }), @@ -724,10 +621,7 @@ describe('Definitions', () => { const finalState = await runTest(events, edges); - expect(finalState.get('m.room.join_rules:')).toHaveProperty( - 'eventId', - 'JRI:example.com', - ); + expect(finalState.get('m.room.join_rules:')).toHaveProperty('eventId', 'JRI:example.com'); expect(finalState.get('m.room.member:@evelyn:example.com')).toBeUndefined(); }); }); diff --git a/packages/room/src/state_resolution/definitions/algorithm/v2.ts b/packages/room/src/state_resolution/definitions/algorithm/v2.ts index 4f9ac6d79..bc7450ae7 100644 --- a/packages/room/src/state_resolution/definitions/algorithm/v2.ts +++ b/packages/room/src/state_resolution/definitions/algorithm/v2.ts @@ -58,7 +58,8 @@ // i am too early in this to remember everything by heart. import assert from 'node:assert'; -import { PersistentEventBase, State } from '../../../manager/event-wrapper'; + +import type { PersistentEventBase, State } from '../../../manager/event-wrapper'; import { PowerLevelEvent } from '../../../manager/power-level-event-wrapper'; import type { EventID, StateMapKey } from '../../../types/_common'; import { @@ -73,9 +74,7 @@ import { reverseTopologicalPowerSort, } from '../definitions'; -export const isTruthy = ( - value: T | null | undefined | false | 0 | '', -): value is T => { +export const isTruthy = (value: T | null | undefined | false | 0 | ''): value is T => { return Boolean(value); }; @@ -131,14 +130,12 @@ export async function resolveStateV2Plus( const [unconflicted, conflicted] = partitionState(eventIdToEventMap.values()); - const unconflictedStateMap = unconflicted - .entries() - .reduce((accum, [stateKey, eventId]) => { - const event = eventIdToEventMap.get(eventId); - assert(event, 'event should not be null'); - accum.set(stateKey, event); - return accum; - }, new Map() as State); + const unconflictedStateMap = unconflicted.entries().reduce((accum, [stateKey, eventId]) => { + const event = eventIdToEventMap.get(eventId); + assert(event, 'event should not be null'); + accum.set(stateKey, event); + return accum; + }, new Map() as State); if (conflicted.size === 0) { // no conflicted state, return the unconflicted state @@ -147,17 +144,11 @@ export async function resolveStateV2Plus( // ajuthchain diff calculation will require non unique statekeys const authChainDifference = await getAuthChainDifference( - states.map( - (state) => - new Map( - state.entries().map(([stateKey, event]) => [stateKey, event.eventId]), - ), - ), + states.map((state) => new Map(state.entries().map(([stateKey, event]) => [stateKey, event.eventId]))), wrappedStore, ); const fullConflictedSet = conflicted.values().reduce((accum, curr) => { - // biome-ignore lint/complexity/noForEach: curr.forEach((c) => accum.add(c)); return accum; @@ -172,7 +163,7 @@ export async function resolveStateV2Plus( // etc. const powerEvents = new Map(); // using map instead of set to store the event objects uniquely - for (const eventid of fullConflictedSet) { + for await (const eventid of fullConflictedSet) { const [event] = await wrappedStore.getEvents([eventid]); if (!event) { console.warn('event not found in eventMap', eventid); @@ -185,11 +176,11 @@ export async function resolveStateV2Plus( // For each such power event P, enlarge X by adding the events in the auth chain of P which also belong to the full conflicted set. - for (const event of powerEvents.values()) { + for await (const event of powerEvents.values()) { // pass cache const authChain = await getAuthChain(event, wrappedStore); - for (const authEventId of authChain) { + for await (const authEventId of authChain) { const [authEvent] = await wrappedStore.getEvents([authEventId]); if (!authEvent) { @@ -278,25 +269,15 @@ export async function resolveStateV2Plus( // since the power level event that allowed X (A) is earlier, the mainline ordering will put X before Y. // mainlineSort([Y, X]) -> [X, Y] because A < B - const sanitizedRemainingEvents = remainingEvents - .map((e) => eventIdToEventMap.get(e)) - .filter(isTruthy); + const sanitizedRemainingEvents = remainingEvents.map((e) => eventIdToEventMap.get(e)).filter(isTruthy); - const orderedRemainingEvents = await mainlineOrdering( - sanitizedRemainingEvents, - wrappedStore, - powerLevelEvent, - ); + const orderedRemainingEvents = await mainlineOrdering(sanitizedRemainingEvents, wrappedStore, powerLevelEvent); // 4. Apply the iterative auth checks algorithm on the partial resolved state and the list of events from the previous step. - const finalState = await iterativeAuthChecks( - orderedRemainingEvents, - partiallyResolvedState, - wrappedStore, - ); + const finalState = await iterativeAuthChecks(orderedRemainingEvents, partiallyResolvedState, wrappedStore); // 5. Update the result by replacing any event with the event with the same key from the unconflicted state map, if such an event exists, to get the final resolved state. - for (const [key, value] of unconflicted) { + for await (const [key, value] of unconflicted) { if (finalState.has(key)) { const [event] = await wrappedStore.getEvents([value]); assert(event, 'event should not be null'); diff --git a/packages/room/src/state_resolution/definitions/definitions.ts b/packages/room/src/state_resolution/definitions/definitions.ts index 73fa2436e..1a839e431 100644 --- a/packages/room/src/state_resolution/definitions/definitions.ts +++ b/packages/room/src/state_resolution/definitions/definitions.ts @@ -1,22 +1,16 @@ +import assert from 'node:assert'; + import { PriorityQueue } from '@datastructures-js/priority-queue'; -import type { - EventID, - StateEventIdMap, - StateMapKey, -} from '../../types/_common'; -import { type PduType } from '../../types/v3-11'; -import assert from 'node:assert'; import { StateResolverAuthorizationError } from '../../authorizartion-rules/errors'; import { checkEventAuthWithState } from '../../authorizartion-rules/rules'; -import { PersistentEventBase, State } from '../../manager/event-wrapper'; +import type { PersistentEventBase, State } from '../../manager/event-wrapper'; import { PowerLevelEvent } from '../../manager/power-level-event-wrapper'; -import { RoomVersion } from '../../manager/type'; +import type { RoomVersion } from '../../manager/type'; +import type { EventID, StateEventIdMap, StateMapKey } from '../../types/_common'; +import { type PduType } from '../../types/v3-11'; -export function getStateMapKey(event: { - type: PduType; - state_key?: string; -}): StateMapKey { +export function getStateMapKey(event: { type: PduType; state_key?: string }): StateMapKey { return `${event.type}:${event.state_key ?? ''}`; } @@ -27,9 +21,7 @@ export function getStateByMapKey( state_key?: string; }, ) { - return map.get(getStateMapKey(filter)) as - | PersistentEventBase - | undefined; + return map.get(getStateMapKey(filter)) as PersistentEventBase | undefined; } // https://spec.matrix.org/v1.12/rooms/v2/#definitions @@ -40,18 +32,14 @@ export function isPowerEvent(event: PersistentEventBase): boolean { event.isPowerLevelEvent() || event.isJoinRuleEvent() || // or a state event with type m.room.member where the membership is leave or ban and the sender does not match the state_key - (event.isMembershipEvent() && - (event.getMembership() === 'leave' || event.getMembership() === 'ban') && - event.sender !== event.stateKey) + (event.isMembershipEvent() && (event.getMembership() === 'leave' || event.getMembership() === 'ban') && event.sender !== event.stateKey) ); } // Unconflicted state map and conflicted state set. // State map S_i is {S_1, S_2, S_3, ...} // iout map takes care of the non-duplication of a set -export function partitionState( - events: Readonly>, -): [StateEventIdMap, Map] { +export function partitionState(events: Readonly>): [StateEventIdMap, Map] { const unconflictedState: StateEventIdMap = new Map(); // Note that the unconflicted state map only has one event for each key K, whereas the conflicted state set may contain multiple events with the same key. @@ -67,7 +55,7 @@ export function partitionState( unconflictedState.set(first.getUniqueStateIdentifier(), first.eventId); for (const event of events) { - const eventId = event.eventId; + const { eventId } = event; const stateKey = event.getUniqueStateIdentifier(); // If a given key K is present in every Si with the same value V in each state map if (unconflictedState.has(stateKey)) { @@ -86,7 +74,6 @@ export function partitionState( conflictedStateEventsMap.set(stateKey, [existingEventid, eventId]); } } else if (conflictedStateEventsMap.has(stateKey)) { - // biome-ignore lint/style/noNonNullAssertion: `has` asserts non-null conflictedStateEventsMap.get(stateKey)!.push(eventId); } else { unconflictedState.set(stateKey, eventId); @@ -105,18 +92,12 @@ export interface EventStore { /* *The auth chain of an event E is the set containing all of E’s auth events, all of their auth events, and so on recursively, stretching back to the start of the room. Put differently, these are the events reachable by walking the graph induced by an event’s auth_events links. */ -export async function getAuthChain( - event: PersistentEventBase, - store: EventStore, -): Promise> { +export async function getAuthChain(event: PersistentEventBase, store: EventStore): Promise> { // TODO: central cache for t6his const eventIdToAuthChainMap = new Map>(); // do not repeat - const _getAuthChain = async ( - event: PersistentEventBase, - existingAuthChainPart: Set, - ) => { - const eventId = event.eventId; + const _getAuthChain = async (event: PersistentEventBase, existingAuthChainPart: Set) => { + const { eventId } = event; if (eventIdToAuthChainMap.has(eventId)) { return eventIdToAuthChainMap.get(eventId)!; @@ -132,11 +113,8 @@ export async function getAuthChain( let newAuthChainPart = existingAuthChainPart.union(authEventIdsSet); - for (const authEvent of authEvents) { - const nextAuthChainPart = await _getAuthChain( - authEvent, - newAuthChainPart, - ); + for await (const authEvent of authEvents) { + const nextAuthChainPart = await _getAuthChain(authEvent, newAuthChainPart); if (!nextAuthChainPart) { continue; } @@ -151,26 +129,20 @@ export async function getAuthChain( // Auth difference // NOTE: https://github.com/element-hq/synapse/blob/a25a37002c851ef419d12925a11dd8bf2233470e/docs/auth_chain_difference_algorithm.md -export async function getAuthChainDifference( - states: Readonly>, - store: EventStore, -) { +export async function getAuthChainDifference(states: Readonly>, store: EventStore) { const authChainSets = [] as Set[]; - for (const state of states) { + for await (const state of states) { const authChainForState = new Set(); - for (const eventid of state.values()) { + for await (const eventid of state.values()) { const [event] = await store.getEvents([eventid]); if (!event) { console.warn('event not found in store or remote', eventid); continue; } // TODO: deb check this I changed to keep the function behaving as the spec - for (const authChainEventId of [ - ...(await getAuthChain(event, store)), - event.eventId, - ]) { + for (const authChainEventId of [...(await getAuthChain(event, store)), event.eventId]) { authChainForState.add(authChainEventId); } } @@ -178,14 +150,8 @@ export async function getAuthChainDifference( authChainSets.push(authChainForState); } - const union = authChainSets.reduce( - (accum, curr) => accum.union(curr), - new Set(), - ); - const intersection = authChainSets.reduce( - (accum, curr) => accum?.intersection(curr), - authChainSets.shift(), - ); + const union = authChainSets.reduce((accum, curr) => accum.union(curr), new Set()); + const intersection = authChainSets.reduce((accum, curr) => accum?.intersection(curr), authChainSets.shift()); if (!intersection) { return union; @@ -278,18 +244,12 @@ export function _kahnsOrder( // get all indegrees // any key in the graph with no edges has zero indegree - // biome-ignore lint/complexity/noForEach: - indegree - .keys() - .forEach((k) => indegree.get(k) === 0 && zeroIndegreeQueue.enqueue(k)); + indegree.keys().forEach((k) => indegree.get(k) === 0 && zeroIndegreeQueue.enqueue(k)); // While the queue is not empty: while (!zeroIndegreeQueue.isEmpty()) { const node = zeroIndegreeQueue.pop(); - assert( - node !== null, - 'undefined element in zeroIndegreeQueue should not happen', - ); + assert(node !== null, 'undefined element in zeroIndegreeQueue should not happen'); result.push(node); @@ -337,35 +297,19 @@ export async function reverseTopologicalPowerSort( // event to the auth events // so, all edges to each node is a parent - const buildIndegreeGraph = async ( - graph: Map>, - event: PersistentEventBase, - ) => { + const buildIndegreeGraph = async (graph: Map>, event: PersistentEventBase) => { graph.set(event.eventId, new Set()); if (event.isPowerLevelEvent()) { - eventToPowerLevelMap.set( - event.eventId, - event - .toPowerLevelEvent() - .getPowerLevelForUser(event.sender, roomCreateEvent), - ); + eventToPowerLevelMap.set(event.eventId, event.toPowerLevelEvent().getPowerLevelForUser(event.sender, roomCreateEvent)); } // auths are the parents, must be on tiop for (const authEvent of await store.getEvents(event.getAuthEventIds())) { eventMap.set(authEvent.eventId, authEvent); - if ( - !eventToPowerLevelMap.has(authEvent.eventId) && - authEvent.isPowerLevelEvent() - ) { - eventToPowerLevelMap.set( - event.eventId, - authEvent - .toPowerLevelEvent() - .getPowerLevelForUser(event.sender, roomCreateEvent), - ); + if (!eventToPowerLevelMap.has(authEvent.eventId) && authEvent.isPowerLevelEvent()) { + eventToPowerLevelMap.set(event.eventId, authEvent.toPowerLevelEvent().getPowerLevelForUser(event.sender, roomCreateEvent)); } if (conflictedSet.has(authEvent.eventId)) { @@ -377,17 +321,11 @@ export async function reverseTopologicalPowerSort( if (!eventToPowerLevelMap.has(event.eventId)) { // use default power level - eventToPowerLevelMap.set( - event.eventId, - PowerLevelEvent.fromDefault().getPowerLevelForUser( - event.sender, - roomCreateEvent, - ), - ); + eventToPowerLevelMap.set(event.eventId, PowerLevelEvent.fromDefault().getPowerLevelForUser(event.sender, roomCreateEvent)); } }; - for (const event of events) { + for await (const event of events) { eventMap.set(event.eventId, event); await buildIndegreeGraph(graph, event); } @@ -406,11 +344,7 @@ export async function reverseTopologicalPowerSort( const sender1PowerLevel = eventToPowerLevelMap.get(event1Id); const sender2PowerLevel = eventToPowerLevelMap.get(event2Id); - if ( - sender1PowerLevel !== undefined && - sender2PowerLevel !== undefined && - sender1PowerLevel !== sender2PowerLevel - ) { + if (sender1PowerLevel !== undefined && sender2PowerLevel !== undefined && sender1PowerLevel !== sender2PowerLevel) { return sender2PowerLevel - sender1PowerLevel; } @@ -435,17 +369,10 @@ export async function mainlineOrdering( // Let P = P0 be an m.room.power_levels event powerLevelEvent?: PersistentEventBase, // of which we will calculate the mainline ): Promise { - const getMainline = async ( - event: PersistentEventBase, - ) => { - const mainline = [] as PersistentEventBase< - RoomVersion, - 'm.room.power_levels' - >[]; - - const fn = async ( - event: PersistentEventBase, - ) => { + const getMainline = async (event: PersistentEventBase) => { + const mainline = [] as PersistentEventBase[]; + + const fn = async (event: PersistentEventBase) => { const authEvents = await store.getEvents(event.getAuthEventIds()); // await new Promise((resolve) => setTimeout(resolve, 3000)); @@ -471,8 +398,7 @@ export async function mainlineOrdering( }; // this is how we get the mainline of an event - const mainline: PersistentEventBase[] = - []; + const mainline: PersistentEventBase[] = []; if (powerLevelEvent?.isPowerLevelEvent()) { mainline.push(...(await getMainline(powerLevelEvent))); @@ -490,9 +416,7 @@ export async function mainlineOrdering( ); } - const getMainlinePositionOfEvent = async ( - event: PersistentEventBase, - ): Promise => { + const getMainlinePositionOfEvent = async (event: PersistentEventBase): Promise => { let _event: PersistentEventBase | null = event; while (_event) { @@ -502,17 +426,13 @@ export async function mainlineOrdering( return mainlineMap.get(_event.eventId) || 0; } - const authEvents: PersistentEventBase[] = await store.getEvents( - _event.getAuthEventIds(), - ); + // eslint-disable-next-line no-await-in-loop + const authEvents: PersistentEventBase[] = await store.getEvents(_event.getAuthEventIds()); _event = null; for (const authEvent of authEvents) { - assert( - authEvent, - 'auth event should not be null, either in our store or remote', - ); + assert(authEvent, 'auth event should not be null, either in our store or remote'); // Find the smallest index j ≥ 1 for which e_j belongs to the mainline of P. if ( @@ -531,30 +451,22 @@ export async function mainlineOrdering( // Let e = e0 be another event (possibly another m.room.power_levels event) // iterating over all, could have been better visualized with a for (let i = 0; i < events.length; i++) loop - for (const event of events) { + for await (const event of events) { // "Now compare these two lists as follows." // since we have to compare it doesn't make sense to fetch mainlines of all events here, too expensive, let's try to calculate on the fly // we just want the mainline position of the event - mainlinePositions.set( - event.eventId, - await getMainlinePositionOfEvent(event), - ); + mainlinePositions.set(event.eventId, await getMainlinePositionOfEvent(event)); } // the mainline ordering based on P of a set of events is the ordering // from smallest to largest - // using the following comparison relation on events: for events x and y, x < y if + // using the following comparison relation on events: for events x and y, x < y if const comparisonFn = (e1: PersistentEventBase, e2: PersistentEventBase) => { // the mainline position of x is greater than the mainline position of y const e1Position = mainlinePositions.get(e1.eventId); const e2Position = mainlinePositions.get(e2.eventId); - if ( - e1Position !== undefined && - e2Position !== undefined && - e1Position < e2Position - ) - return -1; + if (e1Position !== undefined && e2Position !== undefined && e1Position < e2Position) return -1; // x’s origin_server_ts is less than y’s origin_server_ts if (e1.originServerTs !== e2.originServerTs) { @@ -581,9 +493,11 @@ export async function iterativeAuthChecks( ): Promise { const newState: State = new Map(stateMap.entries()) as State; - for (const event of events) { + for await (const event of events) { const authEventStateMap: State = new Map(); - for (const authEvent of await store.getEvents(event.getAuthEventIds())) { + + const authEvents = await store.getEvents(event.getAuthEventIds()); + for (const authEvent of authEvents) { authEventStateMap.set(authEvent.getUniqueStateIdentifier(), authEvent); } diff --git a/packages/room/src/types/_common.ts b/packages/room/src/types/_common.ts index c565145f8..62da8e5cf 100644 --- a/packages/room/src/types/_common.ts +++ b/packages/room/src/types/_common.ts @@ -1,4 +1,5 @@ import z from 'zod'; + import type { Pdu, PduType } from './v3-11'; export type StateKey = string; diff --git a/packages/room/src/types/v3-11.ts b/packages/room/src/types/v3-11.ts index cfee2d9ac..979d0a79a 100644 --- a/packages/room/src/types/v3-11.ts +++ b/packages/room/src/types/v3-11.ts @@ -1,10 +1,7 @@ import { z } from 'zod'; -import { - PduForType, - eventIdSchema, - roomIdSchema, - userIdSchema, -} from './_common'; + +import type { PduForType } from './_common'; +import { eventIdSchema, roomIdSchema, userIdSchema } from './_common'; // Copied from: https://github.com/element-hq/synapse/blob/2277df2a1eb685f85040ef98fa21d41aa4cdd389/synapse/api/constants.py#L103-L141 @@ -54,19 +51,14 @@ export const EventTypeSchema = z.union([PduTypeSchema, EduTypeSchema]); export type EventType = z.infer; export const EventHashSchema = z.object({ - sha256: z - .string() - .describe('The hash of the event, encoded as a base64 string.'), + sha256: z.string().describe('The hash of the event, encoded as a base64 string.'), }); export type EventHash = z.infer; export const SignatureSchema = z.record( z.string().describe('signing server name'), - z.record( - z.string().describe('signing key id'), - z.string().describe('signature in unpadded base64 format'), - ), + z.record(z.string().describe('signing key id'), z.string().describe('signature in unpadded base64 format')), ); export type Signature = z.infer; @@ -79,22 +71,14 @@ export type Signature = z.infer; // https://spec.matrix.org/v1.12/client-server-api/#mroommember -export const PduMembershipTypeSchema = z.enum([ - 'join', - 'leave', - 'invite', - 'ban', - 'knock', -]); +export const PduMembershipTypeSchema = z.enum(['join', 'leave', 'invite', 'ban', 'knock']); export const PduMembershipEventContentSchema = z.object({ avatar_url: z.string().url().optional(), displayname: z.string().optional(), is_direct: z .boolean() - .describe( - 'Flag indicating if the room containing this event was created with the intention of being a direct chat', - ) + .describe('Flag indicating if the room containing this event was created with the intention of being a direct chat') .optional(), join_authorised_via_users_server: z.string().optional(), membership: PduMembershipTypeSchema, @@ -103,11 +87,7 @@ export const PduMembershipEventContentSchema = z.object({ .object({ display_name: z.string().optional(), signed: z.object({ - mxid: z - .string() - .describe( - 'The invited matrix user ID. Must be equal to the user_id property of the event.', - ), + mxid: z.string().describe('The invited matrix user ID. Must be equal to the user_id property of the event.'), signatures: SignatureSchema.describe('The signatures of the event.'), token: z.string(), }), @@ -115,40 +95,32 @@ export const PduMembershipEventContentSchema = z.object({ .optional(), }); -export type PduMembershipEventContent = z.infer< - typeof PduMembershipEventContentSchema ->; +export type PduMembershipEventContent = z.infer; // https://spec.matrix.org/v1.12/client-server-api/#mroomcreate export const PduCreateEventContentSchema = z.object({ - creator: z + 'creator': z .string() .describe( ' The user_id of the room creator. Required for, and only present in, room versions 1 - 10. Starting with room version 11 the event sender should be used instead.', ), 'm.federate': z .boolean() - .describe( - ' Whether users on other servers can join this room. Defaults to true if key does not exist.', - ) + .describe(' Whether users on other servers can join this room. Defaults to true if key does not exist.') .optional(), - predecessor: z + 'predecessor': z .object({ - event_id: z - .string() - .describe('The event ID of the last known event in the old room.'), + event_id: z.string().describe('The event ID of the last known event in the old room.'), room_id: z.string().describe('The ID of the old room.'), }) .optional(), - room_version: z + 'room_version': z .enum(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']) - .describe( - " The version of the room. Defaults to '1' if the key does not exist.", - ) + .describe(" The version of the room. Defaults to '1' if the key does not exist.") .optional() .default('1'), - type: z.string().describe('The type of the event.').optional(), + 'type': z.string().describe('The type of the event.').optional(), }); export type PduCreateEventContent = z.infer; @@ -157,14 +129,7 @@ export type PduCreateEventContent = z.infer; export const PduJoinRuleEventContentSchema = z.object({ join_rule: z - .enum([ - 'public', - 'invite', - 'knock', - 'private', - 'restricted', - 'knock_restricted', - ]) + .enum(['public', 'invite', 'knock', 'private', 'restricted', 'knock_restricted']) .describe('The type of rules used for users wishing to join this room.'), allow: z .array( @@ -187,67 +152,41 @@ export const PduJoinRuleEventContentSchema = z.object({ .optional(), }); -export type PduJoinRuleEventContent = z.infer< - typeof PduJoinRuleEventContentSchema ->; +export type PduJoinRuleEventContent = z.infer; export const PduRoomTopicEventContentSchema = z.object({ topic: z.string().describe('The topic of the room.'), }); -export type PduRoomTopicEventContent = z.infer< - typeof PduRoomTopicEventContentSchema ->; +export type PduRoomTopicEventContent = z.infer; export const PduRoomRedactionContentSchema = z.object({ reason: z.string().optional(), }); -export type PduRoomRedactionContent = z.infer< - typeof PduRoomRedactionContentSchema ->; +export type PduRoomRedactionContent = z.infer; export const PduHistoryVisibilityEventContentSchema = z.object({ - history_visibility: z - .enum(['invited', 'joined', 'shared', 'world_readable']) - .describe('Who can read the room history'), + history_visibility: z.enum(['invited', 'joined', 'shared', 'world_readable']).describe('Who can read the room history'), }); -export type PduHistoryVisibilityEventContent = z.infer< - typeof PduHistoryVisibilityEventContentSchema ->; +export type PduHistoryVisibilityEventContent = z.infer; export const PduGuestAccessEventContentSchema = z.object({ - guest_access: z - .enum(['can_join', 'forbidden']) - .describe('Whether guest users can join the room'), + guest_access: z.enum(['can_join', 'forbidden']).describe('Whether guest users can join the room'), }); -export type PduGuestAccessEventContent = z.infer< - typeof PduGuestAccessEventContentSchema ->; +export type PduGuestAccessEventContent = z.infer; // https://spec.matrix.org/v1.12/client-server-api/#mroomserver_acl export const PduServerAclEventContentSchema = z.object({ - allow: z - .array(z.string()) - .describe('A list of server names to allow, including wildcards.') - .optional(), - deny: z - .array(z.string()) - .describe('A list of server names to deny, including wildcards.') - .optional(), - allow_ip_literals: z - .boolean() - .describe('Whether to allow server names that are IP address literals.') - .optional() - .default(true), + allow: z.array(z.string()).describe('A list of server names to allow, including wildcards.').optional(), + deny: z.array(z.string()).describe('A list of server names to deny, including wildcards.').optional(), + allow_ip_literals: z.boolean().describe('Whether to allow server names that are IP address literals.').optional().default(true), }); -export type PduServerAclEventContent = z.infer< - typeof PduServerAclEventContentSchema ->; +export type PduServerAclEventContent = z.infer; // https://spec.matrix.org/v1.12/client-server-api/#mroompower_levels @@ -260,83 +199,54 @@ export function getPduPowerLevelsEventContentSchema() { // v10 takes numbers // we convert all to numbers at parse/validation - const acceptedValueTypes = z.union([ - z.number(), - z.string().transform((v) => Number.parseInt(v, 10)), - ]); + const acceptedValueTypes = z.union([z.number(), z.string().transform((v) => Number.parseInt(v, 10))]); return z.object({ // The level required to ban a user. - ban: acceptedValueTypes - .describe('The level required to ban a user.') - .optional(), + ban: acceptedValueTypes.describe('The level required to ban a user.').optional(), // The level required to send specific event types. This is a mapping from event type to power level required. events: z .record(z.string(), acceptedValueTypes) - .describe( - 'The level required to send specific event types. This is a mapping from event type to power level required.', - ), + .describe('The level required to send specific event types. This is a mapping from event type to power level required.'), // The default level required to send message events. Can be overridden by the events key. events_default: acceptedValueTypes - .describe( - 'The default level required to send message events. Can be overridden by the events key.', - ) + .describe('The default level required to send message events. Can be overridden by the events key.') .optional(), // The level required to invite a user. Defaults to 0 if unspecified. - invite: acceptedValueTypes - .describe('The level required to invite a user.') - .optional(), + invite: acceptedValueTypes.describe('The level required to invite a user.').optional(), // The level required to kick a user. Defaults to 50 if unspecified. - kick: acceptedValueTypes - .describe('The level required to kick a user.') - .optional(), + kick: acceptedValueTypes.describe('The level required to kick a user.').optional(), // The power level requirements for specific notification types. This is a mapping from key to power level for that notifications key. notifications: z.union([ z.object({ // The level required to trigger an @room notification. Defaults to 50 if unspecified. - room: acceptedValueTypes - .describe('The level required to trigger an @room notification.') - .optional(), + room: acceptedValueTypes.describe('The level required to trigger an @room notification.').optional(), }), // others as said in spec - z - .record(z.string(), acceptedValueTypes) - .describe('The level required to trigger an @room notification.') - .optional(), + z.record(z.string(), acceptedValueTypes).describe('The level required to trigger an @room notification.').optional(), ]), // The level required to redact an event sent by another user. Defaults to 50 if unspecified. - redact: acceptedValueTypes - .describe('The level required to redact an event sent by another user.') - .optional(), + redact: acceptedValueTypes.describe('The level required to redact an event sent by another user.').optional(), // The default level required to send state events. Can be overridden by the events key. Defaults to 50 if unspecified. state_default: acceptedValueTypes - .describe( - 'The default level required to send state events. Can be overridden by the events key.', - ) + .describe('The default level required to send state events. Can be overridden by the events key.') .optional(), // The power levels for specific users. This is a mapping from user_id to power level for that user. users: z .record(z.string(), acceptedValueTypes) - .describe( - 'The power levels for specific users. This is a mapping from user_id to power level for that user.', - ), + .describe('The power levels for specific users. This is a mapping from user_id to power level for that user.'), // The power level for users in the room whose user_id is not mentioned in the users key. Defaults to 0 if unspecified. users_default: acceptedValueTypes - .describe( - 'The power level for users in the room whose user_id is not mentioned in the users key. Defaults to 0 if unspecified.', - ) + .describe('The power level for users in the room whose user_id is not mentioned in the users key. Defaults to 0 if unspecified.') .optional(), // historical: z.number(), TODO: check if historical exists in spec - m.power_levels }); } -export const PduPowerLevelsEventContentSchema = - getPduPowerLevelsEventContentSchema(); +export const PduPowerLevelsEventContentSchema = getPduPowerLevelsEventContentSchema(); -export type PduPowerLevelsEventContent = z.infer< - typeof PduPowerLevelsEventContentSchema ->; +export type PduPowerLevelsEventContent = z.infer; // https://spec.matrix.org/v1.12/client-server-api/#mroomcanonical_alias @@ -353,17 +263,13 @@ export const PduCanonicalAliasEventContentSchema = z.object({ ), }); -export type PduCanonicalAliasEventContent = z.infer< - typeof PduCanonicalAliasEventContentSchema ->; +export type PduCanonicalAliasEventContent = z.infer; export const PduRoomNameEventContentSchema = z.object({ name: z.string().describe('The name of the room.'), }); -export type PduRoomNameEventContent = z.infer< - typeof PduRoomNameEventContentSchema ->; +export type PduRoomNameEventContent = z.infer; export const PduRoomAvatarEventContentSchema = z.object({ url: z.string().optional().describe('The URL of the avatar image.'), @@ -379,45 +285,28 @@ export const PduRoomAvatarEventContentSchema = z.object({ thumbnail_url: z.string().optional().describe('The URL of the thumbnail.'), }); -export type PduRoomAvatarEventContent = z.infer< - typeof PduRoomAvatarEventContentSchema ->; +export type PduRoomAvatarEventContent = z.infer; export const PduRoomPinnedEventsEventContentSchema = z.object({ - pinned: z - .array(eventIdSchema) - .optional() - .describe('An ordered list of event IDs to pin.'), + pinned: z.array(eventIdSchema).optional().describe('An ordered list of event IDs to pin.'), }); -export type PduRoomPinnedEventsEventContent = z.infer< - typeof PduRoomPinnedEventsEventContentSchema ->; +export type PduRoomPinnedEventsEventContent = z.infer; // Base timeline content schema const BaseTimelineContentSchema = z.object({ // Optional fields for message edits and relations aka threads 'm.relates_to': z .object({ - rel_type: z - .enum(['m.replace', 'm.annotation', 'm.thread']) - .describe('The type of the relation.') - .optional(), - event_id: eventIdSchema - .describe('The ID of the event that is being related to.') - .optional(), - is_falling_back: z - .boolean() - .optional() - .describe('Whether this is a fallback for older clients'), + 'rel_type': z.enum(['m.replace', 'm.annotation', 'm.thread']).describe('The type of the relation.').optional(), + 'event_id': eventIdSchema.describe('The ID of the event that is being related to.').optional(), + 'is_falling_back': z.boolean().optional().describe('Whether this is a fallback for older clients'), 'm.in_reply_to': z .object({ - event_id: eventIdSchema.describe( - 'The ID of the latest event in the thread for fallback', - ), + event_id: eventIdSchema.describe('The ID of the latest event in the thread for fallback'), }) .optional(), - key: z.string().optional().describe('The key for reactions (emoji).'), + 'key': z.string().optional().describe('The key for reactions (emoji).'), }) .optional() .describe('Relation information for edits, replies, reactions, etc.'), @@ -427,26 +316,11 @@ const BaseTimelineContentSchema = z.object({ const BaseMessageContentSchema = BaseTimelineContentSchema.extend({ body: z.string().describe('The body of the message.'), msgtype: z - .enum([ - 'm.text', - 'm.image', - 'm.file', - 'm.audio', - 'm.video', - 'm.emote', - 'm.notice', - 'm.location', - ]) + .enum(['m.text', 'm.image', 'm.file', 'm.audio', 'm.video', 'm.emote', 'm.notice', 'm.location']) .describe('The type of the message.'), // Optional fields for message edits and relations aka threads - format: z - .enum(['org.matrix.custom.html']) - .describe('The format of the message content.') - .optional(), - formatted_body: z - .string() - .describe('The formatted body of the message.') - .optional(), + format: z.enum(['org.matrix.custom.html']).describe('The format of the message content.').optional(), + formatted_body: z.string().describe('The formatted body of the message.').optional(), }); // File info schema @@ -455,32 +329,14 @@ const FileInfoSchema = z.object({ mimetype: z.string().describe('The MIME type of the file.').optional(), w: z.number().describe('The width of the image/video in pixels.').optional(), h: z.number().describe('The height of the image/video in pixels.').optional(), - duration: z - .number() - .describe('The duration of the audio/video in milliseconds.') - .optional(), - thumbnail_url: z - .string() - .describe('The URL of the thumbnail image.') - .optional(), + duration: z.number().describe('The duration of the audio/video in milliseconds.').optional(), + thumbnail_url: z.string().describe('The URL of the thumbnail image.').optional(), thumbnail_info: z .object({ - w: z - .number() - .describe('The width of the thumbnail in pixels.') - .optional(), - h: z - .number() - .describe('The height of the thumbnail in pixels.') - .optional(), - mimetype: z - .string() - .describe('The MIME type of the thumbnail.') - .optional(), - size: z - .number() - .describe('The size of the thumbnail in bytes.') - .optional(), + w: z.number().describe('The width of the thumbnail in pixels.').optional(), + h: z.number().describe('The height of the thumbnail in pixels.').optional(), + mimetype: z.string().describe('The MIME type of the thumbnail.').optional(), + size: z.number().describe('The size of the thumbnail in bytes.').optional(), }) .describe('Information about the thumbnail.') .optional(), @@ -529,67 +385,42 @@ const NewContentSchema = z.discriminatedUnion('msgtype', [ // Main message content schema using discriminated union export const PduMessageEventContentSchema = z.union([ TextMessageContentSchema.extend({ - 'm.new_content': NewContentSchema.optional().describe( - 'The new content for message edits.', - ), + 'm.new_content': NewContentSchema.optional().describe('The new content for message edits.'), }), FileMessageContentSchema.extend({ - 'm.new_content': NewContentSchema.optional().describe( - 'The new content for message edits.', - ), + 'm.new_content': NewContentSchema.optional().describe('The new content for message edits.'), }), LocationMessageContentSchema.extend({ - 'm.new_content': NewContentSchema.optional().describe( - 'The new content for message edits.', - ), + 'm.new_content': NewContentSchema.optional().describe('The new content for message edits.'), }), ]); const EncryptedContentSchema = BaseTimelineContentSchema.extend({ - algorithm: z - .enum(['m.megolm.v1.aes-sha2']) - .describe('The algorithm used to encrypt the content.'), + algorithm: z.enum(['m.megolm.v1.aes-sha2']).describe('The algorithm used to encrypt the content.'), ciphertext: z.string().describe('The encrypted content.'), // Optional fields for message edits and relations aka threads - device_id: z - .string() - .describe('The formatted body of the message.') - .optional(), - sender_key: z - .string() - .describe('The formatted body of the message.') - .optional(), - session_id: z - .string() - .describe('The formatted body of the message.') - .optional(), + device_id: z.string().describe('The formatted body of the message.').optional(), + sender_key: z.string().describe('The formatted body of the message.').optional(), + session_id: z.string().describe('The formatted body of the message.').optional(), }); export const PduEncryptionEventContentSchema = z.object({ - algorithm: z - .enum(['m.megolm.v1.aes-sha2']) - .describe('The algorithm used to encrypt the content.'), + algorithm: z.enum(['m.megolm.v1.aes-sha2']).describe('The algorithm used to encrypt the content.'), ciphertext: z.string().describe('The encrypted content.'), }); -export type PduMessageEventContent = z.infer< - typeof PduMessageEventContentSchema ->; +export type PduMessageEventContent = z.infer; export const PduMessageReactionEventContentSchema = z.object({ 'm.relates_to': z.object({ // TODO: add more types rel_type: z.enum(['m.annotation']).describe('The type of the relation.'), - event_id: z - .string() - .describe('The ID of the event that is being annotated.'), + event_id: z.string().describe('The ID of the event that is being annotated.'), key: z.string(), }), }); -export type PduMessageReactionEventContent = z.infer< - typeof PduMessageReactionEventContentSchema ->; +export type PduMessageReactionEventContent = z.infer; // SPEC: https://spec.matrix.org/v1.12/rooms/v1/#event-format export const PduNoContentTimelineEventSchema = { @@ -598,51 +429,26 @@ export const PduNoContentTimelineEventSchema = { .describe( 'A list of event IDs that are required in the room state before this event can be applied. The server will not send this event if it is not satisfied.', ), - depth: z - .number() - .describe( - 'The depth of the event in the DAG. This is a number that is incremented for each event in the DAG.', - ), - hashes: EventHashSchema.describe( - 'The hashes of the event. This is an object with arbitrary keys and values.', - ), + depth: z.number().describe('The depth of the event in the DAG. This is a number that is incremented for each event in the DAG.'), + hashes: EventHashSchema.describe('The hashes of the event. This is an object with arbitrary keys and values.'), origin_server_ts: z .number() - .describe( - 'The timestamp of the event. This is a number that is the number of milliseconds since the Unix epoch.', - ), + .describe('The timestamp of the event. This is a number that is the number of milliseconds since the Unix epoch.'), prev_events: z .array(eventIdSchema) .describe( 'A list of event IDs that are required in the room state before this event can be applied. The server will not send this event if it is not satisfied.', ), - redacts: eventIdSchema - .describe( - 'The ID of the event that this event redacts. This is an optional field.', - ) - .optional(), - room_id: roomIdSchema.describe( - 'The ID of the room that the event is in. This is a unique identifier for the room.', - ), - sender: userIdSchema.describe( - 'The ID of the user that sent the event. This is a unique identifier for the user.', - ), - signatures: SignatureSchema.describe( - 'The signatures of the event. This is an object with arbitrary keys and values.', - ), - unsigned: z - .any() - .describe( - 'An object with arbitrary keys and values. This is an optional field.', - ) - .optional(), + redacts: eventIdSchema.describe('The ID of the event that this event redacts. This is an optional field.').optional(), + room_id: roomIdSchema.describe('The ID of the room that the event is in. This is a unique identifier for the room.'), + sender: userIdSchema.describe('The ID of the user that sent the event. This is a unique identifier for the user.'), + signatures: SignatureSchema.describe('The signatures of the event. This is an object with arbitrary keys and values.'), + unsigned: z.any().describe('An object with arbitrary keys and values. This is an optional field.').optional(), }; export const PduNoContentStateEventSchema = { ...PduNoContentTimelineEventSchema, - state_key: userIdSchema.describe( - 'The state key of the event. This is an optional field.', - ), + state_key: userIdSchema.describe('The state key of the event. This is an optional field.'), }; export const PduNoContentEmptyStateKeyStateEventSchema = { @@ -719,15 +525,10 @@ export const EventPduTypeRoomServerAcl = z.object({ export const PduRoomTombstoneEventContentSchema = z.object({ body: z.string().describe('The body of the tombstone.'), - replacement_room: z - .string() - .describe('The ID of the replacement room.') - .optional(), + replacement_room: z.string().describe('The ID of the replacement room.').optional(), }); -export type PduRoomTombstoneEventContent = z.infer< - typeof PduRoomTombstoneEventContentSchema ->; +export type PduRoomTombstoneEventContent = z.infer; const EventPduTypeRoomTombstone = z.object({ ...PduNoContentTimelineEventSchema, @@ -820,11 +621,8 @@ export const PduTimelineSchema = z.discriminatedUnion('type', [ EventPduTypeRoomRedaction, ]); -export const PduSchema = z.discriminatedUnion('type', [ - ...PduTimelineSchema.options, - ...PduStateEventSchema.options, -]); +export const PduSchema = z.discriminatedUnion('type', [...PduTimelineSchema.options, ...PduStateEventSchema.options]); -export type Pdu = z.infer & {}; +export type Pdu = z.infer; export type PduContent = PduForType['content']; diff --git a/rollup.config.js b/rollup.config.js index 941b3e93d..3c2d36009 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -6,34 +6,12 @@ const input = './packages/federation-sdk/dist/index.d.ts'; // Função para determinar se um módulo deve ser tratado como externo const isExternal = (id) => { // Módulos Node.js nativos - if ( - id.startsWith('node:') || - [ - 'http', - 'https', - 'dns', - 'events', - 'net', - 'stream', - 'tls', - 'worker_threads', - ].includes(id) - ) { + if (id.startsWith('node:') || ['http', 'https', 'dns', 'events', 'net', 'stream', 'tls', 'worker_threads'].includes(id)) { return true; } // Dependências de terceiros - if ( - [ - 'zod', - 'mongodb', - 'tsyringe', - 'pino', - 'pino-std-serializers', - 'sonic-boom', - 'tweetnacl', - ].includes(id) - ) { + if (['zod', 'mongodb', 'tsyringe', 'pino', 'pino-std-serializers', 'sonic-boom', 'tweetnacl'].includes(id)) { return true; } @@ -68,11 +46,7 @@ export default [ }, plugins: [ dts({ - includeExternal: [ - '@rocket.chat/federation-core', - '@rocket.chat/federation-room', - '@rocket.chat/federation-crypto', - ], + includeExternal: ['@rocket.chat/federation-core', '@rocket.chat/federation-room', '@rocket.chat/federation-crypto'], respectExternal: true, }), ],