diff --git a/.config/example.yml b/.config/example.yml
index 9082dfb868..17149f6c3a 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -178,19 +178,13 @@ logLevel: [
 # Media Proxy
 #mediaProxy: https://example.com/proxy
 
-# Proxy remote files (default: false)
+# Proxy remote files (default: true)
 #proxyRemoteFiles: true
 
 #allowedPrivateNetworks: [
 #  '127.0.0.1/32'
 #]
 
-# TWA
-#twa:
-#  nameSpace: android_app
-#  packageName: tld.domain.twa
-#  sha256CertFingerprints: ['AB:CD:EF']
-
 # Upload or download file size limits (bytes)
 #maxFileSize: 262144000
 
diff --git a/.gitlab/issue_templates/bug.md b/.gitlab/issue_templates/bug.md
index 3bffa21cde..f96bdec01e 100644
--- a/.gitlab/issue_templates/bug.md
+++ b/.gitlab/issue_templates/bug.md
@@ -3,30 +3,47 @@
 🔒 Found a security vulnerability? [Please disclose it responsibly.](https://firefish.dev/firefish/firefish/-/blob/develop/SECURITY.md)
 🤝 By submitting this issue, you agree to follow our [Contribution Guidelines.](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md) -->
 
-**What happened?** _(Please give us a brief description of what happened.)_
+## What happened? <!-- Please give us a brief description of what happened. -->
 
-**What did you expect to happen?** _(Please give us a brief description of what you expected to happen.)_
 
-**Version** _(What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information.)_
+## What did you expect to happen? <!-- Please give us a brief description of what you expected to happen. -->
 
-**Instance** _(What instance of firefish are you using?)_
 
-**What type of issue is this?** _(If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side.)_
+## Version <!-- What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information. -->
 
-**What browser are you using? (Client-side issues only)**
 
-**What operating system are you using? (Client-side issues only)**
+## What type of issue is this? <!-- If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side. -->
 
-**How do you deploy Firefish on your server? (Server-side issues only)**
+- [ ] server-side
+- [ ] client-side
+- [ ] not sure
 
-**What operating system are you using? (Server-side issues only)**
+<details>
 
-**Relevant log output** _(Please copy and paste any relevant log output. You can find your log by inspecting the page, and going to the "console" tab. This will be automatically formatted into code, so no need for backticks.)_
+### Instance <!-- What instance of firefish are you using? -->
 
-**Contribution Guidelines**
+
+### What browser are you using? (client-side issues only)
+
+
+### What operating system are you using? (client-side issues only)
+
+
+### How do you deploy Firefish on your server? (server-side issues only)
+
+
+### What operating system are you using? (Server-side issues only)
+
+
+### Relevant log output <!-- Please copy and paste any relevant log output. You can find your log by inspecting the page, and going to the "console" tab. -->
+
+
+</details>
+
+## Contribution Guidelines
 By submitting this issue, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md)
 - [ ] I agree to follow this project's Contribution Guidelines
 - [ ] I have searched the issue tracker for similar issues, and this is not a duplicate.
 
-**Are you willing to fix this bug?** (optional)
+## Are you willing to fix this bug? (optional)
 - [ ] Yes. I will fix this bug and open a merge request if the change is agreed upon.
diff --git a/.gitlab/issue_templates/feature.md b/.gitlab/issue_templates/feature.md
index b4af4884b7..4c9ee56226 100644
--- a/.gitlab/issue_templates/feature.md
+++ b/.gitlab/issue_templates/feature.md
@@ -3,18 +3,22 @@
 🔒 Found a security vulnerability? [Please disclose it responsibly.](https://firefish.dev/firefish/firefish/-/blob/develop/SECURITY.md)
 🤝 By submitting this feature request, you agree to follow our [Contribution Guidelines.](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md) -->
 
-**What feature would you like implemented?** _(Please give us a brief description of what you'd like.)_
+## What feature would you like implemented? <!-- Please give us a brief description of what you'd like. -->
 
-**Why should we add this feature?** _(Please give us a brief description of why your feature is important.)_
 
-**Version** _(What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information.)_
+## Why should we add this feature? <!-- Please give us a brief description of why your feature is important. -->
 
-**Instance** _(What instance of firefish are you using?)_
 
-**Contribution Guidelines**
+## Version <!-- What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information. -->
+
+
+## Instance <!-- What instance of firefish are you using? -->
+
+
+## Contribution Guidelines
 By submitting this issue, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md)
 - [ ] I agree to follow this project's Contribution Guidelines
 - [ ] I have searched the issue tracker for similar requests, and this is not a duplicate.
 
-**Are you willing to implement this feature?** (optional)
+## Are you willing to implement this feature? (optional)
 - [ ] Yes. I will implement this feature and open a merge request if the change is agreed upon.
diff --git a/.gitlab/merge_request_templates/default.md b/.gitlab/merge_request_templates/default.md
index 2a1c926223..d13a146da0 100644
--- a/.gitlab/merge_request_templates/default.md
+++ b/.gitlab/merge_request_templates/default.md
@@ -1,8 +1,9 @@
 <!-- Thanks for taking the time to make Firefish better! It's not required, but please consider using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) when making your commits. If you use VSCode, please use the [Conventional Commits extension](https://marketplace.visualstudio.com/items?itemName=vivaxy.vscode-conventional-commits). -->
 
-**What does this PR do?** _(Please give us a brief description of what this PR does.)_
+## What does this PR do? <!-- Please give us a brief description of what this PR does. -->
 
-**Contribution Guidelines**
+
+## Contribution Guidelines
 By submitting this merge request, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md)
 - [ ] This change is reviewed in an issue / This is a minor bug fix
 - [ ] I agree to follow this project's Contribution Guidelines
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 0083604d44..de0385be2a 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -12,6 +12,7 @@
     "esbenp.prettier-vscode",
     "redhat.vscode-yaml",
     "yoavbls.pretty-ts-errors",
-    "biomejs.biome"
+    "biomejs.biome",
+		"rust-lang.rust-analyzer"
   ]
 }
diff --git a/Cargo.lock b/Cargo.lock
index 93c475fa0b..c6ba96e683 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -59,9 +59,9 @@ checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
 
 [[package]]
 name = "allocator-api2"
-version = "0.2.16"
+version = "0.2.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
+checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
 
 [[package]]
 name = "android-tzdata"
@@ -132,6 +132,18 @@ version = "1.0.82"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
 
+[[package]]
+name = "argon2"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
+dependencies = [
+ "base64ct",
+ "blake2",
+ "cpufeatures",
+ "password-hash",
+]
+
 [[package]]
 name = "arrayvec"
 version = "0.7.4"
@@ -190,11 +202,14 @@ checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
 name = "backend-rs"
 version = "0.0.0"
 dependencies = [
+ "argon2",
  "async-trait",
  "basen",
+ "bcrypt",
  "cfg-if",
  "chrono",
  "cuid2",
+ "emojis",
  "idna",
  "jsonschema",
  "macro_rs",
@@ -238,6 +253,12 @@ version = "0.21.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
 
+[[package]]
+name = "base64"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
+
 [[package]]
 name = "base64ct"
 version = "1.6.0"
@@ -250,6 +271,19 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1dbe4bb73fd931c4d1aaf53b35d1286c8a948ad00ec92c8e3c856f15fd027f43"
 
+[[package]]
+name = "bcrypt"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7"
+dependencies = [
+ "base64 0.22.0",
+ "blowfish",
+ "getrandom",
+ "subtle",
+ "zeroize",
+]
+
 [[package]]
 name = "bigdecimal"
 version = "0.3.1"
@@ -303,6 +337,15 @@ dependencies = [
  "wyz",
 ]
 
+[[package]]
+name = "blake2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
+dependencies = [
+ "digest",
+]
+
 [[package]]
 name = "block-buffer"
 version = "0.10.4"
@@ -312,6 +355,16 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "blowfish"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
+dependencies = [
+ "byteorder",
+ "cipher",
+]
+
 [[package]]
 name = "borsh"
 version = "1.4.0"
@@ -384,9 +437,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
 
 [[package]]
 name = "cc"
-version = "1.0.92"
+version = "1.0.94"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41"
+checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7"
 
 [[package]]
 name = "cfg-if"
@@ -412,7 +465,17 @@ dependencies = [
  "num-traits",
  "serde",
  "wasm-bindgen",
- "windows-targets 0.52.4",
+ "windows-targets 0.52.5",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
 ]
 
 [[package]]
@@ -633,13 +696,22 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
 
 [[package]]
 name = "either"
-version = "1.10.0"
+version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
+checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
 dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "emojis"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ee61eb945bff65ee7d19d157d39c67c33290ff0742907413fd5eefd29edc979"
+dependencies = [
+ "phf",
+]
+
 [[package]]
 name = "encoding_rs"
 version = "0.8.34"
@@ -1075,6 +1147,15 @@ dependencies = [
  "syn 2.0.58",
 ]
 
+[[package]]
+name = "inout"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+dependencies = [
+ "generic-array",
+]
+
 [[package]]
 name = "ipnet"
 version = "2.9.0"
@@ -1122,7 +1203,7 @@ checksum = "2a071f4f7efc9a9118dfb627a0a94ef247986e1ab8606a4c806ae2b3aa3b6978"
 dependencies = [
  "ahash 0.8.11",
  "anyhow",
- "base64",
+ "base64 0.21.7",
  "bytecount",
  "clap",
  "fancy-regex",
@@ -1175,7 +1256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
 dependencies = [
  "cfg-if",
- "windows-targets 0.52.4",
+ "windows-targets 0.52.5",
 ]
 
 [[package]]
@@ -1296,9 +1377,9 @@ dependencies = [
 
 [[package]]
 name = "napi-build"
-version = "2.1.2"
+version = "2.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f9130fccc5f763cf2069b34a089a18f0d0883c66aceb81f2fad541a3d823c43"
+checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a"
 
 [[package]]
 name = "napi-derive"
@@ -1350,9 +1431,9 @@ dependencies = [
 
 [[package]]
 name = "num"
-version = "0.4.1"
+version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af"
+checksum = "3135b08af27d103b0a51f2ae0f8632117b7b185ccf931445affa8df530576a41"
 dependencies = [
  "num-bigint",
  "num-complex",
@@ -1559,6 +1640,17 @@ dependencies = [
  "syn 2.0.58",
 ]
 
+[[package]]
+name = "password-hash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
+dependencies = [
+ "base64ct",
+ "rand_core",
+ "subtle",
+]
+
 [[package]]
 name = "paste"
 version = "1.0.14"
@@ -1580,6 +1672,24 @@ version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
 
+[[package]]
+name = "phf"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
+dependencies = [
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
+dependencies = [
+ "siphasher",
+]
+
 [[package]]
 name = "pin-project-lite"
 version = "0.2.14"
@@ -1801,7 +1911,7 @@ version = "0.11.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
 dependencies = [
- "base64",
+ "base64 0.21.7",
  "bytes",
  "encoding_rs",
  "futures-core",
@@ -1947,7 +2057,7 @@ version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
 dependencies = [
- "base64",
+ "base64 0.21.7",
 ]
 
 [[package]]
@@ -2231,6 +2341,12 @@ version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
 
+[[package]]
+name = "siphasher"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+
 [[package]]
 name = "slab"
 version = "0.4.9"
@@ -2398,7 +2514,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418"
 dependencies = [
  "atoi",
- "base64",
+ "base64 0.21.7",
  "bigdecimal",
  "bitflags 2.5.0",
  "byteorder",
@@ -2445,7 +2561,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e"
 dependencies = [
  "atoi",
- "base64",
+ "base64 0.21.7",
  "bigdecimal",
  "bitflags 2.5.0",
  "byteorder",
@@ -3041,7 +3157,7 @@ version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
 dependencies = [
- "windows-targets 0.52.4",
+ "windows-targets 0.52.5",
 ]
 
 [[package]]
@@ -3059,7 +3175,7 @@ version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
 dependencies = [
- "windows-targets 0.52.4",
+ "windows-targets 0.52.5",
 ]
 
 [[package]]
@@ -3079,17 +3195,18 @@ dependencies = [
 
 [[package]]
 name = "windows-targets"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
+checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
 dependencies = [
- "windows_aarch64_gnullvm 0.52.4",
- "windows_aarch64_msvc 0.52.4",
- "windows_i686_gnu 0.52.4",
- "windows_i686_msvc 0.52.4",
- "windows_x86_64_gnu 0.52.4",
- "windows_x86_64_gnullvm 0.52.4",
- "windows_x86_64_msvc 0.52.4",
+ "windows_aarch64_gnullvm 0.52.5",
+ "windows_aarch64_msvc 0.52.5",
+ "windows_i686_gnu 0.52.5",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.5",
+ "windows_x86_64_gnu 0.52.5",
+ "windows_x86_64_gnullvm 0.52.5",
+ "windows_x86_64_msvc 0.52.5",
 ]
 
 [[package]]
@@ -3100,9 +3217,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
 
 [[package]]
 name = "windows_aarch64_gnullvm"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
+checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
 
 [[package]]
 name = "windows_aarch64_msvc"
@@ -3112,9 +3229,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
 
 [[package]]
 name = "windows_aarch64_msvc"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
+checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
 
 [[package]]
 name = "windows_i686_gnu"
@@ -3124,9 +3241,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
 
 [[package]]
 name = "windows_i686_gnu"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
+checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
 
 [[package]]
 name = "windows_i686_msvc"
@@ -3136,9 +3259,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
 
 [[package]]
 name = "windows_i686_msvc"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
+checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
 
 [[package]]
 name = "windows_x86_64_gnu"
@@ -3148,9 +3271,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
 
 [[package]]
 name = "windows_x86_64_gnu"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
+checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
@@ -3160,9 +3283,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
+checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
 
 [[package]]
 name = "windows_x86_64_msvc"
@@ -3172,9 +3295,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.52.4"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
+checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
 
 [[package]]
 name = "winnow"
diff --git a/Cargo.toml b/Cargo.toml
index f7ba12d884..7ca43c960b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,14 +7,17 @@ macro_rs = { path = "packages/macro-rs" }
 
 napi = { version = "2.16.2", default-features = false }
 napi-derive = "2.16.2"
-napi-build = "2.1.2"
+napi-build = "2.1.3"
 
+argon2 = "0.5.3"
 async-trait = "0.1.80"
 basen = "0.1.0"
+bcrypt = "0.15.1"
 cfg-if = "1.0.0"
 chrono = "0.4.37"
 convert_case = "0.6.0"
 cuid2 = "0.1.2"
+emojis = "0.6.1"
 idna = "0.5.0"
 jsonschema = "0.17.1"
 once_cell = "1.19.0"
diff --git a/biome.json b/biome.json
index 9bf08ad553..21b711f457 100644
--- a/biome.json
+++ b/biome.json
@@ -1,7 +1,7 @@
 {
 	"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
 	"organizeImports": {
-		"enabled": true
+		"enabled": false
 	},
 	"linter": {
 		"enabled": true,
@@ -21,7 +21,8 @@
 						"useImportType": "warn",
 						"useShorthandFunctionType": "warn",
 						"useTemplate": "warn",
-						"noNonNullAssertion": "off"
+						"noNonNullAssertion": "off",
+						"useNodejsImportProtocol": "off"
 					}
 				}
 			}
diff --git a/docker-compose.example.yml b/docker-compose.example.yml
index fc6c0268a2..9cd6d1cdce 100644
--- a/docker-compose.example.yml
+++ b/docker-compose.example.yml
@@ -17,6 +17,7 @@ services:
 #     - web
     environment:
       NODE_ENV: production
+      NODE_OPTIONS: --max-old-space-size=3072
     volumes:
       - ./custom:/firefish/custom:ro
       - ./files:/firefish/files
diff --git a/docs/api-change.md b/docs/api-change.md
index fc82e12806..f3ed584c32 100644
--- a/docs/api-change.md
+++ b/docs/api-change.md
@@ -2,6 +2,10 @@
 
 Breaking changes are indicated by the :warning: icon.
 
+## v20240413
+
+- :warning: Removed `patrons` endpoint.
+
 ## v20240405
 
 - Added `notes/history` endpoint.
diff --git a/docs/changelog.md b/docs/changelog.md
index f0076197bc..70f8b34fbe 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -5,9 +5,16 @@ Critical security updates are indicated by the :warning: icon.
 - Server administrators should check [notice-for-admins.md](./notice-for-admins.md) as well.
 - Third-party client/bot developers may want to check [api-change.md](./api-change.md) as well.
 
-## Unreleased
+## [v20240421](https://firefish.dev/firefish/firefish/-/merge_requests/10756/commits)
+
+- Fix bugs
+
+## [v20240413](https://firefish.dev/firefish/firefish/-/merge_requests/10741/commits)
 
 - Add "Media" tab to user page
+- Improve federation and rendering of mathematical expressions
+- Remove donor information from the web client
+	- See also: https://info.firefish.dev/notes/9s1n283sb10rh869
 - Fix bugs
 
 ## [v20240405](https://firefish.dev/firefish/firefish/-/merge_requests/10733/commits)
diff --git a/docs/downgrade.sql b/docs/downgrade.sql
index 0b6f25e08c..44222f818f 100644
--- a/docs/downgrade.sql
+++ b/docs/downgrade.sql
@@ -1,6 +1,8 @@
 BEGIN;
 
 DELETE FROM "migrations" WHERE name IN (
+    'AddDriveFileUsage1713451569342',
+    'ConvertCwVarcharToText1713225866247',
     'FixChatFileConstraint1712855579316',
     'DropTimeZone1712425488543',
     'ExpandNoteEdit1711936358554',
@@ -22,6 +24,15 @@ DELETE FROM "migrations" WHERE name IN (
     'RemoveNativeUtilsMigration1705877093218'
 );
 
+-- AddDriveFileUsage
+ALTER TABLE "drive_file" DROP COLUMN "usageHint";
+DROP TYPE "drive_file_usage_hint_enum";
+
+-- convert-cw-varchar-to-text
+DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f";
+ALTER TABLE "note" ALTER COLUMN "cw" TYPE character varying(512);
+CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw" pgroonga_varchar_full_text_search_ops_v2);
+
 -- fix-chat-file-constraint
 ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_535def119223ac05ad3fa9ef64b";
 ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_535def119223ac05ad3fa9ef64b" FOREIGN KEY ("fileId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
diff --git a/docs/install.md b/docs/install.md
index 061000fa32..324923c6a7 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -154,7 +154,7 @@ sudo apt install ffmpeg
 1. Build
     ```sh
     pnpm install --frozen-lockfile
-    NODE_ENV=production pnpm run build
+    NODE_ENV=production NODE_OPTIONS='--max-old-space-size=3072' pnpm run build
     ```
 1. Execute database migrations
     ```sh
@@ -242,6 +242,7 @@ In this instruction, we use [Caddy](https://caddyserver.com/) to make the Firefi
     WorkingDirectory=/home/firefish/firefish
     Environment="NODE_ENV=production"
     Environment="npm_config_cache=/tmp"
+		Environment="NODE_OPTIONS=--max-old-space-size=3072"
     # uncomment the following line if you use jemalloc (note that the path varies on different environments)
     # Environment="LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2"
     StandardOutput=journal
diff --git a/docs/notice-for-admins.md b/docs/notice-for-admins.md
index abfaaea2fc..9bff40a65c 100644
--- a/docs/notice-for-admins.md
+++ b/docs/notice-for-admins.md
@@ -2,7 +2,7 @@
 
 You can skip intermediate versions when upgrading from an old version, but please read the notices and follow the instructions for each intermediate version before [upgrading](./upgrade.md).
 
-## Unreleased
+## v20240413
 
 ### For all users
 
@@ -21,8 +21,8 @@ The number of posts stored on your database can be found at `https://yourserver.
 
 - Please remove `packages/backend-rs/target` before building Firefish.
     ```sh
-		rm --recursive --force packages/backend-rs/target
-		```
+    rm --recursive --force packages/backend-rs/target
+    ```
 - Please do not terminate `pnpm run migrate` even if it appears to be frozen.
 
 ### For Docker/Podman users
diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml
index 35fe237190..622074056c 100644
--- a/locales/ar-SA.yml
+++ b/locales/ar-SA.yml
@@ -893,9 +893,6 @@ _aboutFirefish:
   source: "الشفرة المصدرية"
   translation: "ترجم ميسكي"
   donate: "تبرع لميسكي"
-  morePatrons: "نحن نقدر الدعم الذي قدمه العديد من الأشخاص الذين لم نذكرهم. شكرًا
-    لكم 🥰"
-  patrons: "الداعمون"
 _nsfw:
   respect: "اخف الوسائط ذات المحتوى الحساس"
   ignore: "اعرض الوسائط ذات المحتوى الحساس"
diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml
index d0869736ef..d17766b499 100644
--- a/locales/bn-BD.yml
+++ b/locales/bn-BD.yml
@@ -975,8 +975,6 @@ _aboutFirefish:
   source: "সোর্স কোড"
   translation: "Firefish অনুবাদ করুন"
   donate: "Firefish তে দান করুন"
-  morePatrons: "আরও অনেকে আমাদের সাহায্য করছেন। তাদের সবাইকে ধন্যবাদ 🥰"
-  patrons: "সমর্থনকারী"
 _nsfw:
   respect: "স্পর্শকাতর মিডিয়া লুকান"
   ignore: "স্পর্শকাতর মিডিয়া লুকাবেন না"
diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index b8049c1b18..47328cb554 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -1586,18 +1586,12 @@ _aboutFirefish:
   translation: Tradueix Firefish
   about: Firefish és una bifurcació de Misskey feta per ThatOneCalculator, que està
     en desenvolupament des del 2022.
-  morePatrons: També agraïm el suport de molts altres ajudants que no figuren aquí.
-    Gràcies! 🥰
-  patrons: Mecenes de Firefish
-  patronsList: Llistats cronològicament, no per la quantitat donada. Fes una donació
-    amb l'enllaç de dalt per veure el teu nom aquí!
   donateTitle: T'agrada Firefish?
   pleaseDonateToFirefish: Penseu en fer una donació a Firefish per donar suport al
     seu desenvolupament.
   pleaseDonateToHost: Penseu també en fer una donació a la vostre instància, {host},
     per ajudar-lo a suportar els costos de funcionament.
   donateHost: Fes una donació a {host}
-  sponsors: Patrocinadors de Calckey
   misskeyContributors: Col·laboradors de Misskey
 unknown: Desconegut
 pageLikesCount: Nombre de pàgines amb M'agrada
@@ -2084,9 +2078,9 @@ _experiments:
   release: Publicà
   title: Experiments
   enablePostImports: Activar l'importació de publicacions
-  postImportsCaption: Permet els usuaris importar publicacions desde comptes a Firefish,
+  postImportsCaption: Permet als usuaris importar publicacions des de comptes de Firefish,
     Misskey, Mastodon, Akkoma i Pleroma. Pot fer que el servidor vagi més lent durant
-    la càrrega si tens un coll d'ampolla a la cua.
+    la importació si la teva cua de feina és saturada.
 noGraze: Si us plau, desactiva l'extensió del navegador "Graze for Mastodon", ja que
   interfereix amb Firefish.
 accessibility: Accessibilitat
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index 10901246e1..012f1ef3ea 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -1092,17 +1092,11 @@ _aboutFirefish:
   source: "Quellcode"
   translation: "Firefish übersetzen"
   donate: "An Firefish spenden"
-  morePatrons: "Wir schätzen ebenso die Unterstützung vieler anderer hier nicht gelisteter
-    Personen sehr. Danke! 🥰"
-  patrons: "UnterstützerInnen"
-  patronsList: Auflistung chonologisch, nicht nach Spenden-Größe. Spende über den
-    Link oben, um hier aufgeführt zu werden!
   donateTitle: Gefällt dir Firefish?
   pleaseDonateToFirefish: Bitte erwäge eine Spende an Firefish, um dessen Entwicklung
     zu unterstützen.
   pleaseDonateToHost: Bitte erwäge auch, an deinen Heimatserver {host} zu spenden,
     um bei der Deckung der Betriebskosten zu helfen.
-  sponsors: Firefish-Sponsoren
   donateHost: Spende an {host}
   misskeyContributors: Misskey-Mitwirkende
 _nsfw:
diff --git a/locales/en-US.yml b/locales/en-US.yml
index a8663375a8..f44c3d1842 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -1337,12 +1337,6 @@ _aboutFirefish:
   pleaseDonateToHost: "Please also consider donating to your home server, {host},
     to help support its operation costs."
   donateHost: "Donate to {host}"
-  morePatrons: "We also appreciate the support of many other helpers not listed here.
-    Thank you! 🥰"
-  sponsors: "Firefish sponsors"
-  patrons: "Firefish patrons"
-  patronsList: "Listed chronologically, not by donation size. Donate with the link
-    above to get your name on here!"
 _nsfw:
   respect: "Hide NSFW media"
   ignore: "Don't hide NSFW media"
diff --git a/locales/es-ES.yml b/locales/es-ES.yml
index bfc2268a27..9980607521 100644
--- a/locales/es-ES.yml
+++ b/locales/es-ES.yml
@@ -1073,17 +1073,11 @@ _aboutFirefish:
   source: "Código fuente"
   translation: "Traducir Firefish"
   donate: "Donar a Firefish"
-  morePatrons: "También apreciamos el apoyo de muchos más que no están enlistados
-    aquí. ¡Gracias! 🥰"
-  patrons: "Mecenas de Firefish"
   pleaseDonateToFirefish: Por favor considera donar a Firefish para apollar su desarrollo.
   donateHost: Dona a {host}
-  patronsList: Listados cronológicamente no por monto de la donación. ¡Dona con el
-    vínculo de arriba para que tu nombre aparezca aquí!
   donateTitle: ¿Te gusta Firefish?
   pleaseDonateToHost: También considera donar a tu propio servidor , {host}, para
     ayudar con los costos de operación.
-  sponsors: Patrocinadores de Firefish
   misskeyContributors: Contribuidores de Misskey
 _nsfw:
   respect: "Ocultar medios NSFW"
diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
index da56856077..6367dda9f4 100644
--- a/locales/fr-FR.yml
+++ b/locales/fr-FR.yml
@@ -996,18 +996,12 @@ _aboutFirefish:
   source: "Code source"
   translation: "Traduire Firefish"
   donate: "Soutenir Firefish"
-  morePatrons: "Nous apprécions vraiment le soutien de nombreuses autres personnes
-    non mentionnées ici. Merci à toutes et à tous ! 🥰"
-  patrons: "Contributeurs"
   pleaseDonateToFirefish: Merci de considérer de faire un don pour soutenir le développement
     de Firefish.
-  sponsors: Sponsors Firefish
   donateTitle: Firefish vous plaît ?
   pleaseDonateToHost: Également, veuillez envisager de faire un don à votre serveur
     d'accueil, {host}, pour contribuer à couvrir ses frais de fonctionnement.
   donateHost: Faire un don à {host}
-  patronsList: Listé chronologiquement, pas par taille de donation. Faite un don avec
-    le lien ci-dessus pour avoir votre nom affiché ici !
   misskeyContributors: Contributeurs Misskey
 _nsfw:
   respect: "Cacher les médias marqués comme contenu sensible (NSFW)"
@@ -2324,3 +2318,4 @@ markLocalFilesNsfwByDefaultDescription: Indépendamment de ce réglage, les util
   peuvent supprimer le drapeau « sensible » (NSFW) eux-mêmes. Les fichiers existants
   ne sont pas affectés.
 noteEditHistory: Historique des publications
+media: Multimédia
diff --git a/locales/id-ID.yml b/locales/id-ID.yml
index 20efbdc458..4535492e7d 100644
--- a/locales/id-ID.yml
+++ b/locales/id-ID.yml
@@ -985,12 +985,6 @@ _aboutFirefish:
   source: "Sumber kode"
   translation: "Terjemahkan Firefish"
   donate: "Donasi ke Firefish"
-  morePatrons: "Kami sangat mengapresiasi dukungan dari banyak penolong lain yang
-    tidak tercantum disini. Terima kasih! 🥰"
-  patrons: "Pendukung"
-  patronsList: Diurutkan secara kronologis, bukan berdasarkan jumlah donasi. Berdonasilah
-    dengan tautan di atas supaya nama kamu ada di sini!
-  sponsors: Sponsor Firefish
   donateTitle: Suka Firefish?
   pleaseDonateToFirefish: Silakan pertimbangkan berdonasi ke Firefish untuk mendukung
     pengembangannya.
diff --git a/locales/it-IT.yml b/locales/it-IT.yml
index 2bd5d1e1a2..e686df5890 100644
--- a/locales/it-IT.yml
+++ b/locales/it-IT.yml
@@ -934,18 +934,12 @@ _aboutFirefish:
   source: "Codice sorgente"
   translation: "Traduzione di Firefish"
   donate: "Sostieni Firefish"
-  morePatrons: "Apprezziamo sinceramente l'aiuto di tante altre persone non elencate
-    qui. Grazie mille! 🥰"
-  patrons: "Sostenitori"
-  sponsors: Gli sponsor di Firefish
   misskeyContributors: Contributori di Misskey
   donateTitle: Ti piace Firefish?
   pleaseDonateToFirefish: Con una donazione puoi supportare lo sviluppo di Firefish.
   pleaseDonateToHost: Considera anche una donazione al server che ti ospita, {host},
     per contribuire ai costi che sostiene.
   donateHost: Dona a {host}
-  patronsList: Elencati in ordine cronologico, non per importo. Dona con il link sopra
-    per apparire in questa lista!
 _nsfw:
   respect: "Nascondi i media sensibli (NSFW)"
   ignore: "Mostra i media sensibili (NSFW)"
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 372dddf579..3f900d1a5e 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1109,14 +1109,10 @@ _aboutFirefish:
   source: "ソースコード"
   translation: "Firefishを翻訳"
   donate: "Firefishに寄付"
-  morePatrons: "他にも多くの方が支援してくれています。ありがとうございます! 🥰"
-  patrons: "支援者"
-  patronsList: 寄付額ではなく時系列順に並んでいます。上記のリンクから寄付を行ってここにあなたのIDを載せましょう!
   pleaseDonateToFirefish: Firefish開発への寄付をご検討ください。
   pleaseDonateToHost: また、このサーバー {host} の運営者への寄付もご検討ください。
   donateHost: '{host} に寄付する'
   donateTitle: Firefishを気に入りましたか?
-  sponsors: Firefish の支援者
 _nsfw:
   respect: "閲覧注意のメディアは隠す"
   ignore: "閲覧注意のメディアを隠さない"
diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml
index 8a39260db1..970a27d0ed 100644
--- a/locales/ja-KS.yml
+++ b/locales/ja-KS.yml
@@ -874,8 +874,6 @@ _aboutFirefish:
   source: "ソースコード"
   translation: "Firefishを翻訳"
   donate: "Firefishに寄付"
-  morePatrons: "他にもぎょうさんの人からサポートしてもろてんねん。ほんまおおきに🥰"
-  patrons: "支援者"
   misskeyContributors: フォーク元のMisskeyを作らはった人ら
 _mfm:
   cheatSheet: "MFMチートシート"
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index d9750a53df..d67e37d4cf 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -992,10 +992,6 @@ _aboutFirefish:
   source: "소스 코드"
   translation: "Firefish를 번역하기"
   donate: "Firefish에 기부하기"
-  morePatrons: "이 외에도 다른 많은 분들이 도움을 주시고 계십니다. 감사합니다🥰"
-  patrons: "후원자"
-  patronsList: 기부 금액이 아닌 시간 순서로 정렬합니다. 위 링크를 통해 후원하여 당신의 이름을 새겨 보세요!
-  sponsors: Firefish 스폰서
   pleaseDonateToHost: 또한, 이 서버 {host} 의 운영자에게 기부하는 것도 검토하여 주십시오.
   pleaseDonateToFirefish: Firefish의 개발에 후원하는 것을 검토하여 주십시오.
   donateHost: '{host} 에게 기부하기'
diff --git a/locales/no-NO.yml b/locales/no-NO.yml
index 1321d46a3f..b446ff4359 100644
--- a/locales/no-NO.yml
+++ b/locales/no-NO.yml
@@ -987,8 +987,6 @@ _aboutFirefish:
   pleaseDonateToFirefish: Du kan vurdere å donere en slant til Firefish for å støtte
     videre utvikling og feilretting.
   donateHost: Donér til {host}
-  morePatrons: Vi er også takknemlige for bidragene fra mange andre som ikke er listet
-    her. Takk til dere alle! 🥰
   contributors: Hovedutviklere
   source: Kildekode
   allContributors: Alle bidragsytere
@@ -996,10 +994,6 @@ _aboutFirefish:
   pleaseDonateToHost: Du kan også vurdere å donere til hjemme-tjeneren din, {host},
     for å hjelpe dem med driftskostnadene for tjenesten.
   about: Firefish ble opprettet av ThatOneCalculator i 2022, basert på Misskey.
-  sponsors: Firefishs sponsorer
-  patrons: Firefishs patroner
-  patronsList: Listen er kronologisk, ikke etter donert beløp. Doner med lenken over
-    for å få navnet ditt her!
 isBot: Denne kontoen er en bot
 _nsfw:
   respect: Skjul NSFW-merket media
diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml
index fb57556f06..43eeb3c8f0 100644
--- a/locales/pl-PL.yml
+++ b/locales/pl-PL.yml
@@ -990,9 +990,6 @@ _aboutFirefish:
   source: "Kod źródłowy"
   translation: "Tłumacz Firefish"
   donate: "Przekaż darowiznę na Firefish"
-  morePatrons: "Naprawdę doceniam wsparcie ze strony wielu niewymienionych tu osób.
-    Dziękuję! 🥰"
-  patrons: "Wspierający"
 _nsfw:
   respect: "Ukrywaj media NSFW"
   ignore: "Nie ukrywaj mediów NSFW"
diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml
index 2650aa714a..5c81e19a04 100644
--- a/locales/ru-RU.yml
+++ b/locales/ru-RU.yml
@@ -986,12 +986,6 @@ _aboutFirefish:
   source: "Исходный код"
   translation: "Перевод Firefish"
   donate: "Пожертвование на Firefish"
-  morePatrons: "Большое спасибо и многим другим, кто принял участие в этом проекте!
-    🥰"
-  patrons: "Материальная поддержка"
-  patronsList: Перечислены в хронологическом порядке, а не по размеру пожертвования.
-    Сделайте взнос по ссылке выше, чтобы ваше имя было здесь!
-  sponsors: Спонсоры Firefish
   donateTitle: Понравился Firefish?
   pleaseDonateToFirefish: Пожалуйста, поддержите разработку Firefish.
   pleaseDonateToHost: Также не забудьте поддержать ваш домашний сервер {host}, чтобы
diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml
index 71513af387..e1ed198e4e 100644
--- a/locales/sk-SK.yml
+++ b/locales/sk-SK.yml
@@ -1036,9 +1036,6 @@ _aboutFirefish:
   source: "Zdrojový kód"
   translation: "Preložiť Firefish"
   donate: "Podporiť Firefish"
-  morePatrons: "Takisto oceňujeme podporu mnoých ďalších, ktorí tu nie sú uvedení.
-    Ďakujeme! 🥰"
-  patrons: "Prispievatelia"
 _nsfw:
   respect: "Skryť NSFW médiá"
   ignore: "Neskrývať NSFW médiá"
diff --git a/locales/th-TH.yml b/locales/th-TH.yml
index fa096a2c4d..4a668f910a 100644
--- a/locales/th-TH.yml
+++ b/locales/th-TH.yml
@@ -1022,9 +1022,6 @@ _aboutFirefish:
   source: "ซอร์สโค้ด"
   translation: "รับแปลภาษา Firefish"
   donate: "บริจาคให้กับ Firefish"
-  morePatrons: "เราขอขอบคุณสำหรับความช่วยเหลือจากผู้ช่วยอื่นๆ ที่ไม่ได้ระบุไว้ที่นี่นะ
-    ขอขอบคุณ! 🥰"
-  patrons: "สมาชิกพันธมิตร"
 _nsfw:
   respect: "ซ่อนสื่อ NSFW"
   ignore: "อย่าซ่อนสื่อ NSFW"
diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml
index 148dbb4757..cb36a6a07c 100644
--- a/locales/tr-TR.yml
+++ b/locales/tr-TR.yml
@@ -1910,14 +1910,9 @@ _preferencesBackups:
   updatedAt: 'Güncelleme tarihi: {date} {time}'
   cannotLoad: Yüklenemedi
 _aboutFirefish:
-  patronsList: Bağış büyüklüğüne göre değil, kronolojik olarak listelenmiştir. Adınızı
-    buraya almak için yukarıdaki bağlantıyla bağış yapın!
   about: Firefish, 2022'den beri geliştirilmekte olan ThatOneCalculator tarafından
     yapılan bir Misskey çatalıdır.
   allContributors: Tüm katkıda bulunanlar
-  patrons: Firefish patronları
-  morePatrons: Burada listelenmeyen diğer birçok yardımcının desteğini de takdir ediyoruz.
-    Teşekkür ederim! 🥰
   donate: Firefish'e bağışta bulunun
   contributors: Ana katkıda bulunanlar
   source: Kaynak Kodu
@@ -1928,7 +1923,6 @@ _aboutFirefish:
   pleaseDonateToHost: İşletme maliyetlerini desteklemek için lütfen ev sunucunuz {host}'a
     bağış yapmayı da düşünün.
   donateHost: '{ev sahibi} için bağış yapın'
-  sponsors: Firefish sponsorları
   misskeyContributors: Misskey'e katkıda bulunanlar
 _weekday:
   saturday: Cumartesi
diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml
index 64db5e0022..aa669f596f 100644
--- a/locales/uk-UA.yml
+++ b/locales/uk-UA.yml
@@ -825,17 +825,11 @@ _aboutFirefish:
   source: "Вихідний код"
   translation: "Перекладати Firefish"
   donate: "Пожертвувати Firefish"
-  morePatrons: "Ми дуже цінуємо підтримку багатьох інших помічників, не перелічених
-    тут. Дякуємо! 🥰"
-  patrons: "Підтримали"
-  patronsList: Перераховані в хронологічному порядку, а не за розміром пожертви. Зробіть
-    внесок за посиланням вище, щоб ваше ім'я було тут!
   donateTitle: Сподобався Firefish?
   pleaseDonateToFirefish: Будь ласка, підтримайте розробку Firefish.
   pleaseDonateToHost: Також не забудьте підтримати ваш домашній сервер {host}, щоб
     допомогти з його операційними витратами.
   donateHost: Зробити внесок на рахунок {host}
-  sponsors: Спонсори Firefish
   misskeyContributors: Контрибутори Misskey
 _nsfw:
   respect: "Приховувати NSFW медіа"
diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml
index c6899c5b16..4c6a01f1ae 100644
--- a/locales/vi-VN.yml
+++ b/locales/vi-VN.yml
@@ -1050,15 +1050,10 @@ _aboutFirefish:
   source: "Mã nguồn"
   translation: "Dịch Firefish"
   donate: "Ủng hộ Firefish"
-  morePatrons: "Chúng tôi cũng trân trọng sự hỗ trợ của nhiều người đóng góp khác
-    không được liệt kê ở đây. Cảm ơn! 🥰"
-  patrons: "Người ủng hộ"
-  patronsList: Liệt kê theo thứ tự, không theo số tiền ủng hộ. Hãy để tên bạn ở đây!
   donateTitle: Thích Firefish?
   pleaseDonateToFirefish: Hãy cân nhắc ủng hộ Firefish phát triển.
   donateHost: Ủng hộ {host}
   pleaseDonateToHost: Cũng như ủng hộ chi phí vận hành máy chủ {host} của bạn.
-  sponsors: Nhà tài trợ Firefish
   misskeyContributors: Người đóng góp Misskey
 _nsfw:
   respect: "Ẩn nội dung NSFW"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index 9e7a0ed19e..c8579a4c1a 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -996,10 +996,6 @@ _aboutFirefish:
   source: "源代码"
   translation: "翻译 Firefish"
   donate: "赞助 Firefish"
-  morePatrons: "还有很多其它的人也在支持我们,非常感谢🥰"
-  patrons: "Firefish 赞助者"
-  patronsList: 按时间顺序而不是捐赠金额排列。通过上面的链接捐款,让您的名字出现在这里!
-  sponsors: Firefish 赞助者们
   donateTitle: 喜欢 Firefish 吗?
   pleaseDonateToFirefish: 请考虑赞助 Firefish 以支持其开发。
   pleaseDonateToHost: 也请考虑赞助您的主服务器 {host},以帮助支持其运营成本。
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index 367449350d..5a722933e6 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -991,10 +991,6 @@ _aboutFirefish:
   source: "原始碼"
   translation: "翻譯Firefish"
   donate: "贊助Firefish"
-  morePatrons: "還有許許多多幫助我們的其他人,非常感謝你們。 🥰"
-  patrons: "贊助者"
-  patronsList: 按時間順序列出,而不是按贊助規模列出。使用上面的連結贊助,在這裡獲得顯示您名字的機會!
-  sponsors: Firefish 贊助者們
   donateTitle: 覺得 Firefish 棒嗎?
   pleaseDonateToFirefish: 請考慮向 Firefish 贊助以支持其發展。
   pleaseDonateToHost: 還請考慮捐贈給您在使用的伺服器 {host},以支援龐大的運營成本。
diff --git a/package.json b/package.json
index bff235df6e..93594a5698 100644
--- a/package.json
+++ b/package.json
@@ -1,11 +1,11 @@
 {
 	"name": "firefish",
-	"version": "20240405",
+	"version": "20240421",
 	"repository": {
 		"type": "git",
 		"url": "https://firefish.dev/firefish/firefish.git"
 	},
-	"packageManager": "pnpm@8.15.6",
+	"packageManager": "pnpm@8.15.7",
 	"private": true,
 	"scripts": {
 		"rebuild": "pnpm run clean && pnpm run build",
@@ -26,7 +26,9 @@
 		"debug": "pnpm run build:debug && pnpm run start",
 		"build:debug": "pnpm run clean && pnpm node ./scripts/dev-build.mjs && pnpm run gulp",
 		"mocha": "pnpm --filter backend run mocha",
-		"test": "pnpm run mocha",
+		"test": "pnpm run test:ts && pnpm run test:rs",
+		"test:ts": "pnpm run mocha",
+		"test:rs": "cargo test",
 		"format": "pnpm run format:ts; pnpm run format:rs",
 		"format:ts": "pnpm -r --parallel run format",
 		"format:rs": "cargo fmt --all --",
@@ -36,11 +38,11 @@
 		"clean-all": "pnpm run clean && pnpm run clean-cargo && pnpm run clean-npm"
 	},
 	"dependencies": {
-		"js-yaml": "4.1.0",
 		"gulp": "4.0.2",
 		"gulp-cssnano": "2.1.3",
 		"gulp-replace": "1.1.4",
-		"gulp-terser": "2.1.0"
+		"gulp-terser": "2.1.0",
+		"js-yaml": "4.1.0"
 	},
 	"devDependencies": {
 		"@biomejs/biome": "1.6.4",
@@ -50,7 +52,7 @@
 		"@biomejs/cli-linux-x64": "^1.6.4",
 		"@types/node": "20.12.7",
 		"execa": "8.0.1",
-		"pnpm": "8.15.6",
+		"pnpm": "8.15.7",
 		"typescript": "5.4.5"
 	}
 }
diff --git a/packages/README.md b/packages/README.md
index 56356ec2d0..6637462910 100644
--- a/packages/README.md
+++ b/packages/README.md
@@ -4,6 +4,7 @@ This directory contains all of the packages Firefish uses.
 
 - `backend`: Main backend code written in TypeScript for NodeJS
 - `backend-rs`: Backend code written in Rust, bound to NodeJS by [NAPI-RS](https://napi.rs/)
+- `macro-rs`: Procedural macros for backend-rs
 - `client`: Web interface written in Vue3 and TypeScript
 - `sw`: Web [Service Worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) written in TypeScript
 - `firefish-js`: TypeScript SDK for both backend and client
diff --git a/packages/backend-rs/Cargo.toml b/packages/backend-rs/Cargo.toml
index 783f630942..af9e10cdc1 100644
--- a/packages/backend-rs/Cargo.toml
+++ b/packages/backend-rs/Cargo.toml
@@ -17,11 +17,14 @@ macro_rs = { workspace = true }
 napi = { workspace = true, optional = true, default-features = false, features = ["napi9", "tokio_rt", "chrono_date", "serde-json"] }
 napi-derive = { workspace = true, optional = true }
 
+argon2 = { workspace = true, features = ["std"] }
 async-trait = { workspace = true }
 basen = { workspace = true }
+bcrypt = { workspace = true }
 cfg-if = { workspace = true }
 chrono = { workspace = true }
 cuid2 = { workspace = true }
+emojis = { workspace = true }
 idna = { workspace = true }
 jsonschema = { workspace = true }
 once_cell = { workspace = true }
diff --git a/packages/backend-rs/Makefile b/packages/backend-rs/Makefile
index f91a56aae7..11b614c82a 100644
--- a/packages/backend-rs/Makefile
+++ b/packages/backend-rs/Makefile
@@ -7,9 +7,9 @@ SRC += $(call recursive_wildcard, src, *)
 .PHONY: regenerate-entities
 regenerate-entities:
 	sea-orm-cli generate entity \
-		--output-dir='src/model/entity' \
-		--database-url='postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@localhost:25432/$(POSTGRES_DB)' \
-		--date-time-crate='chrono' \
+	  --output-dir='src/model/entity' \
+	  --database-url='postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@localhost:25432/$(POSTGRES_DB)' \
+	  --date-time-crate='chrono' \
 	  --model-extra-attributes='NAPI_EXTRA_ATTR_PLACEHOLDER' && \
 	for file in src/model/entity/*; do \
 	  base=$$(basename -- "$${file}"); \
@@ -17,7 +17,7 @@ regenerate-entities:
 	  attribute=$$(printf 'cfg_attr(feature = "napi", napi_derive::napi(object, js_name = "%s", use_nullable = true))' "$${jsname}"); \
 	  sed -i "s/NAPI_EXTRA_ATTR_PLACEHOLDER/$${attribute}/" "$${file}"; \
 	done
-	sed -i 's/#\[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)\]/#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]\n#[cfg_attr(not(feature = "napi"), derive(Clone))]\n#[cfg_attr(feature = "napi", napi_derive::napi)]/' \
+	sed -i 's/#\[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)\]/#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]\n#[cfg_attr(not(feature = "napi"), derive(Clone))]\n#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]/' \
 	  src/model/entity/sea_orm_active_enums.rs
 	cargo fmt --all --
 
diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts
index 509c3d912d..04b26c3e5a 100644
--- a/packages/backend-rs/index.d.ts
+++ b/packages/backend-rs/index.d.ts
@@ -3,6 +3,16 @@
 
 /* auto-generated by NAPI-RS */
 
+export interface EnvConfig {
+  onlyQueue: boolean
+  onlyServer: boolean
+  noDaemons: boolean
+  disableClustering: boolean
+  verbose: boolean
+  withLogTime: boolean
+  slow: boolean
+}
+export function readEnvironmentConfig(): EnvConfig
 export interface ServerConfig {
   url: string
   port: number
@@ -118,7 +128,8 @@ export interface Acct {
 }
 export function stringToAcct(acct: string): Acct
 export function acctToString(acct: Acct): string
-export interface NoteLike {
+/** TODO: handle name collisions better */
+export interface NoteLikeForCheckWordMute {
   fileIds: Array<string>
   userId: string | null
   text: string | null
@@ -126,15 +137,52 @@ export interface NoteLike {
   renoteId: string | null
   replyId: string | null
 }
-export function checkWordMute(note: NoteLike, mutedWordLists: Array<Array<string>>, mutedPatterns: Array<string>): Promise<boolean>
+export function checkWordMute(note: NoteLikeForCheckWordMute, mutedWordLists: Array<Array<string>>, mutedPatterns: Array<string>): Promise<boolean>
 export function getFullApAccount(username: string, host?: string | undefined | null): string
 export function isSelfHost(host?: string | undefined | null): boolean
 export function isSameOrigin(uri: string): boolean
 export function extractHost(uri: string): string
 export function toPuny(host: string): string
+export function isUnicodeEmoji(s: string): boolean
+export function sqlLikeEscape(src: string): string
+export function safeForSql(src: string): boolean
+/** Convert milliseconds to a human readable string */
+export function formatMilliseconds(milliseconds: number): string
+/** TODO: handle name collisions better */
+export interface NoteLikeForGetNoteSummary {
+  fileIds: Array<string>
+  text: string | null
+  cw: string | null
+  hasPoll: boolean
+}
+export function getNoteSummary(note: NoteLikeForGetNoteSummary): string
 export function toMastodonId(firefishId: string): string | null
 export function fromMastodonId(mastodonId: string): string | null
+export function fetchMeta(useCache: boolean): Promise<Meta>
+export interface PugArgs {
+  img: string | null
+  title: string
+  instanceName: string
+  desc: string | null
+  icon: string | null
+  splashIcon: string | null
+  themeColor: string | null
+  randomMotd: string
+  privateMode: boolean | null
+}
+export function metaToPugArgs(meta: Meta): PugArgs
 export function nyaify(text: string, lang?: string | undefined | null): string
+export function hashPassword(password: string): string
+export function verifyPassword(password: string, hash: string): boolean
+export function isOldPasswordAlgorithm(hash: string): boolean
+export interface DecodedReaction {
+  reaction: string
+  name: string | null
+  host: string | null
+}
+export function decodeReaction(reaction: string): DecodedReaction
+export function countReactions(reactions: Record<string, number>): Record<string, number>
+export function toDbReaction(reaction?: string | undefined | null, host?: string | undefined | null): Promise<string>
 export interface AbuseUserReport {
   id: string
   createdAt: Date
@@ -300,6 +348,7 @@ export interface DriveFile {
   webpublicType: string | null
   requestHeaders: Json | null
   requestIp: string | null
+  usageHint: DriveFileUsageHintEnum | null
 }
 export interface DriveFolder {
   id: string
@@ -725,81 +774,85 @@ export interface ReplyMuting {
   muteeId: string
   muterId: string
 }
-export const enum AntennaSrcEnum {
-  All = 0,
-  Group = 1,
-  Home = 2,
-  Instances = 3,
-  List = 4,
-  Users = 5
+export enum AntennaSrcEnum {
+  All = 'all',
+  Group = 'group',
+  Home = 'home',
+  Instances = 'instances',
+  List = 'list',
+  Users = 'users'
 }
-export const enum MutedNoteReasonEnum {
-  Manual = 0,
-  Other = 1,
-  Spam = 2,
-  Word = 3
+export enum DriveFileUsageHintEnum {
+  UserAvatar = 'userAvatar',
+  UserBanner = 'userBanner'
 }
-export const enum NoteVisibilityEnum {
-  Followers = 0,
-  Hidden = 1,
-  Home = 2,
-  Public = 3,
-  Specified = 4
+export enum MutedNoteReasonEnum {
+  Manual = 'manual',
+  Other = 'other',
+  Spam = 'spam',
+  Word = 'word'
 }
-export const enum NotificationTypeEnum {
-  App = 0,
-  Follow = 1,
-  FollowRequestAccepted = 2,
-  GroupInvited = 3,
-  Mention = 4,
-  PollEnded = 5,
-  PollVote = 6,
-  Quote = 7,
-  Reaction = 8,
-  ReceiveFollowRequest = 9,
-  Renote = 10,
-  Reply = 11
+export enum NoteVisibilityEnum {
+  Followers = 'followers',
+  Hidden = 'hidden',
+  Home = 'home',
+  Public = 'public',
+  Specified = 'specified'
 }
-export const enum PageVisibilityEnum {
-  Followers = 0,
-  Public = 1,
-  Specified = 2
+export enum NotificationTypeEnum {
+  App = 'app',
+  Follow = 'follow',
+  FollowRequestAccepted = 'followRequestAccepted',
+  GroupInvited = 'groupInvited',
+  Mention = 'mention',
+  PollEnded = 'pollEnded',
+  PollVote = 'pollVote',
+  Quote = 'quote',
+  Reaction = 'reaction',
+  ReceiveFollowRequest = 'receiveFollowRequest',
+  Renote = 'renote',
+  Reply = 'reply'
 }
-export const enum PollNotevisibilityEnum {
-  Followers = 0,
-  Home = 1,
-  Public = 2,
-  Specified = 3
+export enum PageVisibilityEnum {
+  Followers = 'followers',
+  Public = 'public',
+  Specified = 'specified'
 }
-export const enum RelayStatusEnum {
-  Accepted = 0,
-  Rejected = 1,
-  Requesting = 2
+export enum PollNotevisibilityEnum {
+  Followers = 'followers',
+  Home = 'home',
+  Public = 'public',
+  Specified = 'specified'
 }
-export const enum UserEmojimodpermEnum {
-  Add = 0,
-  Full = 1,
-  Mod = 2,
-  Unauthorized = 3
+export enum RelayStatusEnum {
+  Accepted = 'accepted',
+  Rejected = 'rejected',
+  Requesting = 'requesting'
 }
-export const enum UserProfileFfvisibilityEnum {
-  Followers = 0,
-  Private = 1,
-  Public = 2
+export enum UserEmojimodpermEnum {
+  Add = 'add',
+  Full = 'full',
+  Mod = 'mod',
+  Unauthorized = 'unauthorized'
 }
-export const enum UserProfileMutingnotificationtypesEnum {
-  App = 0,
-  Follow = 1,
-  FollowRequestAccepted = 2,
-  GroupInvited = 3,
-  Mention = 4,
-  PollEnded = 5,
-  PollVote = 6,
-  Quote = 7,
-  Reaction = 8,
-  ReceiveFollowRequest = 9,
-  Renote = 10,
-  Reply = 11
+export enum UserProfileFfvisibilityEnum {
+  Followers = 'followers',
+  Private = 'private',
+  Public = 'public'
+}
+export enum UserProfileMutingnotificationtypesEnum {
+  App = 'app',
+  Follow = 'follow',
+  FollowRequestAccepted = 'followRequestAccepted',
+  GroupInvited = 'groupInvited',
+  Mention = 'mention',
+  PollEnded = 'pollEnded',
+  PollVote = 'pollVote',
+  Quote = 'quote',
+  Reaction = 'reaction',
+  ReceiveFollowRequest = 'receiveFollowRequest',
+  Renote = 'renote',
+  Reply = 'reply'
 }
 export interface Signin {
   id: string
diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js
index f2942a9aa5..6d64f6ec75 100644
--- a/packages/backend-rs/index.js
+++ b/packages/backend-rs/index.js
@@ -310,8 +310,9 @@ if (!nativeBinding) {
   throw new Error(`Failed to load native binding`)
 }
 
-const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, toMastodonId, fromMastodonId, nyaify, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
+const { readEnvironmentConfig, readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
 
+module.exports.readEnvironmentConfig = readEnvironmentConfig
 module.exports.readServerConfig = readServerConfig
 module.exports.stringToAcct = stringToAcct
 module.exports.acctToString = acctToString
@@ -321,10 +322,24 @@ module.exports.isSelfHost = isSelfHost
 module.exports.isSameOrigin = isSameOrigin
 module.exports.extractHost = extractHost
 module.exports.toPuny = toPuny
+module.exports.isUnicodeEmoji = isUnicodeEmoji
+module.exports.sqlLikeEscape = sqlLikeEscape
+module.exports.safeForSql = safeForSql
+module.exports.formatMilliseconds = formatMilliseconds
+module.exports.getNoteSummary = getNoteSummary
 module.exports.toMastodonId = toMastodonId
 module.exports.fromMastodonId = fromMastodonId
+module.exports.fetchMeta = fetchMeta
+module.exports.metaToPugArgs = metaToPugArgs
 module.exports.nyaify = nyaify
+module.exports.hashPassword = hashPassword
+module.exports.verifyPassword = verifyPassword
+module.exports.isOldPasswordAlgorithm = isOldPasswordAlgorithm
+module.exports.decodeReaction = decodeReaction
+module.exports.countReactions = countReactions
+module.exports.toDbReaction = toDbReaction
 module.exports.AntennaSrcEnum = AntennaSrcEnum
+module.exports.DriveFileUsageHintEnum = DriveFileUsageHintEnum
 module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum
 module.exports.NoteVisibilityEnum = NoteVisibilityEnum
 module.exports.NotificationTypeEnum = NotificationTypeEnum
diff --git a/packages/backend-rs/package.json b/packages/backend-rs/package.json
index 69ae09fa49..1f3f49e9fb 100644
--- a/packages/backend-rs/package.json
+++ b/packages/backend-rs/package.json
@@ -33,8 +33,8 @@
 	},
 	"scripts": {
 		"artifacts": "napi artifacts",
-		"build": "napi build --features napi --platform --release ./built/",
-		"build:debug": "napi build --features napi --platform ./built/",
+		"build": "napi build --features napi --no-const-enum --platform --release ./built/",
+		"build:debug": "napi build --features napi --no-const-enum --platform ./built/",
 		"prepublishOnly": "napi prepublish -t npm",
 		"test": "pnpm run cargo:test && pnpm run build:debug && ava",
 		"universal": "napi universal",
diff --git a/packages/backend-rs/src/config/environment.rs b/packages/backend-rs/src/config/environment.rs
new file mode 100644
index 0000000000..7d66aec7ba
--- /dev/null
+++ b/packages/backend-rs/src/config/environment.rs
@@ -0,0 +1,27 @@
+// FIXME: Are these options used?
+#[crate::export(object)]
+pub struct EnvConfig {
+    pub only_queue: bool,
+    pub only_server: bool,
+    pub no_daemons: bool,
+    pub disable_clustering: bool,
+    pub verbose: bool,
+    pub with_log_time: bool,
+    pub slow: bool,
+}
+
+#[crate::export]
+pub fn read_environment_config() -> EnvConfig {
+    let node_env = std::env::var("NODE_ENV").unwrap_or_default().to_lowercase();
+    let is_testing = node_env == "test";
+
+    EnvConfig {
+        only_queue: std::env::var("MK_ONLY_QUEUE").is_ok(),
+        only_server: std::env::var("MK_ONLY_SERVER").is_ok(),
+        no_daemons: is_testing || std::env::var("MK_NO_DAEMONS").is_ok(),
+        disable_clustering: is_testing || std::env::var("MK_DISABLE_CLUSTERING").is_ok(),
+        verbose: std::env::var("MK_VERBOSE").is_ok(),
+        with_log_time: std::env::var("MK_WITH_LOG_TIME").is_ok(),
+        slow: std::env::var("MK_SLOW").is_ok(),
+    }
+}
diff --git a/packages/backend-rs/src/config/mod.rs b/packages/backend-rs/src/config/mod.rs
index 74f47ad347..b708f2b265 100644
--- a/packages/backend-rs/src/config/mod.rs
+++ b/packages/backend-rs/src/config/mod.rs
@@ -1 +1,2 @@
+pub mod environment;
 pub mod server;
diff --git a/packages/backend-rs/src/misc/check_word_mute.rs b/packages/backend-rs/src/misc/check_word_mute.rs
index 801175c2af..18b550c29b 100644
--- a/packages/backend-rs/src/misc/check_word_mute.rs
+++ b/packages/backend-rs/src/misc/check_word_mute.rs
@@ -4,7 +4,8 @@ use once_cell::sync::Lazy;
 use regex::Regex;
 use sea_orm::{prelude::*, QuerySelect};
 
-#[crate::export(object)]
+/// TODO: handle name collisions better
+#[crate::export(object, js_name = "NoteLikeForCheckWordMute")]
 pub struct NoteLike {
     pub file_ids: Vec<String>,
     pub user_id: Option<String>,
diff --git a/packages/backend-rs/src/misc/emoji.rs b/packages/backend-rs/src/misc/emoji.rs
new file mode 100644
index 0000000000..df7d33848c
--- /dev/null
+++ b/packages/backend-rs/src/misc/emoji.rs
@@ -0,0 +1,31 @@
+#[inline]
+#[crate::export]
+pub fn is_unicode_emoji(s: &str) -> bool {
+    emojis::get(s).is_some()
+}
+
+#[cfg(test)]
+mod unit_test {
+    use super::is_unicode_emoji;
+
+    #[test]
+    fn test_unicode_emoji_check() {
+        assert!(is_unicode_emoji("⭐"));
+        assert!(is_unicode_emoji("👍"));
+        assert!(is_unicode_emoji("❤"));
+        assert!(is_unicode_emoji("♥️"));
+        assert!(is_unicode_emoji("❤️"));
+        assert!(is_unicode_emoji("💙"));
+        assert!(is_unicode_emoji("🩷"));
+        assert!(is_unicode_emoji("🖖🏿"));
+        assert!(is_unicode_emoji("🏃‍➡️"));
+        assert!(is_unicode_emoji("👩‍❤️‍👨"));
+        assert!(is_unicode_emoji("👩‍👦‍👦"));
+        assert!(is_unicode_emoji("🏳️‍🌈"));
+
+        assert!(!is_unicode_emoji("⭐⭐"));
+        assert!(!is_unicode_emoji("x"));
+        assert!(!is_unicode_emoji("\t"));
+        assert!(!is_unicode_emoji(":meow_aww:"));
+    }
+}
diff --git a/packages/backend-rs/src/misc/escape_sql.rs b/packages/backend-rs/src/misc/escape_sql.rs
new file mode 100644
index 0000000000..c575e088ce
--- /dev/null
+++ b/packages/backend-rs/src/misc/escape_sql.rs
@@ -0,0 +1,36 @@
+#[crate::export]
+pub fn sql_like_escape(src: &str) -> String {
+    src.replace('%', r"\%").replace('_', r"\_")
+}
+
+#[crate::export]
+pub fn safe_for_sql(src: &str) -> bool {
+    !src.contains([
+        '\0', '\x08', '\x09', '\x1a', '\n', '\r', '"', '\'', '\\', '%',
+    ])
+}
+
+#[cfg(test)]
+mod unit_test {
+    use super::{safe_for_sql, sql_like_escape};
+    use pretty_assertions::assert_eq;
+
+    #[test]
+    fn sql_like_escape_test() {
+        assert_eq!(sql_like_escape(""), "");
+        assert_eq!(sql_like_escape("abc"), "abc");
+        assert_eq!(sql_like_escape("a%bc"), r"a\%bc");
+        assert_eq!(sql_like_escape("a呼%吸bc"), r"a呼\%吸bc");
+        assert_eq!(sql_like_escape("a呼%吸b%_c"), r"a呼\%吸b\%\_c");
+        assert_eq!(sql_like_escape("_اللغة العربية"), r"\_اللغة العربية");
+    }
+
+    #[test]
+    fn safe_for_sql_test() {
+        assert!(safe_for_sql("123"));
+        assert!(safe_for_sql("人間"));
+        assert!(!safe_for_sql("人間\x09"));
+        assert!(!safe_for_sql("abc\ndef"));
+        assert!(!safe_for_sql("%something%"));
+    }
+}
diff --git a/packages/backend-rs/src/misc/format_milliseconds.rs b/packages/backend-rs/src/misc/format_milliseconds.rs
new file mode 100644
index 0000000000..dfa8df6f62
--- /dev/null
+++ b/packages/backend-rs/src/misc/format_milliseconds.rs
@@ -0,0 +1,46 @@
+/// Convert milliseconds to a human readable string
+#[crate::export]
+pub fn format_milliseconds(milliseconds: u32) -> String {
+    let mut seconds = milliseconds / 1000;
+    let mut minutes = seconds / 60;
+    let mut hours = minutes / 60;
+    let days = hours / 24;
+
+    seconds %= 60;
+    minutes %= 60;
+    hours %= 24;
+
+    let mut buf: Vec<String> = vec![];
+
+    if days > 0 {
+        buf.push(format!("{} day(s)", days));
+    }
+    if hours > 0 {
+        buf.push(format!("{} hour(s)", hours));
+    }
+    if minutes > 0 {
+        buf.push(format!("{} minute(s)", minutes));
+    }
+    if seconds > 0 {
+        buf.push(format!("{} second(s)", seconds));
+    }
+
+    buf.join(", ")
+}
+
+#[cfg(test)]
+mod unit_test {
+    use super::format_milliseconds;
+    use pretty_assertions::assert_eq;
+
+    #[test]
+    fn format_milliseconds_test() {
+        assert_eq!(format_milliseconds(1000), "1 second(s)");
+        assert_eq!(format_milliseconds(1387938), "23 minute(s), 7 second(s)");
+        assert_eq!(format_milliseconds(34200457), "9 hour(s), 30 minute(s)");
+        assert_eq!(
+            format_milliseconds(998244353),
+            "11 day(s), 13 hour(s), 17 minute(s), 24 second(s)"
+        );
+    }
+}
diff --git a/packages/backend-rs/src/misc/get_note_summary.rs b/packages/backend-rs/src/misc/get_note_summary.rs
new file mode 100644
index 0000000000..3b759b04f5
--- /dev/null
+++ b/packages/backend-rs/src/misc/get_note_summary.rs
@@ -0,0 +1,90 @@
+/// TODO: handle name collisions better
+#[crate::export(object, js_name = "NoteLikeForGetNoteSummary")]
+pub struct NoteLike {
+    pub file_ids: Vec<String>,
+    pub text: Option<String>,
+    pub cw: Option<String>,
+    pub has_poll: bool,
+}
+
+#[crate::export]
+pub fn get_note_summary(note: NoteLike) -> String {
+    let mut buf: Vec<String> = vec![];
+
+    if let Some(cw) = note.cw {
+        buf.push(cw)
+    } else if let Some(text) = note.text {
+        buf.push(text)
+    }
+
+    match note.file_ids.len() {
+        0 => (),
+        1 => buf.push("📎".to_string()),
+        n => buf.push(format!("📎 ({})", n)),
+    };
+
+    if note.has_poll {
+        buf.push("📊".to_string())
+    }
+
+    buf.join(" ")
+}
+
+#[cfg(test)]
+mod unit_test {
+    use super::{get_note_summary, NoteLike};
+    use pretty_assertions::assert_eq;
+
+    #[test]
+    fn test_note_summary() {
+        let note = NoteLike {
+            file_ids: vec![],
+            text: Some("Hello world!".to_string()),
+            cw: None,
+            has_poll: false,
+        };
+        assert_eq!(get_note_summary(note), "Hello world!");
+
+        let note_with_cw = NoteLike {
+            file_ids: vec![],
+            text: Some("Hello world!".to_string()),
+            cw: Some("Content warning".to_string()),
+            has_poll: false,
+        };
+        assert_eq!(get_note_summary(note_with_cw), "Content warning");
+
+        let note_with_file_and_cw = NoteLike {
+            file_ids: vec!["9s7fmcqogiq4igin".to_string()],
+            text: None,
+            cw: Some("Selfie, no ec".to_string()),
+            has_poll: false,
+        };
+        assert_eq!(get_note_summary(note_with_file_and_cw), "Selfie, no ec 📎");
+
+        let note_with_files_only = NoteLike {
+            file_ids: vec![
+                "9s7fmcqogiq4igin".to_string(),
+                "9s7qrld5u14cey98".to_string(),
+                "9s7gebs5zgts4kca".to_string(),
+                "9s5z3e4vefqd29ee".to_string(),
+            ],
+            text: None,
+            cw: None,
+            has_poll: false,
+        };
+        assert_eq!(get_note_summary(note_with_files_only), "📎 (4)");
+
+        let note_all = NoteLike {
+            file_ids: vec![
+                "9s7fmcqogiq4igin".to_string(),
+                "9s7qrld5u14cey98".to_string(),
+                "9s7gebs5zgts4kca".to_string(),
+                "9s5z3e4vefqd29ee".to_string(),
+            ],
+            text: Some("Hello world!".to_string()),
+            cw: Some("Content warning".to_string()),
+            has_poll: true,
+        };
+        assert_eq!(get_note_summary(note_all), "Content warning 📎 (4) 📊");
+    }
+}
diff --git a/packages/backend-rs/src/misc/meta.rs b/packages/backend-rs/src/misc/meta.rs
new file mode 100644
index 0000000000..5aed617038
--- /dev/null
+++ b/packages/backend-rs/src/misc/meta.rs
@@ -0,0 +1,83 @@
+use crate::database::db_conn;
+use crate::model::entity::meta;
+use rand::prelude::*;
+use sea_orm::{prelude::*, ActiveValue};
+use std::sync::Mutex;
+
+type Meta = meta::Model;
+
+static CACHE: Mutex<Option<Meta>> = Mutex::new(None);
+fn update_cache(meta: &Meta) {
+    let _ = CACHE.lock().map(|mut cache| *cache = Some(meta.clone()));
+}
+
+#[crate::export]
+pub async fn fetch_meta(use_cache: bool) -> Result<Meta, DbErr> {
+    // try using cache
+    if use_cache {
+        if let Some(cache) = CACHE.lock().ok().and_then(|cache| cache.clone()) {
+            return Ok(cache);
+        }
+    }
+
+    // try fetching from db
+    let db = db_conn().await?;
+    let meta = meta::Entity::find().one(db).await?;
+    if let Some(meta) = meta {
+        update_cache(&meta);
+        return Ok(meta);
+    }
+
+    // create a new meta object and insert into db
+    let meta = meta::Entity::insert(meta::ActiveModel {
+        id: ActiveValue::Set("x".to_owned()),
+        ..Default::default()
+    })
+    .exec_with_returning(db)
+    .await?;
+    update_cache(&meta);
+    Ok(meta)
+}
+
+#[crate::export(object)]
+pub struct PugArgs {
+    pub img: Option<String>,
+    pub title: String,
+    pub instance_name: String,
+    pub desc: Option<String>,
+    pub icon: Option<String>,
+    pub splash_icon: Option<String>,
+    pub theme_color: Option<String>,
+    pub random_motd: String,
+    pub private_mode: Option<bool>,
+}
+
+#[crate::export]
+pub fn meta_to_pug_args(meta: Meta) -> PugArgs {
+    let mut rng = rand::thread_rng();
+
+    let splash_icon = meta
+        .custom_splash_icons
+        .choose(&mut rng)
+        .map(|s| s.to_owned())
+        .or_else(|| meta.icon_url.to_owned());
+
+    let random_motd = meta
+        .custom_motd
+        .choose(&mut rng)
+        .map(|s| s.to_owned())
+        .unwrap_or_else(|| "Loading...".to_owned());
+
+    let name = meta.name.unwrap_or_else(|| "Firefish".to_owned());
+    PugArgs {
+        img: meta.banner_url,
+        title: name.clone(),
+        instance_name: name.clone(),
+        desc: meta.description,
+        icon: meta.icon_url,
+        splash_icon,
+        theme_color: meta.theme_color,
+        random_motd,
+        private_mode: meta.private_mode,
+    }
+}
diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs
index 701e35a4eb..a9d7074dbf 100644
--- a/packages/backend-rs/src/misc/mod.rs
+++ b/packages/backend-rs/src/misc/mod.rs
@@ -1,5 +1,12 @@
 pub mod acct;
 pub mod check_word_mute;
 pub mod convert_host;
+pub mod emoji;
+pub mod escape_sql;
+pub mod format_milliseconds;
+pub mod get_note_summary;
 pub mod mastodon_id;
+pub mod meta;
 pub mod nyaify;
+pub mod password;
+pub mod reaction;
diff --git a/packages/backend-rs/src/misc/password.rs b/packages/backend-rs/src/misc/password.rs
new file mode 100644
index 0000000000..b21ff73499
--- /dev/null
+++ b/packages/backend-rs/src/misc/password.rs
@@ -0,0 +1,69 @@
+use argon2::{
+    password_hash,
+    password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
+    Argon2,
+};
+
+#[crate::export]
+pub fn hash_password(password: &str) -> Result<String, password_hash::errors::Error> {
+    let salt = SaltString::generate(&mut OsRng);
+    Ok(Argon2::default()
+        .hash_password(password.as_bytes(), &salt)?
+        .to_string())
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum VerifyError {
+    #[error("An error occured while bcrypt verification: {0}")]
+    BcryptError(#[from] bcrypt::BcryptError),
+    #[error("Invalid argon2 password hash: {0}")]
+    InvalidArgon2Hash(#[from] password_hash::Error),
+    #[error("An error occured while argon2 verification: {0}")]
+    Argon2Error(#[from] argon2::Error),
+}
+
+#[crate::export]
+pub fn verify_password(password: &str, hash: &str) -> Result<bool, VerifyError> {
+    if is_old_password_algorithm(hash) {
+        Ok(bcrypt::verify(password, hash)?)
+    } else {
+        let parsed_hash = PasswordHash::new(hash)?;
+        Ok(Argon2::default()
+            .verify_password(password.as_bytes(), &parsed_hash)
+            .is_ok())
+    }
+}
+
+#[inline]
+#[crate::export]
+pub fn is_old_password_algorithm(hash: &str) -> bool {
+    // bcrypt hashes start with $2[ab]$
+    hash.starts_with("$2")
+}
+
+#[cfg(test)]
+mod unit_test {
+    use super::{hash_password, is_old_password_algorithm, verify_password};
+
+    #[test]
+    fn verify_password_test() {
+        let password = "omWc*%sD^fn7o2cXmc9e2QasBdrbRuhNB*gx!J5";
+
+        let hash = hash_password(password).unwrap();
+        assert!(verify_password(password, hash.as_str()).unwrap());
+
+        let argon2_hash = "$argon2id$v=19$m=19456,t=2,p=1$jty3puDFd4ENv/lgHn3ROQ$kRHDdEoVv2rruvnF731E74NxnYlvj5FMgePdGIIq3Jk";
+        let argon2_invalid_hash = "$argon2id$v=19$m=19456,t=2,p=1$jty3puDFd4ENv/lgHn3ROQ$kRHDdEoVv2rruvnF731E74NxnYlvj4FMgePdGIIq3Jk";
+        let bcrypt_hash = "$2a$12$WzUc.20jgbHmQjUMqTr8vOhKqYbS1BUvubapv/GLjCK1IN.h4e4la";
+        let bcrypt_invalid_hash = "$2a$12$WzUc.20jgbHmQjUMqTr7vOhKqYbS1BUvubapv/GLjCK1IN.h4e4la";
+
+        assert!(!is_old_password_algorithm(argon2_hash));
+        assert!(is_old_password_algorithm(bcrypt_hash));
+
+        assert!(verify_password(password, argon2_hash).unwrap());
+        assert!(verify_password(password, bcrypt_hash).unwrap());
+
+        assert!(!verify_password(password, argon2_invalid_hash).unwrap());
+        assert!(!verify_password(password, bcrypt_invalid_hash).unwrap());
+    }
+}
diff --git a/packages/backend-rs/src/misc/reaction.rs b/packages/backend-rs/src/misc/reaction.rs
new file mode 100644
index 0000000000..a29ddf95de
--- /dev/null
+++ b/packages/backend-rs/src/misc/reaction.rs
@@ -0,0 +1,191 @@
+use crate::database::db_conn;
+use crate::misc::{convert_host::to_puny, emoji::is_unicode_emoji, meta::fetch_meta};
+use crate::model::entity::emoji;
+use once_cell::sync::Lazy;
+use regex::Regex;
+use sea_orm::prelude::*;
+use std::collections::HashMap;
+
+#[derive(PartialEq, Debug)]
+#[crate::export(object)]
+pub struct DecodedReaction {
+    pub reaction: String,
+    pub name: Option<String>,
+    pub host: Option<String>,
+}
+
+#[crate::export]
+pub fn decode_reaction(reaction: &str) -> DecodedReaction {
+    // Misskey allows you to include "+" and "-" in emoji shortcodes
+    // MFM spec: https://github.com/misskey-dev/mfm.js/blob/6aaf68089023c6adebe44123eebbc4dcd75955e0/docs/syntax.md?plain=1#L583
+    // Misskey's implementation: https://github.com/misskey-dev/misskey/blob/bba3097765317cbf95d09627961b5b5dce16a972/packages/backend/src/core/ReactionService.ts#L68
+    static RE: Lazy<Regex> =
+        Lazy::new(|| Regex::new(r"^:([0-9A-Za-z_+-]+)(?:@([0-9A-Za-z_.-]+))?:$").unwrap());
+
+    if let Some(captures) = RE.captures(reaction) {
+        let name = &captures[1];
+        let host = captures.get(2).map(|s| s.as_str());
+
+        DecodedReaction {
+            reaction: format!(":{}@{}:", name, host.unwrap_or(".")),
+            name: Some(name.to_owned()),
+            host: host.map(|s| s.to_owned()),
+        }
+    } else {
+        DecodedReaction {
+            reaction: reaction.to_owned(),
+            name: None,
+            host: None,
+        }
+    }
+}
+
+#[crate::export]
+pub fn count_reactions(reactions: &HashMap<String, u32>) -> HashMap<String, u32> {
+    let mut res = HashMap::<String, u32>::new();
+
+    for (reaction, count) in reactions.iter() {
+        if count > &0 {
+            let decoded = decode_reaction(reaction).reaction;
+            let total = res.entry(decoded).or_insert(0);
+            *total += count;
+        }
+    }
+
+    res
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+    #[error("Idna error: {0}")]
+    IdnaError(#[from] idna::Errors),
+    #[error("Database error: {0}")]
+    DbError(#[from] DbErr),
+}
+
+#[crate::export]
+pub async fn to_db_reaction(reaction: Option<&str>, host: Option<&str>) -> Result<String, Error> {
+    if let Some(reaction) = reaction {
+        // FIXME: Is it okay to do this only here?
+        // This was introduced in https://firefish.dev/firefish/firefish/-/commit/af730e75b6fc1a57ca680ce83459d7e433b130cf
+        if reaction.contains('❤') || reaction.contains("♥️") {
+            return Ok("❤️".to_owned());
+        }
+
+        if is_unicode_emoji(reaction) {
+            return Ok(reaction.to_owned());
+        }
+
+        static RE: Lazy<Regex> =
+            Lazy::new(|| Regex::new(r"^:([0-9A-Za-z_+-]+)(?:@\.)?:$").unwrap());
+
+        if let Some(captures) = RE.captures(reaction) {
+            let name = &captures[1];
+            let db = db_conn().await?;
+
+            if let Some(host) = host {
+                // remote emoji
+                let ascii_host = to_puny(host)?;
+
+                // TODO: Does SeaORM have the `exists` method?
+                if emoji::Entity::find()
+                    .filter(emoji::Column::Name.eq(name))
+                    .filter(emoji::Column::Host.eq(&ascii_host))
+                    .one(db)
+                    .await?
+                    .is_some()
+                {
+                    return Ok(format!(":{name}@{ascii_host}:"));
+                }
+            } else {
+                // local emoji
+                // TODO: Does SeaORM have the `exists` method?
+                if emoji::Entity::find()
+                    .filter(emoji::Column::Name.eq(name))
+                    .filter(emoji::Column::Host.is_null())
+                    .one(db)
+                    .await?
+                    .is_some()
+                {
+                    return Ok(format!(":{name}:"));
+                }
+            }
+        };
+    };
+
+    Ok(fetch_meta(true).await?.default_reaction)
+}
+
+#[cfg(test)]
+mod unit_test {
+    use super::{decode_reaction, DecodedReaction};
+    use pretty_assertions::{assert_eq, assert_ne};
+
+    #[test]
+    fn test_decode_reaction() {
+        let unicode_emoji_1 = DecodedReaction {
+            reaction: "⭐".to_string(),
+            name: None,
+            host: None,
+        };
+        let unicode_emoji_2 = DecodedReaction {
+            reaction: "🩷".to_string(),
+            name: None,
+            host: None,
+        };
+
+        assert_eq!(decode_reaction("⭐"), unicode_emoji_1);
+        assert_eq!(decode_reaction("🩷"), unicode_emoji_2);
+
+        assert_ne!(decode_reaction("⭐"), unicode_emoji_2);
+        assert_ne!(decode_reaction("🩷"), unicode_emoji_1);
+
+        let unicode_emoji_3 = DecodedReaction {
+            reaction: "🖖🏿".to_string(),
+            name: None,
+            host: None,
+        };
+        assert_eq!(decode_reaction("🖖🏿"), unicode_emoji_3);
+
+        let local_emoji = DecodedReaction {
+            reaction: ":meow_melt_tears@.:".to_string(),
+            name: Some("meow_melt_tears".to_string()),
+            host: None,
+        };
+        assert_eq!(decode_reaction(":meow_melt_tears:"), local_emoji);
+
+        let remote_emoji_1 = DecodedReaction {
+            reaction: ":meow_uwu@some-domain.example.org:".to_string(),
+            name: Some("meow_uwu".to_string()),
+            host: Some("some-domain.example.org".to_string()),
+        };
+        assert_eq!(
+            decode_reaction(":meow_uwu@some-domain.example.org:"),
+            remote_emoji_1
+        );
+
+        let remote_emoji_2 = DecodedReaction {
+            reaction: ":C++23@xn--eckwd4c7c.example.org:".to_string(),
+            name: Some("C++23".to_string()),
+            host: Some("xn--eckwd4c7c.example.org".to_string()),
+        };
+        assert_eq!(
+            decode_reaction(":C++23@xn--eckwd4c7c.example.org:"),
+            remote_emoji_2
+        );
+
+        let invalid_reaction_1 = DecodedReaction {
+            reaction: ":foo".to_string(),
+            name: None,
+            host: None,
+        };
+        assert_eq!(decode_reaction(":foo"), invalid_reaction_1);
+
+        let invalid_reaction_2 = DecodedReaction {
+            reaction: ":foo&@example.com:".to_string(),
+            name: None,
+            host: None,
+        };
+        assert_eq!(decode_reaction(":foo&@example.com:"), invalid_reaction_2);
+    }
+}
diff --git a/packages/backend-rs/src/model/entity/drive_file.rs b/packages/backend-rs/src/model/entity/drive_file.rs
index e3e4622a62..a6926e7af2 100644
--- a/packages/backend-rs/src/model/entity/drive_file.rs
+++ b/packages/backend-rs/src/model/entity/drive_file.rs
@@ -1,5 +1,6 @@
 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15
 
+use super::sea_orm_active_enums::DriveFileUsageHintEnum;
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
@@ -52,6 +53,8 @@ pub struct Model {
     pub request_headers: Option<Json>,
     #[sea_orm(column_name = "requestIp")]
     pub request_ip: Option<String>,
+    #[sea_orm(column_name = "usageHint")]
+    pub usage_hint: Option<DriveFileUsageHintEnum>,
 }
 
 #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
diff --git a/packages/backend-rs/src/model/entity/note.rs b/packages/backend-rs/src/model/entity/note.rs
index cb82f3d94a..5903216c1a 100644
--- a/packages/backend-rs/src/model/entity/note.rs
+++ b/packages/backend-rs/src/model/entity/note.rs
@@ -21,6 +21,7 @@ pub struct Model {
     #[sea_orm(column_type = "Text", nullable)]
     pub text: Option<String>,
     pub name: Option<String>,
+    #[sea_orm(column_type = "Text", nullable)]
     pub cw: Option<String>,
     #[sea_orm(column_name = "userId")]
     pub user_id: String,
diff --git a/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs b/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs
index 861b3a18d0..36281f4dc5 100644
--- a/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs
+++ b/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs
@@ -4,7 +4,7 @@ use sea_orm::entity::prelude::*;
 
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
-#[cfg_attr(feature = "napi", napi_derive::napi)]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "antenna_src_enum")]
 pub enum AntennaSrcEnum {
     #[sea_orm(string_value = "all")]
@@ -22,7 +22,21 @@ pub enum AntennaSrcEnum {
 }
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
-#[cfg_attr(feature = "napi", napi_derive::napi)]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
+#[sea_orm(
+    rs_type = "String",
+    db_type = "Enum",
+    enum_name = "drive_file_usage_hint_enum"
+)]
+pub enum DriveFileUsageHintEnum {
+    #[sea_orm(string_value = "userAvatar")]
+    UserAvatar,
+    #[sea_orm(string_value = "userBanner")]
+    UserBanner,
+}
+#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
+#[cfg_attr(not(feature = "napi"), derive(Clone))]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(
     rs_type = "String",
     db_type = "Enum",
@@ -40,7 +54,7 @@ pub enum MutedNoteReasonEnum {
 }
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
-#[cfg_attr(feature = "napi", napi_derive::napi)]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(
     rs_type = "String",
     db_type = "Enum",
@@ -60,7 +74,7 @@ pub enum NoteVisibilityEnum {
 }
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
-#[cfg_attr(feature = "napi", napi_derive::napi)]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(
     rs_type = "String",
     db_type = "Enum",
@@ -94,7 +108,7 @@ pub enum NotificationTypeEnum {
 }
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
-#[cfg_attr(feature = "napi", napi_derive::napi)]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(
     rs_type = "String",
     db_type = "Enum",
@@ -110,7 +124,7 @@ pub enum PageVisibilityEnum {
 }
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
-#[cfg_attr(feature = "napi", napi_derive::napi)]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(
     rs_type = "String",
     db_type = "Enum",
@@ -128,7 +142,7 @@ pub enum PollNotevisibilityEnum {
 }
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
-#[cfg_attr(feature = "napi", napi_derive::napi)]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "relay_status_enum")]
 pub enum RelayStatusEnum {
     #[sea_orm(string_value = "accepted")]
@@ -140,7 +154,7 @@ pub enum RelayStatusEnum {
 }
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
-#[cfg_attr(feature = "napi", napi_derive::napi)]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(
     rs_type = "String",
     db_type = "Enum",
@@ -158,7 +172,7 @@ pub enum UserEmojimodpermEnum {
 }
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
-#[cfg_attr(feature = "napi", napi_derive::napi)]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(
     rs_type = "String",
     db_type = "Enum",
@@ -174,7 +188,7 @@ pub enum UserProfileFfvisibilityEnum {
 }
 #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
 #[cfg_attr(not(feature = "napi"), derive(Clone))]
-#[cfg_attr(feature = "napi", napi_derive::napi)]
+#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
 #[sea_orm(
     rs_type = "String",
     db_type = "Enum",
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 5382f1d361..9289c2f7ea 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -22,9 +22,9 @@
 		"@swc/core-android-arm64": "1.3.11"
 	},
 	"dependencies": {
-		"@bull-board/api": "5.15.3",
-		"@bull-board/koa": "5.15.3",
-		"@bull-board/ui": "5.15.3",
+		"@bull-board/api": "5.15.5",
+		"@bull-board/koa": "5.15.5",
+		"@bull-board/ui": "5.15.5",
 		"@discordapp/twemoji": "^15.0.3",
 		"@koa/cors": "5.0.0",
 		"@koa/multer": "3.0.2",
@@ -33,15 +33,12 @@
 		"@peertube/http-signature": "1.7.0",
 		"@redocly/openapi-core": "1.11.0",
 		"@sinonjs/fake-timers": "11.2.2",
-		"@twemoji/parser": "^15.1.1",
 		"adm-zip": "0.5.10",
 		"ajv": "8.12.0",
 		"archiver": "7.0.1",
-		"argon2": "^0.40.1",
-		"aws-sdk": "2.1597.0",
+		"aws-sdk": "2.1599.0",
 		"axios": "^1.6.8",
 		"backend-rs": "workspace:*",
-		"bcryptjs": "2.4.3",
 		"blurhash": "2.0.5",
 		"bull": "4.12.2",
 		"cacheable-lookup": "TheEssem/cacheable-lookup",
@@ -54,7 +51,7 @@
 		"date-fns": "3.6.0",
 		"decompress": "^4.2.1",
 		"deep-email-validator": "0.1.21",
-		"deepl-node": "1.12.0",
+		"deepl-node": "1.13.0",
 		"escape-regexp": "0.0.1",
 		"feed": "4.2.2",
 		"file-type": "19.0.0",
@@ -100,7 +97,7 @@
 		"punycode": "2.3.1",
 		"pureimage": "0.4.13",
 		"qrcode": "1.5.3",
-		"qs": "6.12.0",
+		"qs": "6.12.1",
 		"random-seed": "0.3.0",
 		"ratelimiter": "3.4.1",
 		"redis-semaphore": "5.5.1",
@@ -130,7 +127,6 @@
 		"@swc/cli": "0.3.12",
 		"@swc/core": "1.4.13",
 		"@types/adm-zip": "^0.5.5",
-		"@types/bcryptjs": "2.4.6",
 		"@types/color-convert": "^2.0.3",
 		"@types/content-disposition": "^0.5.8",
 		"@types/escape-regexp": "0.0.3",
diff --git a/packages/backend/src/boot/index.ts b/packages/backend/src/boot/index.ts
index 8caf7d062e..9854d2dce4 100644
--- a/packages/backend/src/boot/index.ts
+++ b/packages/backend/src/boot/index.ts
@@ -3,7 +3,7 @@ import chalk from "chalk";
 import Xev from "xev";
 
 import Logger from "@/services/logger.js";
-import { envOption } from "../env.js";
+import { envOption } from "@/config/index.js";
 import { inspect } from "node:util";
 
 // for typeorm
@@ -76,9 +76,7 @@ cluster.on("exit", (worker) => {
 });
 
 // Display detail of unhandled promise rejection
-if (!envOption.quiet) {
-	process.on("unhandledRejection", console.dir);
-}
+process.on("unhandledRejection", console.dir);
 
 // Display detail of uncaught exception
 process.on("uncaughtException", (err) => {
diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts
index e54a2889e2..3ba2d0cf50 100644
--- a/packages/backend/src/boot/master.ts
+++ b/packages/backend/src/boot/master.ts
@@ -10,7 +10,7 @@ import semver from "semver";
 import Logger from "@/services/logger.js";
 import loadConfig from "@/config/load.js";
 import type { Config } from "@/config/types.js";
-import { envOption } from "@/env.js";
+import { envOption } from "@/config/index.js";
 import { showMachineInfo } from "@/misc/show-machine-info.js";
 import { db, initDb } from "@/db/postgre.js";
 import { inspect } from "node:util";
@@ -28,58 +28,56 @@ const bootLogger = logger.createSubLogger("boot", "magenta", false);
 const themeColor = chalk.hex("#31748f");
 
 function greet() {
-	if (!envOption.quiet) {
-		//#region Firefish logo
-		console.log(
-			themeColor(
-				"██████╗ ██╗██████╗ ███████╗███████╗██╗███████╗██╗  ██╗    ○     ▄    ▄    ",
-			),
-		);
-		console.log(
-			themeColor(
-				"██╔════╝██║██╔══██╗██╔════╝██╔════╝██║██╔════╝██║  ██║      ⚬   █▄▄  █▄▄ ",
-			),
-		);
-		console.log(
-			themeColor(
-				"█████╗  ██║██████╔╝█████╗  █████╗  ██║███████╗███████║      ▄▄▄▄▄▄   ▄    ",
-			),
-		);
-		console.log(
-			themeColor(
-				"██╔══╝  ██║██╔══██╗██╔══╝  ██╔══╝  ██║╚════██║██╔══██║     █      █  █▄▄  ",
-			),
-		);
-		console.log(
-			themeColor(
-				"██║     ██║██║  ██║███████╗██║     ██║███████║██║  ██║     █ ● ●  █       ",
-			),
-		);
-		console.log(
-			themeColor(
-				"╚═╝     ╚═╝╚═╝  ╚═╝╚══════╝╚═╝     ╚═╝╚══════╝╚═╝  ╚═╝     ▀▄▄▄▄▄▄▀       ",
-			),
-		);
-		//#endregion
+	//#region Firefish logo
+	console.log(
+		themeColor(
+			"██████╗ ██╗██████╗ ███████╗███████╗██╗███████╗██╗  ██╗    ○     ▄    ▄    ",
+		),
+	);
+	console.log(
+		themeColor(
+			"██╔════╝██║██╔══██╗██╔════╝██╔════╝██║██╔════╝██║  ██║      ⚬   █▄▄  █▄▄ ",
+		),
+	);
+	console.log(
+		themeColor(
+			"█████╗  ██║██████╔╝█████╗  █████╗  ██║███████╗███████║      ▄▄▄▄▄▄   ▄    ",
+		),
+	);
+	console.log(
+		themeColor(
+			"██╔══╝  ██║██╔══██╗██╔══╝  ██╔══╝  ██║╚════██║██╔══██║     █      █  █▄▄  ",
+		),
+	);
+	console.log(
+		themeColor(
+			"██║     ██║██║  ██║███████╗██║     ██║███████║██║  ██║     █ ● ●  █       ",
+		),
+	);
+	console.log(
+		themeColor(
+			"╚═╝     ╚═╝╚═╝  ╚═╝╚══════╝╚═╝     ╚═╝╚══════╝╚═╝  ╚═╝     ▀▄▄▄▄▄▄▀       ",
+		),
+	);
+	//#endregion
 
-		console.log(
-			" Firefish is an open-source decentralized microblogging platform.",
-		);
-		console.log(
-			chalk.rgb(
-				255,
-				136,
-				0,
-			)(
-				" If you like Firefish, please consider contributing to the repo. https://firefish.dev/firefish/firefish",
-			),
-		);
+	console.log(
+		" Firefish is an open-source decentralized microblogging platform.",
+	);
+	console.log(
+		chalk.rgb(
+			255,
+			136,
+			0,
+		)(
+			" If you like Firefish, please consider contributing to the repo. https://firefish.dev/firefish/firefish",
+		),
+	);
 
-		console.log("");
-		console.log(
-			chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`,
-		);
-	}
+	console.log("");
+	console.log(
+		chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`,
+	);
 
 	bootLogger.info("Welcome to Firefish!");
 	bootLogger.info(`Firefish v${meta.version}`, null, true);
diff --git a/packages/backend/src/config/index.ts b/packages/backend/src/config/index.ts
index ae197b09ca..fe87e5026a 100644
--- a/packages/backend/src/config/index.ts
+++ b/packages/backend/src/config/index.ts
@@ -1,3 +1,5 @@
 import load from "./load.js";
+import { readEnvironmentConfig } from "backend-rs";
 
 export default load();
+export const envOption = readEnvironmentConfig();
diff --git a/packages/backend/src/config/load.ts b/packages/backend/src/config/load.ts
index 2b286b9439..682bf309d2 100644
--- a/packages/backend/src/config/load.ts
+++ b/packages/backend/src/config/load.ts
@@ -55,6 +55,7 @@ export default function load() {
 	mixin.userAgent = `Firefish/${meta.version} (${config.url})`;
 	mixin.clientEntry = clientManifest["src/init.ts"];
 
+	if (config.proxyRemoteFiles == null) config.proxyRemoteFiles = true;
 	if (!config.redis.prefix) config.redis.prefix = mixin.hostname;
 	if (config.cacheServer && !config.cacheServer.prefix)
 		config.cacheServer.prefix = mixin.hostname;
diff --git a/packages/backend/src/daemons/server-stats.ts b/packages/backend/src/daemons/server-stats.ts
index 58a9b1491b..df1f9b3032 100644
--- a/packages/backend/src/daemons/server-stats.ts
+++ b/packages/backend/src/daemons/server-stats.ts
@@ -1,7 +1,7 @@
 import si from "systeminformation";
 import Xev from "xev";
 import * as osUtils from "os-utils";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 
 const ev = new Xev();
 
@@ -13,16 +13,15 @@ const round = (num: number) => Math.round(num * 10) / 10;
 /**
  * Report server stats regularly
  */
-export default function () {
+export default async function () {
 	const log = [] as any[];
 
 	ev.on("requestServerStatsLog", (x) => {
 		ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length || 50));
 	});
 
-	fetchMeta().then((meta) => {
-		if (!meta.enableServerMachineStats) return;
-	});
+	const meta = await fetchMeta(true);
+	if (!meta.enableServerMachineStats) return;
 
 	async function tick() {
 		const cpu = await cpuUsage();
diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts
index 19f7e7816a..6baccaa271 100644
--- a/packages/backend/src/db/postgre.ts
+++ b/packages/backend/src/db/postgre.ts
@@ -237,31 +237,3 @@ export async function initDb(force = false) {
 		await db.initialize();
 	}
 }
-
-export async function resetDb() {
-	const reset = async () => {
-		await redisClient.flushdb();
-		const tables = await db.query(`SELECT relname AS "table"
-		FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
-		WHERE nspname NOT IN ('pg_catalog', 'information_schema')
-			AND C.relkind = 'r'
-			AND nspname !~ '^pg_toast';`);
-		for (const table of tables) {
-			await db.query(`DELETE FROM "${table.table}" CASCADE`);
-		}
-	};
-
-	for (let i = 1; i <= 3; i++) {
-		try {
-			await reset();
-		} catch (e) {
-			if (i === 3) {
-				throw e;
-			} else {
-				await new Promise((resolve) => setTimeout(resolve, 1000));
-				continue;
-			}
-		}
-		break;
-	}
-}
diff --git a/packages/backend/src/env.ts b/packages/backend/src/env.ts
deleted file mode 100644
index a788a0fba2..0000000000
--- a/packages/backend/src/env.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-const envOption = {
-	onlyQueue: false,
-	onlyServer: false,
-	noDaemons: false,
-	disableClustering: false,
-	verbose: false,
-	withLogTime: false,
-	quiet: false,
-	slow: false,
-};
-
-for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) {
-	if (
-		process.env[
-			`MK_${key.replace(/[A-Z]/g, (letter) => `_${letter}`).toUpperCase()}`
-		]
-	)
-		envOption[key] = true;
-}
-
-if (process.env.NODE_ENV === "test") envOption.disableClustering = true;
-if (process.env.NODE_ENV === "test") envOption.quiet = true;
-if (process.env.NODE_ENV === "test") envOption.noDaemons = true;
-
-export { envOption };
diff --git a/packages/backend/src/migration/1713225866247-convert-cw-varchar-to-text.ts b/packages/backend/src/migration/1713225866247-convert-cw-varchar-to-text.ts
new file mode 100644
index 0000000000..93c87a98a8
--- /dev/null
+++ b/packages/backend/src/migration/1713225866247-convert-cw-varchar-to-text.ts
@@ -0,0 +1,21 @@
+import type { MigrationInterface, QueryRunner } from "typeorm";
+
+export class ConvertCwVarcharToText1713225866247 implements MigrationInterface {
+	public async up(queryRunner: QueryRunner): Promise<void> {
+		queryRunner.query(`DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f"`);
+		queryRunner.query(`ALTER TABLE "note" ALTER COLUMN "cw" TYPE text`);
+		queryRunner.query(
+			`CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw")`,
+		);
+	}
+
+	public async down(queryRunner: QueryRunner): Promise<void> {
+		queryRunner.query(`DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f"`);
+		queryRunner.query(
+			`ALTER TABLE "note" ALTER COLUMN "cw" TYPE character varying(512)`,
+		);
+		queryRunner.query(
+			`CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw" pgroonga_varchar_full_text_search_ops_v2)`,
+		);
+	}
+}
diff --git a/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts b/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts
new file mode 100644
index 0000000000..3bdb1aafc8
--- /dev/null
+++ b/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts
@@ -0,0 +1,17 @@
+import type { MigrationInterface, QueryRunner } from "typeorm";
+
+export class AddDriveFileUsage1713451569342 implements MigrationInterface {
+	public async up(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(
+			`CREATE TYPE drive_file_usage_hint_enum AS ENUM ('userAvatar', 'userBanner')`,
+		);
+		await queryRunner.query(
+			`ALTER TABLE "drive_file" ADD "usageHint" drive_file_usage_hint_enum DEFAULT NULL`,
+		);
+	}
+
+	public async down(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "usageHint"`);
+		await queryRunner.query(`DROP TYPE drive_file_usage_hint_enum`);
+	}
+}
diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts
index 30a50e5714..e99b17a5f7 100644
--- a/packages/backend/src/misc/cache.ts
+++ b/packages/backend/src/misc/cache.ts
@@ -1,6 +1,6 @@
 import { redisClient } from "@/db/redis.js";
 import { encode, decode } from "msgpackr";
-import { ChainableCommander } from "ioredis";
+import type { ChainableCommander } from "ioredis";
 
 export class Cache<T> {
 	private ttl: number;
diff --git a/packages/backend/src/misc/convert-milliseconds.ts b/packages/backend/src/misc/convert-milliseconds.ts
deleted file mode 100644
index d8c163ffda..0000000000
--- a/packages/backend/src/misc/convert-milliseconds.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-export function convertMilliseconds(ms: number) {
-	let seconds = Math.round(ms / 1000);
-	let minutes = Math.round(seconds / 60);
-	let hours = Math.round(minutes / 60);
-	const days = Math.round(hours / 24);
-	seconds %= 60;
-	minutes %= 60;
-	hours %= 24;
-
-	const result = [];
-	if (days > 0) result.push(`${days} day(s)`);
-	if (hours > 0) result.push(`${hours} hour(s)`);
-	if (minutes > 0) result.push(`${minutes} minute(s)`);
-	if (seconds > 0) result.push(`${seconds} second(s)`);
-
-	return result.join(", ");
-}
diff --git a/packages/backend/src/misc/emoji-regex.ts b/packages/backend/src/misc/emoji-regex.ts
deleted file mode 100644
index 72d6a62d9a..0000000000
--- a/packages/backend/src/misc/emoji-regex.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import twemoji from "@twemoji/parser/dist/lib/regex.js";
-const twemojiRegex = twemoji.default;
-
-export const emojiRegex = new RegExp(`(${twemojiRegex.source})`);
-export const emojiRegexAtStartToEnd = new RegExp(`^(${twemojiRegex.source})$`);
diff --git a/packages/backend/src/misc/fetch-meta.ts b/packages/backend/src/misc/fetch-meta.ts
deleted file mode 100644
index fdc978b5a3..0000000000
--- a/packages/backend/src/misc/fetch-meta.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { db } from "@/db/postgre.js";
-import { Meta } from "@/models/entities/meta.js";
-
-let cache: Meta;
-
-export function metaToPugArgs(meta: Meta): object {
-	let motd = ["Loading..."];
-	if (meta.customMotd.length > 0) {
-		motd = meta.customMotd;
-	}
-	let splashIconUrl = meta.iconUrl;
-	if (meta.customSplashIcons.length > 0) {
-		splashIconUrl =
-			meta.customSplashIcons[
-				Math.floor(Math.random() * meta.customSplashIcons.length)
-			];
-	}
-
-	return {
-		img: meta.bannerUrl,
-		title: meta.name || "Firefish",
-		instanceName: meta.name || "Firefish",
-		desc: meta.description,
-		icon: meta.iconUrl,
-		splashIcon: splashIconUrl,
-		themeColor: meta.themeColor,
-		randomMOTD: motd[Math.floor(Math.random() * motd.length)],
-		privateMode: meta.privateMode,
-	};
-}
-
-export async function fetchMeta(noCache = false): Promise<Meta> {
-	if (!noCache && cache) return cache;
-
-	return await db.transaction(async (transactionalEntityManager) => {
-		// New IDs are prioritized because multiple records may have been created due to past bugs.
-		const metas = await transactionalEntityManager.find(Meta, {
-			order: {
-				id: "DESC",
-			},
-		});
-
-		const meta = metas[0];
-
-		if (meta) {
-			cache = meta;
-			return meta;
-		} else {
-			// If fetchMeta is called at the same time when meta is empty, this part may be called at the same time, so use fail-safe upsert.
-			const saved = await transactionalEntityManager
-				.upsert(
-					Meta,
-					{
-						id: "x",
-					},
-					["id"],
-				)
-				.then((x) =>
-					transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0]),
-				);
-
-			cache = saved;
-			return saved;
-		}
-	});
-}
-
-setInterval(() => {
-	fetchMeta(true).then((meta) => {
-		cache = meta;
-	});
-}, 1000 * 10);
diff --git a/packages/backend/src/misc/fetch-proxy-account.ts b/packages/backend/src/misc/fetch-proxy-account.ts
index a277db6fb9..8d015da25d 100644
--- a/packages/backend/src/misc/fetch-proxy-account.ts
+++ b/packages/backend/src/misc/fetch-proxy-account.ts
@@ -1,9 +1,9 @@
-import { fetchMeta } from "./fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import type { ILocalUser } from "@/models/entities/user.js";
 import { Users } from "@/models/index.js";
 
 export async function fetchProxyAccount(): Promise<ILocalUser | null> {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.proxyAccountId == null) return null;
 	return (await Users.findOneByOrFail({
 		id: meta.proxyAccountId,
diff --git a/packages/backend/src/misc/get-note-summary.ts b/packages/backend/src/misc/get-note-summary.ts
deleted file mode 100644
index 0a662e434e..0000000000
--- a/packages/backend/src/misc/get-note-summary.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import type { Packed } from "./schema.js";
-
-/**
- * 投稿を表す文字列を取得します。
- * @param {*} note (packされた)投稿
- */
-export const getNoteSummary = (note: Packed<"Note">): string => {
-	if (note.deletedAt) {
-		return "❌";
-	}
-
-	let summary = "";
-
-	// 本文
-	if (note.cw != null) {
-		summary += note.cw;
-	} else {
-		summary += note.text ? note.text : "";
-	}
-
-	// ファイルが添付されているとき
-	if ((note.files || []).length !== 0) {
-		const len = note.files?.length;
-		summary += ` 📎${len !== 1 ? ` (${len})` : ""}`;
-	}
-
-	// 投票が添付されているとき
-	if (note.poll) {
-		summary += " 📊";
-	}
-
-	/*
-	// 返信のとき
-	if (note.replyId) {
-		if (note.reply) {
-			summary += `\n\nRE: ${getNoteSummary(note.reply)}`;
-		} else {
-			summary += '\n\nRE: ...';
-		}
-	}
-
-	// Renoteのとき
-	if (note.renoteId) {
-		if (note.renote) {
-			summary += `\n\nRN: ${getNoteSummary(note.renote)}`;
-		} else {
-			summary += '\n\nRN: ...';
-		}
-	}
-	*/
-
-	return summary.trim();
-};
diff --git a/packages/backend/src/misc/password.ts b/packages/backend/src/misc/password.ts
deleted file mode 100644
index c63f89f5c9..0000000000
--- a/packages/backend/src/misc/password.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import bcrypt from "bcryptjs";
-import * as argon2 from "argon2";
-
-export async function hashPassword(password: string): Promise<string> {
-	return argon2.hash(password);
-}
-
-export async function comparePassword(
-	password: string,
-	hash: string,
-): Promise<boolean> {
-	if (isOldAlgorithm(hash)) return bcrypt.compare(password, hash);
-
-	return argon2.verify(hash, password);
-}
-
-export function isOldAlgorithm(hash: string): boolean {
-	// bcrypt hashes start with $2[ab]$
-	return hash.startsWith("$2");
-}
diff --git a/packages/backend/src/misc/populate-emojis.ts b/packages/backend/src/misc/populate-emojis.ts
index f18b23c9a4..4ca60b222f 100644
--- a/packages/backend/src/misc/populate-emojis.ts
+++ b/packages/backend/src/misc/populate-emojis.ts
@@ -3,8 +3,7 @@ import { Emojis } from "@/models/index.js";
 import type { Emoji } from "@/models/entities/emoji.js";
 import type { Note } from "@/models/entities/note.js";
 import { Cache } from "./cache.js";
-import { isSelfHost, toPuny } from "backend-rs";
-import { decodeReaction } from "./reaction-lib.js";
+import { decodeReaction, isSelfHost, toPuny } from "backend-rs";
 import config from "@/config/index.js";
 import { query } from "@/prelude/url.js";
 import { redisClient } from "@/db/redis.js";
diff --git a/packages/backend/src/misc/post.ts b/packages/backend/src/misc/post.ts
index dbe703d1a0..0b107ed009 100644
--- a/packages/backend/src/misc/post.ts
+++ b/packages/backend/src/misc/post.ts
@@ -12,7 +12,7 @@ export function parse(acct: any): Post {
 		cw: acct.cw,
 		localOnly: acct.localOnly,
 		createdAt: new Date(acct.createdAt),
-		visibility: "hidden" + (acct.visibility || ""),
+		visibility: `hidden${acct.visibility || ""}`,
 	};
 }
 
diff --git a/packages/backend/src/misc/reaction-lib.ts b/packages/backend/src/misc/reaction-lib.ts
deleted file mode 100644
index 4304d3b9e1..0000000000
--- a/packages/backend/src/misc/reaction-lib.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { emojiRegex } from "./emoji-regex.js";
-import { fetchMeta } from "./fetch-meta.js";
-import { Emojis } from "@/models/index.js";
-import { toPuny } from "backend-rs";
-import { IsNull } from "typeorm";
-
-export function convertReactions(reactions: Record<string, number>) {
-	const result = new Map();
-
-	for (const reaction in reactions) {
-		if (reactions[reaction] <= 0) continue;
-
-		const decoded = decodeReaction(reaction).reaction;
-		result.set(decoded, (result.get(decoded) || 0) + reactions[reaction]);
-	}
-
-	return Object.fromEntries(result);
-}
-
-export async function toDbReaction(
-	reaction?: string | null,
-	reacterHost?: string | null,
-): Promise<string> {
-	if (!reaction) return (await fetchMeta()).defaultReaction;
-
-	reacterHost = reacterHost == null ? null : toPuny(reacterHost);
-
-	if (reaction.includes("❤") || reaction.includes("♥️")) return "❤️";
-
-	// Allow unicode reactions
-	const match = emojiRegex.exec(reaction);
-	if (match) {
-		const unicode = match[0];
-		return unicode;
-	}
-
-	const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
-	if (custom) {
-		const name = custom[1];
-		const emoji = await Emojis.findOneBy({
-			host: reacterHost || IsNull(),
-			name,
-		});
-
-		if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
-	}
-
-	return (await fetchMeta()).defaultReaction;
-}
-
-type DecodedReaction = {
-	/**
-	 * リアクション名 (Unicode Emoji or ':name@hostname' or ':name@.')
-	 */
-	reaction: string;
-
-	/**
-	 * name (カスタム絵文字の場合name, Emojiクエリに使う)
-	 */
-	name?: string;
-
-	/**
-	 * host (カスタム絵文字の場合host, Emojiクエリに使う)
-	 */
-	host?: string | null;
-};
-
-export function decodeReaction(str: string): DecodedReaction {
-	const custom = str.match(/^:([\w+-]+)(?:@([\w.-]+))?:$/);
-
-	if (custom) {
-		const name = custom[1];
-		const host = custom[2] || null;
-
-		return {
-			reaction: `:${name}@${host || "."}:`, // ローカル分は@以降を省略するのではなく.にする
-			name,
-			host,
-		};
-	}
-
-	return {
-		reaction: str,
-		name: undefined,
-		host: undefined,
-	};
-}
diff --git a/packages/backend/src/misc/safe-for-sql.ts b/packages/backend/src/misc/safe-for-sql.ts
deleted file mode 100644
index 02eb7f0a26..0000000000
--- a/packages/backend/src/misc/safe-for-sql.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export function safeForSql(text: string): boolean {
-	return !/[\0\x08\x09\x1a\n\r"'\\\%]/g.test(text);
-}
diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts
index c793d3b2ed..73600832ce 100644
--- a/packages/backend/src/misc/schema.ts
+++ b/packages/backend/src/misc/schema.ts
@@ -33,8 +33,10 @@ import { packedGalleryPostSchema } from "@/models/schema/gallery-post.js";
 import { packedEmojiSchema } from "@/models/schema/emoji.js";
 import { packedNoteEdit } from "@/models/schema/note-edit.js";
 import { packedNoteFileSchema } from "@/models/schema/note-file.js";
+import { packedAbuseUserReportSchema } from "@/models/schema/abuse-user-report.js";
 
 export const refs = {
+	AbuseUserReport: packedAbuseUserReportSchema,
 	UserLite: packedUserLiteSchema,
 	UserDetailedNotMeOnly: packedUserDetailedNotMeOnlySchema,
 	MeDetailedOnly: packedMeDetailedOnlySchema,
diff --git a/packages/backend/src/misc/should-block-instance.ts b/packages/backend/src/misc/should-block-instance.ts
index 35ed307931..465be41f2a 100644
--- a/packages/backend/src/misc/should-block-instance.ts
+++ b/packages/backend/src/misc/should-block-instance.ts
@@ -1,4 +1,4 @@
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import type { Instance } from "@/models/entities/instance.js";
 import type { Meta } from "@/models/entities/meta.js";
 
@@ -13,7 +13,7 @@ export async function shouldBlockInstance(
 	host: Instance["host"],
 	meta?: Meta,
 ): Promise<boolean> {
-	const { blockedHosts } = meta ?? (await fetchMeta());
+	const { blockedHosts } = meta ?? (await fetchMeta(true));
 	return blockedHosts.some(
 		(blockedHost) => host === blockedHost || host.endsWith(`.${blockedHost}`),
 	);
@@ -30,7 +30,7 @@ export async function shouldSilenceInstance(
 	host: Instance["host"],
 	meta?: Meta,
 ): Promise<boolean> {
-	const { silencedHosts } = meta ?? (await fetchMeta());
+	const { silencedHosts } = meta ?? (await fetchMeta(true));
 	return silencedHosts.some(
 		(silencedHost) =>
 			host === silencedHost || host.endsWith(`.${silencedHost}`),
diff --git a/packages/backend/src/misc/skipped-instances.ts b/packages/backend/src/misc/skipped-instances.ts
index 785393022a..14b26a3032 100644
--- a/packages/backend/src/misc/skipped-instances.ts
+++ b/packages/backend/src/misc/skipped-instances.ts
@@ -1,5 +1,5 @@
 import { Brackets } from "typeorm";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { Instances } from "@/models/index.js";
 import type { Instance } from "@/models/entities/instance.js";
 import { DAY } from "@/const.js";
@@ -19,7 +19,7 @@ export async function skippedInstances(
 	hosts: Instance["host"][],
 ): Promise<Instance["host"][]> {
 	// first check for blocked instances since that info may already be in memory
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	const shouldSkip = await Promise.all(
 		hosts.map((host) => shouldBlockInstance(host, meta)),
 	);
diff --git a/packages/backend/src/misc/sql-like-escape.ts b/packages/backend/src/misc/sql-like-escape.ts
deleted file mode 100644
index 453947d6ec..0000000000
--- a/packages/backend/src/misc/sql-like-escape.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export function sqlLikeEscape(s: string) {
-	return s.replace(/([%_])/g, "\\$1");
-}
diff --git a/packages/backend/src/misc/translate.ts b/packages/backend/src/misc/translate.ts
index e622171ec6..3395ce93be 100644
--- a/packages/backend/src/misc/translate.ts
+++ b/packages/backend/src/misc/translate.ts
@@ -1,7 +1,7 @@
 import fetch from "node-fetch";
 import { Converter } from "opencc-js";
 import { getAgentByUrl } from "@/misc/fetch.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import type { PostLanguage } from "@/misc/langmap";
 import * as deepl from "deepl-node";
 
@@ -26,7 +26,7 @@ export async function translate(
 	from: PostLanguage | null,
 	to: PostLanguage,
 ) {
-	const instance = await fetchMeta();
+	const instance = await fetchMeta(true);
 
 	if (instance.deeplAuthKey == null && instance.libreTranslateApiUrl == null) {
 		throw Error("No translator is set up on this server.");
diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts
index 3c49e89fd5..81f564115f 100644
--- a/packages/backend/src/models/entities/drive-file.ts
+++ b/packages/backend/src/models/entities/drive-file.ts
@@ -16,6 +16,8 @@ import { DriveFolder } from "./drive-folder.js";
 import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
 import { NoteFile } from "./note-file.js";
 
+export type DriveFileUsageHint = "userAvatar" | "userBanner" | null;
+
 @Entity()
 @Index(["userId", "folderId", "id"])
 export class DriveFile {
@@ -177,6 +179,14 @@ export class DriveFile {
 	})
 	public isSensitive: boolean;
 
+	// Hint for what this file is used for
+	@Column({
+		type: "enum",
+		enum: ["userAvatar", "userBanner"],
+		nullable: true,
+	})
+	public usageHint: DriveFileUsageHint;
+
 	/**
 	 * 外部の(信頼されていない)URLへの直リンクか否か
 	 */
diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts
index 738e43d442..94cd8c7b66 100644
--- a/packages/backend/src/models/entities/note.ts
+++ b/packages/backend/src/models/entities/note.ts
@@ -72,9 +72,8 @@ export class Note {
 	})
 	public name: string | null;
 
-	@Index() // USING pgroonga pgroonga_varchar_full_text_search_ops_v2
-	@Column("varchar", {
-		length: 512,
+	@Index() // USING pgroonga
+	@Column("text", {
 		nullable: true,
 	})
 	public cw: string | null;
diff --git a/packages/backend/src/models/repositories/abuse-user-report.ts b/packages/backend/src/models/repositories/abuse-user-report.ts
index 16ce159955..b8d953d052 100644
--- a/packages/backend/src/models/repositories/abuse-user-report.ts
+++ b/packages/backend/src/models/repositories/abuse-user-report.ts
@@ -2,6 +2,7 @@ import { db } from "@/db/postgre.js";
 import { Users } from "../index.js";
 import { AbuseUserReport } from "@/models/entities/abuse-user-report.js";
 import { awaitAll } from "@/prelude/await-all.js";
+import type { Packed } from "@/misc/schema.js";
 
 export const AbuseUserReportRepository = db
 	.getRepository(AbuseUserReport)
@@ -10,7 +11,7 @@ export const AbuseUserReportRepository = db
 			const report =
 				typeof src === "object" ? src : await this.findOneByOrFail({ id: src });
 
-			return await awaitAll({
+			const packed: Packed<"AbuseUserReport"> = await awaitAll({
 				id: report.id,
 				createdAt: report.createdAt.toISOString(),
 				comment: report.comment,
@@ -31,9 +32,10 @@ export const AbuseUserReportRepository = db
 					: null,
 				forwarded: report.forwarded,
 			});
+			return packed;
 		},
 
-		packMany(reports: any[]) {
+		packMany(reports: (AbuseUserReport["id"] | AbuseUserReport)[]) {
 			return Promise.all(reports.map((x) => this.pack(x)));
 		},
 	});
diff --git a/packages/backend/src/models/repositories/channel.ts b/packages/backend/src/models/repositories/channel.ts
index 857470f4ec..809129db6c 100644
--- a/packages/backend/src/models/repositories/channel.ts
+++ b/packages/backend/src/models/repositories/channel.ts
@@ -40,6 +40,7 @@ export const ChannelRepository = db.getRepository(Channel).extend({
 			name: channel.name,
 			description: channel.description,
 			userId: channel.userId,
+			bannerId: channel.bannerId,
 			bannerUrl: banner ? DriveFiles.getPublicUrl(banner, false) : null,
 			usersCount: channel.usersCount,
 			notesCount: channel.notesCount,
diff --git a/packages/backend/src/models/repositories/drive-file.ts b/packages/backend/src/models/repositories/drive-file.ts
index 2321f20d4c..18b139caff 100644
--- a/packages/backend/src/models/repositories/drive-file.ts
+++ b/packages/backend/src/models/repositories/drive-file.ts
@@ -152,6 +152,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
 			md5: file.md5,
 			size: file.size,
 			isSensitive: file.isSensitive,
+			usageHint: file.usageHint,
 			blurhash: file.blurhash,
 			properties: opts.self ? file.properties : this.getPublicProperties(file),
 			url: opts.self ? file.url : this.getPublicUrl(file, false),
@@ -193,6 +194,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
 			md5: file.md5,
 			size: file.size,
 			isSensitive: file.isSensitive,
+			usageHint: file.usageHint,
 			blurhash: file.blurhash,
 			properties: opts.self ? file.properties : this.getPublicProperties(file),
 			url: opts.self ? file.url : this.getPublicUrl(file, false),
diff --git a/packages/backend/src/models/repositories/gallery-post.ts b/packages/backend/src/models/repositories/gallery-post.ts
index d91fb9de2a..c4078e9091 100644
--- a/packages/backend/src/models/repositories/gallery-post.ts
+++ b/packages/backend/src/models/repositories/gallery-post.ts
@@ -19,7 +19,9 @@ export const GalleryPostRepository = db.getRepository(GalleryPost).extend({
 			createdAt: post.createdAt.toISOString(),
 			updatedAt: post.updatedAt.toISOString(),
 			userId: post.userId,
-			user: Users.pack(post.user || post.userId, me),
+			user: Users.pack(post.user || post.userId, me, {
+				detail: true,
+			}),
 			title: post.title,
 			description: post.description,
 			fileIds: post.fileIds,
diff --git a/packages/backend/src/models/repositories/note-reaction.ts b/packages/backend/src/models/repositories/note-reaction.ts
index 20aae2876f..47e16ced0c 100644
--- a/packages/backend/src/models/repositories/note-reaction.ts
+++ b/packages/backend/src/models/repositories/note-reaction.ts
@@ -2,7 +2,7 @@ import { db } from "@/db/postgre.js";
 import { NoteReaction } from "@/models/entities/note-reaction.js";
 import { Notes, Users } from "../index.js";
 import type { Packed } from "@/misc/schema.js";
-import { decodeReaction } from "@/misc/reaction-lib.js";
+import { decodeReaction } from "backend-rs";
 import type { User } from "@/models/entities/user.js";
 
 export const NoteReactionRepository = db.getRepository(NoteReaction).extend({
diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts
index ed6ecc4a5c..c877048709 100644
--- a/packages/backend/src/models/repositories/note.ts
+++ b/packages/backend/src/models/repositories/note.ts
@@ -12,9 +12,8 @@ import {
 	Channels,
 } from "../index.js";
 import type { Packed } from "@/misc/schema.js";
-import { nyaify } from "backend-rs";
+import { countReactions, decodeReaction, nyaify } from "backend-rs";
 import { awaitAll } from "@/prelude/await-all.js";
-import { convertReactions, decodeReaction } from "@/misc/reaction-lib.js";
 import type { NoteReaction } from "@/models/entities/note-reaction.js";
 import {
 	aggregateNoteEmojis,
@@ -214,7 +213,7 @@ export const NoteRepository = db.getRepository(Note).extend({
 				note.visibility === "specified" ? note.visibleUserIds : undefined,
 			renoteCount: note.renoteCount,
 			repliesCount: note.repliesCount,
-			reactions: convertReactions(note.reactions),
+			reactions: countReactions(note.reactions),
 			reactionEmojis: reactionEmoji,
 			emojis: noteEmoji,
 			tags: note.tags.length > 0 ? note.tags : undefined,
@@ -233,6 +232,7 @@ export const NoteRepository = db.getRepository(Note).extend({
 			uri: note.uri || undefined,
 			url: note.url || undefined,
 			updatedAt: note.updatedAt?.toISOString() || undefined,
+			hasPoll: note.hasPoll,
 			poll: note.hasPoll ? populatePoll(note, meId) : undefined,
 			...(meId
 				? {
diff --git a/packages/backend/src/models/schema/abuse-user-report.ts b/packages/backend/src/models/schema/abuse-user-report.ts
new file mode 100644
index 0000000000..47e56c7415
--- /dev/null
+++ b/packages/backend/src/models/schema/abuse-user-report.ts
@@ -0,0 +1,69 @@
+export const packedAbuseUserReportSchema = {
+	type: "object",
+	properties: {
+		id: {
+			type: "string",
+			optional: false,
+			nullable: false,
+			format: "id",
+			example: "xxxxxxxxxx",
+		},
+		createdAt: {
+			type: "string",
+			optional: false,
+			nullable: false,
+			format: "date-time",
+		},
+		comment: {
+			type: "string",
+			optional: false,
+			nullable: false,
+		},
+		resolved: {
+			type: "boolean",
+			optional: false,
+			nullable: false,
+		},
+		reporterId: {
+			type: "string",
+			optional: false,
+			nullable: false,
+			format: "id",
+		},
+		targetUserId: {
+			type: "string",
+			optional: false,
+			nullable: false,
+			format: "id",
+		},
+		assigneeId: {
+			type: "string",
+			optional: false,
+			nullable: true,
+			format: "id",
+		},
+		reporter: {
+			type: "object",
+			optional: false,
+			nullable: false,
+			ref: "UserDetailed",
+		},
+		targetUser: {
+			type: "object",
+			optional: false,
+			nullable: false,
+			ref: "UserDetailed",
+		},
+		assignee: {
+			type: "object",
+			optional: true,
+			nullable: true,
+			ref: "UserDetailed",
+		},
+		forwarded: {
+			type: "boolean",
+			optional: false,
+			nullable: false,
+		},
+	},
+} as const;
diff --git a/packages/backend/src/models/schema/channel.ts b/packages/backend/src/models/schema/channel.ts
index 67833cb0dd..d3ec222c8d 100644
--- a/packages/backend/src/models/schema/channel.ts
+++ b/packages/backend/src/models/schema/channel.ts
@@ -36,6 +36,13 @@ export const packedChannelSchema = {
 			nullable: true,
 			optional: false,
 		},
+		bannerId: {
+			type: "string",
+			optional: false,
+			nullable: true,
+			format: "id",
+			example: "xxxxxxxxxx",
+		},
 		notesCount: {
 			type: "number",
 			nullable: false,
@@ -57,5 +64,10 @@ export const packedChannelSchema = {
 			optional: false,
 			format: "id",
 		},
+		hasUnreadNote: {
+			type: "boolean",
+			optional: true,
+			nullable: false,
+		},
 	},
 } as const;
diff --git a/packages/backend/src/models/schema/drive-file.ts b/packages/backend/src/models/schema/drive-file.ts
index 30db9e7d48..929dbb472e 100644
--- a/packages/backend/src/models/schema/drive-file.ts
+++ b/packages/backend/src/models/schema/drive-file.ts
@@ -44,6 +44,12 @@ export const packedDriveFileSchema = {
 			optional: false,
 			nullable: false,
 		},
+		usageHint: {
+			type: "string",
+			optional: false,
+			nullable: true,
+			enum: ["userAvatar", "userBanner"],
+		},
 		blurhash: {
 			type: "string",
 			optional: false,
diff --git a/packages/backend/src/models/schema/gallery-post.ts b/packages/backend/src/models/schema/gallery-post.ts
index 9ac348e1fb..ae22507643 100644
--- a/packages/backend/src/models/schema/gallery-post.ts
+++ b/packages/backend/src/models/schema/gallery-post.ts
@@ -38,7 +38,7 @@ export const packedGalleryPostSchema = {
 		},
 		user: {
 			type: "object",
-			ref: "UserLite",
+			ref: "UserDetailed",
 			optional: false,
 			nullable: false,
 		},
@@ -79,5 +79,15 @@ export const packedGalleryPostSchema = {
 			optional: false,
 			nullable: false,
 		},
+		isLiked: {
+			type: "boolean",
+			optional: true,
+			nullable: false,
+		},
+		likedCount: {
+			type: "number",
+			optional: false,
+			nullable: false,
+		},
 	},
 } as const;
diff --git a/packages/backend/src/models/schema/note.ts b/packages/backend/src/models/schema/note.ts
index 7dcdbc9b03..fff872b69f 100644
--- a/packages/backend/src/models/schema/note.ts
+++ b/packages/backend/src/models/schema/note.ts
@@ -28,7 +28,7 @@ export const packedNoteSchema = {
 		},
 		cw: {
 			type: "string",
-			optional: true,
+			optional: false,
 			nullable: true,
 		},
 		userId: {
@@ -98,7 +98,7 @@ export const packedNoteSchema = {
 		},
 		fileIds: {
 			type: "array",
-			optional: true,
+			optional: false,
 			nullable: false,
 			items: {
 				type: "string",
@@ -128,6 +128,11 @@ export const packedNoteSchema = {
 				nullable: false,
 			},
 		},
+		hasPoll: {
+			type: "boolean",
+			optional: false,
+			nullable: false,
+		},
 		poll: {
 			type: "object",
 			optional: true,
diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts
index 58a0ae7486..e4e413be52 100644
--- a/packages/backend/src/queue/index.ts
+++ b/packages/backend/src/queue/index.ts
@@ -5,7 +5,7 @@ import config from "@/config/index.js";
 import type { DriveFile } from "@/models/entities/drive-file.js";
 import type { IActivity } from "@/remote/activitypub/type.js";
 import type { Webhook, webhookEventTypes } from "@/models/entities/webhook.js";
-import { envOption } from "../env.js";
+import { envOption } from "@/config/index.js";
 
 import processDeliver from "./processors/deliver.js";
 import processInbox from "./processors/inbox.js";
diff --git a/packages/backend/src/queue/initialize.ts b/packages/backend/src/queue/initialize.ts
index 0f9c83132f..a874005fbd 100644
--- a/packages/backend/src/queue/initialize.ts
+++ b/packages/backend/src/queue/initialize.ts
@@ -34,7 +34,7 @@ export function initialize<T>(name: string, limitPerSec = -1) {
 function apBackoff(attemptsMade: number, err: Error) {
 	const baseDelay = 60 * 1000; // 1min
 	const maxBackoff = 8 * 60 * 60 * 1000; // 8hours
-	let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay;
+	let backoff = (2 ** attemptsMade - 1) * baseDelay;
 	backoff = Math.min(backoff, maxBackoff);
 	backoff += Math.round(backoff * Math.random() * 0.2);
 	return backoff;
diff --git a/packages/backend/src/queue/processors/inbox.ts b/packages/backend/src/queue/processors/inbox.ts
index 46f9939a41..0ea72306b6 100644
--- a/packages/backend/src/queue/processors/inbox.ts
+++ b/packages/backend/src/queue/processors/inbox.ts
@@ -5,7 +5,7 @@ import perform from "@/remote/activitypub/perform.js";
 import Logger from "@/services/logger.js";
 import { registerOrFetchInstanceDoc } from "@/services/register-or-fetch-instance-doc.js";
 import { Instances } from "@/models/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { toPuny, extractHost } from "backend-rs";
 import { getApId } from "@/remote/activitypub/type.js";
 import { fetchInstanceMetadata } from "@/services/fetch-instance-metadata.js";
@@ -41,7 +41,7 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
 	const host = toPuny(new URL(signature.keyId).hostname);
 
 	// interrupt if blocked
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (await shouldBlockInstance(host, meta)) {
 		return `Blocked request: ${host}`;
 	}
diff --git a/packages/backend/src/remote/activitypub/check-fetch.ts b/packages/backend/src/remote/activitypub/check-fetch.ts
index c0d40278ee..12ea63a931 100644
--- a/packages/backend/src/remote/activitypub/check-fetch.ts
+++ b/packages/backend/src/remote/activitypub/check-fetch.ts
@@ -1,7 +1,7 @@
 import { URL } from "url";
 import httpSignature, { IParsedSignature } from "@peertube/http-signature";
 import config from "@/config/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { toPuny } from "backend-rs";
 import DbResolver from "@/remote/activitypub/db-resolver.js";
 import { getApId } from "@/remote/activitypub/type.js";
@@ -12,7 +12,7 @@ import type { UserPublickey } from "@/models/entities/user-publickey.js";
 import { verify } from "node:crypto";
 
 export async function hasSignature(req: IncomingMessage): Promise<string> {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	const required = meta.secureMode || meta.privateMode;
 
 	try {
@@ -27,7 +27,7 @@ export async function hasSignature(req: IncomingMessage): Promise<string> {
 }
 
 export async function checkFetch(req: IncomingMessage): Promise<number> {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		if (req.headers.host !== config.host) return 400;
 
diff --git a/packages/backend/src/remote/activitypub/kernel/delete/note.ts b/packages/backend/src/remote/activitypub/kernel/delete/note.ts
index 4656480c2f..ae3a593d05 100644
--- a/packages/backend/src/remote/activitypub/kernel/delete/note.ts
+++ b/packages/backend/src/remote/activitypub/kernel/delete/note.ts
@@ -1,5 +1,5 @@
 import type { CacheableRemoteUser } from "@/models/entities/user.js";
-import deleteNode from "@/services/note/delete.js";
+import deleteNote from "@/services/note/delete.js";
 import { apLogger } from "../../logger.js";
 import DbResolver from "../../db-resolver.js";
 import { getApLock } from "@/misc/app-lock.js";
@@ -36,7 +36,7 @@ export default async function (
 			return "The user trying to delete the post is not the post author";
 		}
 
-		await deleteNode(actor, note);
+		await deleteNote(actor, note);
 		return "ok: note deleted";
 	} finally {
 		await lock.release();
diff --git a/packages/backend/src/remote/activitypub/models/image.ts b/packages/backend/src/remote/activitypub/models/image.ts
index 2cf0c6c152..a6ac698feb 100644
--- a/packages/backend/src/remote/activitypub/models/image.ts
+++ b/packages/backend/src/remote/activitypub/models/image.ts
@@ -1,9 +1,12 @@
 import { uploadFromUrl } from "@/services/drive/upload-from-url.js";
 import type { CacheableRemoteUser } from "@/models/entities/user.js";
 import Resolver from "../resolver.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { apLogger } from "../logger.js";
-import type { DriveFile } from "@/models/entities/drive-file.js";
+import type {
+	DriveFile,
+	DriveFileUsageHint,
+} from "@/models/entities/drive-file.js";
 import { DriveFiles } from "@/models/index.js";
 import { truncate } from "@/misc/truncate.js";
 import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
@@ -16,6 +19,7 @@ const logger = apLogger;
 export async function createImage(
 	actor: CacheableRemoteUser,
 	value: any,
+	usage: DriveFileUsageHint,
 ): Promise<DriveFile> {
 	// Skip if author is frozen.
 	if (actor.isSuspended) {
@@ -34,7 +38,7 @@ export async function createImage(
 
 	logger.info(`Creating the Image: ${image.url}`);
 
-	const instance = await fetchMeta();
+	const instance = await fetchMeta(true);
 
 	let file = await uploadFromUrl({
 		url: image.url,
@@ -43,6 +47,7 @@ export async function createImage(
 		sensitive: image.sensitive,
 		isLink: !instance.cacheRemoteFiles,
 		comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH),
+		usageHint: usage,
 	});
 
 	if (file.isLink) {
@@ -73,9 +78,10 @@ export async function createImage(
 export async function resolveImage(
 	actor: CacheableRemoteUser,
 	value: any,
+	usage: DriveFileUsageHint,
 ): Promise<DriveFile> {
 	// TODO
 
 	// Fetch from remote server and register
-	return await createImage(actor, value);
+	return await createImage(actor, value, usage);
 }
diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts
index ad59930457..b2fd67288c 100644
--- a/packages/backend/src/remote/activitypub/models/note.ts
+++ b/packages/backend/src/remote/activitypub/models/note.ts
@@ -213,7 +213,8 @@ export async function createNote(
 		? (
 				await Promise.all(
 					note.attachment.map(
-						(x) => limit(() => resolveImage(actor, x)) as Promise<DriveFile>,
+						(x) =>
+							limit(() => resolveImage(actor, x, null)) as Promise<DriveFile>,
 					),
 				)
 			).filter((image) => image != null)
@@ -616,7 +617,7 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) {
 			fileList.map(
 				(x) =>
 					limit(async () => {
-						const file = await resolveImage(actor, x);
+						const file = await resolveImage(actor, x, null);
 						const update: Partial<DriveFile> = {};
 
 						const altText = truncate(x.name, DB_MAX_IMAGE_COMMENT_LENGTH);
diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts
index e91280125f..4baa2c021b 100644
--- a/packages/backend/src/remote/activitypub/models/person.ts
+++ b/packages/backend/src/remote/activitypub/models/person.ts
@@ -10,6 +10,7 @@ import {
 	Followings,
 	UserProfiles,
 	UserPublickeys,
+	DriveFiles,
 } from "@/models/index.js";
 import type { IRemoteUser, CacheableUser } from "@/models/entities/user.js";
 import { User } from "@/models/entities/user.js";
@@ -362,10 +363,14 @@ export async function createPerson(
 
 	//#region Fetch avatar and header image
 	const [avatar, banner] = await Promise.all(
-		[person.icon, person.image].map((img) =>
+		[person.icon, person.image].map((img, index) =>
 			img == null
 				? Promise.resolve(null)
-				: resolveImage(user!, img).catch(() => null),
+				: resolveImage(
+						user,
+						img,
+						index === 0 ? "userAvatar" : index === 1 ? "userBanner" : null,
+					).catch(() => null),
 		),
 	);
 
@@ -438,10 +443,14 @@ export async function updatePerson(
 
 	// Fetch avatar and header image
 	const [avatar, banner] = await Promise.all(
-		[person.icon, person.image].map((img) =>
+		[person.icon, person.image].map((img, index) =>
 			img == null
 				? Promise.resolve(null)
-				: resolveImage(user, img).catch(() => null),
+				: resolveImage(
+						user,
+						img,
+						index === 0 ? "userAvatar" : index === 1 ? "userBanner" : null,
+					).catch(() => null),
 		),
 	);
 
@@ -561,10 +570,14 @@ export async function updatePerson(
 	} as Partial<User>;
 
 	if (avatar) {
+		if (user?.avatarId)
+			await DriveFiles.update(user.avatarId, { usageHint: null });
 		updates.avatarId = avatar.id;
 	}
 
 	if (banner) {
+		if (user?.bannerId)
+			await DriveFiles.update(user.bannerId, { usageHint: null });
 		updates.bannerId = banner.id;
 	}
 
diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts
index 973f12cdc2..79b7962b72 100644
--- a/packages/backend/src/remote/activitypub/resolver.ts
+++ b/packages/backend/src/remote/activitypub/resolver.ts
@@ -1,7 +1,7 @@
 import config from "@/config/index.js";
 import type { ILocalUser } from "@/models/entities/user.js";
 import { getInstanceActor } from "@/services/instance-actor.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { extractHost, isSelfHost } from "backend-rs";
 import { apGet } from "./request.js";
 import type { IObject, ICollection, IOrderedCollection } from "./type.js";
@@ -100,7 +100,7 @@ export default class Resolver {
 			return await this.resolveLocal(value);
 		}
 
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		if (await shouldBlockInstance(host, meta)) {
 			throw new Error("Instance is blocked");
 		}
diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts
index 7e5fb5a281..71d95709b7 100644
--- a/packages/backend/src/server/activitypub.ts
+++ b/packages/backend/src/server/activitypub.ts
@@ -9,7 +9,7 @@ import renderKey from "@/remote/activitypub/renderer/key.js";
 import { renderPerson } from "@/remote/activitypub/renderer/person.js";
 import renderEmoji from "@/remote/activitypub/renderer/emoji.js";
 import { inbox as processInbox } from "@/queue/index.js";
-import { isSelfHost } from "backend-rs";
+import { fetchMeta, isSelfHost } from "backend-rs";
 import {
 	Notes,
 	Users,
@@ -25,7 +25,6 @@ import {
 	getSignatureUser,
 } from "@/remote/activitypub/check-fetch.js";
 import { getInstanceActor } from "@/services/instance-actor.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
 import renderFollow from "@/remote/activitypub/renderer/follow.js";
 import Featured from "./activitypub/featured.js";
 import Following from "./activitypub/following.js";
@@ -33,7 +32,7 @@ import Followers from "./activitypub/followers.js";
 import Outbox, { packActivity } from "./activitypub/outbox.js";
 import { serverLogger } from "./index.js";
 import config from "@/config/index.js";
-import Koa from "koa";
+import type Koa from "koa";
 import * as crypto from "node:crypto";
 import { inspect } from "node:util";
 import type { IActivity } from "@/remote/activitypub/type.js";
@@ -238,7 +237,7 @@ router.get("/notes/:note", async (ctx, next) => {
 
 	ctx.body = renderActivity(await renderNote(note, false));
 
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 	} else {
@@ -268,7 +267,7 @@ router.get("/notes/:note/activity", async (ctx) => {
 	}
 
 	ctx.body = renderActivity(await packActivity(note));
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 	} else {
@@ -323,7 +322,7 @@ router.get("/users/:user/publickey", async (ctx) => {
 
 	if (Users.isLocalUser(user)) {
 		ctx.body = renderActivity(renderKey(user, keypair));
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		if (meta.secureMode || meta.privateMode) {
 			ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 		} else {
@@ -343,7 +342,7 @@ async function userInfo(ctx: Router.RouterContext, user: User | null) {
 	}
 
 	ctx.body = renderActivity(await renderPerson(user as ILocalUser));
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 	} else {
@@ -426,8 +425,8 @@ router.get("/emojis/:emoji", async (ctx) => {
 		return;
 	}
 
-	ctx.body = renderActivity(await renderEmoji(emoji));
-	const meta = await fetchMeta();
+	ctx.body = renderActivity(renderEmoji(emoji));
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 	} else {
@@ -459,7 +458,7 @@ router.get("/likes/:like", async (ctx) => {
 	}
 
 	ctx.body = renderActivity(await renderLike(reaction, note));
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 	} else {
@@ -497,7 +496,7 @@ router.get(
 		}
 
 		ctx.body = renderActivity(renderFollow(follower, followee));
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		if (meta.secureMode || meta.privateMode) {
 			ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 		} else {
@@ -540,7 +539,7 @@ router.get("/follows/:followRequestId", async (ctx: Router.RouterContext) => {
 		return;
 	}
 
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 	} else {
diff --git a/packages/backend/src/server/activitypub/featured.ts b/packages/backend/src/server/activitypub/featured.ts
index 464a7f769d..e7ea6f238e 100644
--- a/packages/backend/src/server/activitypub/featured.ts
+++ b/packages/backend/src/server/activitypub/featured.ts
@@ -5,7 +5,7 @@ import renderOrderedCollection from "@/remote/activitypub/renderer/ordered-colle
 import renderNote from "@/remote/activitypub/renderer/note.js";
 import { Users, Notes, UserNotePinings } from "@/models/index.js";
 import { checkFetch } from "@/remote/activitypub/check-fetch.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { setResponseType } from "../activitypub.js";
 import type Router from "@koa/router";
 
@@ -57,7 +57,7 @@ export default async (ctx: Router.RouterContext) => {
 
 	ctx.body = renderActivity(rendered);
 
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 	} else {
diff --git a/packages/backend/src/server/activitypub/followers.ts b/packages/backend/src/server/activitypub/followers.ts
index 3c9e5fa201..576a672d6d 100644
--- a/packages/backend/src/server/activitypub/followers.ts
+++ b/packages/backend/src/server/activitypub/followers.ts
@@ -8,7 +8,7 @@ import renderFollowUser from "@/remote/activitypub/renderer/follow-user.js";
 import { Users, Followings, UserProfiles } from "@/models/index.js";
 import type { Following } from "@/models/entities/following.js";
 import { checkFetch } from "@/remote/activitypub/check-fetch.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { setResponseType } from "../activitypub.js";
 import type { FindOptionsWhere } from "typeorm";
 import type Router from "@koa/router";
@@ -110,7 +110,7 @@ export default async (ctx: Router.RouterContext) => {
 		ctx.body = renderActivity(rendered);
 		setResponseType(ctx);
 	}
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 	} else {
diff --git a/packages/backend/src/server/activitypub/following.ts b/packages/backend/src/server/activitypub/following.ts
index cfbe985911..76b4e79716 100644
--- a/packages/backend/src/server/activitypub/following.ts
+++ b/packages/backend/src/server/activitypub/following.ts
@@ -8,7 +8,7 @@ import renderFollowUser from "@/remote/activitypub/renderer/follow-user.js";
 import { Users, Followings, UserProfiles } from "@/models/index.js";
 import type { Following } from "@/models/entities/following.js";
 import { checkFetch } from "@/remote/activitypub/check-fetch.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { setResponseType } from "../activitypub.js";
 import type { FindOptionsWhere } from "typeorm";
 import type Router from "@koa/router";
@@ -110,7 +110,7 @@ export default async (ctx: Router.RouterContext) => {
 		ctx.body = renderActivity(rendered);
 		setResponseType(ctx);
 	}
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 	} else {
diff --git a/packages/backend/src/server/activitypub/outbox.ts b/packages/backend/src/server/activitypub/outbox.ts
index 53aa6f4ad5..305102cf12 100644
--- a/packages/backend/src/server/activitypub/outbox.ts
+++ b/packages/backend/src/server/activitypub/outbox.ts
@@ -11,7 +11,7 @@ import * as url from "@/prelude/url.js";
 import { Users, Notes } from "@/models/index.js";
 import type { Note } from "@/models/entities/note.js";
 import { checkFetch } from "@/remote/activitypub/check-fetch.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { makePaginationQuery } from "../api/common/make-pagination-query.js";
 import { setResponseType } from "../activitypub.js";
 import type Router from "@koa/router";
@@ -117,7 +117,7 @@ export default async (ctx: Router.RouterContext) => {
 
 		setResponseType(ctx);
 	}
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.secureMode || meta.privateMode) {
 		ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
 	} else {
diff --git a/packages/backend/src/server/api/api-handler.ts b/packages/backend/src/server/api/api-handler.ts
index 620b754f30..5e65636427 100644
--- a/packages/backend/src/server/api/api-handler.ts
+++ b/packages/backend/src/server/api/api-handler.ts
@@ -2,7 +2,7 @@ import type Koa from "koa";
 
 import type { User } from "@/models/entities/user.js";
 import { UserIps } from "@/models/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import type { IEndpoint } from "./endpoints.js";
 import authenticate, { AuthenticationError } from "./authenticate.js";
 import call from "./call.js";
@@ -84,7 +84,7 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) =>
 
 				// Log IP
 				if (user) {
-					fetchMeta().then((meta) => {
+					fetchMeta(true).then((meta) => {
 						if (!meta.enableIpLogging) return;
 						const ip = ctx.ip;
 						const ips = userIpHistories.get(user.id);
diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts
index 2faef7b0e8..3107156a9b 100644
--- a/packages/backend/src/server/api/call.ts
+++ b/packages/backend/src/server/api/call.ts
@@ -10,7 +10,7 @@ import endpoints from "./endpoints.js";
 import compatibility from "./compatibility.js";
 import { ApiError } from "./error.js";
 import { apiLogger } from "./logger.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 
 const accessDenied = {
 	message: "Access denied.",
@@ -117,7 +117,7 @@ export default async (
 	}
 
 	// private mode
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (
 		meta.privateMode &&
 		ep.meta.requireCredentialPrivateMode &&
diff --git a/packages/backend/src/server/api/common/signup.ts b/packages/backend/src/server/api/common/signup.ts
index a267baf9c0..58b88b7d02 100644
--- a/packages/backend/src/server/api/common/signup.ts
+++ b/packages/backend/src/server/api/common/signup.ts
@@ -4,12 +4,11 @@ import { User } from "@/models/entities/user.js";
 import { Users, UsedUsernames } from "@/models/index.js";
 import { UserProfile } from "@/models/entities/user-profile.js";
 import { IsNull } from "typeorm";
-import { genId, toPuny } from "backend-rs";
+import { genId, hashPassword, toPuny } from "backend-rs";
 import { UserKeypair } from "@/models/entities/user-keypair.js";
 import { UsedUsername } from "@/models/entities/used-username.js";
 import { db } from "@/db/postgre.js";
 import config from "@/config/index.js";
-import { hashPassword } from "@/misc/password.js";
 
 export async function signup(opts: {
 	username: User["username"];
@@ -40,7 +39,7 @@ export async function signup(opts: {
 		}
 
 		// Generate hash of password
-		hash = await hashPassword(password);
+		hash = hashPassword(password);
 	}
 
 	// Generate secret
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 9a0de00b8b..734534b3ea 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -286,7 +286,6 @@ import * as ep___pinnedUsers from "./endpoints/pinned-users.js";
 import * as ep___customMotd from "./endpoints/custom-motd.js";
 import * as ep___customSplashIcons from "./endpoints/custom-splash-icons.js";
 import * as ep___latestVersion from "./endpoints/latest-version.js";
-import * as ep___patrons from "./endpoints/patrons.js";
 import * as ep___release from "./endpoints/release.js";
 import * as ep___promo_read from "./endpoints/promo/read.js";
 import * as ep___requestResetPassword from "./endpoints/request-reset-password.js";
@@ -636,7 +635,6 @@ const eps = [
 	["custom-motd", ep___customMotd],
 	["custom-splash-icons", ep___customSplashIcons],
 	["latest-version", ep___latestVersion],
-	["patrons", ep___patrons],
 	["release", ep___release],
 	["promo/read", ep___promo_read],
 	["request-reset-password", ep___requestResetPassword],
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
index 78034917f0..4063af5c5c 100644
--- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
@@ -16,68 +16,7 @@ export const meta = {
 			type: "object",
 			optional: false,
 			nullable: false,
-			properties: {
-				id: {
-					type: "string",
-					nullable: false,
-					optional: false,
-					format: "id",
-					example: "xxxxxxxxxx",
-				},
-				createdAt: {
-					type: "string",
-					nullable: false,
-					optional: false,
-					format: "date-time",
-				},
-				comment: {
-					type: "string",
-					nullable: false,
-					optional: false,
-				},
-				resolved: {
-					type: "boolean",
-					nullable: false,
-					optional: false,
-					example: false,
-				},
-				reporterId: {
-					type: "string",
-					nullable: false,
-					optional: false,
-					format: "id",
-				},
-				targetUserId: {
-					type: "string",
-					nullable: false,
-					optional: false,
-					format: "id",
-				},
-				assigneeId: {
-					type: "string",
-					nullable: true,
-					optional: false,
-					format: "id",
-				},
-				reporter: {
-					type: "object",
-					nullable: false,
-					optional: false,
-					ref: "User",
-				},
-				targetUser: {
-					type: "object",
-					nullable: false,
-					optional: false,
-					ref: "User",
-				},
-				assignee: {
-					type: "object",
-					nullable: true,
-					optional: true,
-					ref: "User",
-				},
-			},
+			ref: "AbuseUserReport",
 		},
 	},
 } as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
index 5c3e19d9e0..9c7a5180d3 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
@@ -1,8 +1,7 @@
 import define from "@/server/api/define.js";
 import { Emojis } from "@/models/index.js";
-import { toPuny } from "backend-rs";
+import { sqlLikeEscape, toPuny } from "backend-rs";
 import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js";
-import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
 import { ApiError } from "@/server/api/error.js";
 
 export const meta = {
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
index 434b679608..98a69090db 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
@@ -1,8 +1,8 @@
 import define from "@/server/api/define.js";
 import { Emojis } from "@/models/index.js";
-import { makePaginationQuery } from "../../../common/make-pagination-query.js";
+import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js";
 import type { Emoji } from "@/models/entities/emoji.js";
-//import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
+//import { sqlLikeEscape } from "backend-rs";
 import { ApiError } from "@/server/api/error.js";
 
 export const meta = {
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 1f382f1254..ab04800944 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -1,5 +1,5 @@
 import config from "@/config/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js";
 import define from "@/server/api/define.js";
 
@@ -471,7 +471,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async () => {
-	const instance = await fetchMeta(true);
+	const instance = await fetchMeta(false);
 
 	return {
 		maintainerName: instance.maintainerName,
diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts
index 5fbed130e6..ace0b581d7 100644
--- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts
+++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts
@@ -1,8 +1,7 @@
 import define from "@/server/api/define.js";
-// import bcrypt from "bcryptjs";
 import rndstr from "rndstr";
 import { Users, UserProfiles } from "@/models/index.js";
-import { hashPassword } from "@/misc/password.js";
+import { hashPassword } from "backend-rs";
 
 export const meta = {
 	tags: ["admin"],
@@ -48,8 +47,7 @@ export default define(meta, paramDef, async (ps) => {
 	const passwd = rndstr("a-zA-Z0-9", 8);
 
 	// Generate hash of password
-	// const hash = bcrypt.hashSync(passwd);
-	const hash = await hashPassword(passwd);
+	const hash = hashPassword(passwd);
 
 	await UserProfiles.update(
 		{
diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts
index 1e6ebeda93..8a892c3606 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-users.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts
@@ -1,6 +1,6 @@
 import { Users } from "@/models/index.js";
 import define from "@/server/api/define.js";
-import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
+import { sqlLikeEscape } from "backend-rs";
 
 export const meta = {
 	tags: ["admin"],
diff --git a/packages/backend/src/server/api/endpoints/channels/search.ts b/packages/backend/src/server/api/endpoints/channels/search.ts
index b2fab701c5..ed44250a37 100644
--- a/packages/backend/src/server/api/endpoints/channels/search.ts
+++ b/packages/backend/src/server/api/endpoints/channels/search.ts
@@ -2,7 +2,7 @@ import define from "@/server/api/define.js";
 import { Brackets } from "typeorm";
 import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js";
 import { Channels } from "@/models/index.js";
-import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
+import { sqlLikeEscape } from "backend-rs";
 
 export const meta = {
 	tags: ["channels"],
diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts
index 0de7a837a1..fdd21da65f 100644
--- a/packages/backend/src/server/api/endpoints/channels/update.ts
+++ b/packages/backend/src/server/api/endpoints/channels/update.ts
@@ -83,7 +83,7 @@ export default define(meta, paramDef, async (ps, me) => {
 	await Channels.update(channel.id, {
 		...(ps.name !== undefined ? { name: ps.name } : {}),
 		...(ps.description !== undefined ? { description: ps.description } : {}),
-		...(banner ? { bannerId: banner.id } : {}),
+		...(banner ? { bannerId: banner.id } : { bannerId: null }),
 	});
 
 	return await Channels.pack(channel.id, me);
diff --git a/packages/backend/src/server/api/endpoints/custom-motd.ts b/packages/backend/src/server/api/endpoints/custom-motd.ts
index 2939355b94..ac1012258d 100644
--- a/packages/backend/src/server/api/endpoints/custom-motd.ts
+++ b/packages/backend/src/server/api/endpoints/custom-motd.ts
@@ -1,5 +1,5 @@
 // import { IsNull } from 'typeorm';
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import define from "@/server/api/define.js";
 
 export const meta = {
@@ -27,7 +27,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async () => {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	const motd = await Promise.all(meta.customMotd.map((x) => x));
 	return motd;
 });
diff --git a/packages/backend/src/server/api/endpoints/custom-splash-icons.ts b/packages/backend/src/server/api/endpoints/custom-splash-icons.ts
index f63a1b9600..4eb35aa3e5 100644
--- a/packages/backend/src/server/api/endpoints/custom-splash-icons.ts
+++ b/packages/backend/src/server/api/endpoints/custom-splash-icons.ts
@@ -1,5 +1,5 @@
 // import { IsNull } from 'typeorm';
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import define from "@/server/api/define.js";
 
 export const meta = {
@@ -27,7 +27,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async () => {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	const icons = await Promise.all(meta.customSplashIcons.map((x) => x));
 	return icons;
 });
diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts
index 164e7b8f93..c04f219a9b 100644
--- a/packages/backend/src/server/api/endpoints/drive.ts
+++ b/packages/backend/src/server/api/endpoints/drive.ts
@@ -1,4 +1,4 @@
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { DriveFiles } from "@/models/index.js";
 import define from "@/server/api/define.js";
 
@@ -35,7 +35,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async (ps, user) => {
-	const instance = await fetchMeta(true);
+	const instance = await fetchMeta(false);
 
 	// Calculate drive usage
 	const usage = await DriveFiles.calcDriveUsageOf(user.id);
diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts
index a3e3fafa2f..44e388a9bd 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/create.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts
@@ -2,7 +2,7 @@ import { addFile } from "@/services/drive/add-file.js";
 import { DriveFiles } from "@/models/index.js";
 import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
 import { IdentifiableError } from "@/misc/identifiable-error.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { MINUTE } from "@/const.js";
 import define from "@/server/api/define.js";
 import { apiLogger } from "@/server/api/logger.js";
@@ -96,7 +96,7 @@ export default define(
 			name = null;
 		}
 
-		const instanceMeta = await fetchMeta();
+		const instanceMeta = await fetchMeta(true);
 
 		try {
 			// Create file
diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts
index 27a6dabb49..362ab098fb 100644
--- a/packages/backend/src/server/api/endpoints/federation/instances.ts
+++ b/packages/backend/src/server/api/endpoints/federation/instances.ts
@@ -1,7 +1,6 @@
 import define from "@/server/api/define.js";
 import { Instances } from "@/models/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
-import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
+import { fetchMeta, sqlLikeEscape } from "backend-rs";
 
 export const meta = {
 	tags: ["federation"],
@@ -101,7 +100,7 @@ export default define(meta, paramDef, async (ps, me) => {
 	}
 
 	if (typeof ps.blocked === "boolean") {
-		const meta = await fetchMeta(true);
+		const meta = await fetchMeta(false);
 		if (ps.blocked) {
 			if (meta.blockedHosts.length === 0) {
 				return [];
@@ -117,7 +116,7 @@ export default define(meta, paramDef, async (ps, me) => {
 	}
 
 	if (typeof ps.silenced === "boolean") {
-		const meta = await fetchMeta(true);
+		const meta = await fetchMeta(false);
 		if (ps.silenced) {
 			if (meta.silencedHosts.length === 0) {
 				return [];
diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts
index 1dc1fb4922..8fb5b23f62 100644
--- a/packages/backend/src/server/api/endpoints/hashtags/search.ts
+++ b/packages/backend/src/server/api/endpoints/hashtags/search.ts
@@ -1,6 +1,6 @@
 import define from "@/server/api/define.js";
 import { Hashtags } from "@/models/index.js";
-import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
+import { sqlLikeEscape } from "backend-rs";
 
 export const meta = {
 	tags: ["hashtags"],
diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts
index fe8bba95fd..531a494248 100644
--- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts
+++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts
@@ -1,9 +1,8 @@
 import { Brackets } from "typeorm";
 import define from "@/server/api/define.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta, safeForSql } from "backend-rs";
 import { Notes } from "@/models/index.js";
 import type { Note } from "@/models/entities/note.js";
-import { safeForSql } from "@/misc/safe-for-sql.js";
 import { normalizeForSearch } from "@/misc/normalize-for-search.js";
 
 /*
@@ -67,7 +66,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async () => {
-	const instance = await fetchMeta(true);
+	const instance = await fetchMeta(false);
 	const hiddenTags = instance.hiddenTags.map((t) => normalizeForSearch(t));
 
 	const now = new Date(); // 5分単位で丸めた現在日時
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
index 0e52dd0d78..6c99217e7d 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
@@ -9,7 +9,7 @@ import {
 import config from "@/config/index.js";
 import { procedures, hash } from "@/server/api/2fa.js";
 import { publishMainStream } from "@/services/stream.js";
-import { comparePassword } from "@/misc/password.js";
+import { verifyPassword } from "backend-rs";
 
 const rpIdHashReal = hash(Buffer.from(config.hostname, "utf-8"));
 
@@ -40,8 +40,8 @@ export const paramDef = {
 export default define(meta, paramDef, async (ps, user) => {
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
 
-	// Compare password
-	const same = await comparePassword(ps.password, profile.password!);
+	// Compare passwords
+	const same = verifyPassword(ps.password, profile.password!);
 
 	if (!same) {
 		throw new Error("incorrect password");
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
index b275b5705d..4991e8fc90 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
@@ -2,9 +2,8 @@ import define from "@/server/api/define.js";
 import { UserProfiles, AttestationChallenges } from "@/models/index.js";
 import { promisify } from "node:util";
 import * as crypto from "node:crypto";
-import { genId } from "backend-rs";
+import { genId, verifyPassword } from "backend-rs";
 import { hash } from "@/server/api/2fa.js";
-import { comparePassword } from "@/misc/password.js";
 
 const randomBytes = promisify(crypto.randomBytes);
 
@@ -25,8 +24,8 @@ export const paramDef = {
 export default define(meta, paramDef, async (ps, user) => {
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
 
-	// Compare password
-	const same = await comparePassword(ps.password, profile.password!);
+	// Compare passwords
+	const same = verifyPassword(ps.password, profile.password!);
 
 	if (!same) {
 		throw new Error("incorrect password");
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
index 52e1df39f4..c0e6137d5d 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
@@ -3,7 +3,7 @@ import * as QRCode from "qrcode";
 import config from "@/config/index.js";
 import { UserProfiles } from "@/models/index.js";
 import define from "@/server/api/define.js";
-import { comparePassword } from "@/misc/password.js";
+import { verifyPassword } from "backend-rs";
 
 export const meta = {
 	requireCredential: true,
@@ -22,8 +22,8 @@ export const paramDef = {
 export default define(meta, paramDef, async (ps, user) => {
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
 
-	// Compare password
-	const same = await comparePassword(ps.password, profile.password!);
+	// Compare passwords
+	const same = verifyPassword(ps.password, profile.password!);
 
 	if (!same) {
 		throw new Error("incorrect password");
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
index 0cdf8780ef..4259d8f70d 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
@@ -1,4 +1,4 @@
-import { comparePassword } from "@/misc/password.js";
+import { verifyPassword } from "backend-rs";
 import define from "@/server/api/define.js";
 import { UserProfiles, UserSecurityKeys, Users } from "@/models/index.js";
 import { publishMainStream } from "@/services/stream.js";
@@ -21,8 +21,8 @@ export const paramDef = {
 export default define(meta, paramDef, async (ps, user) => {
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
 
-	// Compare password
-	const same = await comparePassword(ps.password, profile.password!);
+	// Compare passwords
+	const same = verifyPassword(ps.password, profile.password!);
 
 	if (!same) {
 		throw new Error("incorrect password");
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
index c4e78eecb5..240ff2b34e 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
@@ -1,7 +1,7 @@
 import { publishMainStream } from "@/services/stream.js";
 import define from "@/server/api/define.js";
 import { Users, UserProfiles } from "@/models/index.js";
-import { comparePassword } from "@/misc/password.js";
+import { verifyPassword } from "backend-rs";
 
 export const meta = {
 	requireCredential: true,
@@ -20,8 +20,8 @@ export const paramDef = {
 export default define(meta, paramDef, async (ps, user) => {
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
 
-	// Compare password
-	const same = await comparePassword(ps.password, profile.password!);
+	// Compare passwords
+	const same = verifyPassword(ps.password, profile.password!);
 
 	if (!same) {
 		throw new Error("incorrect password");
diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts
index b0dc8bba60..1634676748 100644
--- a/packages/backend/src/server/api/endpoints/i/change-password.ts
+++ b/packages/backend/src/server/api/endpoints/i/change-password.ts
@@ -1,6 +1,6 @@
 import define from "@/server/api/define.js";
 import { UserProfiles } from "@/models/index.js";
-import { hashPassword, comparePassword } from "@/misc/password.js";
+import { hashPassword, verifyPassword } from "backend-rs";
 
 export const meta = {
 	requireCredential: true,
@@ -20,8 +20,8 @@ export const paramDef = {
 export default define(meta, paramDef, async (ps, user) => {
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
 
-	// Compare password
-	const same = await comparePassword(ps.currentPassword, profile.password!);
+	// Compare passwords
+	const same = verifyPassword(ps.currentPassword, profile.password!);
 
 	if (!same) {
 		throw new Error("incorrect password");
diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts
index 606cde82e1..538798261d 100644
--- a/packages/backend/src/server/api/endpoints/i/delete-account.ts
+++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts
@@ -1,7 +1,7 @@
 import { UserProfiles, Users } from "@/models/index.js";
 import { deleteAccount } from "@/services/delete-account.js";
 import define from "@/server/api/define.js";
-import { comparePassword } from "@/misc/password.js";
+import { verifyPassword } from "backend-rs";
 
 export const meta = {
 	requireCredential: true,
@@ -24,8 +24,8 @@ export default define(meta, paramDef, async (ps, user) => {
 		return;
 	}
 
-	// Compare password
-	const same = await comparePassword(ps.password, profile.password!);
+	// Compare passwords
+	const same = verifyPassword(ps.password, profile.password!);
 
 	if (!same) {
 		throw new Error("incorrect password");
diff --git a/packages/backend/src/server/api/endpoints/i/import-posts.ts b/packages/backend/src/server/api/endpoints/i/import-posts.ts
index b8b52be98f..225306ebc5 100644
--- a/packages/backend/src/server/api/endpoints/i/import-posts.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-posts.ts
@@ -3,7 +3,7 @@ import { createImportPostsJob } from "@/queue/index.js";
 import { ApiError } from "@/server/api/error.js";
 import { DriveFiles } from "@/models/index.js";
 import { DAY } from "@/const.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 
 export const meta = {
 	secure: true,
@@ -45,7 +45,7 @@ export const paramDef = {
 export default define(meta, paramDef, async (ps, user) => {
 	const file = await DriveFiles.findOneBy({ id: ps.fileId });
 
-	const instanceMeta = await fetchMeta();
+	const instanceMeta = await fetchMeta(true);
 	if (instanceMeta.experimentalFeatures?.postImports === false)
 		throw new ApiError(meta.errors.importsDisabled);
 
diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
index c1b4325adb..fd3023ab7a 100644
--- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
+++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
@@ -6,7 +6,7 @@ import {
 import generateUserToken from "@/server/api/common/generate-native-user-token.js";
 import define from "@/server/api/define.js";
 import { Users, UserProfiles } from "@/models/index.js";
-import { comparePassword } from "@/misc/password.js";
+import { verifyPassword } from "backend-rs";
 
 export const meta = {
 	requireCredential: true,
@@ -28,8 +28,8 @@ export default define(meta, paramDef, async (ps, user) => {
 
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
 
-	// Compare password
-	const same = await comparePassword(ps.password, profile.password!);
+	// Compare passwords
+	const same = verifyPassword(ps.password, profile.password!);
 
 	if (!same) {
 		throw new Error("incorrect password");
diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts
index a48252ed1a..234127f584 100644
--- a/packages/backend/src/server/api/endpoints/i/update-email.ts
+++ b/packages/backend/src/server/api/endpoints/i/update-email.ts
@@ -7,7 +7,7 @@ import { sendEmail } from "@/services/send-email.js";
 import { ApiError } from "@/server/api/error.js";
 import { validateEmailForAccount } from "@/services/validate-email-for-account.js";
 import { HOUR } from "@/const.js";
-import { comparePassword } from "@/misc/password.js";
+import { verifyPassword } from "backend-rs";
 
 export const meta = {
 	requireCredential: true,
@@ -46,8 +46,8 @@ export const paramDef = {
 export default define(meta, paramDef, async (ps, user) => {
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
 
-	// Compare password
-	const same = await comparePassword(ps.password, profile.password!);
+	// Compare passwords
+	const same = verifyPassword(ps.password, profile.password!);
 
 	if (!same) {
 		throw new ApiError(meta.errors.incorrectPassword);
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 4389688a12..4f65c59a9e 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -13,6 +13,7 @@ import { normalizeForSearch } from "@/misc/normalize-for-search.js";
 import { verifyLink } from "@/services/fetch-rel-me.js";
 import { ApiError } from "@/server/api/error.js";
 import define from "@/server/api/define.js";
+import { DriveFile } from "@/models/entities/drive-file";
 
 export const meta = {
 	tags: ["account"],
@@ -241,8 +242,9 @@ export default define(meta, paramDef, async (ps, _user, token) => {
 	if (ps.emailNotificationTypes !== undefined)
 		profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
 
+	let avatar: DriveFile | null = null;
 	if (ps.avatarId) {
-		const avatar = await DriveFiles.findOneBy({ id: ps.avatarId });
+		avatar = await DriveFiles.findOneBy({ id: ps.avatarId });
 
 		if (avatar == null || avatar.userId !== user.id)
 			throw new ApiError(meta.errors.noSuchAvatar);
@@ -250,8 +252,9 @@ export default define(meta, paramDef, async (ps, _user, token) => {
 			throw new ApiError(meta.errors.avatarNotAnImage);
 	}
 
+	let banner: DriveFile | null = null;
 	if (ps.bannerId) {
-		const banner = await DriveFiles.findOneBy({ id: ps.bannerId });
+		banner = await DriveFiles.findOneBy({ id: ps.bannerId });
 
 		if (banner == null || banner.userId !== user.id)
 			throw new ApiError(meta.errors.noSuchBanner);
@@ -328,6 +331,20 @@ export default define(meta, paramDef, async (ps, _user, token) => {
 	updateUsertags(user, tags);
 	//#endregion
 
+	// Update old/new avatar usage hints
+	if (avatar) {
+		if (user.avatarId)
+			await DriveFiles.update(user.avatarId, { usageHint: null });
+		await DriveFiles.update(avatar.id, { usageHint: "userAvatar" });
+	}
+
+	// Update old/new banner usage hints
+	if (banner) {
+		if (user.bannerId)
+			await DriveFiles.update(user.bannerId, { usageHint: null });
+		await DriveFiles.update(banner.id, { usageHint: "userBanner" });
+	}
+
 	if (Object.keys(updates).length > 0) await Users.update(user.id, updates);
 	if (Object.keys(profileUpdates).length > 0)
 		await UserProfiles.update(user.id, profileUpdates);
diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts
index cb494bf27e..ec8f701976 100644
--- a/packages/backend/src/server/api/endpoints/meta.ts
+++ b/packages/backend/src/server/api/endpoints/meta.ts
@@ -1,7 +1,7 @@
 import JSON5 from "json5";
 import { IsNull, MoreThan } from "typeorm";
 import config from "@/config/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { Ads, Emojis, Users } from "@/models/index.js";
 import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js";
 import define from "@/server/api/define.js";
@@ -403,7 +403,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async (ps, me) => {
-	const instance = await fetchMeta(true);
+	const instance = await fetchMeta(false);
 
 	const emojis = await Emojis.find({
 		where: {
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index 270c33abd0..c2302f4c8d 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -114,7 +114,7 @@ export const paramDef = {
 			enum: Object.keys(langmap),
 			nullable: true,
 		},
-		cw: { type: "string", nullable: true, maxLength: 100 },
+		cw: { type: "string", nullable: true, maxLength: MAX_NOTE_TEXT_LENGTH },
 		localOnly: { type: "boolean", default: false },
 		noExtractMentions: { type: "boolean", default: false },
 		noExtractHashtags: { type: "boolean", default: false },
diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
index 142b380f71..476375dc0b 100644
--- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
@@ -1,4 +1,4 @@
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { Notes } from "@/models/index.js";
 import { activeUsersChart } from "@/services/chart/index.js";
 import define from "@/server/api/define.js";
@@ -64,7 +64,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async (ps, user) => {
-	const m = await fetchMeta();
+	const m = await fetchMeta(true);
 	if (m.disableGlobalTimeline) {
 		if (user == null || !(user.isAdmin || user.isModerator)) {
 			throw new ApiError(meta.errors.gtlDisabled);
diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
index c9800f2e1f..e6ab910040 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -1,5 +1,5 @@
 import { Brackets } from "typeorm";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { Followings, Notes } from "@/models/index.js";
 import { activeUsersChart } from "@/services/chart/index.js";
 import define from "@/server/api/define.js";
@@ -71,7 +71,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async (ps, user) => {
-	const m = await fetchMeta();
+	const m = await fetchMeta(true);
 	if (m.disableLocalTimeline && !user.isAdmin && !user.isModerator) {
 		throw new ApiError(meta.errors.stlDisabled);
 	}
diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
index b9cb68c2a0..2a99c1236c 100644
--- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
@@ -1,5 +1,5 @@
 import { Brackets } from "typeorm";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { Notes } from "@/models/index.js";
 import { activeUsersChart } from "@/services/chart/index.js";
 import define from "@/server/api/define.js";
@@ -74,7 +74,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async (ps, user) => {
-	const m = await fetchMeta();
+	const m = await fetchMeta(true);
 	if (m.disableLocalTimeline) {
 		if (user == null || !(user.isAdmin || user.isModerator)) {
 			throw new ApiError(meta.errors.ltlDisabled);
diff --git a/packages/backend/src/server/api/endpoints/notes/make-private.ts b/packages/backend/src/server/api/endpoints/notes/make-private.ts
index 7b9ebc4d1a..5ddf1f3bf1 100644
--- a/packages/backend/src/server/api/endpoints/notes/make-private.ts
+++ b/packages/backend/src/server/api/endpoints/notes/make-private.ts
@@ -53,7 +53,7 @@ export default define(meta, paramDef, async (ps, user) => {
 		throw new ApiError(meta.errors.accessDenied);
 	}
 
-	await deleteNote(user, note, false, false);
+	await deleteNote(user, note, false);
 	await Notes.update(note.id, {
 		visibility: "specified",
 		visibleUserIds: [],
diff --git a/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts b/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts
index f71822f926..073a8f8569 100644
--- a/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts
@@ -1,5 +1,5 @@
 import { Brackets } from "typeorm";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { Notes } from "@/models/index.js";
 import { activeUsersChart } from "@/services/chart/index.js";
 import define from "@/server/api/define.js";
@@ -74,7 +74,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async (ps, user) => {
-	const m = await fetchMeta();
+	const m = await fetchMeta(true);
 	if (m.disableRecommendedTimeline) {
 		if (user == null || !(user.isAdmin || user.isModerator)) {
 			throw new ApiError(meta.errors.rtlDisabled);
diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
index e87725e342..f449ea081a 100644
--- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
+++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
@@ -1,6 +1,6 @@
 import { Brackets } from "typeorm";
 import { Notes } from "@/models/index.js";
-import { safeForSql } from "@/misc/safe-for-sql.js";
+import { safeForSql } from "backend-rs";
 import { normalizeForSearch } from "@/misc/normalize-for-search.js";
 import define from "@/server/api/define.js";
 import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js";
diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts
index b159a91944..f28208cba9 100644
--- a/packages/backend/src/server/api/endpoints/notes/search.ts
+++ b/packages/backend/src/server/api/endpoints/notes/search.ts
@@ -1,11 +1,11 @@
 import { Notes } from "@/models/index.js";
-import { Note } from "@/models/entities/note.js";
+import type { Note } from "@/models/entities/note.js";
 import define from "@/server/api/define.js";
 import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js";
 import { generateVisibilityQuery } from "@/server/api/common/generate-visibility-query.js";
 import { generateMutedUserQuery } from "@/server/api/common/generate-muted-user-query.js";
 import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js";
-import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
+import { sqlLikeEscape } from "backend-rs";
 import type { SelectQueryBuilder } from "typeorm";
 
 export const meta = {
diff --git a/packages/backend/src/server/api/endpoints/patrons.ts b/packages/backend/src/server/api/endpoints/patrons.ts
deleted file mode 100644
index 7da72eb81e..0000000000
--- a/packages/backend/src/server/api/endpoints/patrons.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import define from "@/server/api/define.js";
-import * as fs from "node:fs/promises";
-import { fileURLToPath } from "node:url";
-import { dirname } from "node:path";
-
-const _filename = fileURLToPath(import.meta.url);
-const _dirname = dirname(_filename);
-
-export const meta = {
-	tags: ["meta"],
-	description: "Get Firefish patrons",
-
-	requireCredential: false,
-	requireCredentialPrivateMode: false,
-} as const;
-
-export const paramDef = {
-	type: "object",
-	properties: {
-		forceUpdate: { type: "boolean", default: false },
-	},
-	required: [],
-} as const;
-
-export default define(meta, paramDef, async (ps) => {
-	const patrons = JSON.parse(
-		await fs.readFile(`${_dirname}/../../../../../../patrons.json`, "utf-8"),
-	);
-	return {
-		patrons: patrons.patrons,
-		sponsors: patrons.sponsors,
-	};
-});
diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts
index 6d6519e47b..65241becae 100644
--- a/packages/backend/src/server/api/endpoints/pinned-users.ts
+++ b/packages/backend/src/server/api/endpoints/pinned-users.ts
@@ -1,6 +1,6 @@
 import { IsNull } from "typeorm";
 import { Users } from "@/models/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { stringToAcct } from "backend-rs";
 import type { User } from "@/models/entities/user.js";
 import define from "@/server/api/define.js";
@@ -31,7 +31,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async (ps, me) => {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 
 	const users = await Promise.all(
 		meta.pinnedUsers
@@ -44,9 +44,7 @@ export default define(meta, paramDef, async (ps, me) => {
 			),
 	);
 
-	return await Users.packMany(
-		users.filter((x) => x !== undefined) as User[],
-		me,
-		{ detail: true },
-	);
+	return await Users.packMany(users.filter((x) => x != null) as User[], me, {
+		detail: true,
+	});
 });
diff --git a/packages/backend/src/server/api/endpoints/recommended-instances.ts b/packages/backend/src/server/api/endpoints/recommended-instances.ts
index b235678428..5c5e267b2e 100644
--- a/packages/backend/src/server/api/endpoints/recommended-instances.ts
+++ b/packages/backend/src/server/api/endpoints/recommended-instances.ts
@@ -1,5 +1,5 @@
 // import { IsNull } from 'typeorm';
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import define from "@/server/api/define.js";
 
 export const meta = {
@@ -27,7 +27,7 @@ export const paramDef = {
 } as const;
 
 export default define(meta, paramDef, async () => {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	const instances = await Promise.all(meta.recommendedInstances.map((x) => x));
 	return instances;
 });
diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts
index ff5c8d987f..b69b1b17d3 100644
--- a/packages/backend/src/server/api/endpoints/reset-password.ts
+++ b/packages/backend/src/server/api/endpoints/reset-password.ts
@@ -1,6 +1,6 @@
 import { UserProfiles, PasswordResetRequests } from "@/models/index.js";
 import define from "@/server/api/define.js";
-import { hashPassword } from "@/misc/password.js";
+import { hashPassword } from "backend-rs";
 
 export const meta = {
 	tags: ["reset password"],
@@ -32,7 +32,7 @@ export default define(meta, paramDef, async (ps, user) => {
 	}
 
 	// Generate hash of password
-	const hash = await hashPassword(ps.password);
+	const hash = hashPassword(ps.password);
 
 	await UserProfiles.update(req.userId, {
 		password: hash,
diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts
index d3b6a08074..1a1ecad688 100644
--- a/packages/backend/src/server/api/endpoints/server-info.ts
+++ b/packages/backend/src/server/api/endpoints/server-info.ts
@@ -1,7 +1,7 @@
 import * as os from "node:os";
 import si from "systeminformation";
 import define from "@/server/api/define.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 
 export const meta = {
 	requireCredential: false,
@@ -30,7 +30,7 @@ export default define(meta, paramDef, async () => {
 		}
 	}
 
-	const instanceMeta = await fetchMeta();
+	const instanceMeta = await fetchMeta(true);
 	if (!instanceMeta.enableServerMachineStats) {
 		return {
 			machine: "Not specified",
diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts
index 528d3106e0..69b3f6779b 100644
--- a/packages/backend/src/server/api/endpoints/sw/register.ts
+++ b/packages/backend/src/server/api/endpoints/sw/register.ts
@@ -1,4 +1,4 @@
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { genId } from "backend-rs";
 import { SwSubscriptions } from "@/models/index.js";
 import define from "@/server/api/define.js";
@@ -64,7 +64,7 @@ export default define(meta, paramDef, async (ps, me) => {
 		publickey: ps.publickey,
 	});
 
-	const instance = await fetchMeta(true);
+	const instance = await fetchMeta(false);
 
 	// if already subscribed
 	if (subscription != null) {
diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
index 1b43762cb6..fda4aa0bb8 100644
--- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts
+++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
@@ -4,7 +4,7 @@ import { publishAdminStream } from "@/services/stream.js";
 import { AbuseUserReports, UserProfiles, Users } from "@/models/index.js";
 import { genId } from "backend-rs";
 import { sendEmail } from "@/services/send-email.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { getUser } from "@/server/api/common/getters.js";
 import { ApiError } from "@/server/api/error.js";
 import define from "@/server/api/define.js";
@@ -86,7 +86,7 @@ export default define(meta, paramDef, async (ps, me) => {
 			],
 		});
 
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		for (const moderator of moderators) {
 			publishAdminStream(moderator.id, "newAbuseUserReport", {
 				id: report.id,
diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
index 517ef615b1..fe15ae18c0 100644
--- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
+++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
@@ -2,7 +2,7 @@ import { Brackets } from "typeorm";
 import { Followings, Users } from "@/models/index.js";
 import type { User } from "@/models/entities/user.js";
 import define from "@/server/api/define.js";
-import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
+import { sqlLikeEscape } from "backend-rs";
 
 export const meta = {
 	tags: ["users"],
diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts
index a15a0feb4b..df0701709b 100644
--- a/packages/backend/src/server/api/endpoints/users/search.ts
+++ b/packages/backend/src/server/api/endpoints/users/search.ts
@@ -2,7 +2,7 @@ import { Brackets } from "typeorm";
 import { UserProfiles, Users } from "@/models/index.js";
 import type { User } from "@/models/entities/user.js";
 import define from "@/server/api/define.js";
-import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
+import { sqlLikeEscape } from "backend-rs";
 
 export const meta = {
 	tags: ["users"],
diff --git a/packages/backend/src/server/api/limiter.ts b/packages/backend/src/server/api/limiter.ts
index f03f8754cf..d4a9353ade 100644
--- a/packages/backend/src/server/api/limiter.ts
+++ b/packages/backend/src/server/api/limiter.ts
@@ -2,7 +2,7 @@ import Limiter from "ratelimiter";
 import Logger from "@/services/logger.js";
 import { redisClient } from "@/db/redis.js";
 import type { IEndpointMeta } from "./endpoints.js";
-import { convertMilliseconds } from "@/misc/convert-milliseconds.js";
+import { formatMilliseconds } from "backend-rs";
 
 const logger = new Logger("limiter");
 
@@ -78,7 +78,7 @@ export const limiter = (
 				if (info.remaining === 0) {
 					reject({
 						message: "RATE_LIMIT_EXCEEDED",
-						remainingTime: convertMilliseconds(info.resetMs - Date.now()),
+						remainingTime: formatMilliseconds(info.resetMs - Date.now()),
 					});
 				} else {
 					ok();
diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts
index b392403578..b2259e6ed5 100644
--- a/packages/backend/src/server/api/mastodon/converters.ts
+++ b/packages/backend/src/server/api/mastodon/converters.ts
@@ -1,4 +1,4 @@
-import { Entity } from "megalodon";
+import type { Entity } from "megalodon";
 import { toMastodonId } from "backend-rs";
 
 function simpleConvert(data: any) {
@@ -15,7 +15,19 @@ export function convertAnnouncement(announcement: Entity.Announcement) {
 	return simpleConvert(announcement);
 }
 export function convertAttachment(attachment: Entity.Attachment) {
-	return simpleConvert(attachment);
+	const converted = simpleConvert(attachment);
+	// ref: https://github.com/whitescent/Mastify/pull/102
+	if (converted.meta == null) return converted;
+	const result = {
+		...converted,
+		meta: {
+			...converted.meta,
+			original: {
+				...converted.meta,
+			},
+		},
+	};
+	return result;
 }
 export function convertFilter(filter: Entity.Filter) {
 	return simpleConvert(filter);
diff --git a/packages/backend/src/server/api/mastodon/endpoints/meta.ts b/packages/backend/src/server/api/mastodon/endpoints/meta.ts
index fbad7d5ef4..5c304929a1 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/meta.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/meta.ts
@@ -1,6 +1,6 @@
 import { Entity } from "megalodon";
 import config from "@/config/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { Users, Notes } from "@/models/index.js";
 import { IsNull } from "typeorm";
 import { MAX_NOTE_TEXT_LENGTH, FILE_TYPE_BROWSERSAFE } from "@/const.js";
@@ -10,7 +10,7 @@ export async function getInstance(
 	contact: Entity.Account,
 ) {
 	const [meta, totalUsers, totalStatuses] = await Promise.all([
-		fetchMeta(),
+		fetchMeta(true),
 		Users.count({ where: { host: IsNull() } }),
 		Notes.count({ where: { userHost: IsNull() } }),
 	]);
diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts
index 5e6c0edaae..6fa70717e7 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/status.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts
@@ -1,17 +1,15 @@
 import Router from "@koa/router";
 import { getClient } from "../ApiMastodonCompatibleService.js";
-import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js";
 import querystring from "node:querystring";
 import qs from "qs";
 import { convertTimelinesArgsId, limitToInt } from "./timeline.js";
-import { fromMastodonId } from "backend-rs";
+import { fetchMeta, fromMastodonId, isUnicodeEmoji } from "backend-rs";
 import {
 	convertAccount,
 	convertAttachment,
 	convertPoll,
 	convertStatus,
 } from "../converters.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
 import { apiLogger } from "@/server/api/logger.js";
 import { inspect } from "node:util";
 
@@ -38,7 +36,7 @@ export function apiStatusMastodon(router: Router): void {
 			}
 			const text = body.status;
 			const removed = text.replace(/@\S+/g, "").replace(/\s|​/g, "");
-			const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed);
+			const isDefaultEmoji = isUnicodeEmoji(removed);
 			const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed);
 			if ((body.in_reply_to_id && isDefaultEmoji) || isCustomEmoji) {
 				const a = await client.createEmojiReaction(
@@ -213,7 +211,7 @@ export function apiStatusMastodon(router: Router): void {
 	router.post<{ Params: { id: string } }>(
 		"/v1/statuses/:id/favourite",
 		async (ctx) => {
-			const meta = await fetchMeta();
+			const meta = await fetchMeta(true);
 			const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
@@ -235,7 +233,7 @@ export function apiStatusMastodon(router: Router): void {
 	router.post<{ Params: { id: string } }>(
 		"/v1/statuses/:id/unfavourite",
 		async (ctx) => {
-			const meta = await fetchMeta();
+			const meta = await fetchMeta(true);
 			const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
 			const accessTokens = ctx.headers.authorization;
 			const client = getClient(BASE_URL, accessTokens);
diff --git a/packages/backend/src/server/api/private/signin.ts b/packages/backend/src/server/api/private/signin.ts
index 8692b01ad1..a7eb623062 100644
--- a/packages/backend/src/server/api/private/signin.ts
+++ b/packages/backend/src/server/api/private/signin.ts
@@ -10,12 +10,12 @@ import {
 	AttestationChallenges,
 } from "@/models/index.js";
 import type { ILocalUser } from "@/models/entities/user.js";
-import { genId } from "backend-rs";
 import {
-	comparePassword,
+	genId,
 	hashPassword,
-	isOldAlgorithm,
-} from "@/misc/password.js";
+	isOldPasswordAlgorithm,
+	verifyPassword,
+} from "backend-rs";
 import { verifyLogin, hash } from "@/server/api/2fa.js";
 import { randomBytes } from "node:crypto";
 import { IsNull } from "typeorm";
@@ -91,11 +91,11 @@ export default async (ctx: Koa.Context) => {
 
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
 
-	// Compare password
-	const same = await comparePassword(password, profile.password!);
+	// Compare passwords
+	const same = verifyPassword(password, profile.password!);
 
-	if (same && isOldAlgorithm(profile.password!)) {
-		profile.password = await hashPassword(password);
+	if (same && isOldPasswordAlgorithm(profile.password!)) {
+		profile.password = hashPassword(password);
 		await UserProfiles.save(profile);
 	}
 
diff --git a/packages/backend/src/server/api/private/signup.ts b/packages/backend/src/server/api/private/signup.ts
index 8179300884..5af5d65b50 100644
--- a/packages/backend/src/server/api/private/signup.ts
+++ b/packages/backend/src/server/api/private/signup.ts
@@ -1,20 +1,17 @@
 import type Koa from "koa";
 import rndstr from "rndstr";
-import { fetchMeta } from "@/misc/fetch-meta.js";
 import { verifyHcaptcha, verifyRecaptcha } from "@/misc/captcha.js";
 import { Users, RegistrationTickets, UserPendings } from "@/models/index.js";
 import { signup } from "@/server/api/common/signup.js";
 import config from "@/config/index.js";
 import { sendEmail } from "@/services/send-email.js";
-import { genId } from "backend-rs";
+import { fetchMeta, genId, hashPassword } from "backend-rs";
 import { validateEmailForAccount } from "@/services/validate-email-for-account.js";
-import { hashPassword } from "@/misc/password.js";
-import { inspect } from "node:util";
 
 export default async (ctx: Koa.Context) => {
 	const body = ctx.request.body;
 
-	const instance = await fetchMeta(true);
+	const instance = await fetchMeta(false);
 
 	// Verify *Captcha
 	// ただしテスト時はこの機構は障害となるため無効にする
@@ -85,7 +82,7 @@ export default async (ctx: Koa.Context) => {
 		const code = rndstr("a-z0-9", 16);
 
 		// Generate hash of password
-		const hash = await hashPassword(password);
+		const hash = hashPassword(password);
 
 		await UserPendings.insert({
 			id: genId(),
diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts
index 97295af57a..1760d5abf7 100644
--- a/packages/backend/src/server/api/stream/channels/global-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts
@@ -1,6 +1,5 @@
 import Channel from "../channel.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
-import { checkWordMute } from "backend-rs";
+import { checkWordMute, fetchMeta } from "backend-rs";
 import { isInstanceMuted } from "@/misc/is-instance-muted.js";
 import { isUserRelated } from "@/misc/is-user-related.js";
 import type { Packed } from "@/misc/schema.js";
@@ -17,7 +16,7 @@ export default class extends Channel {
 	}
 
 	public async init(params: any) {
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		if (meta.disableGlobalTimeline) {
 			if (this.user == null || !(this.user.isAdmin || this.user.isModerator))
 				return;
diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
index 9052e7c2a5..5100a48efd 100644
--- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
@@ -1,6 +1,5 @@
 import Channel from "../channel.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
-import { checkWordMute } from "backend-rs";
+import { checkWordMute, fetchMeta } from "backend-rs";
 import { isUserRelated } from "@/misc/is-user-related.js";
 import { isInstanceMuted } from "@/misc/is-instance-muted.js";
 import type { Packed } from "@/misc/schema.js";
@@ -17,7 +16,7 @@ export default class extends Channel {
 	}
 
 	public async init(params: any) {
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		if (
 			meta.disableLocalTimeline &&
 			!this.user!.isAdmin &&
diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts
index bd31c94f9d..2c9a38d677 100644
--- a/packages/backend/src/server/api/stream/channels/local-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -1,6 +1,5 @@
 import Channel from "../channel.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
-import { checkWordMute } from "backend-rs";
+import { checkWordMute, fetchMeta } from "backend-rs";
 import { isUserRelated } from "@/misc/is-user-related.js";
 import type { Packed } from "@/misc/schema.js";
 
@@ -16,7 +15,7 @@ export default class extends Channel {
 	}
 
 	public async init(params: any) {
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		if (meta.disableLocalTimeline) {
 			if (this.user == null || !(this.user.isAdmin || this.user.isModerator))
 				return;
diff --git a/packages/backend/src/server/api/stream/channels/recommended-timeline.ts b/packages/backend/src/server/api/stream/channels/recommended-timeline.ts
index 26c3cbfc68..5d0d6fc602 100644
--- a/packages/backend/src/server/api/stream/channels/recommended-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/recommended-timeline.ts
@@ -1,6 +1,5 @@
 import Channel from "../channel.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
-import { checkWordMute } from "backend-rs";
+import { checkWordMute, fetchMeta } from "backend-rs";
 import { isUserRelated } from "@/misc/is-user-related.js";
 import { isInstanceMuted } from "@/misc/is-instance-muted.js";
 import type { Packed } from "@/misc/schema.js";
@@ -17,7 +16,7 @@ export default class extends Channel {
 	}
 
 	public async init(params: any) {
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		if (
 			meta.disableRecommendedTimeline &&
 			!this.user!.isAdmin &&
@@ -37,7 +36,7 @@ export default class extends Channel {
 		// チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または
 		// チャンネルの投稿ではなく、全体公開のローカルの投稿 または
 		// フォローしているチャンネルの投稿 の場合だけ
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		if (
 			!(
 				note.user.host != null &&
diff --git a/packages/backend/src/server/api/streaming.ts b/packages/backend/src/server/api/streaming.ts
index a25984ec3e..12f97d8018 100644
--- a/packages/backend/src/server/api/streaming.ts
+++ b/packages/backend/src/server/api/streaming.ts
@@ -1,6 +1,6 @@
 import type * as http from "node:http";
-import { EventEmitter } from "events";
-import type { ParsedUrlQuery } from "querystring";
+import { EventEmitter } from "node:events";
+import type { ParsedUrlQuery } from "node:querystring";
 import * as websocket from "websocket";
 
 import { subscriber as redisClient } from "@/db/redis.js";
diff --git a/packages/backend/src/server/file/byte-range-readable.ts b/packages/backend/src/server/file/byte-range-readable.ts
index 96dcbc4a52..9699f95092 100644
--- a/packages/backend/src/server/file/byte-range-readable.ts
+++ b/packages/backend/src/server/file/byte-range-readable.ts
@@ -1,4 +1,4 @@
-import { Readable, ReadableOptions } from "node:stream";
+import { Readable, type ReadableOptions } from "node:stream";
 import { Buffer } from "node:buffer";
 import * as fs from "node:fs";
 
diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts
index 2a6dfdf674..17358a4758 100644
--- a/packages/backend/src/server/index.ts
+++ b/packages/backend/src/server/index.ts
@@ -13,15 +13,14 @@ import koaLogger from "koa-logger";
 import * as slow from "koa-slow";
 
 import { IsNull } from "typeorm";
-import config from "@/config/index.js";
+import config, { envOption } from "@/config/index.js";
 import Logger from "@/services/logger.js";
 import { Users } from "@/models/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { genIdenticon } from "@/misc/gen-identicon.js";
 import { createTemp } from "@/misc/create-temp.js";
 import { stringToAcct } from "backend-rs";
-import { envOption } from "@/env.js";
-import megalodon, { MegalodonInterface } from "megalodon";
+import megalodon, { type MegalodonInterface } from "megalodon";
 import activityPub from "./activitypub.js";
 import nodeinfo from "./nodeinfo.js";
 import wellKnown from "./well-known.js";
@@ -126,7 +125,7 @@ router.get("/avatar/@:acct", async (ctx) => {
 });
 
 router.get("/identicon/:x", async (ctx) => {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.enableIdenticonGeneration) {
 		const [temp, cleanup] = await createTemp();
 		await genIdenticon(ctx.params.x, fs.createWriteStream(temp));
diff --git a/packages/backend/src/server/nodeinfo.ts b/packages/backend/src/server/nodeinfo.ts
index 1cb8eb1eaf..7359878b19 100644
--- a/packages/backend/src/server/nodeinfo.ts
+++ b/packages/backend/src/server/nodeinfo.ts
@@ -1,6 +1,6 @@
 import Router from "@koa/router";
 import config from "@/config/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { Users, Notes } from "@/models/index.js";
 import { IsNull, MoreThan } from "typeorm";
 import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js";
@@ -27,7 +27,7 @@ const nodeinfo2 = async () => {
 	const now = Date.now();
 	const [meta, total, activeHalfyear, activeMonth, localPosts] =
 		await Promise.all([
-			fetchMeta(true),
+			fetchMeta(false),
 			Users.count({ where: { host: IsNull() } }),
 			Users.count({
 				where: {
diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts
index bb17cd279a..4473165be7 100644
--- a/packages/backend/src/server/web/index.ts
+++ b/packages/backend/src/server/web/index.ts
@@ -10,13 +10,12 @@ import Router from "@koa/router";
 import send from "koa-send";
 import favicon from "koa-favicon";
 import views from "@ladjs/koa-views";
-import sharp from "sharp";
 import { createBullBoard } from "@bull-board/api";
 import { BullAdapter } from "@bull-board/api/bullAdapter.js";
 import { KoaAdapter } from "@bull-board/koa";
 
 import { In, IsNull } from "typeorm";
-import { fetchMeta, metaToPugArgs } from "@/misc/fetch-meta.js";
+import { fetchMeta, metaToPugArgs } from "backend-rs";
 import config from "@/config/index.js";
 import {
 	Users,
@@ -28,8 +27,7 @@ import {
 	Emojis,
 	GalleryPosts,
 } from "@/models/index.js";
-import { stringToAcct } from "backend-rs";
-import { getNoteSummary } from "@/misc/get-note-summary.js";
+import { getNoteSummary, stringToAcct } from "backend-rs";
 import { queues } from "@/queue/queues.js";
 import { genOpenapiSpec } from "../api/openapi/gen-spec.js";
 import { urlPreviewHandler } from "./url-preview.js";
@@ -56,6 +54,10 @@ app.use(async (ctx, next) => {
 	const url = decodeURI(ctx.path);
 
 	if (url === bullBoardPath || url.startsWith(`${bullBoardPath}/`)) {
+		if (!url.startsWith(`${bullBoardPath}/static/`)) {
+			ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
+		}
+
 		const token = ctx.cookies.get("token");
 		if (token == null) {
 			ctx.status = 401;
@@ -326,7 +328,7 @@ const getFeed = async (
 	noRenotes: string,
 	noReplies: string,
 ) => {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	if (meta.privateMode) {
 		return;
 	}
@@ -475,7 +477,7 @@ const userPage: Router.Middleware = async (ctx, next) => {
 	}
 
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 	const me = profile.fields
 		? profile.fields
 				.filter((filed) => filed.value?.match(/^https?:/))
@@ -518,22 +520,22 @@ router.get("/notes/:note", async (ctx, next) => {
 	});
 
 	try {
-		if (note) {
-			const _note = await Notes.pack(note);
+		if (note != null) {
+			const packedNote = await Notes.pack(note);
 
 			const profile = await UserProfiles.findOneByOrFail({
 				userId: note.userId,
 			});
-			const meta = await fetchMeta();
+			const meta = await fetchMeta(true);
 			await ctx.render("note", {
 				...metaToPugArgs(meta),
-				note: _note,
+				note: packedNote,
 				profile,
 				avatarUrl: await Users.getAvatarUrl(
 					await Users.findOneByOrFail({ id: note.userId }),
 				),
 				// TODO: Let locale changeable by instance setting
-				summary: getNoteSummary(_note),
+				summary: getNoteSummary(note),
 			});
 
 			ctx.set("Cache-Control", "public, max-age=15");
@@ -558,7 +560,7 @@ router.get("/posts/:note", async (ctx, next) => {
 	if (note) {
 		const _note = await Notes.pack(note);
 		const profile = await UserProfiles.findOneByOrFail({ userId: note.userId });
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		await ctx.render("note", {
 			...metaToPugArgs(meta),
 			note: _note,
@@ -596,7 +598,7 @@ router.get("/@:user/pages/:page", async (ctx, next) => {
 	if (page) {
 		const _page = await Pages.pack(page);
 		const profile = await UserProfiles.findOneByOrFail({ userId: page.userId });
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		await ctx.render("page", {
 			...metaToPugArgs(meta),
 			page: _page,
@@ -628,7 +630,7 @@ router.get("/clips/:clip", async (ctx, next) => {
 	if (clip) {
 		const _clip = await Clips.pack(clip);
 		const profile = await UserProfiles.findOneByOrFail({ userId: clip.userId });
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		await ctx.render("clip", {
 			...metaToPugArgs(meta),
 			clip: _clip,
@@ -653,7 +655,7 @@ router.get("/gallery/:post", async (ctx, next) => {
 	if (post) {
 		const _post = await GalleryPosts.pack(post);
 		const profile = await UserProfiles.findOneByOrFail({ userId: post.userId });
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		await ctx.render("gallery-post", {
 			...metaToPugArgs(meta),
 			post: _post,
@@ -679,7 +681,7 @@ router.get("/channels/:channel", async (ctx, next) => {
 
 	if (channel) {
 		const _channel = await Channels.pack(channel);
-		const meta = await fetchMeta();
+		const meta = await fetchMeta(true);
 		await ctx.render("channel", {
 			...metaToPugArgs(meta),
 			channel: _channel,
@@ -732,7 +734,7 @@ router.get("/api/v1/streaming", async (ctx) => {
 
 // Render base html for all requests
 router.get("(.*)", async (ctx) => {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 
 	await ctx.render("base", {
 		...metaToPugArgs(meta),
diff --git a/packages/backend/src/server/web/manifest.ts b/packages/backend/src/server/web/manifest.ts
index bbcf639ffe..a4c615c7ab 100644
--- a/packages/backend/src/server/web/manifest.ts
+++ b/packages/backend/src/server/web/manifest.ts
@@ -1,5 +1,5 @@
 import type Koa from "koa";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import config from "@/config/index.js";
 import manifest from "./manifest.json" assert { type: "json" };
 
@@ -8,7 +8,7 @@ export const manifestHandler = async (ctx: Koa.Context) => {
 	//const res = structuredClone(manifest);
 	const res = JSON.parse(JSON.stringify(manifest));
 
-	const instance = await fetchMeta(true);
+	const instance = await fetchMeta(false);
 
 	res.short_name = instance.name || "Firefish";
 	res.name = instance.name || "Firefish";
diff --git a/packages/backend/src/server/web/url-preview.ts b/packages/backend/src/server/web/url-preview.ts
index 07d3bf7f2c..f59f3f357a 100644
--- a/packages/backend/src/server/web/url-preview.ts
+++ b/packages/backend/src/server/web/url-preview.ts
@@ -1,6 +1,6 @@
 import type Koa from "koa";
 import summaly from "summaly";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import Logger from "@/services/logger.js";
 import config from "@/config/index.js";
 import { query } from "@/prelude/url.js";
@@ -22,7 +22,7 @@ export const urlPreviewHandler = async (ctx: Koa.Context) => {
 		return;
 	}
 
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 
 	logger.info(
 		meta.summalyProxy
diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug
index bdbe153fbe..738d3ffc01 100644
--- a/packages/backend/src/server/web/views/base.pug
+++ b/packages/backend/src/server/web/views/base.pug
@@ -72,8 +72,8 @@ html
 		div#splash
 			img#splashIcon(src= splashIcon || `/static-assets/splash.svg?${ timestamp }`)
 			span#splashText
-				block randomMOTD
-					= randomMOTD
+				block randomMotd
+					= randomMotd
 			div#splashSpinner
 				<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
 					<g transform="matrix(1,0,0,1,12,12)">
diff --git a/packages/backend/src/services/chart/charts/active-users.ts b/packages/backend/src/services/chart/charts/active-users.ts
index 3f4b7e3381..067334005e 100644
--- a/packages/backend/src/services/chart/charts/active-users.ts
+++ b/packages/backend/src/services/chart/charts/active-users.ts
@@ -1,4 +1,3 @@
-import type { KVs } from "../core.js";
 import Chart from "../core.js";
 import type { User } from "@/models/entities/user.js";
 import { name, schema } from "./entities/active-users.js";
diff --git a/packages/backend/src/services/create-system-user.ts b/packages/backend/src/services/create-system-user.ts
index 345de7ad17..802c59b288 100644
--- a/packages/backend/src/services/create-system-user.ts
+++ b/packages/backend/src/services/create-system-user.ts
@@ -4,17 +4,16 @@ import { genRsaKeyPair } from "@/misc/gen-key-pair.js";
 import { User } from "@/models/entities/user.js";
 import { UserProfile } from "@/models/entities/user-profile.js";
 import { IsNull } from "typeorm";
-import { genId } from "backend-rs";
+import { genId, hashPassword } from "backend-rs";
 import { UserKeypair } from "@/models/entities/user-keypair.js";
 import { UsedUsername } from "@/models/entities/used-username.js";
 import { db } from "@/db/postgre.js";
-import { hashPassword } from "@/misc/password.js";
 
 export async function createSystemUser(username: string) {
 	const password = uuid();
 
 	// Generate hash of password
-	const hash = await hashPassword(password);
+	const hash = hashPassword(password);
 
 	// Generate secret
 	const secret = generateNativeUserToken();
diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts
index 6320277eef..d180bbabf3 100644
--- a/packages/backend/src/services/drive/add-file.ts
+++ b/packages/backend/src/services/drive/add-file.ts
@@ -6,7 +6,7 @@ import type S3 from "aws-sdk/clients/s3.js"; // TODO: migrate to SDK v3
 import sharp from "sharp";
 import { IsNull } from "typeorm";
 import { publishMainStream, publishDriveStream } from "@/services/stream.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { contentDisposition } from "@/misc/content-disposition.js";
 import { getFileInfo } from "@/misc/get-file-info.js";
 import {
@@ -16,6 +16,7 @@ import {
 	UserProfiles,
 } from "@/models/index.js";
 import { DriveFile } from "@/models/entities/drive-file.js";
+import type { DriveFileUsageHint } from "@/models/entities/drive-file.js";
 import type { IRemoteUser, User } from "@/models/entities/user.js";
 import { genId } from "backend-rs";
 import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js";
@@ -65,6 +66,7 @@ function urlPathJoin(
  * @param type Content-Type for original
  * @param hash Hash for original
  * @param size Size for original
+ * @param usage Optional usage hint for file (f.e. "userAvatar")
  */
 async function save(
 	file: DriveFile,
@@ -73,11 +75,12 @@ async function save(
 	type: string,
 	hash: string,
 	size: number,
+	usage: DriveFileUsageHint = null,
 ): Promise<DriveFile> {
 	// thunbnail, webpublic を必要なら生成
 	const alts = await generateAlts(path, type, !file.uri);
 
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 
 	if (meta.useObjectStorage) {
 		//#region ObjectStorage params
@@ -161,6 +164,7 @@ async function save(
 		file.md5 = hash;
 		file.size = size;
 		file.storedInternal = false;
+		file.usageHint = usage ?? null;
 
 		return await DriveFiles.insert(file).then((x) =>
 			DriveFiles.findOneByOrFail(x.identifiers[0]),
@@ -204,6 +208,7 @@ async function save(
 		file.type = type;
 		file.md5 = hash;
 		file.size = size;
+		file.usageHint = usage ?? null;
 
 		return await DriveFiles.insert(file).then((x) =>
 			DriveFiles.findOneByOrFail(x.identifiers[0]),
@@ -360,7 +365,7 @@ async function upload(
 	if (type === "image/apng") type = "image/png";
 	if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = "application/octet-stream";
 
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 
 	const params = {
 		Bucket: meta.objectStorageBucket,
@@ -450,6 +455,9 @@ type AddFileArgs = {
 
 	requestIp?: string | null;
 	requestHeaders?: Record<string, string> | null;
+
+	/** Whether this file has a known use case, like user avatar or instance icon */
+	usageHint?: DriveFileUsageHint;
 };
 
 /**
@@ -469,6 +477,7 @@ export async function addFile({
 	sensitive = null,
 	requestIp = null,
 	requestHeaders = null,
+	usageHint = null,
 }: AddFileArgs): Promise<DriveFile> {
 	const info = await getFileInfo(path);
 	logger.info(`${JSON.stringify(info)}`);
@@ -495,7 +504,7 @@ export async function addFile({
 		const usage = await DriveFiles.calcDriveUsageOf(user);
 		const u = await Users.findOneBy({ id: user.id });
 
-		const instance = await fetchMeta();
+		const instance = await fetchMeta(true);
 		let driveCapacity =
 			1024 *
 			1024 *
@@ -567,7 +576,7 @@ export async function addFile({
 		: null;
 
 	const folder = await fetchFolder();
-	const instance = await fetchMeta();
+	const instance = await fetchMeta(true);
 
 	let file = new DriveFile();
 	file.id = genId();
@@ -581,6 +590,7 @@ export async function addFile({
 	file.isLink = isLink;
 	file.requestIp = requestIp;
 	file.requestHeaders = requestHeaders;
+	file.usageHint = usageHint;
 	file.isSensitive = user
 		? Users.isLocalUser(user) &&
 			(instance!.markLocalFilesNsfwByDefault || profile!.alwaysMarkNsfw)
@@ -639,6 +649,7 @@ export async function addFile({
 			info.type.mime,
 			info.md5,
 			info.size,
+			usageHint,
 		);
 	}
 
diff --git a/packages/backend/src/services/drive/delete-file.ts b/packages/backend/src/services/drive/delete-file.ts
index 16c0219e71..b4b5580a1c 100644
--- a/packages/backend/src/services/drive/delete-file.ts
+++ b/packages/backend/src/services/drive/delete-file.ts
@@ -2,7 +2,7 @@ import type { DriveFile } from "@/models/entities/drive-file.js";
 import { InternalStorage } from "./internal-storage.js";
 import { DriveFiles } from "@/models/index.js";
 import { createDeleteObjectStorageFileJob } from "@/queue/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import { getS3 } from "./s3.js";
 import { v4 as uuid } from "uuid";
 
@@ -82,7 +82,7 @@ async function postProcess(file: DriveFile, isExpired = false) {
 }
 
 export async function deleteObjectStorageFile(key: string) {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 
 	const s3 = getS3(meta);
 
diff --git a/packages/backend/src/services/drive/upload-from-url.ts b/packages/backend/src/services/drive/upload-from-url.ts
index 551d3757ca..e7b084bda1 100644
--- a/packages/backend/src/services/drive/upload-from-url.ts
+++ b/packages/backend/src/services/drive/upload-from-url.ts
@@ -3,7 +3,10 @@ import type { User } from "@/models/entities/user.js";
 import { createTemp } from "@/misc/create-temp.js";
 import { downloadUrl, isPrivateIp } from "@/misc/download-url.js";
 import type { DriveFolder } from "@/models/entities/drive-folder.js";
-import type { DriveFile } from "@/models/entities/drive-file.js";
+import type {
+	DriveFile,
+	DriveFileUsageHint,
+} from "@/models/entities/drive-file.js";
 import { DriveFiles } from "@/models/index.js";
 import { driveLogger } from "./logger.js";
 import { addFile } from "./add-file.js";
@@ -13,7 +16,11 @@ const logger = driveLogger.createSubLogger("downloader");
 
 type Args = {
 	url: string;
-	user: { id: User["id"]; host: User["host"] } | null;
+	user: {
+		id: User["id"];
+		host: User["host"];
+		driveCapacityOverrideMb: User["driveCapacityOverrideMb"];
+	} | null;
 	folderId?: DriveFolder["id"] | null;
 	uri?: string | null;
 	sensitive?: boolean;
@@ -22,6 +29,7 @@ type Args = {
 	comment?: string | null;
 	requestIp?: string | null;
 	requestHeaders?: Record<string, string> | null;
+	usageHint?: DriveFileUsageHint;
 };
 
 export async function uploadFromUrl({
@@ -35,6 +43,7 @@ export async function uploadFromUrl({
 	comment = null,
 	requestIp = null,
 	requestHeaders = null,
+	usageHint = null,
 }: Args): Promise<DriveFile> {
 	const parsedUrl = new URL(url);
 	if (
@@ -75,9 +84,10 @@ export async function uploadFromUrl({
 			sensitive,
 			requestIp,
 			requestHeaders,
+			usageHint,
 		});
 		logger.succ(`Got: ${driveFile.id}`);
-		return driveFile!;
+		return driveFile;
 	} catch (e) {
 		logger.error(`Failed to create drive file:\n${inspect(e)}`);
 		throw e;
diff --git a/packages/backend/src/services/fetch-rel-me.ts b/packages/backend/src/services/fetch-rel-me.ts
index c9a37d1c88..70faa01aa7 100644
--- a/packages/backend/src/services/fetch-rel-me.ts
+++ b/packages/backend/src/services/fetch-rel-me.ts
@@ -1,4 +1,5 @@
 import { Window } from "happy-dom";
+import type { HTMLAnchorElement, HTMLLinkElement } from "happy-dom";
 import config from "@/config/index.js";
 
 async function getRelMeLinks(url: string): Promise<string[]> {
diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts
index 63eb3d00b9..f4b4454ef8 100644
--- a/packages/backend/src/services/logger.ts
+++ b/packages/backend/src/services/logger.ts
@@ -2,8 +2,7 @@ import cluster from "node:cluster";
 import chalk from "chalk";
 import { default as convertColor } from "color-convert";
 import { format as dateFormat } from "date-fns";
-import { envOption } from "@/env.js";
-import config from "@/config/index.js";
+import config, { envOption } from "@/config/index.js";
 
 import * as SyslogPro from "syslog-pro";
 
@@ -29,9 +28,9 @@ export default class Logger {
 
 		if (config.syslog) {
 			this.syslogClient = new SyslogPro.RFC5424({
-				applacationName: "Firefish",
+				applicationName: "Firefish",
 				timestamp: true,
-				encludeStructuredData: true,
+				includeStructuredData: true,
 				color: true,
 				extendedColor: true,
 				server: {
@@ -56,7 +55,6 @@ export default class Logger {
 		subDomains: Domain[] = [],
 		store = true,
 	): void {
-		if (envOption.quiet) return;
 		if (
 			!(typeof config.logLevel === "undefined") &&
 			!config.logLevel.includes(level)
@@ -146,12 +144,12 @@ export default class Logger {
 		}
 	}
 
+	// Used when the process can't continue (fatal error)
 	public error(
 		x: string | Error,
 		data?: Record<string, any> | null,
 		important = false,
 	): void {
-		// 実行を継続できない状況で使う
 		if (x instanceof Error) {
 			data = data || {};
 			data.e = x;
@@ -168,30 +166,30 @@ export default class Logger {
 		}
 	}
 
+	// Used when the process can continue but some action should be taken
 	public warn(
 		message: string,
 		data?: Record<string, any> | null,
 		important = false,
 	): void {
-		// 実行を継続できるが改善すべき状況で使う
 		this.log("warning", message, data, important);
 	}
 
+	// Used when something is successful
 	public succ(
 		message: string,
 		data?: Record<string, any> | null,
 		important = false,
 	): void {
-		// 何かに成功した状況で使う
 		this.log("success", message, data, important);
 	}
 
+	// Used for debugging (information necessary for developers but unnecessary for users)
 	public debug(
 		message: string,
 		data?: Record<string, any> | null,
 		important = false,
 	): void {
-		// Used for debugging (information necessary for developers but unnecessary for users)
 		// Fixed if statement is ignored when logLevel includes debug
 		if (
 			config.logLevel?.includes("debug") ||
@@ -202,12 +200,12 @@ export default class Logger {
 		}
 	}
 
+	// Other generic logs
 	public info(
 		message: string,
 		data?: Record<string, any> | null,
 		important = false,
 	): void {
-		// それ以外
 		this.log("info", message, data, important);
 	}
 }
diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts
index ac3515cfae..be3bf1e8b2 100644
--- a/packages/backend/src/services/note/delete.ts
+++ b/packages/backend/src/services/note/delete.ts
@@ -38,7 +38,6 @@ async function recalculateNotesCountOfLocalUser(user: {
 export default async function (
 	user: { id: User["id"]; uri: User["uri"]; host: User["host"] },
 	note: Note,
-	quiet = false,
 	deleteFromDb = true,
 ) {
 	const deletedAt = new Date();
@@ -67,87 +66,80 @@ export default async function (
 	}
 	const instanceNotesCountDecreasement: Record<string, number> = {};
 
-	if (!quiet) {
-		// Only broadcast "deleted" to local if the note is deleted from db
+	// Only broadcast "deleted" to local if the note is deleted from db
+	if (deleteFromDb) {
+		publishNoteStream(note.id, "deleted", {
+			deletedAt: deletedAt,
+		});
+	}
+
+	//#region ローカルの投稿なら削除アクティビティを配送
+	if (Users.isLocalUser(user) && !note.localOnly) {
+		let renote: Note | null = null;
+
+		// if deletd note is renote
+		if (
+			note.renoteId &&
+			note.text == null &&
+			!note.hasPoll &&
+			(note.fileIds == null || note.fileIds.length === 0)
+		) {
+			renote = await Notes.findOneBy({
+				id: note.renoteId,
+			});
+		}
+
+		const content = renderActivity(
+			renote
+				? renderUndo(
+						renderAnnounce(
+							renote.uri || `${config.url}/notes/${renote.id}`,
+							note,
+						),
+						user,
+					)
+				: renderDelete(renderTombstone(`${config.url}/notes/${note.id}`), user),
+		);
+
+		deliverToConcerned(user, note, content);
+	}
+
+	// also deliever delete activity to cascaded notes
+	for (const cascadingNote of cascadingNotes) {
 		if (deleteFromDb) {
-			publishNoteStream(note.id, "deleted", {
+			// For other notes, publishNoteStream is also required.
+			publishNoteStream(cascadingNote.id, "deleted", {
 				deletedAt: deletedAt,
 			});
 		}
 
-		//#region ローカルの投稿なら削除アクティビティを配送
-		if (Users.isLocalUser(user) && !note.localOnly) {
-			let renote: Note | null = null;
-
-			// if deletd note is renote
-			if (
-				note.renoteId &&
-				note.text == null &&
-				!note.hasPoll &&
-				(note.fileIds == null || note.fileIds.length === 0)
-			) {
-				renote = await Notes.findOneBy({
-					id: note.renoteId,
-				});
-			}
-
-			const content = renderActivity(
-				renote
-					? renderUndo(
-							renderAnnounce(
-								renote.uri || `${config.url}/notes/${renote.id}`,
-								note,
-							),
-							user,
-						)
-					: renderDelete(
-							renderTombstone(`${config.url}/notes/${note.id}`),
-							user,
-						),
-			);
-
-			deliverToConcerned(user, note, content);
+		if (!cascadingNote.user) continue;
+		if (!Users.isLocalUser(cascadingNote.user)) {
+			if (!Users.isRemoteUser(cascadingNote.user)) continue;
+			instanceNotesCountDecreasement[cascadingNote.user.host] ??= 0;
+			instanceNotesCountDecreasement[cascadingNote.user.host]++;
+			continue; // filter out remote users
 		}
+		affectedLocalUsers[cascadingNote.user.id] ??= cascadingNote.user;
+		if (cascadingNote.localOnly) continue; // filter out local-only notes
+		const content = renderActivity(
+			renderDelete(
+				renderTombstone(`${config.url}/notes/${cascadingNote.id}`),
+				cascadingNote.user,
+			),
+		);
+		deliverToConcerned(cascadingNote.user, cascadingNote, content);
+	}
+	//#endregion
 
-		// also deliever delete activity to cascaded notes
-		for (const cascadingNote of cascadingNotes) {
-			if (deleteFromDb) {
-				// For other notes, publishNoteStream is also required.
-				publishNoteStream(cascadingNote.id, "deleted", {
-					deletedAt: deletedAt,
-				});
-			}
-
-			if (!cascadingNote.user) continue;
-			if (!Users.isLocalUser(cascadingNote.user)) {
-				if (!Users.isRemoteUser(cascadingNote.user)) continue;
-				instanceNotesCountDecreasement[cascadingNote.user.host] ??= 0;
-				instanceNotesCountDecreasement[cascadingNote.user.host]++;
-				continue; // filter out remote users
-			}
-			affectedLocalUsers[cascadingNote.user.id] ??= cascadingNote.user;
-			if (cascadingNote.localOnly) continue; // filter out local-only notes
-			const content = renderActivity(
-				renderDelete(
-					renderTombstone(`${config.url}/notes/${cascadingNote.id}`),
-					cascadingNote.user,
-				),
-			);
-			deliverToConcerned(cascadingNote.user, cascadingNote, content);
-		}
-		//#endregion
-
-		if (Users.isRemoteUser(user)) {
-			instanceNotesCountDecreasement[user.host] ??= 0;
-			instanceNotesCountDecreasement[user.host]++;
-		}
-		for (const [host, count] of Object.entries(
-			instanceNotesCountDecreasement,
-		)) {
-			registerOrFetchInstanceDoc(host).then((i) => {
-				Instances.decrement({ id: i.id }, "notesCount", count);
-			});
-		}
+	if (Users.isRemoteUser(user)) {
+		instanceNotesCountDecreasement[user.host] ??= 0;
+		instanceNotesCountDecreasement[user.host]++;
+	}
+	for (const [host, count] of Object.entries(instanceNotesCountDecreasement)) {
+		registerOrFetchInstanceDoc(host).then((i) => {
+			Instances.decrement({ id: i.id }, "notesCount", count);
+		});
 	}
 
 	if (deleteFromDb) {
diff --git a/packages/backend/src/services/note/reaction/create.ts b/packages/backend/src/services/note/reaction/create.ts
index a07ffdabad..3b8b97cefd 100644
--- a/packages/backend/src/services/note/reaction/create.ts
+++ b/packages/backend/src/services/note/reaction/create.ts
@@ -2,7 +2,6 @@ import { publishNoteStream } from "@/services/stream.js";
 import { renderLike } from "@/remote/activitypub/renderer/like.js";
 import DeliverManager from "@/remote/activitypub/deliver-manager.js";
 import { renderActivity } from "@/remote/activitypub/renderer/index.js";
-import { toDbReaction, decodeReaction } from "@/misc/reaction-lib.js";
 import type { User, IRemoteUser } from "@/models/entities/user.js";
 import type { Note } from "@/models/entities/note.js";
 import {
@@ -14,7 +13,7 @@ import {
 	Blockings,
 } from "@/models/index.js";
 import { IsNull, Not } from "typeorm";
-import { genId } from "backend-rs";
+import { decodeReaction, genId, toDbReaction } from "backend-rs";
 import { createNotification } from "@/services/create-notification.js";
 import deleteReaction from "./delete.js";
 import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js";
@@ -95,7 +94,7 @@ export default async (
 
 	const emoji = await Emojis.findOne({
 		where: {
-			name: decodedReaction.name,
+			name: decodedReaction.name ?? undefined,
 			host: decodedReaction.host ?? IsNull(),
 		},
 		select: ["name", "host", "originalUrl", "publicUrl"],
diff --git a/packages/backend/src/services/note/reaction/delete.ts b/packages/backend/src/services/note/reaction/delete.ts
index 49879a0c02..e5416a78a8 100644
--- a/packages/backend/src/services/note/reaction/delete.ts
+++ b/packages/backend/src/services/note/reaction/delete.ts
@@ -7,7 +7,7 @@ import { IdentifiableError } from "@/misc/identifiable-error.js";
 import type { User, IRemoteUser } from "@/models/entities/user.js";
 import type { Note } from "@/models/entities/note.js";
 import { NoteReactions, Users, Notes } from "@/models/index.js";
-import { decodeReaction } from "@/misc/reaction-lib.js";
+import { decodeReaction } from "backend-rs";
 
 export default async (
 	user: { id: User["id"]; host: User["host"] },
diff --git a/packages/backend/src/services/note/read.ts b/packages/backend/src/services/note/read.ts
index 3c49501416..d7fda27a85 100644
--- a/packages/backend/src/services/note/read.ts
+++ b/packages/backend/src/services/note/read.ts
@@ -1,12 +1,7 @@
 import { publishMainStream } from "@/services/stream.js";
 import type { Note } from "@/models/entities/note.js";
 import type { User } from "@/models/entities/user.js";
-import {
-	NoteUnreads,
-	Users,
-	Followings,
-	ChannelFollowings,
-} from "@/models/index.js";
+import { NoteUnreads, Followings, ChannelFollowings } from "@/models/index.js";
 import { Not, IsNull, In } from "typeorm";
 import type { Channel } from "@/models/entities/channel.js";
 import { readNotificationByQuery } from "@/server/api/common/read-notification.js";
@@ -120,34 +115,4 @@ export default async function (
 			]),
 		});
 	}
-
-	// if (readAntennaNotes.length > 0) {
-	// 	await AntennaNotes.update(
-	// 		{
-	// 			antennaId: In(myAntennas.map((a) => a.id)),
-	// 			noteId: In(readAntennaNotes.map((n) => n.id)),
-	// 		},
-	// 		{
-	// 			read: true,
-	// 		},
-	// 	);
-
-	// 	// TODO: まとめてクエリしたい
-	// 	for (const antenna of myAntennas) {
-	// 		const count = await AntennaNotes.countBy({
-	// 			antennaId: antenna.id,
-	// 			read: false,
-	// 		});
-
-	// 		if (count === 0) {
-	// 			publishMainStream(userId, "readAntenna", antenna);
-	// 		}
-	// 	}
-
-	// 	Users.getHasUnreadAntenna(userId).then((unread) => {
-	// 		if (!unread) {
-	// 			publishMainStream(userId, "readAllAntennas");
-	// 		}
-	// 	});
-	// }
 }
diff --git a/packages/backend/src/services/push-notification.ts b/packages/backend/src/services/push-notification.ts
index 09749059a9..3f1f2cfb1a 100644
--- a/packages/backend/src/services/push-notification.ts
+++ b/packages/backend/src/services/push-notification.ts
@@ -1,9 +1,8 @@
 import push from "web-push";
 import config from "@/config/index.js";
 import { SwSubscriptions } from "@/models/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta, getNoteSummary } from "backend-rs";
 import type { Packed } from "@/misc/schema.js";
-import { getNoteSummary } from "@/misc/get-note-summary.js";
 
 // Defined also packages/sw/types.ts#L14-L21
 type pushNotificationsTypes = {
@@ -17,15 +16,15 @@ type pushNotificationsTypes = {
 
 // プッシュメッセージサーバーには文字数制限があるため、内容を削減します
 function truncateNotification(notification: Packed<"Notification">): any {
-	if (notification.note) {
+	if (notification.note != null) {
 		return {
 			...notification,
 			note: {
 				...notification.note,
-				// textをgetNoteSummaryしたものに置き換える
+				// replace the text with summary
 				text: getNoteSummary(
-					notification.type === "renote"
-						? (notification.note.renote as Packed<"Note">)
+					notification.type === "renote" && notification.note.renote != null
+						? notification.note.renote
 						: notification.note,
 				),
 
@@ -45,7 +44,7 @@ export async function pushNotification<T extends keyof pushNotificationsTypes>(
 	type: T,
 	body: pushNotificationsTypes[T],
 ) {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 
 	if (
 		!meta.enableServiceWorker ||
diff --git a/packages/backend/src/services/send-email.ts b/packages/backend/src/services/send-email.ts
index aa96cfc014..11a899d267 100644
--- a/packages/backend/src/services/send-email.ts
+++ b/packages/backend/src/services/send-email.ts
@@ -1,5 +1,5 @@
 import * as nodemailer from "nodemailer";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 import Logger from "@/services/logger.js";
 import config from "@/config/index.js";
 import { inspect } from "node:util";
@@ -12,7 +12,7 @@ export async function sendEmail(
 	html: string,
 	text: string,
 ) {
-	const meta = await fetchMeta(true);
+	const meta = await fetchMeta(false);
 
 	const iconUrl = `${config.url}/static-assets/mi-white.png`;
 	const emailSettingUrl = `${config.url}/settings/email`;
diff --git a/packages/backend/src/services/validate-email-for-account.ts b/packages/backend/src/services/validate-email-for-account.ts
index 4d05afcc6d..5aa091a5ac 100644
--- a/packages/backend/src/services/validate-email-for-account.ts
+++ b/packages/backend/src/services/validate-email-for-account.ts
@@ -1,12 +1,12 @@
 import { validate as validateEmail } from "deep-email-validator";
 import { UserProfiles } from "@/models/index.js";
-import { fetchMeta } from "@/misc/fetch-meta.js";
+import { fetchMeta } from "backend-rs";
 
 export async function validateEmailForAccount(emailAddress: string): Promise<{
 	available: boolean;
 	reason: null | "used" | "format" | "disposable" | "mx" | "smtp";
 }> {
-	const meta = await fetchMeta();
+	const meta = await fetchMeta(true);
 
 	const exist = await UserProfiles.countBy({
 		emailVerified: true,
diff --git a/packages/client/.eslintrc.json b/packages/client/.eslintrc.json
index b0e97b2fa6..37d80f6588 100644
--- a/packages/client/.eslintrc.json
+++ b/packages/client/.eslintrc.json
@@ -4,10 +4,10 @@
 	"ignorePatterns": ["**/*.json5"],
 	"rules": {
 		"file-progress/activate": 1,
-		"prettier/prettier": 0,
-		"one-var": ["error", "never"],
+		"prettier/prettier": "off",
+		"one-var": ["warn", "never"],
 		"@typescript-eslint/no-unused-vars": [
-			"error",
+			"warn",
 			{
 				"argsIgnorePattern": "^_",
 				"varsIgnorePattern": "^_",
diff --git a/packages/client/@types/global.d.ts b/packages/client/@types/global.d.ts
index c757482900..3ac4f09b0c 100644
--- a/packages/client/@types/global.d.ts
+++ b/packages/client/@types/global.d.ts
@@ -1,3 +1,4 @@
+// biome-ignore lint/suspicious/noExplicitAny:
 type FIXME = any;
 
 declare const _LANGS_: string[][];
diff --git a/packages/client/@types/window.d.ts b/packages/client/@types/window.d.ts
new file mode 100644
index 0000000000..1ae20c0d20
--- /dev/null
+++ b/packages/client/@types/window.d.ts
@@ -0,0 +1,6 @@
+declare global {
+	interface Window {
+		__misskey_input_ref__?: HTMLInputElement | null;
+	}
+}
+export type {};
diff --git a/packages/client/package.json b/packages/client/package.json
index 594f07c607..97a5f83ef7 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -67,9 +67,9 @@
 		"photoswipe": "5.4.3",
 		"prismjs": "1.29.0",
 		"punycode": "2.3.1",
-		"rollup": "4.14.1",
+		"rollup": "4.14.2",
 		"s-age": "1.1.2",
-		"sass": "1.74.1",
+		"sass": "1.75.0",
 		"seedrandom": "3.0.5",
 		"stringz": "2.1.0",
 		"swiper": "11.1.1",
@@ -88,6 +88,6 @@
 		"vue-draggable-plus": "^0.4.0",
 		"vue-plyr": "^7.0.0",
 		"vue-prism-editor": "2.0.0-alpha.2",
-		"vue-tsc": "2.0.12"
+		"vue-tsc": "2.0.13"
 	}
 }
diff --git a/packages/client/src/components/MkAbuseReport.vue b/packages/client/src/components/MkAbuseReport.vue
index 5536523948..b190652052 100644
--- a/packages/client/src/components/MkAbuseReport.vue
+++ b/packages/client/src/components/MkAbuseReport.vue
@@ -67,6 +67,7 @@
 <script lang="ts" setup>
 import { ref } from "vue";
 
+import type { entities } from "firefish-js";
 import MkButton from "@/components/MkButton.vue";
 import MkSwitch from "@/components/form/switch.vue";
 import MkKeyValue from "@/components/MkKeyValue.vue";
@@ -74,11 +75,11 @@ import * as os from "@/os";
 import { i18n } from "@/i18n";
 
 const props = defineProps<{
-	report: any;
+	report: entities.AbuseUserReport;
 }>();
 
 const emit = defineEmits<{
-	(ev: "resolved", reportId: string): void;
+	resolved: [reportId: string];
 }>();
 
 const forward = ref(props.report.forwarded);
diff --git a/packages/client/src/components/MkActiveUsersHeatmap.vue b/packages/client/src/components/MkActiveUsersHeatmap.vue
index 58eb42f5a6..8099e812f4 100644
--- a/packages/client/src/components/MkActiveUsersHeatmap.vue
+++ b/packages/client/src/components/MkActiveUsersHeatmap.vue
@@ -18,8 +18,8 @@ import { initChart } from "@/scripts/init-chart";
 
 initChart();
 
-const rootEl = shallowRef<HTMLDivElement>();
-const chartEl = shallowRef<HTMLCanvasElement>();
+const rootEl = shallowRef<HTMLDivElement | null>(null);
+const chartEl = shallowRef<HTMLCanvasElement | null>(null);
 const now = new Date();
 let chartInstance: Chart | null = null;
 const fetching = ref(true);
@@ -33,8 +33,8 @@ async function renderActiveUsersChart() {
 		chartInstance.destroy();
 	}
 
-	const wide = rootEl.value.offsetWidth > 700;
-	const narrow = rootEl.value.offsetWidth < 400;
+	const wide = rootEl.value!.offsetWidth > 700;
+	const narrow = rootEl.value!.offsetWidth < 400;
 
 	const weeks = wide ? 50 : narrow ? 10 : 25;
 	const chartLimit = 7 * weeks;
diff --git a/packages/client/src/components/MkAnnouncement.vue b/packages/client/src/components/MkAnnouncement.vue
index 4f26cd8bac..3af8e0163f 100644
--- a/packages/client/src/components/MkAnnouncement.vue
+++ b/packages/client/src/components/MkAnnouncement.vue
@@ -30,6 +30,7 @@
 
 <script lang="ts" setup>
 import { shallowRef } from "vue";
+import type { entities } from "firefish-js";
 import MkModal from "@/components/MkModal.vue";
 import MkSparkle from "@/components/MkSparkle.vue";
 import MkButton from "@/components/MkButton.vue";
@@ -37,7 +38,7 @@ import { i18n } from "@/i18n";
 import * as os from "@/os";
 
 const props = defineProps<{
-	announcement: Announcement;
+	announcement: entities.Announcement;
 }>();
 
 const { id, text, title, imageUrl, isGoodNews } = props.announcement;
@@ -45,7 +46,7 @@ const { id, text, title, imageUrl, isGoodNews } = props.announcement;
 const modal = shallowRef<InstanceType<typeof MkModal>>();
 
 const gotIt = () => {
-	modal.value.close();
+	modal.value!.close();
 	os.api("i/read-announcement", { announcementId: id });
 };
 </script>
diff --git a/packages/client/src/components/MkAutocomplete.vue b/packages/client/src/components/MkAutocomplete.vue
index 9a8582f19a..332785b467 100644
--- a/packages/client/src/components/MkAutocomplete.vue
+++ b/packages/client/src/components/MkAutocomplete.vue
@@ -62,7 +62,7 @@
 				<span v-else class="emoji">{{ emoji.emoji }}</span>
 				<span
 					class="name"
-					v-html="emoji.name.replace(q, `<b>${q}</b>`)"
+					v-html="q ? emoji.name.replace(q, `<b>${q}</b>`) : emoji.name"
 				></span>
 				<span v-if="emoji.aliasOf" class="alias"
 					>({{ emoji.aliasOf }})</span
@@ -107,7 +107,7 @@ interface EmojiDef {
 	emoji: string;
 	name: string;
 	aliasOf?: string;
-	url?: string;
+	url: string;
 	isCustomEmoji?: boolean;
 }
 
diff --git a/packages/client/src/components/MkAvatars.vue b/packages/client/src/components/MkAvatars.vue
index d92eee22c1..c1edf1e391 100644
--- a/packages/client/src/components/MkAvatars.vue
+++ b/packages/client/src/components/MkAvatars.vue
@@ -8,13 +8,14 @@
 
 <script lang="ts" setup>
 import { onMounted, ref } from "vue";
+import type { entities } from "firefish-js";
 import * as os from "@/os";
 
 const props = defineProps<{
 	userIds: string[];
 }>();
 
-const users = ref([]);
+const users = ref<entities.UserDetailed[]>([]);
 
 onMounted(async () => {
 	users.value = await os.api("users/show", {
diff --git a/packages/client/src/components/MkButton.vue b/packages/client/src/components/MkButton.vue
index 48864be62a..ab62fd166c 100644
--- a/packages/client/src/components/MkButton.vue
+++ b/packages/client/src/components/MkButton.vue
@@ -16,7 +16,7 @@
 		v-else
 		class="bghgjjyj _button"
 		:class="{ inline, primary, gradate, danger, rounded, full, mini }"
-		:to="to"
+		:to="to!"
 		@mousedown="onMousedown"
 	>
 		<div ref="ripples" class="ripples"></div>
@@ -36,6 +36,7 @@ const props = defineProps<{
 	gradate?: boolean;
 	rounded?: boolean;
 	inline?: boolean;
+	// FIXME: if `link`, `to` is necessary
 	link?: boolean;
 	to?: string;
 	autofocus?: boolean;
@@ -47,7 +48,7 @@ const props = defineProps<{
 }>();
 
 const emit = defineEmits<{
-	(ev: "click", payload: MouseEvent): void;
+	click: [payload: MouseEvent];
 }>();
 
 const el = ref<HTMLElement | null>(null);
@@ -61,11 +62,19 @@ onMounted(() => {
 	}
 });
 
-function distance(p, q): number {
+function distance(
+	p: { x: number; y: number },
+	q: { x: number; y: number },
+): number {
 	return Math.hypot(p.x - q.x, p.y - q.y);
 }
 
-function calcCircleScale(boxW, boxH, circleCenterX, circleCenterY): number {
+function calcCircleScale(
+	boxW: number,
+	boxH: number,
+	circleCenterX: number,
+	circleCenterY: number,
+): number {
 	const origin = { x: circleCenterX, y: circleCenterY };
 	const dist1 = distance({ x: 0, y: 0 }, origin);
 	const dist2 = distance({ x: boxW, y: 0 }, origin);
@@ -79,8 +88,8 @@ function onMousedown(evt: MouseEvent): void {
 	const rect = target.getBoundingClientRect();
 
 	const ripple = document.createElement("div");
-	ripple.style.top = (evt.clientY - rect.top - 1).toString() + "px";
-	ripple.style.left = (evt.clientX - rect.left - 1).toString() + "px";
+	ripple.style.top = `${(evt.clientY - rect.top - 1).toString()}px`;
+	ripple.style.left = `${(evt.clientX - rect.left - 1).toString()}px`;
 
 	ripples.value!.appendChild(ripple);
 
@@ -97,7 +106,7 @@ function onMousedown(evt: MouseEvent): void {
 	vibrate(10);
 
 	window.setTimeout(() => {
-		ripple.style.transform = "scale(" + scale / 2 + ")";
+		ripple.style.transform = `scale(${scale / 2})`;
 	}, 1);
 	window.setTimeout(() => {
 		ripple.style.transition = "all 1s ease";
diff --git a/packages/client/src/components/MkCaptcha.vue b/packages/client/src/components/MkCaptcha.vue
index 146c512fb8..135eee8e90 100644
--- a/packages/client/src/components/MkCaptcha.vue
+++ b/packages/client/src/components/MkCaptcha.vue
@@ -50,7 +50,7 @@ const props = defineProps<{
 }>();
 
 const emit = defineEmits<{
-	(ev: "update:modelValue", v: string | null): void;
+	"update:modelValue": [v: string | null];
 }>();
 
 const available = ref(false);
@@ -93,7 +93,9 @@ if (loaded) {
 				src: src.value,
 			}),
 		)
-	).addEventListener("load", () => (available.value = true));
+	)
+		// biome-ignore lint/suspicious/noAssignInExpressions: assign it intentially
+		.addEventListener("load", () => (available.value = true));
 }
 
 function reset() {
diff --git a/packages/client/src/components/MkChannelFollowButton.vue b/packages/client/src/components/MkChannelFollowButton.vue
index c1910bc595..3ff907b25b 100644
--- a/packages/client/src/components/MkChannelFollowButton.vue
+++ b/packages/client/src/components/MkChannelFollowButton.vue
@@ -24,13 +24,14 @@
 
 <script lang="ts" setup>
 import { ref } from "vue";
+import type { entities } from "firefish-js";
 import * as os from "@/os";
 import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
 
 const props = withDefaults(
 	defineProps<{
-		channel: Record<string, any>;
+		channel: entities.Channel;
 		full?: boolean;
 	}>(),
 	{
@@ -38,7 +39,7 @@ const props = withDefaults(
 	},
 );
 
-const isFollowing = ref<boolean>(props.channel.isFollowing);
+const isFollowing = ref<boolean>(props.channel.isFollowing ?? false);
 const wait = ref(false);
 
 async function onClick() {
diff --git a/packages/client/src/components/MkChannelList.vue b/packages/client/src/components/MkChannelList.vue
index 4b5937e4db..5030f28663 100644
--- a/packages/client/src/components/MkChannelList.vue
+++ b/packages/client/src/components/MkChannelList.vue
@@ -11,7 +11,7 @@
 			</div>
 		</template>
 
-		<template #default="{ items }">
+		<template #default="{ items }: { items: entities.Channel[] }">
 			<MkChannelPreview
 				v-for="item in items"
 				:key="item.id"
@@ -29,14 +29,15 @@ import type { PagingOf } from "@/components/MkPagination.vue";
 import MkPagination from "@/components/MkPagination.vue";
 import { i18n } from "@/i18n";
 
-const props = withDefaults(
+withDefaults(
 	defineProps<{
 		pagination: PagingOf<entities.Channel>;
 		noGap?: boolean;
-		extractor?: (item: any) => any;
+		// TODO: this function is not used and may can be removed
+		extractor?: (item: entities.Channel) => entities.Channel;
 	}>(),
 	{
-		extractor: (item) => item,
+		extractor: (item: entities.Channel) => item,
 	},
 );
 </script>
diff --git a/packages/client/src/components/MkChannelPreview.vue b/packages/client/src/components/MkChannelPreview.vue
index f824a1b2f5..b7462f5504 100644
--- a/packages/client/src/components/MkChannelPreview.vue
+++ b/packages/client/src/components/MkChannelPreview.vue
@@ -52,11 +52,12 @@
 
 <script lang="ts" setup>
 import { computed } from "vue";
+import type { entities } from "firefish-js";
 import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
 
 const props = defineProps<{
-	channel: Record<string, any>;
+	channel: entities.Channel;
 }>();
 
 const bannerStyle = computed(() => {
diff --git a/packages/client/src/components/MkChart.vue b/packages/client/src/components/MkChart.vue
index d62f52d26f..abca01f199 100644
--- a/packages/client/src/components/MkChart.vue
+++ b/packages/client/src/components/MkChart.vue
@@ -100,9 +100,9 @@ const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
 const negate = (arr) => arr.map((x) => -x);
 const alpha = (hex, a) => {
 	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
-	const r = parseInt(result[1], 16);
-	const g = parseInt(result[2], 16);
-	const b = parseInt(result[3], 16);
+	const r = Number.parseInt(result[1], 16);
+	const g = Number.parseInt(result[2], 16);
+	const b = Number.parseInt(result[3], 16);
 	return `rgba(${r}, ${g}, ${b}, ${a})`;
 };
 
diff --git a/packages/client/src/components/MkChartTooltip.vue b/packages/client/src/components/MkChartTooltip.vue
index 659dc6d399..cafc40413b 100644
--- a/packages/client/src/components/MkChartTooltip.vue
+++ b/packages/client/src/components/MkChartTooltip.vue
@@ -28,7 +28,6 @@
 </template>
 
 <script lang="ts" setup>
-import {} from "vue";
 import MkTooltip from "./MkTooltip.vue";
 
 const props = defineProps<{
diff --git a/packages/client/src/components/MkChatPreview.vue b/packages/client/src/components/MkChatPreview.vue
index 1b330b7b2d..62235b9a22 100644
--- a/packages/client/src/components/MkChatPreview.vue
+++ b/packages/client/src/components/MkChatPreview.vue
@@ -4,14 +4,14 @@
 		:class="{
 			isMe: isMe(message),
 			isRead: message.groupId
-				? message.reads.includes(me?.id)
+				? message.reads.includes(me!.id)
 				: message.isRead,
 		}"
 		:to="
 			message.groupId
 				? `/my/messaging/group/${message.groupId}`
 				: `/my/messaging/${acct.toString(
-						isMe(message) ? message.recipient : message.user,
+						isMe(message) ? message.recipient! : message.user,
 					)}`
 		"
 	>
@@ -22,27 +22,27 @@
 					message.groupId
 						? message.user
 						: isMe(message)
-							? message.recipient
+							? message.recipient!
 							: message.user
 				"
 				:show-indicator="true"
 				disable-link
 			/>
 			<header v-if="message.groupId">
-				<span class="name">{{ message.group.name }}</span>
+				<span class="name">{{ message.group!.name }}</span>
 				<MkTime :time="message.createdAt" class="time" />
 			</header>
 			<header v-else>
 				<span class="name"
 					><MkUserName
 						:user="
-							isMe(message) ? message.recipient : message.user
+							isMe(message) ? message.recipient! : message.user
 						"
 				/></span>
 				<span class="username"
 					>@{{
 						acct.toString(
-							isMe(message) ? message.recipient : message.user,
+							isMe(message) ? message.recipient! : message.user,
 						)
 					}}</span
 				>
@@ -65,16 +65,16 @@
 </template>
 
 <script lang="ts" setup>
-import { acct } from "firefish-js";
+import { acct, type entities } from "firefish-js";
 import { i18n } from "@/i18n";
 import { me } from "@/me";
 
 defineProps<{
-	message: Record<string, any>;
+	message: entities.MessagingMessage;
 }>();
 
-function isMe(message): boolean {
-	return message.userId === me?.id;
+function isMe(message: entities.MessagingMessage): boolean {
+	return message.userId === me!.id;
 }
 </script>
 
diff --git a/packages/client/src/components/MkCode.core.vue b/packages/client/src/components/MkCode.core.vue
index 11720b4f62..7e3cdac562 100644
--- a/packages/client/src/components/MkCode.core.vue
+++ b/packages/client/src/components/MkCode.core.vue
@@ -29,6 +29,7 @@ if (props.lang != null && !(props.lang in Prism.languages)) {
 	const { lang } = props;
 	loadLanguage(props.lang).then(
 		// onLoaded
+		// biome-ignore lint/suspicious/noAssignInExpressions: assign intentionally
 		() => (prismLang.value = lang),
 		// onError
 		() => {},
diff --git a/packages/client/src/components/MkContainer.vue b/packages/client/src/components/MkContainer.vue
index 404f984415..6f7f19dbf0 100644
--- a/packages/client/src/components/MkContainer.vue
+++ b/packages/client/src/components/MkContainer.vue
@@ -1,5 +1,6 @@
 <template>
 	<div
+		ref="el"
 		v-size="{ max: [380] }"
 		class="ukygtjoj _panel"
 		:class="{
@@ -59,123 +60,110 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from "vue";
+<script lang="ts" setup>
+import { onMounted, ref, watch } from "vue";
 import { i18n } from "@/i18n";
 import { defaultStore } from "@/store";
 import icon from "@/scripts/icon";
 
-export default defineComponent({
-	props: {
-		showHeader: {
-			type: Boolean,
-			required: false,
-			default: true,
-		},
-		thin: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		naked: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		foldable: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		expanded: {
-			type: Boolean,
-			required: false,
-			default: true,
-		},
-		scrollable: {
-			type: Boolean,
-			required: false,
-			default: false,
-		},
-		maxHeight: {
-			type: Number,
-			required: false,
-			default: null,
-		},
+const props = withDefaults(
+	defineProps<{
+		showHeader?: boolean;
+		thin?: boolean;
+		naked?: boolean;
+		foldable?: boolean;
+		expanded?: boolean;
+		scrollable?: boolean;
+		maxHeight?: number | null;
+	}>(),
+	{
+		showHeader: true,
+		thin: false,
+		naked: false,
+		foldable: false,
+		expanded: true,
+		scrollable: false,
+		maxHeight: null,
 	},
-	data() {
-		return {
-			showBody: this.expanded,
-			omitted: null,
-			ignoreOmit: false,
-			i18n,
-			icon,
-			defaultStore,
-		};
-	},
-	mounted() {
-		this.$watch(
-			"showBody",
-			(showBody) => {
-				const headerHeight = this.showHeader
-					? this.$refs.header.offsetHeight
-					: 0;
-				this.$el.style.minHeight = `${headerHeight}px`;
-				if (showBody) {
-					this.$el.style.flexBasis = "auto";
-				} else {
-					this.$el.style.flexBasis = `${headerHeight}px`;
-				}
-			},
-			{
-				immediate: true,
-			},
-		);
+);
 
-		this.$el.style.setProperty("--maxHeight", this.maxHeight + "px");
+const showBody = ref(props.expanded);
+const omitted = ref<boolean | null>(null);
+const ignoreOmit = ref(false);
+const el = ref<HTMLElement | null>(null);
+const header = ref<HTMLElement | null>(null);
+const content = ref<HTMLElement | null>(null);
 
-		const calcOmit = () => {
-			if (
-				this.omitted ||
-				this.ignoreOmit ||
-				this.maxHeight == null ||
-				this.$refs.content == null
-			)
-				return;
-			const height = this.$refs.content.offsetHeight;
-			this.omitted = height > this.maxHeight;
-		};
+function toggleContent(show: boolean) {
+	if (!props.foldable) return;
+	showBody.value = show;
+}
 
+function enter(el) {
+	const elementHeight = el.getBoundingClientRect().height;
+	el.style.height = 0;
+	el.offsetHeight; // reflow
+	el.style.height = `${elementHeight}px`;
+}
+function afterEnter(el) {
+	el.style.height = null;
+}
+function leave(el) {
+	const elementHeight = el.getBoundingClientRect().height;
+	el.style.height = `${elementHeight}px`;
+	el.offsetHeight; // reflow
+	el.style.height = 0;
+}
+function afterLeave(el) {
+	el.style.height = null;
+}
+
+onMounted(() => {
+	watch(
+		showBody,
+		(showBody) => {
+			const headerHeight = props.showHeader ? header.value!.offsetHeight : 0;
+			el.value!.style.minHeight = `${headerHeight}px`;
+			if (showBody) {
+				el.value!.style.flexBasis = "auto";
+			} else {
+				el.value!.style.flexBasis = `${headerHeight}px`;
+			}
+		},
+		{
+			immediate: true,
+		},
+	);
+
+	if (props.maxHeight != null) {
+		el.value!.style.setProperty("--maxHeight", `${props.maxHeight}px`);
+	}
+
+	const calcOmit = () => {
+		if (
+			omitted.value ||
+			ignoreOmit.value ||
+			props.maxHeight == null ||
+			content.value == null
+		)
+			return;
+		const height = content.value.offsetHeight;
+		omitted.value = height > props.maxHeight;
+	};
+
+	calcOmit();
+
+	new ResizeObserver((_entries, _observer) => {
 		calcOmit();
-		new ResizeObserver((entries, observer) => {
-			calcOmit();
-		}).observe(this.$refs.content);
-	},
-	methods: {
-		toggleContent(show: boolean) {
-			if (!this.foldable) return;
-			this.showBody = show;
-		},
+	}).observe(content.value!);
+});
 
-		enter(el) {
-			const elementHeight = el.getBoundingClientRect().height;
-			el.style.height = 0;
-			el.offsetHeight; // reflow
-			el.style.height = elementHeight + "px";
-		},
-		afterEnter(el) {
-			el.style.height = null;
-		},
-		leave(el) {
-			const elementHeight = el.getBoundingClientRect().height;
-			el.style.height = elementHeight + "px";
-			el.offsetHeight; // reflow
-			el.style.height = 0;
-		},
-		afterLeave(el) {
-			el.style.height = null;
-		},
-	},
+defineExpose({
+	toggleContent,
+	enter,
+	afterEnter,
+	leave,
+	afterLeave,
 });
 </script>
 
diff --git a/packages/client/src/components/MkContextMenu.vue b/packages/client/src/components/MkContextMenu.vue
index 319f7fd0fe..86ca3b3f3e 100644
--- a/packages/client/src/components/MkContextMenu.vue
+++ b/packages/client/src/components/MkContextMenu.vue
@@ -28,7 +28,7 @@ const emit = defineEmits<{
 	(ev: "closed"): void;
 }>();
 
-const rootEl = ref<HTMLDivElement>();
+const rootEl = ref<HTMLDivElement | null>(null);
 
 const zIndex = ref<number>(os.claimZIndex("high"));
 
@@ -36,8 +36,8 @@ onMounted(() => {
 	let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
 	let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
 
-	const width = rootEl.value.offsetWidth;
-	const height = rootEl.value.offsetHeight;
+	const width = rootEl.value!.offsetWidth;
+	const height = rootEl.value!.offsetHeight;
 
 	if (left + width - window.scrollX > window.innerWidth) {
 		left = window.innerWidth - width + window.scrollX;
@@ -55,8 +55,8 @@ onMounted(() => {
 		left = 0;
 	}
 
-	rootEl.value.style.top = `${top}px`;
-	rootEl.value.style.left = `${left}px`;
+	rootEl.value!.style.top = `${top}px`;
+	rootEl.value!.style.left = `${left}px`;
 
 	document.body.addEventListener("mousedown", onMousedown);
 });
diff --git a/packages/client/src/components/MkCropperDialog.vue b/packages/client/src/components/MkCropperDialog.vue
index 16b42c2f2a..955481bdad 100644
--- a/packages/client/src/components/MkCropperDialog.vue
+++ b/packages/client/src/components/MkCropperDialog.vue
@@ -68,40 +68,48 @@ let cropper: Cropper | null = null;
 const loading = ref(true);
 
 const ok = async () => {
-	const promise = new Promise<entities.DriveFile>(async (res) => {
+	async function UploadCroppedImg(): Promise<entities.DriveFile> {
 		const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas();
-		croppedCanvas.toBlob((blob) => {
-			const formData = new FormData();
-			formData.append("file", blob);
-			if (defaultStore.state.uploadFolder) {
-				formData.append("folderId", defaultStore.state.uploadFolder);
-			}
 
-			fetch(apiUrl + "/drive/files/create", {
-				method: "POST",
-				body: formData,
-				headers: {
-					authorization: `Bearer ${me.token}`,
-				},
-			})
-				.then((response) => response.json())
-				.then((f) => {
-					res(f);
-				});
+		const blob = await new Promise<Blob | null>((resolve) =>
+			croppedCanvas!.toBlob((blob) => resolve(blob)),
+		);
+
+		// MDN says `null` may be passed if the image cannot be created for any reason.
+		// But I don't think this is reachable for normal case.
+		if (blob == null) {
+			throw "Cropping image failed.";
+		}
+
+		const formData = new FormData();
+		formData.append("file", blob);
+		if (defaultStore.state.uploadFolder) {
+			formData.append("folderId", defaultStore.state.uploadFolder);
+		}
+
+		const response = await fetch(`${apiUrl}/drive/files/create`, {
+			method: "POST",
+			body: formData,
+			headers: {
+				authorization: `Bearer ${me!.token}`,
+			},
 		});
-	});
+		return await response.json();
+	}
+
+	const promise = UploadCroppedImg();
 
 	os.promiseDialog(promise);
 
 	const f = await promise;
 
 	emit("ok", f);
-	dialogEl.value.close();
+	dialogEl.value!.close();
 };
 
 const cancel = () => {
 	emit("cancel");
-	dialogEl.value.close();
+	dialogEl.value!.close();
 };
 
 const onImageLoad = () => {
@@ -114,7 +122,7 @@ const onImageLoad = () => {
 };
 
 onMounted(() => {
-	cropper = new Cropper(imgEl.value, {});
+	cropper = new Cropper(imgEl.value!, {});
 
 	const computedStyle = getComputedStyle(document.documentElement);
 
@@ -127,13 +135,13 @@ onMounted(() => {
 	selection.outlined = true;
 
 	window.setTimeout(() => {
-		cropper.getCropperImage()!.$center("contain");
+		cropper!.getCropperImage()!.$center("contain");
 		selection.$center();
 	}, 100);
 
 	// モーダルオープンアニメーションが終わったあとで再度調整
 	window.setTimeout(() => {
-		cropper.getCropperImage()!.$center("contain");
+		cropper!.getCropperImage()!.$center("contain");
 		selection.$center();
 	}, 500);
 });
diff --git a/packages/client/src/components/MkCwButton.vue b/packages/client/src/components/MkCwButton.vue
index e7a2d3d77a..ccdea3ec90 100644
--- a/packages/client/src/components/MkCwButton.vue
+++ b/packages/client/src/components/MkCwButton.vue
@@ -48,7 +48,7 @@ const toggle = () => {
 };
 
 function focus() {
-	el.value.focus();
+	el.value?.focus();
 }
 
 defineExpose({
diff --git a/packages/client/src/components/MkDialog.vue b/packages/client/src/components/MkDialog.vue
index 0b9d2cac36..a345f7dcea 100644
--- a/packages/client/src/components/MkDialog.vue
+++ b/packages/client/src/components/MkDialog.vue
@@ -104,7 +104,7 @@
 			</MkInput>
 			<MkTextarea
 				v-if="input && input.type === 'paragraph'"
-				v-model="inputValue"
+				v-model="(inputValue as string)"
 				autofocus
 				type="paragraph"
 				:placeholder="input.placeholder || undefined"
@@ -204,28 +204,44 @@ import { i18n } from "@/i18n";
 import iconify from "@/scripts/icon";
 
 interface Input {
-	type: HTMLInputElement["type"];
+	type?:
+		| "text"
+		| "number"
+		| "password"
+		| "email"
+		| "url"
+		| "date"
+		| "time"
+		| "search"
+		| "paragraph";
 	placeholder?: string | null;
 	autocomplete?: string;
-	default: string | number | null;
+	default?: string | number | null;
 	minLength?: number;
 	maxLength?: number;
 }
 
-interface Select {
-	items: {
-		value: string;
-		text: string;
-	}[];
-	groupedItems: {
-		label: string;
-		items: {
-			value: string;
-			text: string;
-		}[];
-	}[];
-	default: string | null;
-}
+type Select = {
+	default?: string | null;
+} & (
+	| {
+			items: {
+				value: string;
+				text: string;
+			}[];
+			groupedItems?: undefined;
+	  }
+	| {
+			items?: undefined;
+			groupedItems: {
+				label: string;
+				items: {
+					value: string;
+					text: string;
+				}[];
+			}[];
+	  }
+);
 
 const props = withDefaults(
 	defineProps<{
@@ -237,8 +253,8 @@ const props = withDefaults(
 			| "question"
 			| "waiting"
 			| "search";
-		title: string;
-		text?: string;
+		title?: string | null;
+		text?: string | null;
 		isPlaintext?: boolean;
 		input?: Input;
 		select?: Select;
@@ -246,7 +262,7 @@ const props = withDefaults(
 		actions?: {
 			text: string;
 			primary?: boolean;
-			callback: (...args: any[]) => void;
+			callback: () => void;
 		}[];
 		showOkButton?: boolean;
 		showCancelButton?: boolean;
@@ -268,7 +284,10 @@ const props = withDefaults(
 );
 
 const emit = defineEmits<{
-	(ev: "done", v: { canceled: boolean; result: any }): void;
+	(
+		ev: "done",
+		v: { canceled: boolean; result?: string | number | boolean | null },
+	): void;
 	(ev: "closed"): void;
 }>();
 
@@ -306,7 +325,7 @@ const okButtonDisabled = computed<boolean>(() => {
 
 const inputEl = ref<typeof MkInput>();
 
-function done(canceled: boolean, result?) {
+function done(canceled: boolean, result?: string | number | boolean | null) {
 	emit("done", { canceled, result });
 	modal.value?.close(null);
 }
@@ -342,12 +361,12 @@ function onInputKeydown(evt: KeyboardEvent) {
 	}
 }
 
-function formatDateToYYYYMMDD(date) {
-	const year = date.getFullYear();
-	const month = ("0" + (date.getMonth() + 1)).slice(-2);
-	const day = ("0" + (date.getDate() + 1)).slice(-2);
-	return `${year}-${month}-${day}`;
-}
+// function formatDateToYYYYMMDD(date) {
+// 	const year = date.getFullYear();
+// 	const month = ("0" + (date.getMonth() + 1)).slice(-2);
+// 	const day = ("0" + (date.getDate() + 1)).slice(-2);
+// 	return `${year}-${month}-${day}`;
+// }
 
 /**
  * Appends a new search parameter to the value in the input field.
@@ -355,18 +374,18 @@ function formatDateToYYYYMMDD(date) {
  * begin typing a new criteria.
  * @param value The value to append.
  */
-function appendFilter(value: string) {
-	return (
-		[
-			typeof inputValue.value === "string"
-				? inputValue.value.trim()
-				: inputValue.value,
-			value,
-		]
-			.join(" ")
-			.trim() + " "
-	);
-}
+// function appendFilter(value: string) {
+// 	return (
+// 		[
+// 			typeof inputValue.value === "string"
+// 				? inputValue.value.trim()
+// 				: inputValue.value,
+// 			value,
+// 		]
+// 			.join(" ")
+// 			.trim() + " "
+// 	);
+// }
 
 onMounted(() => {
 	document.addEventListener("keydown", onKeydown);
diff --git a/packages/client/src/components/MkDigitalClock.vue b/packages/client/src/components/MkDigitalClock.vue
index b42ecf19eb..42eabdf749 100644
--- a/packages/client/src/components/MkDigitalClock.vue
+++ b/packages/client/src/components/MkDigitalClock.vue
@@ -26,7 +26,7 @@ const props = withDefaults(
 	},
 );
 
-let intervalId;
+let intervalId: number;
 const hh = ref("");
 const mm = ref("");
 const ss = ref("");
diff --git a/packages/client/src/components/MkDonation.vue b/packages/client/src/components/MkDonation.vue
index 1c13754c13..ad9df629f4 100644
--- a/packages/client/src/components/MkDonation.vue
+++ b/packages/client/src/components/MkDonation.vue
@@ -29,7 +29,7 @@
 					<MkButton
 						v-if="instance.donationLink"
 						gradate
-						@click="openExternal(instance.donationLink)"
+						@click="openExternal(instance.donationLink!)"
 						>{{
 							i18n.t("_aboutFirefish.donateHost", {
 								host: hostname,
@@ -73,7 +73,8 @@ const emit = defineEmits<{
 	(ev: "closed"): void;
 }>();
 
-const hostname = instance.name?.length < 38 ? instance.name : host;
+const hostname =
+	instance.name?.length && instance.name?.length < 38 ? instance.name : host;
 
 const zIndex = os.claimZIndex("low");
 
@@ -97,7 +98,7 @@ function neverShow() {
 	close();
 }
 
-function openExternal(link) {
+function openExternal(link: string) {
 	window.open(link, "_blank");
 }
 </script>
diff --git a/packages/client/src/components/MkDrive.file.vue b/packages/client/src/components/MkDrive.file.vue
index 5e8a50eec2..6d348a33e7 100644
--- a/packages/client/src/components/MkDrive.file.vue
+++ b/packages/client/src/components/MkDrive.file.vue
@@ -47,6 +47,7 @@ import * as os from "@/os";
 import { i18n } from "@/i18n";
 import { me } from "@/me";
 import icon from "@/scripts/icon";
+import type { MenuItem } from "@/types/menu";
 
 const props = withDefaults(
 	defineProps<{
@@ -72,7 +73,7 @@ const title = computed(
 	() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`,
 );
 
-function getMenu() {
+function getMenu(): MenuItem[] {
 	return [
 		{
 			text: i18n.ts.rename,
@@ -180,12 +181,15 @@ function describe() {
 			image: props.file,
 		},
 		{
-			done: (result) => {
+			done: (result: {
+				canceled: boolean;
+				result?: string | null;
+			}) => {
 				if (!result || result.canceled) return;
 				const comment = result.result;
 				os.api("drive/files/update", {
 					fileId: props.file.id,
-					comment: comment.length === 0 ? null : comment,
+					comment: comment || null,
 				});
 			},
 		},
diff --git a/packages/client/src/components/MkDrive.vue b/packages/client/src/components/MkDrive.vue
index 0273e0b40e..ad2f620f6c 100644
--- a/packages/client/src/components/MkDrive.vue
+++ b/packages/client/src/components/MkDrive.vue
@@ -253,7 +253,7 @@ function onStreamDriveFolderDeleted(folderId: string) {
 	removeFolder(folderId);
 }
 
-function onDragover(ev: DragEvent): any {
+function onDragover(ev: DragEvent) {
 	if (!ev.dataTransfer) return;
 
 	// ドラッグ元が自分自身の所有するアイテムだったら
@@ -285,7 +285,7 @@ function onDragleave() {
 	draghover.value = false;
 }
 
-function onDrop(ev: DragEvent): any {
+function onDrop(ev: DragEvent) {
 	draghover.value = false;
 
 	if (!ev.dataTransfer) return;
@@ -493,14 +493,12 @@ function move(target?: entities.DriveFolder) {
 	if (!target) {
 		goRoot();
 		return;
-	} else if (typeof target === "object") {
-		target = target.id;
 	}
 
 	fetching.value = true;
 
 	os.api("drive/folders/show", {
-		folderId: target,
+		folderId: target.id,
 	}).then((folderToMove) => {
 		folder.value = folderToMove;
 		hierarchyFolders.value = [];
diff --git a/packages/client/src/components/MkEmojiPicker.section.vue b/packages/client/src/components/MkEmojiPicker.section.vue
index b91195fe65..9fcd5d363d 100644
--- a/packages/client/src/components/MkEmojiPicker.section.vue
+++ b/packages/client/src/components/MkEmojiPicker.section.vue
@@ -14,7 +14,7 @@
 					class="_button"
 					@click.stop="
 						applyUnicodeSkinTone(
-							props.skinTones.indexOf(skinTone) + 1,
+							props.skinTones!.indexOf(skinTone) + 1,
 						)
 					"
 				>
diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue
index ac2e1a8111..c731fe9b0e 100644
--- a/packages/client/src/components/MkEmojiPicker.vue
+++ b/packages/client/src/components/MkEmojiPicker.vue
@@ -180,6 +180,11 @@ import { i18n } from "@/i18n";
 import { defaultStore } from "@/store";
 import icon from "@/scripts/icon";
 
+// FIXME: This variable doesn't seem to be used at all. I don't know why it was here.
+const isActive = ref<boolean>();
+
+type EmojiDef = string | entities.CustomEmoji | UnicodeEmojiDef;
+
 const props = withDefaults(
 	defineProps<{
 		showPinned?: boolean;
@@ -193,7 +198,7 @@ const props = withDefaults(
 );
 
 const emit = defineEmits<{
-	(ev: "chosen", v: string, ev: MouseEvent): void;
+	chosen: [v: string, ev?: MouseEvent];
 }>();
 
 const search = ref<HTMLInputElement>();
@@ -226,15 +231,9 @@ const unicodeEmojiSkinToneLabels = [
 	i18n.ts._skinTones?.dark ?? "Dark",
 ];
 
-const size = computed(() =>
-	props.asReactionPicker ? reactionPickerSize.value : 1,
-);
-const width = computed(() =>
-	props.asReactionPicker ? reactionPickerWidth.value : 3,
-);
-const height = computed(() =>
-	props.asReactionPicker ? reactionPickerHeight.value : 2,
-);
+const size = reactionPickerSize;
+const width = reactionPickerWidth;
+const height = reactionPickerHeight;
 const customEmojiCategories = emojiCategories;
 const customEmojis = instance.emojis;
 const q = ref<string | null>(null);
@@ -410,13 +409,17 @@ function reset() {
 	q.value = "";
 }
 
-function getKey(
-	emoji: string | entities.CustomEmoji | UnicodeEmojiDef,
-): string {
-	return typeof emoji === "string" ? emoji : emoji.emoji || `:${emoji.name}:`;
+function getKey(emoji: EmojiDef): string {
+	if (typeof emoji === "string") {
+		return emoji;
+	}
+	if ("emoji" in emoji) {
+		return emoji.emoji;
+	}
+	return `:${emoji.name}:`;
 }
 
-function chosen(emoji: any, ev?: MouseEvent) {
+function chosen(emoji: EmojiDef, ev?: MouseEvent) {
 	const el =
 		ev && ((ev.currentTarget ?? ev.target) as HTMLElement | null | undefined);
 	if (el) {
@@ -432,22 +435,33 @@ function chosen(emoji: any, ev?: MouseEvent) {
 	// 最近使った絵文字更新
 	if (!pinned.value.includes(key)) {
 		let recents = defaultStore.state.recentlyUsedEmojis;
-		recents = recents.filter((emoji: any) => emoji !== key);
+		recents = recents.filter((emoji) => emoji !== key);
 		recents.unshift(key);
 		defaultStore.set("recentlyUsedEmojis", recents.splice(0, 32));
 	}
 }
 
-function paste(event: ClipboardEvent) {
-	const paste = (event.clipboardData || window.clipboardData).getData("text");
-	if (done(paste)) {
+async function paste(event: ClipboardEvent) {
+	let pasteStr: string | null = null;
+	if (event.clipboardData) {
+		pasteStr = event.clipboardData.getData("text");
+	} else {
+		// Use native api
+		try {
+			pasteStr = await window.navigator.clipboard.readText();
+		} catch (_err) {
+			// Reading the clipboard requires permission, and the user did not give it
+		}
+	}
+	if (done(pasteStr)) {
 		event.preventDefault();
 	}
 }
 
-function done(query?: any): boolean | void {
+function done(query?: string | null): boolean {
+	// biome-ignore lint/style/noParameterAssign: assign it intentially
 	if (query == null) query = q.value;
-	if (query == null || typeof query !== "string") return;
+	if (query == null || typeof query !== "string") return false;
 
 	const q2 = query.replaceAll(":", "");
 	const exactMatchCustom = customEmojis.find((emoji) => emoji.name === q2);
@@ -470,6 +484,7 @@ function done(query?: any): boolean | void {
 		chosen(searchResultUnicode.value[0]);
 		return true;
 	}
+	return false;
 }
 
 onMounted(() => {
diff --git a/packages/client/src/components/MkEmojiPickerDialog.vue b/packages/client/src/components/MkEmojiPickerDialog.vue
index 7c3319fb9f..decf49fd38 100644
--- a/packages/client/src/components/MkEmojiPickerDialog.vue
+++ b/packages/client/src/components/MkEmojiPickerDialog.vue
@@ -39,7 +39,7 @@ import { defaultStore } from "@/store";
 withDefaults(
 	defineProps<{
 		manualShowing?: boolean | null;
-		src?: HTMLElement;
+		src?: HTMLElement | null;
 		showPinned?: boolean;
 		asReactionPicker?: boolean;
 	}>(),
@@ -51,7 +51,7 @@ withDefaults(
 );
 
 const emit = defineEmits<{
-	(ev: "done", v: any): void;
+	(ev: "done", v: string): void;
 	(ev: "close"): void;
 	(ev: "closed"): void;
 }>();
@@ -64,7 +64,7 @@ function checkForShift(ev?: MouseEvent) {
 	modal.value?.close(ev);
 }
 
-function chosen(emoji: any, ev: MouseEvent) {
+function chosen(emoji: string, ev?: MouseEvent) {
 	emit("done", emoji);
 	checkForShift(ev);
 }
diff --git a/packages/client/src/components/MkFolder.vue b/packages/client/src/components/MkFolder.vue
index 8f545406d1..1e69996054 100644
--- a/packages/client/src/components/MkFolder.vue
+++ b/packages/client/src/components/MkFolder.vue
@@ -31,72 +31,76 @@
 	</section>
 </template>
 
-<script lang="ts">
-import { defineComponent } from "vue";
+<script lang="ts" setup>
+import { ref, watch } from "vue";
 import { getUniqueId } from "@/os";
 import { defaultStore } from "@/store";
 // import icon from "@/scripts/icon";
 
 const localStoragePrefix = "ui:folder:";
 
-export default defineComponent({
-	props: {
-		expanded: {
-			type: Boolean,
-			required: false,
-			default: true,
-		},
-		persistKey: {
-			type: String,
-			required: false,
-			default: null,
-		},
+const props = withDefaults(
+	defineProps<{
+		expanded?: boolean;
+		persistKey?: string | null;
+	}>(),
+	{
+		expanded: true,
+		persistKey: null,
 	},
-	data() {
-		return {
-			bodyId: getUniqueId(),
-			showBody:
-				this.persistKey &&
-				localStorage.getItem(localStoragePrefix + this.persistKey)
-					? localStorage.getItem(localStoragePrefix + this.persistKey) === "t"
-					: this.expanded,
-			animation: defaultStore.state.animation,
-		};
-	},
-	watch: {
-		showBody() {
-			if (this.persistKey) {
-				localStorage.setItem(
-					localStoragePrefix + this.persistKey,
-					this.showBody ? "t" : "f",
-				);
-			}
-		},
-	},
-	methods: {
-		toggleContent(show: boolean) {
-			this.showBody = show;
-		},
+);
 
-		enter(el) {
-			const elementHeight = el.getBoundingClientRect().height;
-			el.style.height = 0;
-			el.offsetHeight; // reflow
-			el.style.height = elementHeight + "px";
-		},
-		afterEnter(el) {
-			el.style.height = null;
-		},
-		leave(el) {
-			const elementHeight = el.getBoundingClientRect().height;
-			el.style.height = elementHeight + "px";
-			el.offsetHeight; // reflow
-			el.style.height = 0;
-		},
-		afterLeave(el) {
-			el.style.height = null;
-		},
-	},
+const bodyId = ref(getUniqueId());
+
+const showBody = ref(
+	props.persistKey &&
+		localStorage.getItem(localStoragePrefix + props.persistKey)
+		? localStorage.getItem(localStoragePrefix + props.persistKey) === "t"
+		: props.expanded,
+);
+
+const animation = defaultStore.state.animation;
+
+watch(showBody, () => {
+	if (props.persistKey) {
+		localStorage.setItem(
+			localStoragePrefix + props.persistKey,
+			showBody.value ? "t" : "f",
+		);
+	}
+});
+
+function toggleContent(show: boolean) {
+	showBody.value = show;
+}
+
+function enter(el) {
+	const elementHeight = el.getBoundingClientRect().height;
+	el.style.height = 0;
+	el.offsetHeight; // reflow
+	// biome-ignore lint/style/useTemplate: <explanation>
+	el.style.height = elementHeight + "px";
+}
+function afterEnter(el) {
+	el.style.height = null;
+}
+function leave(el) {
+	const elementHeight = el.getBoundingClientRect().height;
+	// biome-ignore lint/style/useTemplate: <explanation>
+	el.style.height = elementHeight + "px";
+	el.offsetHeight; // reflow
+	el.style.height = 0;
+}
+function afterLeave(el) {
+	el.style.height = null;
+}
+
+defineExpose({
+	toggleContent,
+	enter,
+	afterEnter,
+	leave,
+	afterLeave,
 });
 </script>
 
diff --git a/packages/client/src/components/MkFollowButton.vue b/packages/client/src/components/MkFollowButton.vue
index ffe1de72af..0920001c7b 100644
--- a/packages/client/src/components/MkFollowButton.vue
+++ b/packages/client/src/components/MkFollowButton.vue
@@ -8,7 +8,7 @@
 		<i :class="icon('ph-dots-three-outline')"></i>
 	</button>
 	<button
-		v-if="!hideFollowButton && isSignedIn && me.id != user.id"
+		v-if="!hideFollowButton && isSignedIn && me!.id != user.id"
 		v-tooltip="full ? null : `${state} ${user.name || user.username}`"
 		class="kpoogebi _button follow-button"
 		:class="{
diff --git a/packages/client/src/components/MkForgotPassword.vue b/packages/client/src/components/MkForgotPassword.vue
index feaaa5f120..2ca1a87e87 100644
--- a/packages/client/src/components/MkForgotPassword.vue
+++ b/packages/client/src/components/MkForgotPassword.vue
@@ -3,7 +3,7 @@
 		ref="dialog"
 		:width="370"
 		:height="400"
-		@close="dialog.close()"
+		@close="dialog!.close()"
 		@closed="emit('closed')"
 	>
 		<template #header>{{ i18n.ts.forgotPassword }}</template>
@@ -76,7 +76,7 @@ const emit = defineEmits<{
 	(ev: "closed"): void;
 }>();
 
-const dialog: InstanceType<typeof XModalWindow> = ref();
+const dialog = ref<InstanceType<typeof XModalWindow> | null>(null);
 
 const username = ref("");
 const email = ref("");
@@ -89,7 +89,7 @@ async function onSubmit() {
 		email: email.value,
 	});
 	emit("done");
-	dialog.value.close();
+	dialog.value!.close();
 }
 </script>
 
diff --git a/packages/client/src/components/MkFormDialog.vue b/packages/client/src/components/MkFormDialog.vue
index 11f1d63e29..b3d02ef515 100644
--- a/packages/client/src/components/MkFormDialog.vue
+++ b/packages/client/src/components/MkFormDialog.vue
@@ -8,7 +8,7 @@
 		@click="cancel()"
 		@ok="ok()"
 		@close="cancel()"
-		@closed="$emit('closed')"
+		@closed="emit('closed')"
 	>
 		<template #header>
 			{{ title }}
@@ -17,86 +17,107 @@
 		<MkSpacer :margin-min="20" :margin-max="32">
 			<div class="_formRoot">
 				<template
-					v-for="item in Object.keys(form).filter(
-						(item) => !form[item].hidden,
-					)"
+					v-for="[formItem, formItemName] in unHiddenForms()"
 				>
 					<FormInput
-						v-if="form[item].type === 'number'"
-						v-model="values[item]"
+						v-if="formItem.type === 'number'"
+						v-model="values[formItemName]"
 						type="number"
-						:step="form[item].step || 1"
+						:step="formItem.step || 1"
 						class="_formBlock"
 					>
 						<template #label
-							><span v-text="form[item].label || item"></span
-							><span v-if="form[item].required === false">
+							><span v-text="formItem.label || formItemName"></span
+							><span v-if="formItem.required === false">
 								({{ i18n.ts.optional }})</span
 							></template
 						>
-						<template v-if="form[item].description" #caption>{{
-							form[item].description
+						<template v-if="formItem.description" #caption>{{
+							formItem.description
 						}}</template>
 					</FormInput>
 					<FormInput
 						v-else-if="
-							form[item].type === 'string' &&
-							!form[item].multiline
+							formItem.type === 'string' &&
+							!formItem.multiline
 						"
-						v-model="values[item]"
+						v-model="values[formItemName]"
 						type="text"
 						class="_formBlock"
 					>
 						<template #label
-							><span v-text="form[item].label || item"></span
-							><span v-if="form[item].required === false">
+							><span v-text="formItem.label || formItemName"></span
+							><span v-if="formItem.required === false">
 								({{ i18n.ts.optional }})</span
 							></template
 						>
-						<template v-if="form[item].description" #caption>{{
-							form[item].description
+						<template v-if="formItem.description" #caption>{{
+							formItem.description
+						}}</template>
+					</FormInput>
+					<FormInput
+						v-else-if="
+							formItem.type === 'email' ||
+							formItem.type === 'password' ||
+							formItem.type === 'url' ||
+							formItem.type === 'date' ||
+							formItem.type === 'time' ||
+							formItem.type === 'search'
+						"
+						v-model="values[formItemName]"
+						:type="formItem.type"
+						class="_formBlock"
+					>
+						<template #label
+							><span v-text="formItem.label || formItemName"></span
+							><span v-if="formItem.required === false">
+								({{ i18n.ts.optional }})</span
+							></template
+						>
+						<template v-if="formItem.description" #caption>{{
+							formItem.description
 						}}</template>
 					</FormInput>
 					<FormTextarea
 						v-else-if="
-							form[item].type === 'string' && form[item].multiline
+							formItem.type === 'string' && formItem.multiline
 						"
-						v-model="values[item]"
+						v-model="values[formItemName]"
 						class="_formBlock"
 					>
 						<template #label
-							><span v-text="form[item].label || item"></span
-							><span v-if="form[item].required === false">
+							><span v-text="formItem.label || formItemName"></span
+							><span v-if="formItem.required === false">
 								({{ i18n.ts.optional }})</span
 							></template
 						>
-						<template v-if="form[item].description" #caption>{{
-							form[item].description
+						<template v-if="formItem.description" #caption>{{
+							formItem.description
 						}}</template>
 					</FormTextarea>
 					<FormSwitch
-						v-else-if="form[item].type === 'boolean'"
-						v-model="values[item]"
+						v-else-if="formItem.type === 'boolean'"
+						v-model="values[formItemName]"
 						class="_formBlock"
 					>
-						<span v-text="form[item].label || item"></span>
-						<template v-if="form[item].description" #caption>{{
-							form[item].description
+						<span v-text="formItem.label || formItemName"></span>
+						<template v-if="formItem.description" #caption>{{
+							formItem.description
 						}}</template>
 					</FormSwitch>
 					<FormSelect
-						v-else-if="form[item].type === 'enum'"
-						v-model="values[item]"
+						v-else-if="formItem.type === 'enum'"
+						v-model="values[formItemName]"
 						class="_formBlock"
 					>
-						<template #label
-							><span v-text="form[item].label || item"></span
-							><span v-if="form[item].required === false">
+						<template #label>
+						<span v-text="formItem.label || formItemName"></span>
+						<span v-if="formItem.required === false">
 								({{ i18n.ts.optional }})</span
-							></template
 						>
+						</template>
 						<option
-							v-for="item in form[item].enum"
+							v-for="item in formItem.enum"
 							:key="item.value"
 							:value="item.value"
 						>
@@ -104,18 +125,18 @@
 						</option>
 					</FormSelect>
 					<FormRadios
-						v-else-if="form[item].type === 'radio'"
-						v-model="values[item]"
+						v-else-if="formItem.type === 'radio'"
+						v-model="values[formItemName]"
 						class="_formBlock"
 					>
 						<template #label
-							><span v-text="form[item].label || item"></span
-							><span v-if="form[item].required === false">
+							><span v-text="formItem.label || formItemName"></span
+							><span v-if="formItem.required === false">
 								({{ i18n.ts.optional }})</span
 							></template
 						>
 						<option
-							v-for="item in form[item].options"
+							v-for="item in formItem.options"
 							:key="item.value"
 							:value="item.value"
 						>
@@ -123,30 +144,30 @@
 						</option>
 					</FormRadios>
 					<FormRange
-						v-else-if="form[item].type === 'range'"
-						v-model="values[item]"
-						:min="form[item].min"
-						:max="form[item].max"
-						:step="form[item].step"
-						:text-converter="form[item].textConverter"
+						v-else-if="formItem.type === 'range'"
+						v-model="values[formItemName]"
+						:min="formItem.min"
+						:max="formItem.max"
+						:step="formItem.step"
+						:text-converter="formItem.textConverter"
 						class="_formBlock"
 					>
 						<template #label
-							><span v-text="form[item].label || item"></span
-							><span v-if="form[item].required === false">
+							><span v-text="formItem.label || formItemName"></span
+							><span v-if="formItem.required === false">
 								({{ i18n.ts.optional }})</span
 							></template
 						>
-						<template v-if="form[item].description" #caption>{{
-							form[item].description
+						<template v-if="formItem.description" #caption>{{
+							formItem.description
 						}}</template>
 					</FormRange>
 					<MkButton
-						v-else-if="form[item].type === 'button'"
+						v-else-if="formItem.type === 'button'"
 						class="_formBlock"
-						@click="form[item].action($event, values)"
+						@click="formItem.action($event, values)"
 					>
-						<span v-text="form[item].content || item"></span>
+						<span v-text="formItem.content || formItemName"></span>
 					</MkButton>
 				</template>
 			</div>
@@ -154,8 +175,8 @@
 	</XModalWindow>
 </template>
 
-<script lang="ts">
-import { defineComponent } from "vue";
+<script lang="ts" setup>
+import { ref } from "vue";
 import FormInput from "./form/input.vue";
 import FormTextarea from "./form/textarea.vue";
 import FormSwitch from "./form/switch.vue";
@@ -165,59 +186,50 @@ import MkButton from "./MkButton.vue";
 import FormRadios from "./form/radios.vue";
 import XModalWindow from "@/components/MkModalWindow.vue";
 import { i18n } from "@/i18n";
+import type { FormItemType } from "@/types/form";
 
-export default defineComponent({
-	components: {
-		XModalWindow,
-		FormInput,
-		FormTextarea,
-		FormSwitch,
-		FormSelect,
-		FormRange,
-		MkButton,
-		FormRadios,
-	},
+const props = defineProps<{
+	title: string;
+	form: Record<string, FormItemType>;
+}>();
 
-	props: {
-		title: {
-			type: String,
-			required: true,
+// biome-ignore lint/suspicious/noExplicitAny: To prevent overly complex types we have to use any here
+type ValueType = Record<string, any>;
+
+const emit = defineEmits<{
+	done: [
+		status: {
+			result?: Record<string, FormItemType["default"]>;
+			canceled?: true;
 		},
-		form: {
-			type: Object,
-			required: true,
-		},
-	},
+	];
+	closed: [];
+}>();
 
-	emits: ["done"],
+const values = ref<ValueType>({});
+const dialog = ref<InstanceType<typeof XModalWindow> | null>(null);
 
-	data() {
-		return {
-			values: {},
-			i18n,
-		};
-	},
+for (const item in props.form) {
+	values.value[item] = props.form[item].default ?? null;
+}
 
-	created() {
-		for (const item in this.form) {
-			this.values[item] = this.form[item].default ?? null;
-		}
-	},
+function unHiddenForms(): [FormItemType, string][] {
+	return Object.keys(props.form)
+		.filter((itemName) => !props.form[itemName].hidden)
+		.map((itemName) => [props.form[itemName], itemName]);
+}
 
-	methods: {
-		ok() {
-			this.$emit("done", {
-				result: this.values,
-			});
-			this.$refs.dialog.close();
-		},
+function ok() {
+	emit("done", {
+		result: values.value,
+	});
+	dialog.value!.close();
+}
 
-		cancel() {
-			this.$emit("done", {
-				canceled: true,
-			});
-			this.$refs.dialog.close();
-		},
-	},
-});
+function cancel() {
+	emit("done", {
+		canceled: true,
+	});
+	dialog.value!.close();
+}
 </script>
diff --git a/packages/client/src/components/MkFormulaCore.vue b/packages/client/src/components/MkFormulaCore.vue
index 2db4c7d00d..b6fb8b37ca 100644
--- a/packages/client/src/components/MkFormulaCore.vue
+++ b/packages/client/src/components/MkFormulaCore.vue
@@ -19,13 +19,11 @@ export default defineComponent({
 		},
 	},
 	computed: {
-		compiledFormula(): any {
-			const katexString = katex.renderToString(this.formula, {
+		compiledFormula() {
+			return katex.renderToString(this.formula, {
 				throwOnError: false,
-			} as any);
-			return this.block
-				? `<div style="text-align:center">${katexString}</div>`
-				: katexString;
+				displayMode: this.block,
+			});
 		},
 	},
 });
diff --git a/packages/client/src/components/MkGalleryPostPreview.vue b/packages/client/src/components/MkGalleryPostPreview.vue
index e393c6acc8..f4da5d20e8 100644
--- a/packages/client/src/components/MkGalleryPostPreview.vue
+++ b/packages/client/src/components/MkGalleryPostPreview.vue
@@ -2,10 +2,24 @@
 	<MkA :to="`/gallery/${post.id}`" class="ttasepnz _panel">
 		<div class="thumbnail">
 			<ImgWithBlurhash
+				v-if="post.files && post.files.length > 0"
 				class="img"
 				:src="post.files[0].thumbnailUrl"
 				:hash="post.files[0].blurhash"
 			/>
+			<div
+				v-else
+				class="_fullinfo"
+			>
+				<!-- If there is no picture
+					This can happen if the user deletes the image in the drive
+				-->
+				<img
+					src="/static-assets/badges/not-found.webp"
+					class="img"
+					:alt="i18n.ts.notFound"
+				/>
+			</div>
 		</div>
 		<article>
 			<header>
@@ -19,10 +33,12 @@
 </template>
 
 <script lang="ts" setup>
+import type { entities } from "firefish-js";
 import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue";
+import { i18n } from "@/i18n";
 
-const props = defineProps<{
-	post: any;
+defineProps<{
+	post: entities.GalleryPost;
 }>();
 </script>
 
diff --git a/packages/client/src/components/MkImageViewer.vue b/packages/client/src/components/MkImageViewer.vue
index c65136718d..27c4d421aa 100644
--- a/packages/client/src/components/MkImageViewer.vue
+++ b/packages/client/src/components/MkImageViewer.vue
@@ -2,16 +2,16 @@
 	<MkModal
 		ref="modal"
 		:z-priority="'middle'"
-		@click="modal.close()"
+		@click="modal!.close()"
 		@closed="emit('closed')"
 	>
 		<div class="xubzgfga">
 			<header>{{ image.name }}</header>
 			<img
 				:src="image.url"
-				:alt="image.comment"
-				:title="image.comment"
-				@click="modal.close()"
+				:alt="image.comment || undefined"
+				:title="image.comment || undefined"
+				@click="modal!.close()"
 			/>
 			<footer>
 				<span>{{ image.type }}</span>
@@ -33,7 +33,7 @@ import bytes from "@/filters/bytes";
 import number from "@/filters/number";
 import MkModal from "@/components/MkModal.vue";
 
-const props = withDefaults(
+withDefaults(
 	defineProps<{
 		image: entities.DriveFile;
 	}>(),
@@ -41,10 +41,10 @@ const props = withDefaults(
 );
 
 const emit = defineEmits<{
-	(ev: "closed"): void;
+	closed: [];
 }>();
 
-const modal = ref<InstanceType<typeof MkModal>>();
+const modal = ref<InstanceType<typeof MkModal> | null>(null);
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/MkImgWithBlurhash.vue b/packages/client/src/components/MkImgWithBlurhash.vue
index 5e70515a93..31aa0e1842 100644
--- a/packages/client/src/components/MkImgWithBlurhash.vue
+++ b/packages/client/src/components/MkImgWithBlurhash.vue
@@ -4,20 +4,20 @@
 		ref="canvas"
 		:width="size"
 		:height="size"
-		:title="title"
+		:title="title || undefined"
 	/>
 	<img
 		v-if="src"
 		:src="src"
-		:title="title"
+		:title="title || undefined"
 		:type="type"
-		:alt="alt"
+		:alt="alt || undefined"
 		:class="{
 			cover,
 			wide: largestDimension === 'width',
 			tall: largestDimension === 'height',
 		}"
-		:style="{ 'object-fit': cover ? 'cover' : null }"
+		:style="{ 'object-fit': cover ? 'cover' : undefined }"
 		loading="lazy"
 		@load="onLoad"
 	/>
diff --git a/packages/client/src/components/MkInstanceCardMini.vue b/packages/client/src/components/MkInstanceCardMini.vue
index c44cfa291f..7c722f07f2 100644
--- a/packages/client/src/components/MkInstanceCardMini.vue
+++ b/packages/client/src/components/MkInstanceCardMini.vue
@@ -23,17 +23,14 @@
 </template>
 
 <script lang="ts" setup>
-import { ref } from "vue";
-
 import type { entities } from "firefish-js";
-import * as os from "@/os";
 import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
 
-const props = defineProps<{
+defineProps<{
 	instance: entities.Instance;
 }>();
 
-function getInstanceIcon(instance): string {
+function getInstanceIcon(instance: entities.Instance): string {
 	return (
 		getProxiedImageUrlNullable(instance.faviconUrl, "preview") ??
 		getProxiedImageUrlNullable(instance.iconUrl, "preview") ??
diff --git a/packages/client/src/components/MkInstanceSelectDialog.vue b/packages/client/src/components/MkInstanceSelectDialog.vue
index 92c060e4dd..e1e0da550c 100644
--- a/packages/client/src/components/MkInstanceSelectDialog.vue
+++ b/packages/client/src/components/MkInstanceSelectDialog.vue
@@ -65,14 +65,14 @@ import * as os from "@/os";
 import { i18n } from "@/i18n";
 
 const emit = defineEmits<{
-	(ev: "ok", selected: entities.Instance): void;
-	(ev: "cancel"): void;
-	(ev: "closed"): void;
+	ok: [selected: entities.Instance];
+	cancel: [];
+	closed: [];
 }>();
 
 const hostname = ref("");
 const instances = ref<entities.Instance[]>([]);
-const selected = ref<entities.Instance | null>();
+const selected = ref<entities.Instance | null>(null);
 const dialogEl = ref<InstanceType<typeof XModalWindow>>();
 
 let searchOrderLatch = 0;
diff --git a/packages/client/src/components/MkInstanceStats.vue b/packages/client/src/components/MkInstanceStats.vue
index 7ae2278d9f..87b1187e2d 100644
--- a/packages/client/src/components/MkInstanceStats.vue
+++ b/packages/client/src/components/MkInstanceStats.vue
@@ -44,6 +44,7 @@
 <script lang="ts" setup>
 import { onMounted, ref, shallowRef } from "vue";
 import { Chart } from "chart.js";
+import type { entities } from "firefish-js";
 import MkSelect from "@/components/form/select.vue";
 import MkChart from "@/components/MkChart.vue";
 import { useChartTooltip } from "@/scripts/use-chart-tooltip";
@@ -67,7 +68,18 @@ const { handler: externalTooltipHandler2 } = useChartTooltip({
 	position: "middle",
 });
 
-function createDoughnut(chartEl, tooltip, data) {
+interface ColorData {
+	name: string;
+	color: string | undefined;
+	value: number;
+	onClick?: () => void;
+}
+
+function createDoughnut(
+	chartEl: HTMLCanvasElement,
+	tooltip: typeof externalTooltipHandler1,
+	data: ColorData[],
+) {
 	const chartInstance = new Chart(chartEl, {
 		type: "doughnut",
 		data: {
@@ -96,13 +108,13 @@ function createDoughnut(chartEl, tooltip, data) {
 			},
 			onClick: (ev) => {
 				const hit = chartInstance.getElementsAtEventForMode(
-					ev,
+					ev as unknown as Event,
 					"nearest",
 					{ intersect: true },
 					false,
 				)[0];
-				if (hit && data[hit.index].onClick) {
-					data[hit.index].onClick();
+				if (hit) {
+					data[hit.index].onClick?.();
 				}
 			},
 			plugins: {
@@ -124,48 +136,41 @@ function createDoughnut(chartEl, tooltip, data) {
 	return chartInstance;
 }
 
+function instance2ColorData(x: entities.Instance): ColorData {
+	return {
+		name: x.host,
+		color: x.themeColor || undefined,
+		value: x.followersCount,
+		onClick: () => {
+			os.pageWindow(`/instance-info/${x.host}`);
+		},
+	};
+}
+
 onMounted(() => {
 	os.apiGet("federation/stats", { limit: 30 }).then((fedStats) => {
 		createDoughnut(
-			subDoughnutEl.value,
+			subDoughnutEl.value!,
 			externalTooltipHandler1,
-			fedStats.topSubInstances
-				.map((x) => ({
-					name: x.host,
-					color: x.themeColor,
-					value: x.followersCount,
-					onClick: () => {
-						os.pageWindow(`/instance-info/${x.host}`);
-					},
-				}))
-				.concat([
-					{
-						name: "(other)",
-						color: "#80808080",
-						value: fedStats.otherFollowersCount,
-					},
-				]),
+			fedStats.topSubInstances.map(instance2ColorData).concat([
+				{
+					name: "(other)",
+					color: "#80808080",
+					value: fedStats.otherFollowersCount,
+				},
+			]),
 		);
 
 		createDoughnut(
-			pubDoughnutEl.value,
+			pubDoughnutEl.value!,
 			externalTooltipHandler2,
-			fedStats.topPubInstances
-				.map((x) => ({
-					name: x.host,
-					color: x.themeColor,
-					value: x.followingCount,
-					onClick: () => {
-						os.pageWindow(`/instance-info/${x.host}`);
-					},
-				}))
-				.concat([
-					{
-						name: "(other)",
-						color: "#80808080",
-						value: fedStats.otherFollowingCount,
-					},
-				]),
+			fedStats.topPubInstances.map(instance2ColorData).concat([
+				{
+					name: "(other)",
+					color: "#80808080",
+					value: fedStats.otherFollowingCount,
+				},
+			]),
 		);
 	});
 });
diff --git a/packages/client/src/components/MkInstanceTicker.vue b/packages/client/src/components/MkInstanceTicker.vue
index 46da872c2c..f27cce02a3 100644
--- a/packages/client/src/components/MkInstanceTicker.vue
+++ b/packages/client/src/components/MkInstanceTicker.vue
@@ -17,30 +17,25 @@
 <script lang="ts" setup>
 import { ref } from "vue";
 
+import type { entities } from "firefish-js";
 import { instanceName, version } from "@/config";
 import { instance as Instance } from "@/instance";
 import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
 
 const props = defineProps<{
-	instance?: {
-		faviconUrl?: string;
-		name: string;
-		themeColor?: string;
-		softwareName?: string;
-		softwareVersion?: string;
-	};
+	instance?: entities.InstanceLite;
 }>();
 
 const ticker = ref<HTMLElement | null>(null);
 
 // if no instance data is given, this is for the local instance
 const instance = props.instance ?? {
-	faviconUrl: Instance.faviconUrl || Instance.iconUrl || "/favicon.ico",
+	faviconUrl: Instance.iconUrl || "/favicon.ico",
 	name: instanceName,
 	themeColor: (
 		document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement
 	)?.content,
-	softwareName: Instance.softwareName ?? "Firefish",
+	softwareName: "Firefish",
 	softwareVersion: version,
 };
 
@@ -67,7 +62,7 @@ const commonNames = new Map<string, string>([
 	["wxwclub", "wxwClub"],
 ]);
 
-const capitalize = (s: string) => {
+const capitalize = (s?: string | null) => {
 	if (s == null) return "Unknown";
 	if (commonNames.has(s)) return commonNames.get(s);
 	return s[0].toUpperCase() + s.slice(1);
diff --git a/packages/client/src/components/MkLaunchPad.vue b/packages/client/src/components/MkLaunchPad.vue
index ddb4dc69d2..b3ca7bd34d 100644
--- a/packages/client/src/components/MkLaunchPad.vue
+++ b/packages/client/src/components/MkLaunchPad.vue
@@ -6,7 +6,7 @@
 		:anchor="anchor"
 		:transparent-bg="true"
 		:src="src"
-		@click="modal.close()"
+		@click="modal!.close()"
 		@closed="emit('closed')"
 	>
 		<div
@@ -73,7 +73,10 @@ import { deviceKind } from "@/scripts/device-kind";
 const props = withDefaults(
 	defineProps<{
 		src?: HTMLElement;
-		anchor?: { x: string; y: string };
+		anchor?: {
+			x: "left" | "center" | "right";
+			y: "top" | "center" | "bottom";
+		};
 	}>(),
 	{
 		anchor: () => ({ x: "right", y: "center" }),
@@ -109,7 +112,7 @@ const items = Object.keys(navbarItemDef)
 	}));
 
 function close() {
-	modal.value.close();
+	modal.value!.close();
 }
 </script>
 
diff --git a/packages/client/src/components/MkManyAnnouncements.vue b/packages/client/src/components/MkManyAnnouncements.vue
index c65cd0a9ab..903891b64c 100644
--- a/packages/client/src/components/MkManyAnnouncements.vue
+++ b/packages/client/src/components/MkManyAnnouncements.vue
@@ -23,7 +23,7 @@ import { i18n } from "@/i18n";
 
 const modal = shallowRef<InstanceType<typeof MkModal>>();
 const checkAnnouncements = () => {
-	modal.value.close();
+	modal.value!.close();
 	location.href = "/announcements";
 };
 </script>
diff --git a/packages/client/src/components/MkMedia.vue b/packages/client/src/components/MkMedia.vue
index ef8912f138..c05a11e448 100644
--- a/packages/client/src/components/MkMedia.vue
+++ b/packages/client/src/components/MkMedia.vue
@@ -50,7 +50,7 @@
 			>
 				<video
 					:poster="media.thumbnailUrl"
-					:aria-label="media.comment"
+					:aria-label="media.comment || undefined"
 					preload="none"
 					controls
 					playsinline
diff --git a/packages/client/src/components/MkMediaBanner.vue b/packages/client/src/components/MkMediaBanner.vue
index d703fde470..cee8425c36 100644
--- a/packages/client/src/components/MkMediaBanner.vue
+++ b/packages/client/src/components/MkMediaBanner.vue
@@ -64,7 +64,7 @@ import "vue-plyr/dist/vue-plyr.css";
 import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
 
-const props = withDefaults(
+withDefaults(
 	defineProps<{
 		media: entities.DriveFile;
 	}>(),
diff --git a/packages/client/src/components/MkMediaCaption.vue b/packages/client/src/components/MkMediaCaption.vue
index 88bf24631b..b09024adb7 100644
--- a/packages/client/src/components/MkMediaCaption.vue
+++ b/packages/client/src/components/MkMediaCaption.vue
@@ -1,5 +1,5 @@
 <template>
-	<MkModal ref="modal" @click="done(true)" @closed="$emit('closed')">
+	<MkModal ref="modal" @click="done(true)" @closed="emit('closed')">
 		<div class="container">
 			<div class="fullwidth top-caption">
 				<div class="mk-dialog">
@@ -48,9 +48,9 @@
 				<img
 					id="imgtocaption"
 					:src="image.url"
-					:alt="image.comment"
-					:title="image.comment"
-					@click="$refs.modal.close()"
+					:alt="image.comment || undefined"
+					:title="image.comment || undefined"
+					@click="modal!.close()"
 				/>
 				<footer>
 					<span>{{ image.type }}</span>
@@ -65,10 +65,11 @@
 	</MkModal>
 </template>
 
-<script lang="ts">
-import { defineComponent } from "vue";
+<script lang="ts" setup>
+import { computed, onBeforeUnmount, onMounted, ref } from "vue";
 import insertTextAtCursor from "insert-text-at-cursor";
 import { length } from "stringz";
+import type { entities } from "firefish-js";
 import * as os from "@/os";
 import MkModal from "@/components/MkModal.vue";
 import MkButton from "@/components/MkButton.vue";
@@ -77,121 +78,98 @@ import number from "@/filters/number";
 import { i18n } from "@/i18n";
 import { instance } from "@/instance";
 
-export default defineComponent({
-	components: {
-		MkModal,
-		MkButton,
-	},
-
-	props: {
-		image: {
-			type: Object,
-			required: true,
-		},
-		title: {
-			type: String,
-			required: false,
-		},
+const props = withDefaults(
+	defineProps<{
+		image: entities.DriveFile;
 		input: {
-			required: true,
-		},
-		showOkButton: {
-			type: Boolean,
-			default: true,
-		},
-		showCaptionButton: {
-			type: Boolean,
-			default: true,
-		},
-		showCancelButton: {
-			type: Boolean,
-			default: true,
-		},
-		cancelableByBgClick: {
-			type: Boolean,
-			default: true,
-		},
-	},
-
-	emits: ["done", "closed"],
-
-	data() {
-		return {
-			inputValue: this.input.default ? this.input.default : null,
-			i18n,
+			placeholder: string;
+			default: string;
 		};
+		title?: string;
+		showOkButton?: boolean;
+		showCaptionButton?: boolean;
+		showCancelButton?: boolean;
+		cancelableByBgClick?: boolean;
+	}>(),
+	{
+		showOkButton: true,
+		showCaptionButton: true,
+		showCancelButton: true,
+		cancelableByBgClick: true,
 	},
+);
 
-	computed: {
-		remainingLength(): number {
-			const maxCaptionLength = instance.maxCaptionTextLength ?? 512;
-			if (typeof this.inputValue !== "string") return maxCaptionLength;
-			return maxCaptionLength - length(this.inputValue);
-		},
-	},
+const emit = defineEmits<{
+	done: [result: { canceled: boolean; result?: string | null }];
+	closed: [];
+}>();
 
-	mounted() {
-		document.addEventListener("keydown", this.onKeydown);
-	},
+const modal = ref<InstanceType<typeof MkModal> | null>(null);
 
-	beforeUnmount() {
-		document.removeEventListener("keydown", this.onKeydown);
-	},
+const inputValue = ref(props.input.default ? props.input.default : null);
 
-	methods: {
-		bytes,
-		number,
+const remainingLength = computed(() => {
+	const maxCaptionLength = instance.maxCaptionTextLength ?? 512;
+	if (typeof inputValue.value !== "string") return maxCaptionLength;
+	return maxCaptionLength - length(inputValue.value);
+});
 
-		done(canceled, result?) {
-			this.$emit("done", { canceled, result });
-			this.$refs.modal.close();
-		},
+function done(canceled: boolean, result?: string | null) {
+	emit("done", { canceled, result });
+	modal.value!.close();
+}
 
-		async ok() {
-			if (!this.showOkButton) return;
+async function ok() {
+	if (!props.showOkButton) return;
 
-			const result = this.inputValue;
-			this.done(false, result);
-		},
+	const result = inputValue.value;
+	done(false, result);
+}
 
-		cancel() {
-			this.done(true);
-		},
+function cancel() {
+	done(true);
+}
 
-		onBgClick() {
-			if (this.cancelableByBgClick) {
-				this.cancel();
-			}
-		},
+// function onBgClick() {
+// 	if (props.cancelableByBgClick) {
+// 		cancel();
+// 	}
+// }
 
-		onKeydown(evt) {
-			if (evt.which === 27) {
-				// ESC
-				this.cancel();
-			}
-		},
+function onKeydown(evt) {
+	if (evt.which === 27) {
+		// ESC
+		cancel();
+	}
+}
 
-		onInputKeydown(evt) {
-			if (evt.which === 13) {
-				// Enter
-				if (evt.ctrlKey) {
-					evt.preventDefault();
-					evt.stopPropagation();
-					this.ok();
-				}
-			}
-		},
+function onInputKeydown(evt) {
+	if (evt.which === 13) {
+		// Enter
+		if (evt.ctrlKey) {
+			evt.preventDefault();
+			evt.stopPropagation();
+			ok();
+		}
+	}
+}
 
-		caption() {
-			const img = document.getElementById("imgtocaption") as HTMLImageElement;
-			const ta = document.getElementById("captioninput") as HTMLTextAreaElement;
-			os.api("drive/files/caption-image", {
-				url: img.src,
-			}).then((text) => {
-				insertTextAtCursor(ta, text.slice(0, 512 - ta.value.length));
-			});
-		},
-	},
+function caption() {
+	const img = document.getElementById("imgtocaption") as HTMLImageElement;
+	const ta = document.getElementById("captioninput") as HTMLTextAreaElement;
+	os.api("drive/files/caption-image", {
+		url: img.src,
+	}).then((text) => {
+		insertTextAtCursor(ta, text.slice(0, 512 - ta.value.length));
+	});
+}
+
+onMounted(() => {
+	document.addEventListener("keydown", onKeydown);
+});
+
+onBeforeUnmount(() => {
+	document.removeEventListener("keydown", onKeydown);
 });
 </script>
 
diff --git a/packages/client/src/components/MkMediaList.vue b/packages/client/src/components/MkMediaList.vue
index af93e24165..39f618779f 100644
--- a/packages/client/src/components/MkMediaList.vue
+++ b/packages/client/src/components/MkMediaList.vue
@@ -22,7 +22,7 @@
 							media.type.startsWith('video') ||
 							media.type.startsWith('image')
 						"
-						:key="media.id"
+						:key="`m-${media.id}`"
 						:class="{ image: media.type.startsWith('image') }"
 						:data-id="media.id"
 						:media="media"
@@ -30,7 +30,7 @@
 					/>
 					<XModPlayer
 						v-else-if="isModule(media)"
-						:key="media.id"
+						:key="`p-${media.id}`"
 						:module="media"
 					/>
 				</template>
@@ -48,7 +48,7 @@ import "photoswipe/style.css";
 import XBanner from "@/components/MkMediaBanner.vue";
 import XMedia from "@/components/MkMedia.vue";
 import XModPlayer from "@/components/MkModPlayer.vue";
-import * as os from "@/os";
+// import * as os from "@/os";
 import {
 	FILE_EXT_TRACKER_MODULES,
 	FILE_TYPE_BROWSERSAFE,
@@ -61,8 +61,8 @@ const props = defineProps<{
 	inDm?: boolean;
 }>();
 
-const gallery = ref(null);
-const pswpZIndex = os.claimZIndex("middle");
+const gallery = ref<HTMLElement | null>(null);
+// const pswpZIndex = os.claimZIndex("middle");
 
 onMounted(() => {
 	const lightbox = new PhotoSwipeLightbox({
@@ -79,7 +79,7 @@ onMounted(() => {
 					src: media.url,
 					w: media.properties.width,
 					h: media.properties.height,
-					alt: media.comment,
+					alt: media.comment || undefined,
 				};
 				if (
 					media.properties.orientation != null &&
@@ -89,7 +89,7 @@ onMounted(() => {
 				}
 				return item;
 			}),
-		gallery: gallery.value,
+		gallery: gallery.value || undefined,
 		children: ".image",
 		thumbSelector: ".image img",
 		loop: false,
@@ -119,9 +119,13 @@ onMounted(() => {
 		// element is children
 		const { element } = itemData;
 
+		if (element == null) return;
+
 		const id = element.dataset.id;
 		const file = props.mediaList.find((media) => media.id === id);
 
+		if (file == null) return;
+
 		itemData.src = file.url;
 		itemData.w = Number(file.properties.width);
 		itemData.h = Number(file.properties.height);
@@ -132,12 +136,12 @@ onMounted(() => {
 			[itemData.w, itemData.h] = [itemData.h, itemData.w];
 		}
 		itemData.msrc = file.thumbnailUrl;
-		itemData.alt = file.comment;
+		itemData.alt = file.comment || undefined;
 		itemData.thumbCropped = true;
 	});
 
 	lightbox.on("uiRegister", () => {
-		lightbox.pswp.ui.registerElement({
+		lightbox.pswp?.ui?.registerElement({
 			name: "altText",
 			className: "pwsp__alt-text-container",
 			appendTo: "wrapper",
@@ -146,7 +150,7 @@ onMounted(() => {
 				textBox.className = "pwsp__alt-text";
 				el.appendChild(textBox);
 
-				const preventProp = function (ev: Event): void {
+				const preventProp = (ev: Event): void => {
 					ev.stopPropagation();
 				};
 
@@ -158,7 +162,7 @@ onMounted(() => {
 				el.onpointermove = preventProp;
 
 				pwsp.on("change", () => {
-					textBox.textContent = pwsp.currSlide.data.alt?.trim();
+					textBox.textContent = pwsp.currSlide?.data.alt?.trim() ?? null;
 				});
 			},
 		});
@@ -168,7 +172,7 @@ onMounted(() => {
 		history.pushState(null, "", location.href);
 		addEventListener("popstate", close);
 		// This is a workaround. Not sure why, but when clicking to open, it doesn't move focus to the photoswipe. Preventing using esc to close. However when using keyboard to open it already focuses the lightbox fine.
-		lightbox.pswp.element.focus();
+		lightbox.pswp?.element?.focus();
 	});
 	lightbox.on("close", () => {
 		removeEventListener("popstate", close);
@@ -180,7 +184,7 @@ onMounted(() => {
 	function close() {
 		removeEventListener("popstate", close);
 		history.forward();
-		lightbox.pswp.close();
+		lightbox.pswp?.close();
 	}
 });
 
@@ -198,7 +202,7 @@ const isModule = (file: entities.DriveFile): boolean => {
 	return (
 		FILE_TYPE_TRACKER_MODULES.includes(file.type) ||
 		FILE_EXT_TRACKER_MODULES.some((ext) => {
-			return file.name.toLowerCase().endsWith("." + ext);
+			return file.name.toLowerCase().endsWith(`.${ext}`);
 		})
 	);
 };
diff --git a/packages/client/src/components/MkMention.vue b/packages/client/src/components/MkMention.vue
index c2c38d313e..f943997459 100644
--- a/packages/client/src/components/MkMention.vue
+++ b/packages/client/src/components/MkMention.vue
@@ -23,7 +23,6 @@
 		:href="url"
 		target="_blank"
 		rel="noopener"
-		:style="{ background: bgCss }"
 		@click.stop
 	>
 		<span class="main">
@@ -54,7 +53,7 @@ const url = `/${canonical}`;
 const isMe =
 	isSignedIn &&
 	`@${props.username}@${toUnicode(props.host)}`.toLowerCase() ===
-		`@${me.username}@${toUnicode(localHost)}`.toLowerCase();
+		`@${me!.username}@${toUnicode(localHost)}`.toLowerCase();
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/MkMenu.child.vue b/packages/client/src/components/MkMenu.child.vue
index 688b3ce3a4..ed788c4375 100644
--- a/packages/client/src/components/MkMenu.child.vue
+++ b/packages/client/src/components/MkMenu.child.vue
@@ -37,8 +37,8 @@ function setPosition() {
 	const rect = props.targetElement.getBoundingClientRect();
 	const left = props.targetElement.offsetWidth;
 	const top = rect.top - rootRect.top - 8;
-	el.value.style.left = left + "px";
-	el.value.style.top = top + "px";
+	el.value!.style.left = `${left}px`;
+	el.value!.style.top = `${top}px`;
 }
 
 function onChildClosed(actioned?: boolean) {
@@ -58,7 +58,7 @@ onMounted(() => {
 
 defineExpose({
 	checkHit: (ev: MouseEvent) => {
-		return ev.target === el.value || el.value.contains(ev.target);
+		return ev.target === el.value || el.value?.contains(ev.target as Node);
 	},
 });
 </script>
diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue
index 2a7db40be8..a90e42fc29 100644
--- a/packages/client/src/components/MkMenu.vue
+++ b/packages/client/src/components/MkMenu.vue
@@ -89,7 +89,8 @@
 						></span>
 					</a>
 					<button
-						v-else-if="item.type === 'user' && !items.hidden"
+						v-else-if="item.type === 'user'"
+						v-show="!item.hidden"
 						class="_button item"
 						:class="{ active: item.active }"
 						:disabled="item.active"
@@ -201,6 +202,7 @@
 
 <script lang="ts" setup>
 import {
+	type Ref,
 	defineAsyncComponent,
 	onBeforeUnmount,
 	onMounted,
@@ -213,6 +215,7 @@ import type {
 	InnerMenuItem,
 	MenuAction,
 	MenuItem,
+	MenuParent,
 	MenuPending,
 } from "@/types/menu";
 import * as os from "@/os";
@@ -234,21 +237,29 @@ const props = defineProps<{
 }>();
 
 const emit = defineEmits<{
-	(ev: "close", actioned?: boolean): void;
+	close: [actioned?: boolean];
 }>();
 
 const itemsEl = ref<HTMLDivElement>();
 
-const items2: InnerMenuItem[] = ref([]);
+/**
+ * Strictly speaking, this type conversion is wrong
+ * because `ref` will deeply unpack the `ref` in `MenuSwitch`.
+ * But it performs correctly, so who cares?
+ */
+const items2 = ref([]) as Ref<InnerMenuItem[]>;
 
 const child = ref<InstanceType<typeof XChild>>();
 
 const childShowingItem = ref<MenuItem | null>();
 
+// FIXME: this is not used
+const isActive = ref();
+
 watch(
 	() => props.items,
 	() => {
-		const items: (MenuItem | MenuPending)[] = [...props.items].filter(
+		const items: (MenuItem | MenuPending)[] = props.items.filter(
 			(item) => item !== undefined,
 		);
 
@@ -288,29 +299,29 @@ function onGlobalMousedown(event: MouseEvent) {
 	if (
 		childTarget.value &&
 		(event.target === childTarget.value ||
-			childTarget.value.contains(event.target))
+			childTarget.value.contains(event.target as Node))
 	)
 		return;
-	if (child.value && child.value.checkHit(event)) return;
+	if (child.value?.checkHit(event)) return;
 	closeChild();
 }
 
 let childCloseTimer: null | number = null;
-function onItemMouseEnter(item) {
+function onItemMouseEnter(_item) {
 	childCloseTimer = window.setTimeout(() => {
 		closeChild();
 	}, 300);
 }
-function onItemMouseLeave(item) {
+function onItemMouseLeave(_item) {
 	if (childCloseTimer) window.clearTimeout(childCloseTimer);
 }
 
-async function showChildren(item: MenuItem, ev: MouseEvent) {
+async function showChildren(item: MenuParent, ev: MouseEvent) {
 	if (props.asDrawer) {
-		os.popupMenu(item.children, ev.currentTarget ?? ev.target);
+		os.popupMenu(item.children, (ev.currentTarget ?? ev.target) as HTMLElement);
 		close();
 	} else {
-		childTarget.value = ev.currentTarget ?? ev.target;
+		childTarget.value = (ev.currentTarget ?? ev.target) as HTMLElement;
 		childMenu.value = item.children;
 		childShowingItem.value = item;
 	}
diff --git a/packages/client/src/components/MkMiniChart.vue b/packages/client/src/components/MkMiniChart.vue
index bfa83f3d80..e82733c715 100644
--- a/packages/client/src/components/MkMiniChart.vue
+++ b/packages/client/src/components/MkMiniChart.vue
@@ -20,7 +20,7 @@
 			:stroke="color"
 			stroke-width="2"
 		/>
-		<circle :cx="headX" :cy="headY" r="3" :fill="color" />
+		<circle :cx="headX ?? undefined" :cy="headY ?? undefined" r="3" :fill="color" />
 	</svg>
 </template>
 
diff --git a/packages/client/src/components/MkModPlayer.vue b/packages/client/src/components/MkModPlayer.vue
index cdaa5f7040..21eecbdf6d 100644
--- a/packages/client/src/components/MkModPlayer.vue
+++ b/packages/client/src/components/MkModPlayer.vue
@@ -140,7 +140,7 @@ const patternShow = ref(false);
 const modPattern = ref<HTMLDivElement>();
 const progress = ref<typeof FormRange>();
 const position = ref(0);
-const patData = shallowRef([] as ModRow[][]);
+const patData = shallowRef<readonly ModRow[][]>([]);
 const currentPattern = ref(0);
 const nbChannels = ref(0);
 const length = ref(1);
@@ -159,7 +159,7 @@ function load() {
 			error.value = false;
 			fetching.value = false;
 		})
-		.catch((e: any) => {
+		.catch((e: unknown) => {
 			console.error(e);
 			error.value = true;
 			fetching.value = false;
@@ -293,12 +293,13 @@ function isRowActive(i: number) {
 		}
 		return true;
 	}
+	return false;
 }
 
 function indexText(i: number) {
 	let rowText = i.toString(16);
 	if (rowText.length === 1) {
-		rowText = "0" + rowText;
+		rowText = `0${rowText}`;
 	}
 	return rowText;
 }
diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue
index a55525c632..2bed9f8295 100644
--- a/packages/client/src/components/MkModal.vue
+++ b/packages/client/src/components/MkModal.vue
@@ -108,8 +108,11 @@ type ModalTypes = "popup" | "dialog" | "dialog:top" | "drawer";
 const props = withDefaults(
 	defineProps<{
 		manualShowing?: boolean | null;
-		anchor?: { x: string; y: string };
-		src?: HTMLElement;
+		anchor?: {
+			x: "left" | "center" | "right";
+			y: "top" | "center" | "bottom";
+		};
+		src?: HTMLElement | null;
 		preferType?: ModalTypes | "auto";
 		zPriority?: "low" | "middle" | "high";
 		noOverlap?: boolean;
@@ -118,7 +121,7 @@ const props = withDefaults(
 	}>(),
 	{
 		manualShowing: null,
-		src: undefined,
+		src: null,
 		anchor: () => ({ x: "center", y: "bottom" }),
 		preferType: "auto",
 		zPriority: "low",
@@ -139,6 +142,9 @@ const emit = defineEmits<{
 
 provide("modal", true);
 
+// FIXME: this may not used
+const isActive = ref();
+
 const maxHeight = ref<number>();
 const fixed = ref(false);
 const transformOrigin = ref("center");
@@ -189,8 +195,8 @@ const transitionDuration = computed(() =>
 
 let contentClicking = false;
 
-const focusedElement = document.activeElement;
-function close(_ev, opts: { useSendAnimation?: boolean } = {}) {
+const focusedElement = document.activeElement as HTMLElement;
+function close(_ev?, opts: { useSendAnimation?: boolean } = {}) {
 	// removeEventListener("popstate", close);
 	// if (props.preferType == "dialog") {
 	// 	history.forward();
@@ -204,7 +210,7 @@ function close(_ev, opts: { useSendAnimation?: boolean } = {}) {
 	showing.value = false;
 	emit("close");
 	if (!props.noReturnFocus) {
-		focusedElement.focus();
+		focusedElement?.focus();
 	}
 }
 
@@ -235,8 +241,8 @@ const align = () => {
 	const width = content.value!.offsetWidth;
 	const height = content.value!.offsetHeight;
 
-	let left: number;
-	let top: number;
+	let left = 0;
+	let top = MARGIN;
 
 	const x = srcRect.left + (fixed.value ? 0 : window.scrollX);
 	const y = srcRect.top + (fixed.value ? 0 : window.scrollY);
diff --git a/packages/client/src/components/MkModalPageWindow.vue b/packages/client/src/components/MkModalPageWindow.vue
index e9c8136ae4..a4dea9ee32 100644
--- a/packages/client/src/components/MkModalPageWindow.vue
+++ b/packages/client/src/components/MkModalPageWindow.vue
@@ -29,7 +29,7 @@
 				<button
 					class="_button"
 					:aria-label="i18n.ts.close"
-					@click="$refs.modal.close()"
+					@click="modal!.close()"
 				>
 					<i :class="icon('ph-x')"></i>
 				</button>
@@ -65,6 +65,7 @@ import type { PageMetadata } from "@/scripts/page-metadata";
 import { provideMetadataReceiver } from "@/scripts/page-metadata";
 import { Router } from "@/nirax";
 import icon from "@/scripts/icon";
+import type { MenuItem } from "@/types/menu";
 
 const props = defineProps<{
 	initialPath: string;
@@ -81,11 +82,11 @@ router.addListener("push", (ctx) => {});
 
 const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
 const rootEl = ref();
-const modal = ref<InstanceType<typeof MkModal>>();
+const modal = ref<InstanceType<typeof MkModal> | null>(null);
 const path = ref(props.initialPath);
 const width = ref(860);
 const height = ref(660);
-const history = [];
+const history: string[] = [];
 
 provide("router", router);
 provideMetadataReceiver((info) => {
@@ -95,7 +96,7 @@ provide("shouldOmitHeaderTitle", true);
 provide("shouldHeaderThin", true);
 
 const pageUrl = computed(() => url + path.value);
-const contextmenu = computed(() => {
+const contextmenu = computed((): MenuItem[] => {
 	return [
 		{
 			type: "label",
@@ -117,7 +118,7 @@ const contextmenu = computed(() => {
 			text: i18n.ts.openInNewTab,
 			action: () => {
 				window.open(pageUrl.value, "_blank");
-				modal.value.close();
+				modal.value!.close();
 			},
 		},
 		{
@@ -130,23 +131,26 @@ const contextmenu = computed(() => {
 	];
 });
 
-function navigate(path, record = true) {
+function navigate(path: string, record = true) {
 	if (record) history.push(router.getCurrentPath());
 	router.push(path);
 }
 
 function back() {
-	navigate(history.pop(), false);
+	const backTo = history.pop();
+	if (backTo) {
+		navigate(backTo, false);
+	}
 }
 
 function expand() {
 	mainRouter.push(path.value);
-	modal.value.close();
+	modal.value!.close();
 }
 
 function popout() {
 	_popout(path.value, rootEl.value);
-	modal.value.close();
+	modal.value!.close();
 }
 
 function onContextmenu(ev: MouseEvent) {
diff --git a/packages/client/src/components/MkModalWindow.vue b/packages/client/src/components/MkModalWindow.vue
index b7ba30dd17..6471bf1722 100644
--- a/packages/client/src/components/MkModalWindow.vue
+++ b/packages/client/src/components/MkModalWindow.vue
@@ -15,7 +15,7 @@
 					height: scroll
 						? height
 							? `${props.height}px`
-							: null
+							: undefined
 						: height
 							? `min(${props.height}px, 100%)`
 							: '100%',
@@ -54,7 +54,10 @@
 					</button>
 				</div>
 				<div class="body">
-					<slot></slot>
+					<slot
+						:width="width"
+						:height="height"
+					></slot>
 				</div>
 			</div>
 		</FocusTrap>
@@ -62,7 +65,7 @@
 </template>
 
 <script lang="ts" setup>
-import { shallowRef } from "vue";
+import { ref, shallowRef } from "vue";
 
 import { FocusTrap } from "focus-trap-vue";
 import MkModal from "./MkModal.vue";
@@ -93,11 +96,14 @@ const emit = defineEmits<{
 	(event: "ok"): void;
 }>();
 
+// FIXME: seems that this is not used
+const isActive = ref();
+
 const modal = shallowRef<InstanceType<typeof MkModal>>();
 const rootEl = shallowRef<HTMLElement>();
 const headerEl = shallowRef<HTMLElement>();
 
-const close = (ev) => {
+const close = (ev?) => {
 	modal.value?.close(ev);
 };
 
diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue
index 65dbd17d80..6a22652f60 100644
--- a/packages/client/src/components/MkNote.vue
+++ b/packages/client/src/components/MkNote.vue
@@ -9,7 +9,7 @@
 		v-vibrate="5"
 		:aria-label="accessibleLabel"
 		class="tkcbzcuz note-container"
-		:tabindex="!isDeleted ? '-1' : null"
+		:tabindex="!isDeleted ? '-1' : undefined"
 		:class="{ renote: isRenote }"
 	>
 		<MkNoteSub
@@ -112,9 +112,9 @@
 						:note="appearNote"
 						:detailed="true"
 						:detailed-view="detailedView"
-						:parent-id="appearNote.parentId"
+						:parent-id="appearNote.id"
 						@push="(e) => router.push(notePage(e))"
-						@focusfooter="footerEl.focus()"
+						@focusfooter="footerEl!.focus()"
 						@expanded="(e) => setPostExpanded(e)"
 					></MkSubNoteContent>
 					<div v-if="translating || translation" class="translation">
@@ -312,11 +312,17 @@ import { notePage } from "@/filters/note";
 import { deepClone } from "@/scripts/clone";
 import { getNoteSummary } from "@/scripts/get-note-summary";
 import icon from "@/scripts/icon";
+import type { NoteTranslation } from "@/types/note";
 
 const router = useRouter();
 
+type NoteType = entities.Note & {
+	_featuredId_?: string;
+	_prId_?: string;
+};
+
 const props = defineProps<{
-	note: entities.Note;
+	note: NoteType;
 	pinned?: boolean;
 	detailedView?: boolean;
 	collapsedReply?: boolean;
@@ -354,18 +360,18 @@ const isRenote =
 	note.value.fileIds.length === 0 &&
 	note.value.poll == null;
 
-const el = ref<HTMLElement>();
+const el = ref<HTMLElement | null>(null);
 const footerEl = ref<HTMLElement>();
 const menuButton = ref<HTMLElement>();
 const starButton = ref<InstanceType<typeof XStarButton>>();
-const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
+const renoteButton = ref<InstanceType<typeof XRenoteButton> | null>(null);
 const renoteTime = ref<HTMLElement>();
-const reactButton = ref<HTMLElement>();
+const reactButton = ref<HTMLElement | null>(null);
 const appearNote = computed(() =>
-	isRenote ? (note.value.renote as entities.Note) : note.value,
+	isRenote ? (note.value.renote as NoteType) : note.value,
 );
-const isMyRenote = isSignedIn && me.id === note.value.userId;
-const showContent = ref(false);
+const isMyRenote = isSignedIn && me!.id === note.value.userId;
+// const showContent = ref(false);
 const isDeleted = ref(false);
 const muted = ref(
 	getWordSoftMute(
@@ -375,7 +381,7 @@ const muted = ref(
 		defaultStore.state.mutedLangs,
 	),
 );
-const translation = ref(null);
+const translation = ref<NoteTranslation | null>(null);
 const translating = ref(false);
 const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
 const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
@@ -391,7 +397,7 @@ const isForeignLanguage: boolean =
 		return postLang !== "" && postLang !== targetLang;
 	})();
 
-async function translate_(noteId, targetLang: string) {
+async function translate_(noteId: string, targetLang: string) {
 	return await os.api("notes/translate", {
 		noteId,
 		targetLang,
@@ -421,12 +427,13 @@ async function translate() {
 const keymap = {
 	r: () => reply(true),
 	"e|a|plus": () => react(true),
-	q: () => renoteButton.value.renote(true),
+	q: () => renoteButton.value!.renote(true),
 	"up|k": focusBefore,
 	"down|j": focusAfter,
 	esc: blur,
 	"m|o": () => menu(true),
-	s: () => showContent.value !== showContent.value,
+	// FIXME: What's this?
+	// s: () => showContent.value !== showContent.value,
 };
 
 if (appearNote.value.historyId == null) {
@@ -437,12 +444,12 @@ if (appearNote.value.historyId == null) {
 	});
 }
 
-function reply(viaKeyboard = false): void {
+function reply(_viaKeyboard = false): void {
 	pleaseLogin();
 	os.post(
 		{
 			reply: appearNote.value,
-			animation: !viaKeyboard,
+			// animation: !viaKeyboard,
 		},
 		() => {
 			focus();
@@ -450,11 +457,11 @@ function reply(viaKeyboard = false): void {
 	);
 }
 
-function react(viaKeyboard = false): void {
+function react(_viaKeyboard = false): void {
 	pleaseLogin();
 	blur();
 	reactionPicker.show(
-		reactButton.value,
+		reactButton.value!,
 		(reaction) => {
 			os.api("notes/reactions/create", {
 				noteId: appearNote.value.id,
@@ -467,7 +474,7 @@ function react(viaKeyboard = false): void {
 	);
 }
 
-function undoReact(note): void {
+function undoReact(note: NoteType): void {
 	const oldReaction = note.myReaction;
 	if (!oldReaction) return;
 	os.api("notes/reactions/delete", {
@@ -481,16 +488,17 @@ const currentClipPage = inject<Ref<entities.Clip> | null>(
 );
 
 function onContextmenu(ev: MouseEvent): void {
-	const isLink = (el: HTMLElement) => {
+	const isLink = (el: HTMLElement): boolean => {
 		if (el.tagName === "A") return true;
 		// The Audio element's context menu is the browser default, such as for selecting playback speed.
 		if (el.tagName === "AUDIO") return true;
 		if (el.parentElement) {
 			return isLink(el.parentElement);
 		}
+		return false;
 	};
-	if (isLink(ev.target)) return;
-	if (window.getSelection().toString() !== "") return;
+	if (isLink(ev.target as HTMLElement)) return;
+	if (window.getSelection()?.toString() !== "") return;
 
 	if (defaultStore.state.useReactionPickerForContextMenu) {
 		ev.preventDefault();
@@ -509,7 +517,7 @@ function onContextmenu(ev: MouseEvent): void {
 						os.pageWindow(notePage(appearNote.value));
 					},
 				},
-				notePage(appearNote.value) != location.pathname
+				notePage(appearNote.value) !== location.pathname
 					? {
 							icon: `${icon("ph-arrows-out-simple")}`,
 							text: i18n.ts.showInPage,
@@ -589,11 +597,11 @@ function showRenoteMenu(viaKeyboard = false): void {
 }
 
 function focus() {
-	el.value.focus();
+	el.value!.focus();
 }
 
 function blur() {
-	el.value.blur();
+	el.value!.blur();
 }
 
 function focusBefore() {
@@ -605,12 +613,12 @@ function focusAfter() {
 }
 
 function scrollIntoView() {
-	el.value.scrollIntoView();
+	el.value!.scrollIntoView();
 }
 
 function noteClick(e) {
 	if (
-		document.getSelection().type === "Range" ||
+		document.getSelection()?.type === "Range" ||
 		props.detailedView ||
 		!expandOnNoteClick
 	) {
diff --git a/packages/client/src/components/MkNoteDetailed.vue b/packages/client/src/components/MkNoteDetailed.vue
index d17e21b2b1..b6c0d784ba 100644
--- a/packages/client/src/components/MkNoteDetailed.vue
+++ b/packages/client/src/components/MkNoteDetailed.vue
@@ -6,7 +6,7 @@
 		v-hotkey="keymap"
 		v-size="{ max: [500, 350, 300] }"
 		class="lxwezrsl _block"
-		:tabindex="!isDeleted ? '-1' : null"
+		:tabindex="!isDeleted ? '-1' : undefined"
 		:class="{ renote: isRenote }"
 	>
 		<MkNoteSub
@@ -64,7 +64,7 @@
 					)
 				}}
 			</option>
-			<option v-if="directQuotes?.length > 0" value="quotes">
+			<option v-if="directQuotes && directQuotes.length > 0" value="quotes">
 				<!-- <i :class="icon('ph-quotes')"></i> -->
 				{{
 					wordWithCount(
@@ -102,7 +102,7 @@
 			:detailed-view="true"
 			:parent-id="note.id"
 		/>
-		<MkLoading v-else-if="tab === 'quotes' && directQuotes.length > 0" />
+		<MkLoading v-else-if="tab === 'quotes' && directQuotes && directQuotes.length > 0" />
 
 		<!-- <MkPagination
 			v-if="tab === 'renotes'"
@@ -225,12 +225,12 @@ if (noteViewInterruptors.length > 0) {
 	});
 }
 
-const el = ref<HTMLElement>();
+const el = ref<HTMLElement | null>(null);
 const noteEl = ref();
 const menuButton = ref<HTMLElement>();
 const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
 const reactButton = ref<HTMLElement>();
-const showContent = ref(false);
+// const showContent = ref(false);
 const isDeleted = ref(false);
 const muted = ref(
 	getWordSoftMute(
@@ -248,7 +248,8 @@ const directReplies = ref<null | entities.Note[]>([]);
 const directQuotes = ref<null | entities.Note[]>([]);
 const clips = ref();
 const renotes = ref();
-let isScrolling;
+const isRenote = ref(note.value.renoteId != null);
+let isScrolling: boolean;
 
 const reactionsCount = Object.values(props.note.reactions).reduce(
 	(x, y) => x + y,
@@ -258,10 +259,10 @@ const reactionsCount = Object.values(props.note.reactions).reduce(
 const keymap = {
 	r: () => reply(true),
 	"e|a|plus": () => react(true),
-	q: () => renoteButton.value.renote(true),
+	q: () => renoteButton.value!.renote(true),
 	esc: blur,
 	"m|o": () => menu(true),
-	s: () => showContent.value !== showContent.value,
+	// s: () => showContent.value !== showContent.value,
 };
 
 useNoteCapture({
@@ -270,21 +271,21 @@ useNoteCapture({
 	isDeletedRef: isDeleted,
 });
 
-function reply(viaKeyboard = false): void {
+function reply(_viaKeyboard = false): void {
 	pleaseLogin();
 	os.post({
 		reply: note.value,
-		animation: !viaKeyboard,
+		// animation: !viaKeyboard,
 	}).then(() => {
 		focus();
 	});
 }
 
-function react(viaKeyboard = false): void {
+function react(_viaKeyboard = false): void {
 	pleaseLogin();
 	blur();
 	reactionPicker.show(
-		reactButton.value,
+		reactButton.value!,
 		(reaction) => {
 			os.api("notes/reactions/create", {
 				noteId: note.value.id,
@@ -297,13 +298,13 @@ function react(viaKeyboard = false): void {
 	);
 }
 
-function undoReact(note): void {
-	const oldReaction = note.myReaction;
-	if (!oldReaction) return;
-	os.api("notes/reactions/delete", {
-		noteId: note.id,
-	});
-}
+// function undoReact(note): void {
+// 	const oldReaction = note.myReaction;
+// 	if (!oldReaction) return;
+// 	os.api("notes/reactions/delete", {
+// 		noteId: note.id,
+// 	});
+// }
 
 function onContextmenu(ev: MouseEvent): void {
 	const isLink = (el: HTMLElement) => {
@@ -312,8 +313,8 @@ function onContextmenu(ev: MouseEvent): void {
 			return isLink(el.parentElement);
 		}
 	};
-	if (isLink(ev.target)) return;
-	if (window.getSelection().toString() !== "") return;
+	if (isLink(ev.target as HTMLElement)) return;
+	if (window.getSelection()?.toString() !== "") return;
 
 	if (defaultStore.state.useReactionPickerForContextMenu) {
 		ev.preventDefault();
@@ -362,12 +363,17 @@ os.api("notes/children", {
 	limit: 30,
 	depth: 12,
 }).then((res) => {
-	res = res.reduce((acc, resNote) => {
-		if (resNote.userId == note.value.userId) {
-			return [...acc, resNote];
-		}
-		return [resNote, ...acc];
-	}, []);
+	// biome-ignore lint/style/noParameterAssign: assign it intentially
+	res = res
+		.filter((n) => n.userId !== note.value.userId)
+		.reverse()
+		.concat(res.filter((n) => n.userId === note.value.userId));
+	// res = res.reduce((acc: entities.Note[], resNote) => {
+	// 	if (resNote.userId === note.value.userId) {
+	// 		return [...acc, resNote];
+	// 	}
+	// 	return [resNote, ...acc];
+	// }, []);
 	replies.value = res;
 	directReplies.value = res
 		.filter((resNote) => resNote.replyId === note.value.id)
@@ -438,7 +444,7 @@ async function onNoteUpdated(
 	}
 
 	switch (type) {
-		case "replied":
+		case "replied": {
 			const { id: createdId } = body;
 			const replyNote = await os.api("notes/show", {
 				noteId: createdId,
@@ -446,10 +452,10 @@ async function onNoteUpdated(
 
 			replies.value.splice(found, 0, replyNote);
 			if (found === 0) {
-				directReplies.value.push(replyNote);
+				directReplies.value!.push(replyNote);
 			}
 			break;
-
+		}
 		case "deleted":
 			if (found === 0) {
 				isDeleted.value = true;
diff --git a/packages/client/src/components/MkNotePreview.vue b/packages/client/src/components/MkNotePreview.vue
index 454936dfbe..9ba8c0025e 100644
--- a/packages/client/src/components/MkNotePreview.vue
+++ b/packages/client/src/components/MkNotePreview.vue
@@ -1,9 +1,9 @@
 <template>
 	<div v-size="{ min: [350, 500] }" class="fefdfafb">
-		<MkAvatar class="avatar" :user="me" disable-link />
+		<MkAvatar class="avatar" :user="me!" disable-link />
 		<div class="main">
 			<div class="header">
-				<MkUserName :user="me" />
+				<MkUserName :user="me!" />
 			</div>
 			<div class="body">
 				<div class="content">
diff --git a/packages/client/src/components/MkNoteSub.vue b/packages/client/src/components/MkNoteSub.vue
index ba91221b87..c7cf06d2e5 100644
--- a/packages/client/src/components/MkNoteSub.vue
+++ b/packages/client/src/components/MkNoteSub.vue
@@ -1,7 +1,7 @@
 <template>
 	<article
 		v-if="!muted.muted || muted.what === 'reply'"
-		:id="detailedView ? appearNote.id : null"
+		:id="detailedView ? appearNote.id : undefined"
 		ref="el"
 		v-size="{ max: [450, 500] }"
 		class="wrpstxzv"
@@ -35,10 +35,10 @@
 						:parent-id="parentId"
 						:conversation="conversation"
 						:detailed-view="detailedView"
-						@focusfooter="footerEl.focus()"
+						@focusfooter="footerEl!.focus()"
 					/>
 					<div v-if="translating || translation" class="translation">
-						<MkLoading v-if="translating" mini />
+						<MkLoading v-if="translating || translation == null" mini />
 						<div v-else class="translated">
 							<b
 								>{{
@@ -217,6 +217,7 @@ import { useNoteCapture } from "@/scripts/use-note-capture";
 import { defaultStore } from "@/store";
 import { deepClone } from "@/scripts/clone";
 import icon from "@/scripts/icon";
+import type { NoteTranslation } from "@/types/note";
 
 const router = useRouter();
 
@@ -256,12 +257,12 @@ const isRenote =
 	note.value.fileIds.length === 0 &&
 	note.value.poll == null;
 
-const el = ref<HTMLElement>();
-const footerEl = ref<HTMLElement>();
+const el = ref<HTMLElement | null>(null);
+const footerEl = ref<HTMLElement | null>(null);
 const menuButton = ref<HTMLElement>();
-const starButton = ref<InstanceType<typeof XStarButton>>();
-const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
-const reactButton = ref<HTMLElement>();
+const starButton = ref<InstanceType<typeof XStarButton> | null>(null);
+const renoteButton = ref<InstanceType<typeof XRenoteButton> | null>(null);
+const reactButton = ref<HTMLElement | null>(null);
 const appearNote = computed(() =>
 	isRenote ? (note.value.renote as entities.Note) : note.value,
 );
@@ -274,7 +275,7 @@ const muted = ref(
 		defaultStore.state.mutedLangs,
 	),
 );
-const translation = ref(null);
+const translation = ref<NoteTranslation | null>(null);
 const translating = ref(false);
 const replies: entities.Note[] =
 	props.conversation
@@ -330,21 +331,21 @@ useNoteCapture({
 	isDeletedRef: isDeleted,
 });
 
-function reply(viaKeyboard = false): void {
+function reply(_viaKeyboard = false): void {
 	pleaseLogin();
 	os.post({
 		reply: appearNote.value,
-		animation: !viaKeyboard,
+		// animation: !viaKeyboard,
 	}).then(() => {
 		focus();
 	});
 }
 
-function react(viaKeyboard = false): void {
+function react(_viaKeyboard = false): void {
 	pleaseLogin();
 	blur();
 	reactionPicker.show(
-		reactButton.value,
+		reactButton.value!,
 		(reaction) => {
 			os.api("notes/reactions/create", {
 				noteId: appearNote.value.id,
@@ -388,14 +389,15 @@ function menu(viaKeyboard = false): void {
 }
 
 function onContextmenu(ev: MouseEvent): void {
-	const isLink = (el: HTMLElement) => {
+	const isLink = (el: HTMLElement | null) => {
+		if (el == null) return;
 		if (el.tagName === "A") return true;
 		if (el.parentElement) {
 			return isLink(el.parentElement);
 		}
 	};
-	if (isLink(ev.target)) return;
-	if (window.getSelection().toString() !== "") return;
+	if (isLink(ev.target as HTMLElement | null)) return;
+	if (window.getSelection()?.toString() !== "") return;
 
 	if (defaultStore.state.useReactionPickerForContextMenu) {
 		ev.preventDefault();
@@ -414,7 +416,7 @@ function onContextmenu(ev: MouseEvent): void {
 						os.pageWindow(notePage(appearNote.value));
 					},
 				},
-				notePage(appearNote.value) != location.pathname
+				notePage(appearNote.value) !== location.pathname
 					? {
 							icon: `${icon("ph-arrows-out-simple")}`,
 							text: i18n.ts.showInPage,
@@ -454,15 +456,15 @@ function onContextmenu(ev: MouseEvent): void {
 }
 
 function focus() {
-	el.value.focus();
+	el.value!.focus();
 }
 
 function blur() {
-	el.value.blur();
+	el.value!.blur();
 }
 
-function noteClick(e) {
-	if (document.getSelection().type === "Range" || !expandOnNoteClick) {
+function noteClick(e: MouseEvent) {
+	if (document.getSelection()?.type === "Range" || !expandOnNoteClick) {
 		e.stopPropagation();
 	} else {
 		router.push(notePage(props.note));
diff --git a/packages/client/src/components/MkNotes.vue b/packages/client/src/components/MkNotes.vue
index b457f69704..09db9dcaf0 100644
--- a/packages/client/src/components/MkNotes.vue
+++ b/packages/client/src/components/MkNotes.vue
@@ -41,7 +41,11 @@
 <script lang="ts" setup>
 import { ref } from "vue";
 import type { entities } from "firefish-js";
-import type { PagingOf } from "@/components/MkPagination.vue";
+import type {
+	MkPaginationType,
+	PagingKeyOf,
+	PagingOf,
+} from "@/components/MkPagination.vue";
 import XNote from "@/components/MkNote.vue";
 import XList from "@/components/MkDateSeparatedList.vue";
 import MkPagination from "@/components/MkPagination.vue";
@@ -56,10 +60,14 @@ defineProps<{
 	disableAutoLoad?: boolean;
 }>();
 
-const pagingComponent = ref<InstanceType<typeof MkPagination>>();
+const pagingComponent = ref<MkPaginationType<
+	PagingKeyOf<entities.Note>
+> | null>(null);
 
 function scrollTop() {
-	scroll(tlEl.value, { top: 0, behavior: "smooth" });
+	if (tlEl.value) {
+		scroll(tlEl.value, { top: 0, behavior: "smooth" });
+	}
 }
 
 defineExpose({
diff --git a/packages/client/src/components/MkNotification.vue b/packages/client/src/components/MkNotification.vue
index b00646cdd6..e3f8259def 100644
--- a/packages/client/src/components/MkNotification.vue
+++ b/packages/client/src/components/MkNotification.vue
@@ -12,12 +12,12 @@
 				:user="notification.note.user"
 			/>
 			<MkAvatar
-				v-else-if="notification.user"
+				v-else-if="'user' in notification"
 				class="icon"
 				:user="notification.user"
 			/>
 			<img
-				v-else-if="notification.icon"
+				v-else-if="'icon' in notification && notification.icon"
 				class="icon"
 				:src="notification.icon"
 				alt=""
@@ -95,7 +95,7 @@
 					i18n.ts._notification.pollEnded
 				}}</span>
 				<MkA
-					v-else-if="notification.user"
+					v-else-if="'user' in notification"
 					v-user-preview="notification.user.id"
 					class="name"
 					:to="userPage(notification.user)"
@@ -133,7 +133,7 @@
 					:plain="true"
 					:nowrap="!full"
 					:lang="notification.note.lang"
-					:custom-emojis="notification.note.renote.emojis"
+					:custom-emojis="notification.note.renote!.emojis"
 				/>
 			</MkA>
 			<MkA
@@ -212,6 +212,7 @@
 				style="opacity: 0.7"
 				>{{ i18n.ts.youGotNewFollower }}
 				<div v-if="full && !hideFollowButton">
+					<!-- FIXME: Provide a UserDetailed here -->
 					<MkFollowButton
 						:user="notification.user"
 						:full="true"
@@ -269,8 +270,10 @@
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, ref, watch } from "vue";
+import { onMounted, onUnmounted, ref, toRef, watch } from "vue";
 import type { entities } from "firefish-js";
+import type { Connection } from "firefish-js/src/streaming";
+import type { Channels } from "firefish-js/src/streaming.types";
 import XReactionIcon from "@/components/MkReactionIcon.vue";
 import MkFollowButton from "@/components/MkFollowButton.vue";
 import XReactionTooltip from "@/components/MkReactionTooltip.vue";
@@ -299,8 +302,8 @@ const props = withDefaults(
 
 const stream = useStream();
 
-const elRef = ref<HTMLElement>(null);
-const reactionRef = ref(null);
+const elRef = ref<HTMLElement | null>(null);
+const reactionRef = ref<InstanceType<typeof XReactionIcon> | null>(null);
 
 const hideFollowButton = defaultStore.state.hideFollowButtons;
 const showEmojiReactions =
@@ -311,7 +314,7 @@ const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReact
 	: "⭐";
 
 let readObserver: IntersectionObserver | undefined;
-let connection;
+let connection: Connection<Channels["main"]> | null = null;
 
 onMounted(() => {
 	if (!props.notification.isRead) {
@@ -323,13 +326,13 @@ onMounted(() => {
 			observer.disconnect();
 		});
 
-		readObserver.observe(elRef.value);
+		readObserver.observe(elRef.value!);
 
 		connection = stream.useChannel("main");
-		connection.on("readAllNotifications", () => readObserver.disconnect());
+		connection.on("readAllNotifications", () => readObserver!.disconnect());
 
-		watch(props.notification.isRead, () => {
-			readObserver.disconnect();
+		watch(toRef(props.notification.isRead), () => {
+			readObserver!.disconnect();
 		});
 	}
 });
@@ -344,38 +347,47 @@ const groupInviteDone = ref(false);
 
 const acceptFollowRequest = () => {
 	followRequestDone.value = true;
-	os.api("following/requests/accept", { userId: props.notification.user.id });
+	os.api("following/requests/accept", {
+		userId: (props.notification as entities.ReceiveFollowRequestNotification)
+			.user.id,
+	});
 };
 
 const rejectFollowRequest = () => {
 	followRequestDone.value = true;
-	os.api("following/requests/reject", { userId: props.notification.user.id });
+	os.api("following/requests/reject", {
+		userId: (props.notification as entities.ReceiveFollowRequestNotification)
+			.user.id,
+	});
 };
 
 const acceptGroupInvitation = () => {
 	groupInviteDone.value = true;
 	os.apiWithDialog("users/groups/invitations/accept", {
-		invitationId: props.notification.invitation.id,
+		invitationId: (props.notification as entities.GroupInvitedNotification)
+			.invitation.id,
 	});
 };
 
 const rejectGroupInvitation = () => {
 	groupInviteDone.value = true;
 	os.api("users/groups/invitations/reject", {
-		invitationId: props.notification.invitation.id,
+		invitationId: (props.notification as entities.GroupInvitedNotification)
+			.invitation.id,
 	});
 };
 
 useTooltip(reactionRef, (showing) => {
+	const n = props.notification as entities.ReactionNotification;
 	os.popup(
 		XReactionTooltip,
 		{
 			showing,
-			reaction: props.notification.reaction
-				? props.notification.reaction.replace(/^:(\w+):$/, ":$1@.:")
-				: props.notification.reaction,
-			emojis: props.notification.note.emojis,
-			targetElement: reactionRef.value.$el,
+			reaction: n.reaction
+				? n.reaction.replace(/^:(\w+):$/, ":$1@.:")
+				: n.reaction,
+			emojis: n.note.emojis,
+			targetElement: reactionRef.value!.$el,
 		},
 		{},
 		"closed",
diff --git a/packages/client/src/components/MkNotificationSettingWindow.vue b/packages/client/src/components/MkNotificationSettingWindow.vue
index fac382f68c..20c3b43cb3 100644
--- a/packages/client/src/components/MkNotificationSettingWindow.vue
+++ b/packages/client/src/components/MkNotificationSettingWindow.vue
@@ -6,7 +6,7 @@
 		:with-ok-button="true"
 		:ok-button-disabled="false"
 		@ok="ok()"
-		@close="dialog.close()"
+		@close="dialog!.close()"
 		@closed="emit('closed')"
 	>
 		<template #header>{{ i18n.ts.notificationSetting }}</template>
@@ -68,7 +68,7 @@ const includingTypes = computed(() => props.includingTypes || []);
 
 const dialog = ref<InstanceType<typeof XModalWindow>>();
 
-const typesMap = ref<Record<(typeof notificationTypes)[number], boolean>>({});
+const typesMap = ref({} as Record<(typeof notificationTypes)[number], boolean>);
 const useGlobalSetting = ref(
 	(includingTypes.value === null || includingTypes.value.length === 0) &&
 		props.showGlobalToggle,
@@ -89,7 +89,7 @@ function ok() {
 		});
 	}
 
-	dialog.value.close();
+	dialog.value!.close();
 }
 
 function disableAll() {
diff --git a/packages/client/src/components/MkNotificationToast.vue b/packages/client/src/components/MkNotificationToast.vue
index c5ca37d36a..32b0727927 100644
--- a/packages/client/src/components/MkNotificationToast.vue
+++ b/packages/client/src/components/MkNotificationToast.vue
@@ -16,12 +16,13 @@
 
 <script lang="ts" setup>
 import { onMounted, ref } from "vue";
+import type { entities } from "firefish-js";
 import XNotification from "@/components/MkNotification.vue";
 import * as os from "@/os";
 import { defaultStore } from "@/store";
 
 defineProps<{
-	notification: any; // TODO
+	notification: entities.Notification;
 }>();
 
 const emit = defineEmits<{
diff --git a/packages/client/src/components/MkNotifications.vue b/packages/client/src/components/MkNotifications.vue
index 7b63b34197..43669c6607 100644
--- a/packages/client/src/components/MkNotifications.vue
+++ b/packages/client/src/components/MkNotifications.vue
@@ -44,7 +44,9 @@
 <script lang="ts" setup>
 import { computed, onMounted, onUnmounted, ref } from "vue";
 import type { StreamTypes, entities, notificationTypes } from "firefish-js";
-import MkPagination from "@/components/MkPagination.vue";
+import MkPagination, {
+	type MkPaginationType,
+} from "@/components/MkPagination.vue";
 import XNotification from "@/components/MkNotification.vue";
 import XList from "@/components/MkDateSeparatedList.vue";
 import XNote from "@/components/MkNote.vue";
@@ -59,7 +61,7 @@ const props = defineProps<{
 
 const stream = useStream();
 
-const pagingComponent = ref<InstanceType<typeof MkPagination>>();
+const pagingComponent = ref<MkPaginationType<"i/notifications"> | null>(null);
 
 const pagination = {
 	endpoint: "i/notifications" as const,
diff --git a/packages/client/src/components/MkPagePreview.vue b/packages/client/src/components/MkPagePreview.vue
index 034c6fed63..8377770aa0 100644
--- a/packages/client/src/components/MkPagePreview.vue
+++ b/packages/client/src/components/MkPagePreview.vue
@@ -3,7 +3,7 @@
 		:to="`/@${page.user.username}/pages/${page.name}`"
 		class="vhpxefrj _block"
 		tabindex="-1"
-		:behavior="`${ui === 'deck' ? 'window' : null}`"
+		:behavior="ui === 'deck' ? 'window' : null"
 	>
 		<div
 			v-if="page.eyeCatchingImage"
@@ -34,11 +34,12 @@
 </template>
 
 <script lang="ts" setup>
+import type { entities } from "firefish-js";
 import { userName } from "@/filters/user";
 import { ui } from "@/config";
 
 defineProps<{
-	page: any;
+	page: entities.Page;
 }>();
 </script>
 
diff --git a/packages/client/src/components/MkPageWindow.vue b/packages/client/src/components/MkPageWindow.vue
index d237f18091..082f9f0159 100644
--- a/packages/client/src/components/MkPageWindow.vue
+++ b/packages/client/src/components/MkPageWindow.vue
@@ -56,23 +56,22 @@ const router = new Router(routes, props.initialPath);
 
 const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
 const windowEl = ref<InstanceType<typeof XWindow>>();
-const history = ref<{ path: string; key: any }[]>([
+const history = ref<{ path: string; key: string }[]>([
 	{
 		path: router.getCurrentPath(),
 		key: router.getCurrentKey(),
 	},
 ]);
 const buttonsLeft = computed(() => {
-	const buttons = [];
-
 	if (history.value.length > 1) {
-		buttons.push({
-			icon: `${icon("ph-caret-left")}`,
-			onClick: back,
-		});
+		return [
+			{
+				icon: `${icon("ph-caret-left")}`,
+				onClick: back,
+			},
+		];
 	}
-
-	return buttons;
+	return [];
 });
 const buttonsRight = computed(() => {
 	const buttons = [
@@ -114,7 +113,7 @@ const contextmenu = computed(() => [
 		text: i18n.ts.openInNewTab,
 		action: () => {
 			window.open(url + router.getCurrentPath(), "_blank");
-			windowEl.value.close();
+			windowEl.value!.close();
 		},
 	},
 	{
@@ -135,17 +134,17 @@ function back() {
 }
 
 function close() {
-	windowEl.value.close();
+	windowEl.value!.close();
 }
 
 function expand() {
 	mainRouter.push(router.getCurrentPath(), "forcePage");
-	windowEl.value.close();
+	windowEl.value!.close();
 }
 
 function popout() {
-	_popout(router.getCurrentPath(), windowEl.value.$el);
-	windowEl.value.close();
+	_popout(router.getCurrentPath(), windowEl.value!.$el);
+	windowEl.value!.close();
 }
 
 defineExpose({
diff --git a/packages/client/src/components/MkPagination.vue b/packages/client/src/components/MkPagination.vue
index 143e3e1656..03a1f0e35f 100644
--- a/packages/client/src/components/MkPagination.vue
+++ b/packages/client/src/components/MkPagination.vue
@@ -67,7 +67,7 @@
 </template>
 
 <script lang="ts" setup generic="E extends PagingKey">
-import type { ComputedRef } from "vue";
+import type { ComponentPublicInstance, ComputedRef } from "vue";
 import { computed, isRef, onActivated, onDeactivated, ref, watch } from "vue";
 import type { Endpoints, TypeUtils } from "firefish-js";
 import * as os from "@/os";
@@ -81,8 +81,30 @@ import MkButton from "@/components/MkButton.vue";
 import { i18n } from "@/i18n";
 import { defaultStore } from "@/store";
 
+/**
+ * ref type of MkPagination<E>
+ * Due to Vue's incomplete type support for generic components,
+ * we have to manually maintain this type instead of
+ * using `InstanceType<typeof MkPagination>`
+ */
+export type MkPaginationType<
+	E extends PagingKey,
+	Item = Endpoints[E]["res"][number],
+> = ComponentPublicInstance & {
+	items: Item[];
+	queue: Item[];
+	backed: boolean;
+	reload: () => Promise<void>;
+	refresh: () => Promise<void>;
+	prepend: (item: Item) => Promise<void>;
+	append: (item: Item) => Promise<void>;
+	removeItem: (finder: (item: Item) => boolean) => boolean;
+	updateItem: (id: string, replacer: (old: Item) => Item) => boolean;
+};
+
+export type PagingKeyOf<T> = TypeUtils.EndpointsOf<T[]>;
 // biome-ignore lint/suspicious/noExplicitAny: Used Intentionally
-export type PagingKey = TypeUtils.EndpointsOf<any[]>;
+export type PagingKey = PagingKeyOf<any>;
 
 export interface Paging<E extends PagingKey = PagingKey> {
 	endpoint: E;
diff --git a/packages/client/src/components/MkPollEditor.vue b/packages/client/src/components/MkPollEditor.vue
index 51bc99ec77..f9dfcf80a8 100644
--- a/packages/client/src/components/MkPollEditor.vue
+++ b/packages/client/src/components/MkPollEditor.vue
@@ -84,25 +84,20 @@ import { formatDateTimeString } from "@/scripts/format-time-string";
 import { addTime } from "@/scripts/time";
 import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
+import type { PollType } from "@/types/post-form";
 
 const props = defineProps<{
-	modelValue: {
-		expiresAt: string;
-		expiredAfter: number;
-		choices: string[];
-		multiple: boolean;
-	};
+	modelValue: PollType;
 }>();
 const emit = defineEmits<{
-	(
-		ev: "update:modelValue",
+	"update:modelValue": [
 		v: {
-			expiresAt: string;
-			expiredAfter: number;
+			expiresAt?: number;
+			expiredAfter?: number | null;
 			choices: string[];
 			multiple: boolean;
 		},
-	): void;
+	];
 }>();
 
 const choices = ref(props.modelValue.choices);
@@ -147,19 +142,19 @@ function get() {
 	};
 
 	const calcAfter = () => {
-		let base = parseInt(after.value);
+		let base = Number.parseInt(after.value.toString());
 		switch (unit.value) {
+			// biome-ignore lint/suspicious/noFallthroughSwitchClause: Fallthrough intentially
 			case "day":
 				base *= 24;
-			// fallthrough
+			// biome-ignore lint/suspicious/noFallthroughSwitchClause: Fallthrough intentially
 			case "hour":
 				base *= 60;
-			// fallthrough
+			// biome-ignore lint/suspicious/noFallthroughSwitchClause: Fallthrough intentially
 			case "minute":
 				base *= 60;
-			// fallthrough
 			case "second":
-				return (base *= 1000);
+				return base * 1000;
 			default:
 				return null;
 		}
diff --git a/packages/client/src/components/MkPopupMenu.vue b/packages/client/src/components/MkPopupMenu.vue
index 9a98648ea2..1bee69a0b2 100644
--- a/packages/client/src/components/MkPopupMenu.vue
+++ b/packages/client/src/components/MkPopupMenu.vue
@@ -35,7 +35,7 @@ defineProps<{
 	align?: "center" | string;
 	width?: number;
 	viaKeyboard?: boolean;
-	src?: any;
+	src?: HTMLElement | null;
 	noReturnFocus?;
 }>();
 
diff --git a/packages/client/src/components/MkPostForm.vue b/packages/client/src/components/MkPostForm.vue
index aeb51f7bf7..582da6c6d4 100644
--- a/packages/client/src/components/MkPostForm.vue
+++ b/packages/client/src/components/MkPostForm.vue
@@ -20,7 +20,7 @@
 				class="account _button"
 				@click="openAccountMenu"
 			>
-				<MkAvatar :user="postAccount ?? me" class="avatar" />
+				<MkAvatar :user="postAccount ?? me!" class="avatar" />
 			</button>
 			<div class="right">
 				<span
@@ -297,14 +297,22 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, inject, nextTick, onMounted, ref, watch } from "vue";
+import {
+	type Ref,
+	computed,
+	inject,
+	nextTick,
+	onMounted,
+	ref,
+	watch,
+} from "vue";
 import * as mfm from "mfm-js";
 import autosize from "autosize";
 import insertTextAtCursor from "insert-text-at-cursor";
 import { length } from "stringz";
 import { toASCII } from "punycode/";
 import { acct } from "firefish-js";
-import type { entities, languages } from "firefish-js";
+import type { ApiTypes, entities, languages } from "firefish-js";
 import { throttle } from "throttle-debounce";
 import XNoteSimple from "@/components/MkNoteSimple.vue";
 import XNotePreview from "@/components/MkNotePreview.vue";
@@ -341,6 +349,7 @@ import type { MenuItem } from "@/types/menu";
 import icon from "@/scripts/icon";
 import MkVisibilityPicker from "@/components/MkVisibilityPicker.vue";
 import type { NoteVisibility } from "@/types/note";
+import type { NoteDraft, PollType } from "@/types/post-form";
 
 const modal = inject("modal");
 
@@ -348,16 +357,16 @@ const props = withDefaults(
 	defineProps<{
 		reply?: entities.Note;
 		renote?: entities.Note;
-		channel?: any; // TODO
+		channel?: entities.Channel;
 		mention?: entities.User;
 		specified?: entities.User;
 		initialText?: string;
 		initialVisibility?: NoteVisibility;
-		initialLanguage?: typeof languages;
+		initialLanguage?: (typeof languages)[number];
 		initialFiles?: entities.DriveFile[];
 		initialLocalOnly?: boolean;
 		initialVisibleUsers?: entities.User[];
-		initialNote?: entities.Note;
+		initialNote?: NoteDraft;
 		instant?: boolean;
 		fixed?: boolean;
 		autofocus?: boolean;
@@ -390,12 +399,7 @@ const showBigPostButton = defaultStore.state.showBigPostButton;
 const posting = ref(false);
 const text = ref(props.initialText ?? "");
 const files = ref(props.initialFiles ?? ([] as entities.DriveFile[]));
-const poll = ref<{
-	choices: string[];
-	multiple: boolean;
-	expiresAt: string | null;
-	expiredAfter: string | null;
-} | null>(null);
+const poll = ref<PollType | null>(null);
 const useCw = ref(false);
 const showPreview = ref(defaultStore.state.showPreviewByDefault);
 const cw = ref<string | null>(null);
@@ -411,12 +415,12 @@ const visibility = ref(
 			: defaultStore.state.defaultNoteVisibility),
 );
 
-const visibleUsers = ref([]);
+const visibleUsers = ref<entities.User[]>([]);
 if (props.initialVisibleUsers) {
 	props.initialVisibleUsers.forEach(pushVisibleUser);
 }
 const draghover = ref(false);
-const quoteId = ref(null);
+const quoteId = ref<string | null>(null);
 const hasNotSpecifiedMentions = ref(false);
 const recentHashtags = ref(
 	JSON.parse(localStorage.getItem("hashtags") || "[]"),
@@ -500,7 +504,9 @@ const canPost = computed((): boolean => {
 const withHashtags = computed(
 	defaultStore.makeGetterSetter("postFormWithHashtags"),
 );
-const hashtags = computed(defaultStore.makeGetterSetter("postFormHashtags"));
+const hashtags = computed(
+	defaultStore.makeGetterSetter("postFormHashtags"),
+) as Ref<string | null>;
 
 watch(text, () => {
 	checkMissingMention();
@@ -525,7 +531,7 @@ if (props.mention) {
 
 if (
 	props.reply &&
-	(props.reply.user.username !== me.username ||
+	(props.reply.user.username !== me!.username ||
 		(props.reply.user.host != null && props.reply.user.host !== host))
 ) {
 	text.value = `@${props.reply.user.username}${
@@ -545,7 +551,7 @@ if (props.reply && props.reply.text != null) {
 				: `@${x.username}@${toASCII(otherHost)}`;
 
 		// exclude me
-		if (me.username === x.username && (x.host == null || x.host === host))
+		if (me!.username === x.username && (x.host == null || x.host === host))
 			continue;
 
 		// remove duplicates
@@ -579,7 +585,7 @@ if (
 		if (props.reply.visibleUserIds) {
 			os.api("users/show", {
 				userIds: props.reply.visibleUserIds.filter(
-					(uid) => uid !== me.id && uid !== props.reply.userId,
+					(uid) => uid !== me!.id && uid !== props.reply!.userId,
 				),
 			}).then((users) => {
 				users.forEach(pushVisibleUser);
@@ -588,7 +594,7 @@ if (
 			visibility.value = "private";
 		}
 
-		if (props.reply.userId !== me.id) {
+		if (props.reply.userId !== me!.id) {
 			os.api("users/show", { userId: props.reply.userId }).then((user) => {
 				pushVisibleUser(user);
 			});
@@ -615,7 +621,7 @@ const addRe = (s: string) => {
 if (defaultStore.state.keepCw && props.reply && props.reply.cw) {
 	useCw.value = true;
 	cw.value =
-		props.reply.user.username === me.username
+		props.reply.user.username === me!.username
 			? props.reply.cw
 			: addRe(props.reply.cw);
 }
@@ -894,11 +900,14 @@ function onCompositionEnd(ev: CompositionEvent) {
 }
 
 async function onPaste(ev: ClipboardEvent) {
+	if (ev.clipboardData == null) return;
+
 	for (const { item, i } of Array.from(ev.clipboardData.items).map(
 		(item, i) => ({ item, i }),
 	)) {
 		if (item.kind === "file") {
 			const file = item.getAsFile();
+			if (file == null) continue;
 			const lio = file.name.lastIndexOf(".");
 			const ext = lio >= 0 ? file.name.slice(lio) : "";
 			const formatted = `${formatTimeString(
@@ -911,7 +920,7 @@ async function onPaste(ev: ClipboardEvent) {
 
 	const paste = ev.clipboardData?.getData("text") ?? "";
 
-	if (!props.renote && !quoteId.value && paste.startsWith(url + "/notes/")) {
+	if (!props.renote && !quoteId.value && paste.startsWith(`${url}/notes/`)) {
 		ev.preventDefault();
 
 		os.yesno({
@@ -919,13 +928,13 @@ async function onPaste(ev: ClipboardEvent) {
 			text: i18n.ts.quoteQuestion,
 		}).then(({ canceled }) => {
 			if (canceled) {
-				insertTextAtCursor(textareaEl.value, paste);
+				insertTextAtCursor(textareaEl.value!, paste);
 				return;
 			}
 
 			quoteId.value = paste
 				.substring(url.length)
-				.match(/^\/notes\/(.+?)\/?$/)[1];
+				.match(/^\/notes\/(.+?)\/?$/)![1];
 		});
 	}
 }
@@ -956,16 +965,17 @@ function onDragover(ev) {
 	}
 }
 
-function onDragenter(ev) {
+function onDragenter(_ev) {
 	draghover.value = true;
 }
 
-function onDragleave(ev) {
+function onDragleave(_ev) {
 	draghover.value = false;
 }
 
-function onDrop(ev): void {
+function onDrop(ev: DragEvent): void {
 	draghover.value = false;
+	if (ev.dataTransfer == null) return;
 
 	// ファイルだったら
 	if (ev.dataTransfer.files.length > 0) {
@@ -1064,7 +1074,7 @@ async function post() {
 
 	const processedText = preprocess(text.value);
 
-	let postData = {
+	let postData: ApiTypes.NoteSubmitReq = {
 		editId: props.editId ? props.editId : undefined,
 		text: processedText === "" ? undefined : processedText,
 		fileIds: files.value.length > 0 ? files.value.map((f) => f.id) : undefined,
@@ -1092,7 +1102,7 @@ async function post() {
 		const hashtags_ = hashtags.value
 			.trim()
 			.split(" ")
-			.map((x) => (x.startsWith("#") ? x : "#" + x))
+			.map((x) => (x.startsWith("#") ? x : `#${x}`))
 			.join(" ");
 		postData.text = postData.text ? `${postData.text} ${hashtags_}` : hashtags_;
 	}
@@ -1104,11 +1114,11 @@ async function post() {
 		}
 	}
 
-	let token;
+	let token: string | undefined;
 
 	if (postAccount.value) {
 		const storedAccounts = await getAccounts();
-		token = storedAccounts.find((x) => x.id === postAccount.value.id)?.token;
+		token = storedAccounts.find((x) => x.id === postAccount.value!.id)?.token;
 	}
 
 	posting.value = true;
@@ -1119,10 +1129,11 @@ async function post() {
 				deleteDraft();
 				emit("posted");
 				if (postData.text && postData.text !== "") {
-					const hashtags_ = mfm
-						.parse(postData.text)
-						.filter((x) => x.type === "hashtag")
-						.map((x) => x.props.hashtag);
+					const hashtags_ = (
+						mfm
+							.parse(postData.text)
+							.filter((x) => x.type === "hashtag") as mfm.MfmHashtag[]
+					).map((x) => x.props.hashtag);
 					const history = JSON.parse(
 						localStorage.getItem("hashtags") || "[]",
 					) as string[];
@@ -1133,14 +1144,14 @@ async function post() {
 				}
 				posting.value = false;
 				postAccount.value = null;
-				nextTick(() => autosize.update(textareaEl.value));
+				nextTick(() => autosize.update(textareaEl.value!));
 			});
 		})
-		.catch((err) => {
+		.catch((err: { message: string; id: string }) => {
 			posting.value = false;
 			os.alert({
 				type: "error",
-				text: err.message + "\n" + (err as any).id,
+				text: `${err.message}\n${err.id}`,
 			});
 		});
 	vibrate([10, 20, 10, 20, 10, 20, 60]);
@@ -1169,19 +1180,23 @@ function cancel() {
 
 function insertMention() {
 	os.selectUser().then((user) => {
-		insertTextAtCursor(textareaEl.value, "@" + acct.toString(user) + " ");
+		insertTextAtCursor(textareaEl.value!, `@${acct.toString(user)} `);
 	});
 }
 
 async function insertEmoji(ev: MouseEvent) {
-	os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, textareaEl.value);
+	os.openEmojiPicker(
+		(ev.currentTarget ?? ev.target) as HTMLElement,
+		{},
+		textareaEl.value!,
+	);
 }
 
 async function openCheatSheet(ev: MouseEvent) {
 	os.popup(XCheatSheet, {}, {}, "closed");
 }
 
-function showActions(ev) {
+function showActions(ev: MouseEvent) {
 	os.popupMenu(
 		postFormActions.map((action) => ({
 			text: action.title,
@@ -1198,7 +1213,7 @@ function showActions(ev) {
 				);
 			},
 		})),
-		ev.currentTarget ?? ev.target,
+		(ev.currentTarget ?? ev.target) as HTMLElement,
 	);
 }
 
@@ -1209,9 +1224,9 @@ function openAccountMenu(ev: MouseEvent) {
 		{
 			withExtraOperation: false,
 			includeCurrentAccount: true,
-			active: postAccount.value != null ? postAccount.value.id : me.id,
+			active: postAccount.value != null ? postAccount.value.id : me!.id,
 			onChoose: (account) => {
-				if (account.id === me.id) {
+				if (account.id === me!.id) {
 					postAccount.value = null;
 				} else {
 					postAccount.value = account;
@@ -1232,14 +1247,14 @@ onMounted(() => {
 	}
 
 	// TODO: detach when unmount
-	new Autocomplete(textareaEl.value, text);
-	new Autocomplete(cwInputEl.value, cw);
-	new Autocomplete(hashtagsInputEl.value, hashtags);
+	new Autocomplete(textareaEl.value!, text);
+	new Autocomplete(cwInputEl.value!, cw as Ref<string>);
+	new Autocomplete(hashtagsInputEl.value!, hashtags as Ref<string>);
 
-	autosize(textareaEl.value);
+	autosize(textareaEl.value!);
 
 	nextTick(() => {
-		autosize(textareaEl.value);
+		autosize(textareaEl.value!);
 		// 書きかけの投稿を復元
 		if (!props.instant && !props.mention && !props.specified) {
 			const draft = JSON.parse(localStorage.getItem("drafts") || "{}")[
@@ -1275,8 +1290,8 @@ onMounted(() => {
 				};
 			}
 			visibility.value = init.visibility;
-			localOnly.value = init.localOnly;
-			language.value = init.lang;
+			localOnly.value = init.localOnly ?? false;
+			language.value = init.lang ?? null;
 			quoteId.value = init.renote ? init.renote.id : null;
 		}
 
@@ -1289,7 +1304,7 @@ onMounted(() => {
 		}
 
 		nextTick(() => watchForDraft());
-		nextTick(() => autosize.update(textareaEl.value));
+		nextTick(() => autosize.update(textareaEl.value!));
 	});
 });
 </script>
diff --git a/packages/client/src/components/MkPostFormDialog.vue b/packages/client/src/components/MkPostFormDialog.vue
index 56f009c338..fafb4afdd4 100644
--- a/packages/client/src/components/MkPostFormDialog.vue
+++ b/packages/client/src/components/MkPostFormDialog.vue
@@ -2,7 +2,7 @@
 	<MkModal
 		ref="modal"
 		:prefer-type="'dialog'"
-		@click="modal.close()"
+		@click="modal!.close()"
 		@closed="onModalClosed()"
 	>
 		<MkPostForm
@@ -12,8 +12,8 @@
 			autofocus
 			freeze-after-posted
 			@posted="onPosted"
-			@cancel="modal.close()"
-			@esc="modal.close()"
+			@cancel="modal!.close()"
+			@esc="modal!.close()"
 		/>
 	</MkModal>
 </template>
@@ -25,20 +25,21 @@ import type { entities, languages } from "firefish-js";
 import MkModal from "@/components/MkModal.vue";
 import MkPostForm from "@/components/MkPostForm.vue";
 import type { NoteVisibility } from "@/types/note";
+import type { NoteDraft } from "@/types/post-form";
 
 const props = defineProps<{
 	reply?: entities.Note;
 	renote?: entities.Note;
-	channel?: any; // TODO
+	channel?: entities.Channel;
 	mention?: entities.User;
 	specified?: entities.User;
 	initialText?: string;
 	initialVisibility?: NoteVisibility;
-	initialLanguage?: typeof languages;
+	initialLanguage?: (typeof languages)[number];
 	initialFiles?: entities.DriveFile[];
 	initialLocalOnly?: boolean;
 	initialVisibleUsers?: entities.User[];
-	initialNote?: entities.Note;
+	initialNote?: NoteDraft;
 	instant?: boolean;
 	fixed?: boolean;
 	autofocus?: boolean;
@@ -53,7 +54,7 @@ const modal = shallowRef<InstanceType<typeof MkModal>>();
 const form = shallowRef<InstanceType<typeof MkPostForm>>();
 
 function onPosted() {
-	modal.value.close({
+	modal.value!.close({
 		useSendAnimation: true,
 	});
 }
diff --git a/packages/client/src/components/MkReactionIcon.vue b/packages/client/src/components/MkReactionIcon.vue
index e9d5a198cc..6608501478 100644
--- a/packages/client/src/components/MkReactionIcon.vue
+++ b/packages/client/src/components/MkReactionIcon.vue
@@ -9,9 +9,11 @@
 </template>
 
 <script lang="ts" setup>
+import type { entities } from "firefish-js";
+
 defineProps<{
 	reaction: string;
-	customEmojis?: any[]; // TODO
+	customEmojis?: entities.EmojiLite[];
 	noStyle?: boolean;
 }>();
 </script>
diff --git a/packages/client/src/components/MkReactionTooltip.vue b/packages/client/src/components/MkReactionTooltip.vue
index 0e83226c94..40e8fefade 100644
--- a/packages/client/src/components/MkReactionTooltip.vue
+++ b/packages/client/src/components/MkReactionTooltip.vue
@@ -3,6 +3,7 @@
 		ref="tooltip"
 		:target-element="targetElement"
 		:max-width="340"
+		:showing="showing"
 		@closed="emit('closed')"
 	>
 		<div class="beeadbfb">
@@ -18,12 +19,14 @@
 </template>
 
 <script lang="ts" setup>
+import type { entities } from "firefish-js";
 import MkTooltip from "./MkTooltip.vue";
 import XReactionIcon from "@/components/MkReactionIcon.vue";
 
 defineProps<{
+	showing: boolean;
 	reaction: string;
-	emojis: any[]; // TODO
+	emojis: entities.EmojiLite[];
 	targetElement: HTMLElement;
 }>();
 
diff --git a/packages/client/src/components/MkReactionsViewer.details.vue b/packages/client/src/components/MkReactionsViewer.details.vue
index 0d992ae431..13f8fd8311 100644
--- a/packages/client/src/components/MkReactionsViewer.details.vue
+++ b/packages/client/src/components/MkReactionsViewer.details.vue
@@ -3,6 +3,7 @@
 		ref="tooltip"
 		:target-element="targetElement"
 		:max-width="340"
+		:showing="showing"
 		@closed="emit('closed')"
 	>
 		<div class="bqxuuuey">
@@ -29,15 +30,17 @@
 </template>
 
 <script lang="ts" setup>
+import type { entities } from "firefish-js";
 import MkTooltip from "./MkTooltip.vue";
 import XReactionIcon from "@/components/MkReactionIcon.vue";
 
 defineProps<{
+	showing: boolean;
 	reaction: string;
-	users: any[]; // TODO
+	users: entities.User[]; // TODO
 	count: number;
-	emojis: any[]; // TODO
-	targetElement: HTMLElement;
+	emojis: entities.EmojiLite[]; // TODO
+	targetElement?: HTMLElement;
 }>();
 
 const emit = defineEmits<{
diff --git a/packages/client/src/components/MkReactionsViewer.reaction.vue b/packages/client/src/components/MkReactionsViewer.reaction.vue
index c403c7003c..89f51797ab 100644
--- a/packages/client/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/client/src/components/MkReactionsViewer.reaction.vue
@@ -89,7 +89,7 @@ useTooltip(
 				emojis: props.note.emojis,
 				users,
 				count: props.count,
-				targetElement: buttonRef.value,
+				targetElement: buttonRef.value!,
 			},
 			{},
 			"closed",
diff --git a/packages/client/src/components/MkRenoteButton.vue b/packages/client/src/components/MkRenoteButton.vue
index 72a0559a0e..7250757da4 100644
--- a/packages/client/src/components/MkRenoteButton.vue
+++ b/packages/client/src/components/MkRenoteButton.vue
@@ -46,7 +46,7 @@ const buttonRef = ref<HTMLElement>();
 const canRenote = computed(
 	() =>
 		["public", "home"].includes(props.note.visibility) ||
-		props.note.userId === me.id,
+		props.note.userId === me?.id,
 );
 
 useTooltip(buttonRef, async (showing) => {
@@ -77,7 +77,7 @@ const hasRenotedBefore = ref(false);
 if (isSignedIn) {
 	os.api("notes/renotes", {
 		noteId: props.note.id,
-		userId: me.id,
+		userId: me!.id,
 		limit: 1,
 	}).then((res) => {
 		hasRenotedBefore.value = res.length > 0;
@@ -251,6 +251,10 @@ const renote = (viaKeyboard = false, ev?: MouseEvent) => {
 
 	os.popupMenu(buttonActions, buttonRef.value, { viaKeyboard });
 };
+
+defineExpose({
+	renote,
+});
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/MkSignin.vue b/packages/client/src/components/MkSignin.vue
index f5c5f0ee4d..d2c1159642 100644
--- a/packages/client/src/components/MkSignin.vue
+++ b/packages/client/src/components/MkSignin.vue
@@ -136,6 +136,7 @@
 <script lang="ts" setup>
 import { defineAsyncComponent, ref } from "vue";
 import { toUnicode } from "punycode/";
+import type { entities } from "firefish-js";
 import MkButton from "@/components/MkButton.vue";
 import MkInput from "@/components/form/input.vue";
 import MkInfo from "@/components/MkInfo.vue";
@@ -147,7 +148,7 @@ import { i18n } from "@/i18n";
 import icon from "@/scripts/icon";
 
 const signing = ref(false);
-const user = ref(null);
+const user = ref<entities.UserDetailed | null>(null);
 const username = ref("");
 const password = ref("");
 const token = ref("");
@@ -249,7 +250,7 @@ function queryKey() {
 function onSubmit() {
 	signing.value = true;
 	console.log("submit");
-	if (window.PublicKeyCredential && user.value.securityKeys) {
+	if (window.PublicKeyCredential && user.value?.securityKeys) {
 		os.api("signin", {
 			username: username.value,
 			password: password.value,
@@ -263,7 +264,7 @@ function onSubmit() {
 				return queryKey();
 			})
 			.catch(loginFailed);
-	} else if (!totpLogin.value && user.value && user.value.twoFactorEnabled) {
+	} else if (!totpLogin.value && user.value?.twoFactorEnabled) {
 		totpLogin.value = true;
 		signing.value = false;
 	} else {
@@ -272,8 +273,7 @@ function onSubmit() {
 			password: password.value,
 			"hcaptcha-response": hCaptchaResponse.value,
 			"g-recaptcha-response": reCaptchaResponse.value,
-			token:
-				user.value && user.value.twoFactorEnabled ? token.value : undefined,
+			token: user.value?.twoFactorEnabled ? token.value : undefined,
 		})
 			.then((res) => {
 				emit("login", res);
diff --git a/packages/client/src/components/MkSignup.vue b/packages/client/src/components/MkSignup.vue
index 6de17b2d48..0807935be3 100644
--- a/packages/client/src/components/MkSignup.vue
+++ b/packages/client/src/components/MkSignup.vue
@@ -305,12 +305,12 @@ const host = toUnicode(config.host);
 const hcaptcha = ref();
 const recaptcha = ref();
 
-const username: string = ref("");
-const password: string = ref("");
-const retypedPassword: string = ref("");
-const invitationCode: string = ref("");
+const username = ref<string>("");
+const password = ref<string>("");
+const retypedPassword = ref<string>("");
+const invitationCode = ref<string>("");
 const email = ref("");
-const usernameState:
+const usernameState = ref<
 	| null
 	| "wait"
 	| "ok"
@@ -318,9 +318,10 @@ const usernameState:
 	| "error"
 	| "invalid-format"
 	| "min-range"
-	| "max-range" = ref(null);
-const invitationState: null | "entered" = ref(null);
-const emailState:
+	| "max-range"
+>(null);
+const invitationState = ref<null | "entered">(null);
+const emailState = ref<
 	| null
 	| "wait"
 	| "ok"
@@ -330,11 +331,12 @@ const emailState:
 	| "unavailable:mx"
 	| "unavailable:smtp"
 	| "unavailable"
-	| "error" = ref(null);
-const passwordStrength: "" | "low" | "medium" | "high" = ref("");
-const passwordRetypeState: null | "match" | "not-match" = ref(null);
-const submitting: boolean = ref(false);
-const ToSAgreement: boolean = ref(false);
+	| "error"
+>(null);
+const passwordStrength = ref<"" | "low" | "medium" | "high">("");
+const passwordRetypeState = ref<null | "match" | "not-match">(null);
+const submitting = ref(false);
+const ToSAgreement = ref(false);
 const hCaptchaResponse = ref(null);
 const reCaptchaResponse = ref(null);
 
diff --git a/packages/client/src/components/MkSubNoteContent.vue b/packages/client/src/components/MkSubNoteContent.vue
index 3c1e2a418b..7d350085bb 100644
--- a/packages/client/src/components/MkSubNoteContent.vue
+++ b/packages/client/src/components/MkSubNoteContent.vue
@@ -31,7 +31,6 @@
 			:text="note.cw"
 			:author="note.user"
 			:lang="note.lang"
-			:i="me"
 			:custom-emojis="note.emojis"
 		/>
 	</p>
@@ -63,8 +62,8 @@
 			<div
 				class="body"
 				v-bind="{
-					'aria-hidden': note.cw && !showContent ? 'true' : null,
-					tabindex: !showContent ? '-1' : null,
+					'aria-hidden': note.cw && !showContent ? 'true' : undefined,
+					tabindex: !showContent ? '-1' : undefined,
 				}"
 			>
 				<span v-if="note.deletedAt" style="opacity: 0.5"
@@ -103,7 +102,6 @@
 					v-if="note.text"
 					:text="note.text"
 					:author="note.user"
-					:i="me"
 					:lang="note.lang"
 					:custom-emojis="note.emojis"
 				/>
@@ -256,7 +254,7 @@ async function toggleMfm() {
 }
 
 function focusFooter(ev) {
-	if (ev.key == "Tab" && !ev.getModifierState("Shift")) {
+	if (ev.key === "Tab" && !ev.getModifierState("Shift")) {
 		emit("focusfooter");
 	}
 }
diff --git a/packages/client/src/components/MkTagCloud.vue b/packages/client/src/components/MkTagCloud.vue
index 40dee3c576..d932cbdc8c 100644
--- a/packages/client/src/components/MkTagCloud.vue
+++ b/packages/client/src/components/MkTagCloud.vue
@@ -76,6 +76,7 @@ onMounted(() => {
 					src: "/client-assets/tagcanvas.min.js",
 				}),
 			)
+			// biome-ignore lint/suspicious/noAssignInExpressions: assign it intentially
 			.addEventListener("load", () => (available.value = true));
 	}
 });
diff --git a/packages/client/src/components/MkTooltip.vue b/packages/client/src/components/MkTooltip.vue
index 883067c51f..becc18fe09 100644
--- a/packages/client/src/components/MkTooltip.vue
+++ b/packages/client/src/components/MkTooltip.vue
@@ -11,7 +11,7 @@
 			:style="{ zIndex, maxWidth: maxWidth + 'px' }"
 		>
 			<slot>
-				<Mfm v-if="asMfm" :text="text" />
+				<Mfm v-if="asMfm" :text="text!" />
 				<span v-else>{{ text }}</span>
 			</slot>
 		</div>
@@ -27,7 +27,7 @@ import { defaultStore } from "@/store";
 const props = withDefaults(
 	defineProps<{
 		showing: boolean;
-		targetElement?: HTMLElement;
+		targetElement?: HTMLElement | null;
 		x?: number;
 		y?: number;
 		text?: string;
@@ -40,6 +40,7 @@ const props = withDefaults(
 		maxWidth: 250,
 		direction: "top",
 		innerMargin: 0,
+		targetElement: null,
 	},
 );
 
@@ -51,7 +52,7 @@ const el = ref<HTMLElement>();
 const zIndex = os.claimZIndex("high");
 
 function setPosition() {
-	const data = calcPopupPosition(el.value, {
+	const data = calcPopupPosition(el.value!, {
 		anchorElement: props.targetElement,
 		direction: props.direction,
 		align: "center",
@@ -60,12 +61,12 @@ function setPosition() {
 		y: props.y,
 	});
 
-	el.value.style.transformOrigin = data.transformOrigin;
-	el.value.style.left = data.left + "px";
-	el.value.style.top = data.top + "px";
+	el.value!.style.transformOrigin = data.transformOrigin;
+	el.value!.style.left = `${data.left}px`;
+	el.value!.style.top = `${data.top}px`;
 }
 
-let loopHandler;
+let loopHandler: number;
 
 onMounted(() => {
 	nextTick(() => {
diff --git a/packages/client/src/components/MkUrlPreview.vue b/packages/client/src/components/MkUrlPreview.vue
index e72bdd704e..95e46ee2cc 100644
--- a/packages/client/src/components/MkUrlPreview.vue
+++ b/packages/client/src/components/MkUrlPreview.vue
@@ -181,10 +181,10 @@ function adjustTweetHeight(message: any) {
 	if (height) tweetHeight.value = height;
 }
 
-(window as any).addEventListener("message", adjustTweetHeight);
+window.addEventListener("message", adjustTweetHeight);
 
 onUnmounted(() => {
-	(window as any).removeEventListener("message", adjustTweetHeight);
+	window.removeEventListener("message", adjustTweetHeight);
 });
 </script>
 
diff --git a/packages/client/src/components/MkUserList.vue b/packages/client/src/components/MkUserList.vue
index 92eb5df953..a9606c69d7 100644
--- a/packages/client/src/components/MkUserList.vue
+++ b/packages/client/src/components/MkUserList.vue
@@ -11,10 +11,10 @@
 			</div>
 		</template>
 
-		<template #default="{ items: users }">
+		<template #default="{ items }: { items: entities.UserDetailed[] }">
 			<div class="efvhhmdq">
 				<MkUserInfo
-					v-for="user in users"
+					v-for="user in items"
 					:key="user.id"
 					class="user"
 					:user="user"
@@ -26,17 +26,22 @@
 
 <script lang="ts" setup>
 import { ref } from "vue";
+import type { entities } from "firefish-js";
 import MkUserInfo from "@/components/MkUserInfo.vue";
-import type { Paging } from "@/components/MkPagination.vue";
+import type {
+	MkPaginationType,
+	PagingKeyOf,
+	PagingOf,
+} from "@/components/MkPagination.vue";
 import MkPagination from "@/components/MkPagination.vue";
 import { i18n } from "@/i18n";
 
 defineProps<{
-	pagination: Paging;
+	pagination: PagingOf<entities.UserDetailed>;
 	noGap?: boolean;
 }>();
 
-const pagingComponent = ref<InstanceType<typeof MkPagination>>();
+const pagingComponent = ref<MkPaginationType<PagingKeyOf<entities.User>>>();
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/MkUserSelectDialog.vue b/packages/client/src/components/MkUserSelectDialog.vue
index 3017e9bd6c..aa3a4c63c1 100644
--- a/packages/client/src/components/MkUserSelectDialog.vue
+++ b/packages/client/src/components/MkUserSelectDialog.vue
@@ -98,16 +98,16 @@ import { defaultStore } from "@/store";
 import { i18n } from "@/i18n";
 
 const emit = defineEmits<{
-	(ev: "ok", selected: entities.UserDetailed): void;
-	(ev: "cancel"): void;
-	(ev: "closed"): void;
+	ok: [selected: entities.UserDetailed];
+	cancel: [];
+	closed: [];
 }>();
 
 const username = ref("");
 const host = ref("");
-const users: entities.UserDetailed[] = ref([]);
-const recentUsers: entities.UserDetailed[] = ref([]);
-const selected: entities.UserDetailed | null = ref(null);
+const users = ref<entities.UserDetailed[]>([]);
+const recentUsers = ref<entities.UserDetailed[]>([]);
+const selected = ref<entities.UserDetailed | null>(null);
 const dialogEl = ref();
 
 const search = () => {
@@ -132,7 +132,7 @@ const ok = () => {
 
 	// 最近使ったユーザー更新
 	let recents = defaultStore.state.recentlyUsedUsers;
-	recents = recents.filter((x) => x !== selected.value.id);
+	recents = recents.filter((x) => x !== selected.value!.id);
 	recents.unshift(selected.value.id);
 	defaultStore.set("recentlyUsedUsers", recents.splice(0, 16));
 };
diff --git a/packages/client/src/components/MkUserSelectLocalDialog.vue b/packages/client/src/components/MkUserSelectLocalDialog.vue
index bd8494cb7a..1a8aff3c83 100644
--- a/packages/client/src/components/MkUserSelectLocalDialog.vue
+++ b/packages/client/src/components/MkUserSelectLocalDialog.vue
@@ -94,9 +94,9 @@ import { defaultStore } from "@/store";
 import { i18n } from "@/i18n";
 
 const emit = defineEmits<{
-	(ev: "ok", selected: entities.UserDetailed): void;
-	(ev: "cancel"): void;
-	(ev: "closed"): void;
+	ok: [selected: entities.UserDetailed];
+	cancel: [];
+	closed: [];
 }>();
 
 const username = ref("");
@@ -114,7 +114,7 @@ const search = () => {
 		query: username.value,
 		origin: "local",
 		limit: 10,
-		detail: false,
+		detail: true,
 	}).then((_users) => {
 		users.value = _users;
 	});
@@ -127,7 +127,7 @@ const ok = () => {
 
 	// 最近使ったユーザー更新
 	let recents = defaultStore.state.recentlyUsedUsers;
-	recents = recents.filter((x) => x !== selected.value.id);
+	recents = recents.filter((x) => x !== selected.value!.id);
 	recents.unshift(selected.value.id);
 	defaultStore.set("recentlyUsedUsers", recents.splice(0, 16));
 };
diff --git a/packages/client/src/components/MkUsersTooltip.vue b/packages/client/src/components/MkUsersTooltip.vue
index 25af3ac121..30213204d6 100644
--- a/packages/client/src/components/MkUsersTooltip.vue
+++ b/packages/client/src/components/MkUsersTooltip.vue
@@ -3,6 +3,7 @@
 		ref="tooltip"
 		:target-element="targetElement"
 		:max-width="250"
+		:showing="showing"
 		@closed="emit('closed')"
 	>
 		<div class="beaffaef">
@@ -18,12 +19,14 @@
 </template>
 
 <script lang="ts" setup>
+import type { entities } from "firefish-js";
 import MkTooltip from "./MkTooltip.vue";
 
 defineProps<{
-	users: any[]; // TODO
+	showing: boolean;
+	users: entities.User[];
 	count: number;
-	targetElement: HTMLElement;
+	targetElement?: HTMLElement;
 }>();
 
 const emit = defineEmits<{
diff --git a/packages/client/src/components/MkVisibilityPicker.vue b/packages/client/src/components/MkVisibilityPicker.vue
index 0e6f8cb153..bfa8102615 100644
--- a/packages/client/src/components/MkVisibilityPicker.vue
+++ b/packages/client/src/components/MkVisibilityPicker.vue
@@ -3,7 +3,7 @@
 		ref="modal"
 		:z-priority="'high'"
 		:src="src"
-		@click="modal.close()"
+		@click="modal!.close()"
 		@closed="emit('closed')"
 	>
 		<div class="_popup" :class="$style.root">
@@ -153,15 +153,15 @@ const props = withDefaults(
 	defineProps<{
 		currentVisibility: NoteVisibility;
 		currentLocalOnly: boolean;
-		src?: HTMLElement;
+		src?: HTMLElement | null;
 	}>(),
 	{},
 );
 
 const emit = defineEmits<{
-	(ev: "changeVisibility", v: NoteVisibility): void;
-	(ev: "changeLocalOnly", v: boolean): void;
-	(ev: "closed"): void;
+	changeVisibility: [v: NoteVisibility];
+	changeLocalOnly: [v: boolean];
+	closed: [];
 }>();
 
 const v = ref(props.currentVisibility);
@@ -175,7 +175,7 @@ function choose(visibility: NoteVisibility): void {
 	v.value = visibility;
 	emit("changeVisibility", visibility);
 	nextTick(() => {
-		modal.value.close();
+		modal.value!.close();
 	});
 }
 </script>
diff --git a/packages/client/src/components/MkWidgets.vue b/packages/client/src/components/MkWidgets.vue
index 1b1dafa522..fb8a449590 100644
--- a/packages/client/src/components/MkWidgets.vue
+++ b/packages/client/src/components/MkWidgets.vue
@@ -85,7 +85,7 @@ import icon from "@/scripts/icon";
 interface Widget {
 	name: string;
 	id: string;
-	data: Record<string, any>;
+	data: Record<string, unknown>;
 }
 
 const props = defineProps<{
@@ -137,12 +137,12 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
 			return isLink(el.parentElement);
 		}
 	};
-	if (isLink(ev.target)) return;
+	if (isLink(ev.target as HTMLElement)) return;
 	if (
 		["INPUT", "TEXTAREA", "IMG", "VIDEO", "CANVAS"].includes(
-			ev.target.tagName,
+			(ev.target as HTMLElement).tagName,
 		) ||
-		ev.target.attributes.contenteditable
+		(ev.target as HTMLElement).getAttribute("contentEditable")
 	)
 		return;
 	if (window.getSelection()?.toString() !== "") return;
diff --git a/packages/client/src/components/MkWindow.vue b/packages/client/src/components/MkWindow.vue
index 147d0bec1f..8e0747abd5 100644
--- a/packages/client/src/components/MkWindow.vue
+++ b/packages/client/src/components/MkWindow.vue
@@ -271,7 +271,7 @@ function onHeaderMousedown(evt: MouseEvent) {
 			? evt.touches[0].clientY
 			: evt.clientY;
 	const moveBaseX = beforeMaximized
-		? parseInt(unMaximizedWidth, 10) / 2
+		? Number.parseInt(unMaximizedWidth, 10) / 2
 		: clickX - position.left; // TODO: parseIntやめる
 	const moveBaseY = beforeMaximized ? 20 : clickY - position.top;
 	const browserWidth = window.innerWidth;
@@ -321,8 +321,8 @@ function onTopHandleMousedown(evt) {
 	const main = rootEl.value;
 
 	const base = evt.clientY;
-	const height = parseInt(getComputedStyle(main, "").height, 10);
-	const top = parseInt(getComputedStyle(main, "").top, 10);
+	const height = Number.parseInt(getComputedStyle(main, "").height, 10);
+	const top = Number.parseInt(getComputedStyle(main, "").top, 10);
 
 	// 動かした時
 	dragListen((me) => {
@@ -349,8 +349,8 @@ function onRightHandleMousedown(evt) {
 	const main = rootEl.value;
 
 	const base = evt.clientX;
-	const width = parseInt(getComputedStyle(main, "").width, 10);
-	const left = parseInt(getComputedStyle(main, "").left, 10);
+	const width = Number.parseInt(getComputedStyle(main, "").width, 10);
+	const left = Number.parseInt(getComputedStyle(main, "").left, 10);
 	const browserWidth = window.innerWidth;
 
 	// 動かした時
@@ -375,8 +375,8 @@ function onBottomHandleMousedown(evt) {
 	const main = rootEl.value;
 
 	const base = evt.clientY;
-	const height = parseInt(getComputedStyle(main, "").height, 10);
-	const top = parseInt(getComputedStyle(main, "").top, 10);
+	const height = Number.parseInt(getComputedStyle(main, "").height, 10);
+	const top = Number.parseInt(getComputedStyle(main, "").top, 10);
 	const browserHeight = window.innerHeight;
 
 	// 動かした時
@@ -401,8 +401,8 @@ function onLeftHandleMousedown(evt) {
 	const main = rootEl.value;
 
 	const base = evt.clientX;
-	const width = parseInt(getComputedStyle(main, "").width, 10);
-	const left = parseInt(getComputedStyle(main, "").left, 10);
+	const width = Number.parseInt(getComputedStyle(main, "").width, 10);
+	const left = Number.parseInt(getComputedStyle(main, "").left, 10);
 
 	// 動かした時
 	dragListen((me) => {
diff --git a/packages/client/src/components/form/range.vue b/packages/client/src/components/form/range.vue
index d12596a86c..1377abf034 100644
--- a/packages/client/src/components/form/range.vue
+++ b/packages/client/src/components/form/range.vue
@@ -48,7 +48,7 @@ const id = os.getUniqueId();
 
 const props = withDefaults(
 	defineProps<{
-		modelValue: number;
+		modelValue: number | null;
 		disabled?: boolean;
 		min: number;
 		max: number;
diff --git a/packages/client/src/components/global/MkAcct.vue b/packages/client/src/components/global/MkAcct.vue
index 02112d4481..dff995f878 100644
--- a/packages/client/src/components/global/MkAcct.vue
+++ b/packages/client/src/components/global/MkAcct.vue
@@ -16,7 +16,7 @@ import { host as hostRaw } from "@/config";
 import { defaultStore } from "@/store";
 
 defineProps<{
-	user: entities.UserDetailed;
+	user: entities.UserLite;
 	detail?: boolean;
 }>();
 
diff --git a/packages/client/src/components/global/MkEmoji.vue b/packages/client/src/components/global/MkEmoji.vue
index 86e3d344b6..57380ba6f4 100644
--- a/packages/client/src/components/global/MkEmoji.vue
+++ b/packages/client/src/components/global/MkEmoji.vue
@@ -54,8 +54,8 @@ const url = computed(() => {
 		return char2filePath(char.value);
 	} else {
 		return defaultStore.state.disableShowingAnimatedImages
-			? getStaticImageUrl(customEmoji.value.url)
-			: customEmoji.value.url;
+			? getStaticImageUrl(customEmoji.value!.url)
+			: customEmoji.value!.url;
 	}
 });
 const alt = computed(() =>
diff --git a/packages/client/src/components/global/MkMisskeyFlavoredMarkdown.vue b/packages/client/src/components/global/MkMisskeyFlavoredMarkdown.vue
index 608a63c528..fd3e419c97 100644
--- a/packages/client/src/components/global/MkMisskeyFlavoredMarkdown.vue
+++ b/packages/client/src/components/global/MkMisskeyFlavoredMarkdown.vue
@@ -26,7 +26,7 @@ withDefaults(
 		text: string;
 		plain?: boolean;
 		nowrap?: boolean;
-		author?: entities.User;
+		author?: entities.User | null;
 		customEmojis?: entities.EmojiLite[];
 		isNote?: boolean;
 		advancedMfm?: boolean;
diff --git a/packages/client/src/components/mfm.ts b/packages/client/src/components/mfm.ts
index 106ab83c52..548e3d51f6 100644
--- a/packages/client/src/components/mfm.ts
+++ b/packages/client/src/components/mfm.ts
@@ -1,6 +1,6 @@
 import { defineComponent, h } from "vue";
 import * as mfm from "mfm-js";
-import type { PropType, VNode } from "vue";
+import type { PropType, VNodeArrayChildren } from "vue";
 import type { entities } from "firefish-js";
 import MkUrl from "@/components/global/MkUrl.vue";
 import MkLink from "@/components/MkLink.vue";
@@ -30,11 +30,12 @@ export default defineComponent({
 			default: false,
 		},
 		author: {
-			type: Object,
+			type: Object as PropType<entities.User | null>,
 			default: null,
 		},
+		// TODO: This variable is not used in the code and may be removed
 		i: {
-			type: Object,
+			type: Object as PropType<entities.User>,
 			default: null,
 		},
 		customEmojis: {
@@ -58,14 +59,16 @@ export default defineComponent({
 
 		const ast = (isPlain ? mfm.parseSimple : mfm.parse)(this.text);
 
-		const validTime = (t: string | null | undefined) => {
+		const validTime = (t: string | null | undefined | boolean) => {
 			if (t == null) return null;
+			if (typeof t !== "string") return null;
 			return t.match(/^[0-9.]+s$/) ? t : null;
 		};
 
-		const validNumber = (n: string | null | undefined) => {
+		const validNumber = (n: string | null | undefined | boolean) => {
 			if (n == null) return null;
-			const parsed = parseFloat(n);
+			if (typeof n !== "string") return null;
+			const parsed = Number.parseFloat(n);
 			return !Number.isNaN(parsed) && Number.isFinite(parsed) && parsed > 0;
 		};
 		// const validEase = (e: string | null | undefined) => {
@@ -77,13 +80,13 @@ export default defineComponent({
 
 		const genEl = (ast: mfm.MfmNode[]) =>
 			concat(
-				ast.map((token, index): VNode[] => {
+				ast.map((token, index): VNodeArrayChildren => {
 					switch (token.type) {
 						case "text": {
 							const text = token.props.text.replace(/(\r\n|\n|\r)/g, "\n");
 
 							if (!this.plain) {
-								const res = [];
+								const res: VNodeArrayChildren = [];
 								for (const t of text.split("\n")) {
 									res.push(h("br"));
 									res.push(t);
@@ -104,18 +107,20 @@ export default defineComponent({
 						}
 
 						case "italic": {
-							return h(
-								"i",
-								{
-									style: "font-style: oblique;",
-								},
-								genEl(token.children),
-							);
+							return [
+								h(
+									"i",
+									{
+										style: "font-style: oblique;",
+									},
+									genEl(token.children),
+								),
+							];
 						}
 
 						case "fn": {
 							// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
-							let style: string;
+							let style: string | null = null;
 							switch (token.props.name) {
 								case "tada": {
 									const speed = validTime(token.props.args.speed) || "1s";
@@ -188,7 +193,7 @@ export default defineComponent({
 									if (reducedMotion()) {
 										return genEl(token.children);
 									}
-									return h(MkSparkle, {}, genEl(token.children));
+									return [h(MkSparkle, {}, genEl(token.children))];
 								}
 								case "fade": {
 									const direction = token.props.args.out
@@ -211,31 +216,37 @@ export default defineComponent({
 									break;
 								}
 								case "x2": {
-									return h(
-										"span",
-										{
-											class: "mfm-x2",
-										},
-										genEl(token.children),
-									);
+									return [
+										h(
+											"span",
+											{
+												class: "mfm-x2",
+											},
+											genEl(token.children),
+										),
+									];
 								}
 								case "x3": {
-									return h(
-										"span",
-										{
-											class: "mfm-x3",
-										},
-										genEl(token.children),
-									);
+									return [
+										h(
+											"span",
+											{
+												class: "mfm-x3",
+											},
+											genEl(token.children),
+										),
+									];
 								}
 								case "x4": {
-									return h(
-										"span",
-										{
-											class: "mfm-x4",
-										},
-										genEl(token.children),
-									);
+									return [
+										h(
+											"span",
+											{
+												class: "mfm-x4",
+											},
+											genEl(token.children),
+										),
+									];
 								}
 								case "font": {
 									const family = token.props.args.serif
@@ -255,13 +266,15 @@ export default defineComponent({
 									break;
 								}
 								case "blur": {
-									return h(
-										"span",
-										{
-											class: "_blur_text",
-										},
-										genEl(token.children),
-									);
+									return [
+										h(
+											"span",
+											{
+												class: "_blur_text",
+											},
+											genEl(token.children),
+										),
+									];
 								}
 								case "rotate": {
 									const rotate = token.props.args.x
@@ -269,77 +282,105 @@ export default defineComponent({
 										: token.props.args.y
 											? "perspective(128px) rotateY"
 											: "rotate";
-									const degrees = parseFloat(token.props.args.deg ?? "90");
+									const degrees = Number.parseFloat(
+										token.props.args.deg.toString() ?? "90",
+									);
 									style = `transform: ${rotate}(${degrees}deg); transform-origin: center center;`;
 									break;
 								}
 								case "position": {
-									const x = parseFloat(token.props.args.x ?? "0");
-									const y = parseFloat(token.props.args.y ?? "0");
+									const x = Number.parseFloat(
+										token.props.args.x.toString() ?? "0",
+									);
+									const y = Number.parseFloat(
+										token.props.args.y.toString() ?? "0",
+									);
 									style = `transform: translateX(${x}em) translateY(${y}em);`;
 									break;
 								}
 								case "crop": {
-									const top = parseFloat(token.props.args.top ?? "0");
-									const right = parseFloat(token.props.args.right ?? "0");
-									const bottom = parseFloat(token.props.args.bottom ?? "0");
-									const left = parseFloat(token.props.args.left ?? "0");
+									const top = Number.parseFloat(
+										token.props.args.top.toString() ?? "0",
+									);
+									const right = Number.parseFloat(
+										token.props.args.right.toString() ?? "0",
+									);
+									const bottom = Number.parseFloat(
+										token.props.args.bottom.toString() ?? "0",
+									);
+									const left = Number.parseFloat(
+										token.props.args.left.toString() ?? "0",
+									);
 									style = `clip-path: inset(${top}% ${right}% ${bottom}% ${left}%);`;
 									break;
 								}
 								case "scale": {
-									const x = Math.min(parseFloat(token.props.args.x ?? "1"), 5);
-									const y = Math.min(parseFloat(token.props.args.y ?? "1"), 5);
+									const x = Math.min(
+										Number.parseFloat(token.props.args.x.toString() ?? "1"),
+										5,
+									);
+									const y = Math.min(
+										Number.parseFloat(token.props.args.y.toString() ?? "1"),
+										5,
+									);
 									style = `transform: scale(${x}, ${y});`;
 									break;
 								}
 								case "fg": {
 									let color = token.props.args.color;
-									if (!/^[0-9a-f]{3,6}$/i.test(color)) color = "f00";
+									if (!/^[0-9a-f]{3,6}$/i.test(color.toString())) color = "f00";
 									style = `color: #${color};`;
 									break;
 								}
 								case "bg": {
 									let color = token.props.args.color;
-									if (!/^[0-9a-f]{3,6}$/i.test(color)) color = "f00";
+									if (!/^[0-9a-f]{3,6}$/i.test(color.toString())) color = "f00";
 									style = `background-color: #${color};`;
 									break;
 								}
 								case "small": {
-									return h(
-										"small",
-										{
-											style: "opacity: 0.7;",
-										},
-										genEl(token.children),
-									);
+									return [
+										h(
+											"small",
+											{
+												style: "opacity: 0.7;",
+											},
+											genEl(token.children),
+										),
+									];
 								}
 								case "center": {
-									return h(
-										"div",
-										{
-											style: "text-align: center;",
-										},
-										genEl(token.children),
-									);
+									return [
+										h(
+											"div",
+											{
+												style: "text-align: center;",
+											},
+											genEl(token.children),
+										),
+									];
 								}
 							}
 							if (style == null) {
-								return h("span", {}, [
-									"$[",
-									token.props.name,
-									" ",
-									...genEl(token.children),
-									"]",
-								]);
+								return [
+									h("span", {}, [
+										"$[",
+										token.props.name,
+										" ",
+										...genEl(token.children),
+										"]",
+									]),
+								];
 							} else {
-								return h(
-									"span",
-									{
-										style: `display: inline-block;${style}`,
-									},
-									genEl(token.children),
-								);
+								return [
+									h(
+										"span",
+										{
+											style: `display: inline-block;${style}`,
+										},
+										genEl(token.children),
+									),
+								];
 							}
 						}
 
@@ -425,7 +466,7 @@ export default defineComponent({
 								h(MkCode, {
 									key: Math.random(),
 									code: token.props.code,
-									lang: token.props.lang,
+									lang: token.props.lang ?? undefined,
 								}),
 							];
 						}
@@ -506,13 +547,15 @@ export default defineComponent({
 								const ast2 = (isPlain ? mfm.parseSimple : mfm.parse)(
 									token.props.content.slice(0, -6) + sentinel,
 								);
+								function isMfmText(n: mfm.MfmNode): n is mfm.MfmText {
+									return n.type === "text";
+								}
+								const txtNode = ast2[ast2.length - 1];
 								if (
-									ast2[ast2.length - 1].type === "text" &&
-									ast2[ast2.length - 1].props.text.endsWith(sentinel)
+									isMfmText(txtNode) &&
+									txtNode.props.text.endsWith(sentinel)
 								) {
-									ast2[ast2.length - 1].props.text = ast2[
-										ast2.length - 1
-									].props.text.slice(0, -1);
+									txtNode.props.text = txtNode.props.text.slice(0, -1);
 								} else {
 									// I don't think this scope is reachable
 									console.warn(
@@ -554,8 +597,10 @@ export default defineComponent({
 						}
 
 						default: {
-							console.error("unrecognized ast type:", token.type);
-
+							console.error(
+								"unrecognized ast type:",
+								(token as { type: never }).type,
+							);
 							return [];
 						}
 					}
diff --git a/packages/client/src/config.ts b/packages/client/src/config.ts
index e77ee87e85..1f9d6cdc7a 100644
--- a/packages/client/src/config.ts
+++ b/packages/client/src/config.ts
@@ -13,7 +13,7 @@ export const wsUrl = `${url
 export const lang = localStorage.getItem("lang");
 export const langs = _LANGS_;
 export const locale = JSON.parse(localStorage.getItem("locale") || "en-US");
-export const version = _VERSION_;
+export const version: string = _VERSION_;
 export const instanceName = siteName === "Firefish" ? host : siteName;
 export const ui = localStorage.getItem("ui");
 export const debug = localStorage.getItem("debug") === "true";
diff --git a/packages/client/src/nirax.ts b/packages/client/src/nirax.ts
index f214060762..da162338b6 100644
--- a/packages/client/src/nirax.ts
+++ b/packages/client/src/nirax.ts
@@ -6,7 +6,7 @@ import { shallowRef } from "vue";
 import { safeURIDecode } from "@/scripts/safe-uri-decode";
 import { pleaseLogin } from "@/scripts/please-login";
 
-interface RouteDef {
+export interface RouteDef {
 	path: string;
 	component: Component;
 	query?: Record<string, string>;
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index b8fac741ea..c7c62852de 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -1,9 +1,9 @@
 // TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
 
 import { EventEmitter } from "eventemitter3";
-import { type entities, api as firefishApi } from "firefish-js";
+import { type Endpoints, type entities, api as firefishApi } from "firefish-js";
 import insertTextAtCursor from "insert-text-at-cursor";
-import type { Component, Ref } from "vue";
+import type { Component, MaybeRef, Ref } from "vue";
 import { defineAsyncComponent, markRaw, ref } from "vue";
 import { i18n } from "./i18n";
 import MkDialog from "@/components/MkDialog.vue";
@@ -13,6 +13,7 @@ import MkWaitingDialog from "@/components/MkWaitingDialog.vue";
 import { apiUrl, url } from "@/config";
 import { me } from "@/me";
 import type { MenuItem } from "@/types/menu";
+import type { Form, GetFormResultType } from "@/types/form";
 
 export const pendingApiRequestsCount = ref(0);
 
@@ -22,7 +23,7 @@ const apiClient = new firefishApi.APIClient({
 
 export const api = ((
 	endpoint: string,
-	data: Record<string, any> = {},
+	data: Record<string, unknown> = {},
 	token?: string | null | undefined,
 	useToken = true,
 ) => {
@@ -51,7 +52,7 @@ export const api = ((
 				if (res.status === 200) {
 					resolve(body);
 				} else if (res.status === 204) {
-					resolve();
+					resolve(undefined);
 				} else {
 					reject(body.error);
 				}
@@ -66,7 +67,7 @@ export const api = ((
 
 export const apiGet = ((
 	endpoint: string,
-	data: Record<string, any> = {},
+	data: URLSearchParams | string | string[][] | Record<string, string> = {},
 	token?: string | null | undefined,
 ) => {
 	pendingApiRequestsCount.value++;
@@ -96,7 +97,7 @@ export const apiGet = ((
 				if (res.status === 200) {
 					resolve(body);
 				} else if (res.status === 204) {
-					resolve();
+					resolve(undefined);
 				} else {
 					reject(body.error);
 				}
@@ -110,27 +111,27 @@ export const apiGet = ((
 }) as typeof apiClient.request;
 
 export const apiWithDialog = ((
-	endpoint: string,
-	data: Record<string, any> = {},
+	endpoint: keyof Endpoints,
+	data: Record<string, unknown> = {},
 	token?: string | null | undefined,
 ) => {
 	const promise = api(endpoint, data, token);
 	promiseDialog(promise, null, (err) => {
 		alert({
 			type: "error",
-			text: err.message + "\n" + (err as any).id,
+			text: `${err.message}\n${err.id}`,
 		});
 	});
 
 	return promise;
 }) as typeof api;
 
-export function promiseDialog<T extends Promise<any>>(
-	promise: T,
-	onSuccess?: ((res: any) => void) | null,
-	onFailure?: ((err: Error) => void) | null,
+export function promiseDialog<T>(
+	promise: Promise<T>,
+	onSuccess?: ((res: T) => void) | null,
+	onFailure?: ((err: firefishApi.APIError) => void) | null,
 	text?: string,
-): T {
+): Promise<T> {
 	const showing = ref(true);
 	const success = ref(false);
 
@@ -174,13 +175,14 @@ export function promiseDialog<T extends Promise<any>>(
 }
 
 let popupIdCount = 0;
-export const popups = ref([]) as Ref<
-	{
-		id: any;
-		component: any;
-		props: Record<string, any>;
-	}[]
->;
+
+interface PopupType {
+	id: number;
+	component: Component;
+	props: Record<string, unknown>;
+	events: Record<string, unknown>;
+}
+export const popups = ref<PopupType[]>([]);
 
 const zIndexes = {
 	low: 1000000,
@@ -196,14 +198,30 @@ export function claimZIndex(
 
 let uniqueId = 0;
 export function getUniqueId(): string {
-	return uniqueId++ + "";
+	return String(uniqueId++);
 }
 
-export async function popup(
-	component: Component,
-	props: Record<string, any>,
-	events = {},
-	disposeEvent?: string,
+interface VueComponentConstructor<P, E> {
+	__isFragment?: never;
+	__isTeleport?: never;
+	__isSuspense?: never;
+	new (): {
+		$props: P;
+	};
+	emits?: E;
+}
+
+type NonArrayAble<A> = A extends Array<unknown> ? never : A;
+
+type CanUseRef<T> = {
+	[K in keyof T]: MaybeRef<T[K]>;
+};
+
+export async function popup<Props, Emits>(
+	component: VueComponentConstructor<Props, Emits>,
+	props: CanUseRef<Props>,
+	events: Partial<NonArrayAble<NonNullable<Emits>>> = {},
+	disposeEvent?: keyof Partial<NonArrayAble<NonNullable<Emits>>>,
 ) {
 	markRaw(component);
 
@@ -216,7 +234,7 @@ export async function popup(
 	};
 	const state = {
 		component,
-		props,
+		props: props as Record<string, unknown>,
 		events: disposeEvent
 			? {
 					...events,
@@ -226,6 +244,7 @@ export async function popup(
 		id,
 	};
 
+	// Hint: Vue will automatically resolve ref here, so it is safe to use ref in props
 	popups.value.push(state);
 
 	return {
@@ -280,7 +299,7 @@ export function alert(props: {
 	text?: string | null;
 	isPlaintext?: boolean;
 }): Promise<void> {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		if (props.text == null && props.type === "error") {
 			props.text = i18n.ts.somethingHappened;
 		}
@@ -305,7 +324,7 @@ export function confirm(props: {
 	cancelText?: string;
 	isPlaintext?: boolean;
 }): Promise<{ canceled: boolean }> {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		popup(
 			MkDialog,
 			{
@@ -328,7 +347,7 @@ export function yesno(props: {
 	text?: string | null;
 	isPlaintext?: boolean;
 }): Promise<{ canceled: boolean }> {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkDialog.vue"),
@@ -360,13 +379,13 @@ export function inputText(props: {
 	minLength?: number;
 	maxLength?: number;
 }): Promise<
-	| { canceled: true; result: undefined }
+	| { canceled: true; result?: undefined }
 	| {
 			canceled: false;
 			result: string;
 	  }
 > {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		popup(
 			MkDialog,
 			{
@@ -383,7 +402,14 @@ export function inputText(props: {
 			},
 			{
 				done: (result) => {
-					resolve(result || { canceled: true });
+					if (result.canceled) {
+						resolve({ canceled: true });
+					} else {
+						resolve({
+							canceled: false,
+							result: String(result.result),
+						});
+					}
 				},
 			},
 			"closed",
@@ -397,13 +423,13 @@ export function inputParagraph(props: {
 	placeholder?: string | null;
 	default?: string | null;
 }): Promise<
-	| { canceled: true; result: undefined }
+	| { canceled: true; result?: undefined }
 	| {
 			canceled: false;
 			result: string;
 	  }
 > {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkDialog.vue"),
@@ -421,7 +447,14 @@ export function inputParagraph(props: {
 			},
 			{
 				done: (result) => {
-					resolve(result || { canceled: true });
+					if (result.canceled) {
+						resolve({ canceled: true });
+					} else {
+						resolve({
+							canceled: false,
+							result: String(result.result),
+						});
+					}
 				},
 			},
 			"closed",
@@ -436,13 +469,13 @@ export function inputNumber(props: {
 	default?: number | null;
 	autocomplete?: string;
 }): Promise<
-	| { canceled: true; result: undefined }
+	| { canceled: true; result?: undefined }
 	| {
 			canceled: false;
 			result: number;
 	  }
 > {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkDialog.vue"),
@@ -461,7 +494,14 @@ export function inputNumber(props: {
 			},
 			{
 				done: (result) => {
-					resolve(result || { canceled: true });
+					if (result.canceled) {
+						resolve({ canceled: true });
+					} else {
+						resolve({
+							canceled: false,
+							result: Number(result.result),
+						});
+					}
 				},
 			},
 			"closed",
@@ -473,15 +513,16 @@ export function inputDate(props: {
 	title?: string | null;
 	text?: string | null;
 	placeholder?: string | null;
-	default?: Date | null;
+	default?: Date | string | null;
 }): Promise<
-	| { canceled: true; result: undefined }
+	| { canceled: true; result?: undefined }
 	| {
 			canceled: false;
 			result: Date;
 	  }
 > {
-	return new Promise((resolve, reject) => {
+	props.default ??= new Date();
+	return new Promise((resolve, _reject) => {
 		popup(
 			MkDialog,
 			{
@@ -490,7 +531,10 @@ export function inputDate(props: {
 				input: {
 					type: "date",
 					placeholder: props.placeholder,
-					default: props.default,
+					default:
+						props.default instanceof Date
+							? props.default.toISOString().slice(0, 10)
+							: props.default,
 				},
 			},
 			{
@@ -498,7 +542,7 @@ export function inputDate(props: {
 					resolve(
 						result
 							? {
-									result: new Date(result.result),
+									result: new Date(result.result as string | number),
 									canceled: false,
 								}
 							: { canceled: true },
@@ -510,7 +554,7 @@ export function inputDate(props: {
 	});
 }
 
-export function select<C = any>(
+export function select<C extends string>(
 	props: {
 		title?: string | null;
 		text?: string | null;
@@ -521,8 +565,10 @@ export function select<C = any>(
 					value: C;
 					text: string;
 				}[];
+				groupedItems?: undefined;
 		  }
 		| {
+				items?: undefined;
 				groupedItems: {
 					label: string;
 					items: {
@@ -533,27 +579,35 @@ export function select<C = any>(
 		  }
 	),
 ): Promise<
-	| { canceled: true; result: undefined }
+	| { canceled: true; result?: undefined }
 	| {
 			canceled: false;
 			result: C;
 	  }
 > {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		popup(
 			MkDialog,
 			{
 				title: props.title,
 				text: props.text,
-				select: {
-					items: props.items,
-					groupedItems: props.groupedItems,
-					default: props.default,
-				},
+				select: props.items
+					? {
+							items: props.items,
+							default: props.default,
+						}
+					: {
+							groupedItems: props.groupedItems,
+							default: props.default,
+						},
 			},
 			{
 				done: (result) => {
-					resolve(result || { canceled: true });
+					if (result.canceled) {
+						resolve({ canceled: true });
+					} else {
+						resolve(result as never);
+					}
 				},
 			},
 			"closed",
@@ -562,7 +616,7 @@ export function select<C = any>(
 }
 
 export function success(): Promise<void> {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		const showing = ref(true);
 		window.setTimeout(() => {
 			showing.value = false;
@@ -582,7 +636,7 @@ export function success(): Promise<void> {
 }
 
 export function waiting(): Promise<void> {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		const showing = ref(true);
 		popup(
 			MkWaitingDialog,
@@ -598,8 +652,11 @@ export function waiting(): Promise<void> {
 	});
 }
 
-export function form(title, form) {
-	return new Promise((resolve, reject) => {
+export function form<T extends Form>(title: string, form: T) {
+	return new Promise<{
+		result?: GetFormResultType<T>;
+		canceled?: true;
+	}>((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkFormDialog.vue"),
@@ -609,7 +666,7 @@ export function form(title, form) {
 			{ title, form },
 			{
 				done: (result) => {
-					resolve(result);
+					resolve(result as never);
 				},
 			},
 			"closed",
@@ -617,8 +674,8 @@ export function form(title, form) {
 	});
 }
 
-export async function selectUser() {
-	return new Promise((resolve, reject) => {
+export function selectUser() {
+	return new Promise<entities.UserDetailed>((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkUserSelectDialog.vue"),
@@ -636,8 +693,8 @@ export async function selectUser() {
 	});
 }
 
-export async function selectLocalUser() {
-	return new Promise((resolve, reject) => {
+export function selectLocalUser() {
+	return new Promise<entities.UserDetailed>((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkUserSelectLocalDialog.vue"),
@@ -655,8 +712,8 @@ export async function selectLocalUser() {
 	});
 }
 
-export async function selectInstance(): Promise<entities.Instance> {
-	return new Promise((resolve, reject) => {
+export function selectInstance(): Promise<entities.Instance> {
+	return new Promise((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkInstanceSelectDialog.vue"),
@@ -674,8 +731,10 @@ export async function selectInstance(): Promise<entities.Instance> {
 	});
 }
 
-export async function selectDriveFile(multiple: boolean) {
-	return new Promise((resolve, reject) => {
+export function selectDriveFile<Multiple extends boolean>(multiple: Multiple) {
+	return new Promise<
+		Multiple extends true ? entities.DriveFile[] : entities.DriveFile
+	>((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkDriveSelectDialog.vue"),
@@ -689,7 +748,7 @@ export async function selectDriveFile(multiple: boolean) {
 			{
 				done: (files) => {
 					if (files) {
-						resolve(multiple ? files : files[0]);
+						resolve((multiple ? files : files[0]) as never);
 					}
 				},
 			},
@@ -699,7 +758,7 @@ export async function selectDriveFile(multiple: boolean) {
 }
 
 export async function selectDriveFolder(multiple: boolean) {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkDriveSelectDialog.vue"),
@@ -723,7 +782,7 @@ export async function selectDriveFolder(multiple: boolean) {
 }
 
 export async function pickEmoji(src: HTMLElement | null, opts) {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkEmojiPickerDialog.vue"),
@@ -750,7 +809,7 @@ export async function cropImage(
 		aspectRatio: number;
 	},
 ): Promise<entities.DriveFile> {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkCropperDialog.vue"),
@@ -773,13 +832,13 @@ export async function cropImage(
 
 type AwaitType<T> = T extends Promise<infer U>
 	? U
-	: T extends (...args: any[]) => Promise<infer V>
+	: T extends (...args: unknown[]) => Promise<infer V>
 		? V
 		: T;
 let openingEmojiPicker: AwaitType<ReturnType<typeof popup>> | null = null;
-let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null;
+let activeTextarea: HTMLTextAreaElement | HTMLInputElement;
 export async function openEmojiPicker(
-	src?: HTMLElement,
+	src: HTMLElement | undefined,
 	opts,
 	initialTextarea: typeof activeTextarea,
 ) {
@@ -787,7 +846,9 @@ export async function openEmojiPicker(
 
 	activeTextarea = initialTextarea;
 
-	const textareas = document.querySelectorAll("textarea, input");
+	const textareas = document.querySelectorAll<
+		HTMLTextAreaElement | HTMLInputElement
+	>("textarea, input");
 	for (const textarea of Array.from(textareas)) {
 		textarea.addEventListener("focus", () => {
 			activeTextarea = textarea;
@@ -799,7 +860,9 @@ export async function openEmojiPicker(
 			for (const node of Array.from(record.addedNodes).filter(
 				(node) => node instanceof HTMLElement,
 			) as HTMLElement[]) {
-				const textareas = node.querySelectorAll("textarea, input");
+				const textareas = node.querySelectorAll<
+					HTMLTextAreaElement | HTMLInputElement
+				>("textarea, input");
 				for (const textarea of Array.from(textareas).filter(
 					(textarea) => textarea.dataset.preventEmojiInsert == null,
 				)) {
@@ -837,7 +900,7 @@ export async function openEmojiPicker(
 				insertTextAtCursor(activeTextarea, emoji);
 			},
 			closed: () => {
-				openingEmojiPicker!.dispose();
+				openingEmojiPicker?.dispose();
 				openingEmojiPicker = null;
 				observer.disconnect();
 			},
@@ -847,7 +910,7 @@ export async function openEmojiPicker(
 
 export function popupMenu(
 	items: MenuItem[] | Ref<MenuItem[]>,
-	src?: HTMLElement,
+	src?: HTMLElement | null,
 	options?: {
 		align?: string;
 		width?: number;
@@ -888,8 +951,8 @@ export function contextMenu(
 	ev: MouseEvent,
 ) {
 	ev.preventDefault();
-	return new Promise((resolve, reject) => {
-		let dispose;
+	return new Promise<void>((resolve, _reject) => {
+		let dispose: () => void;
 		popup(
 			defineAsyncComponent({
 				loader: () => import("@/components/MkContextMenu.vue"),
@@ -912,18 +975,27 @@ export function contextMenu(
 	});
 }
 
-export function post(props: Record<string, any> = {}) {
-	return new Promise((resolve, reject) => {
+export function post(
+	props: InstanceType<typeof MkPostFormDialog>["$props"] = {},
+	onClosed?: () => void,
+) {
+	return new Promise<void>((resolve, _reject) => {
 		// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
 		// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、
 		//       Vueが渡されたコンポーネントに内部的に__propsというプロパティを生やす影響で、
 		//       複数のpost formを開いたときに場合によってはエラーになる
 		//       もちろん複数のpost formを開けること自体Misskeyサイドのバグなのだが
-		let dispose;
+		// NOTE: Text area cannot be auto-focused on iOS when dynamically importing MkPostFormDialog
+		// NOTE: However, if you do not dynamically import, the MkPostFormDialog instance will be reused,
+		// Due to the effect that Vue internally creates a property called __props on the passed component,
+		// Sometimes an error occurs when opening multiple post forms
+		// Of course, opening multiple post forms is itself a bug on Misskey's side.
+		let dispose: () => void;
 		popup(MkPostFormDialog, props, {
 			closed: () => {
 				resolve();
 				dispose();
+				onClosed?.();
 			},
 		}).then((res) => {
 			dispose = res.dispose;
@@ -935,7 +1007,7 @@ export const deckGlobalEvents = new EventEmitter();
 
 /*
 export function checkExistence(fileData: ArrayBuffer): Promise<any> {
-	return new Promise((resolve, reject) => {
+	return new Promise((resolve, _reject) => {
 		const data = new FormData();
 		data.append('md5', getMD5(fileData));
 
diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue
index caf70856af..f757d3a1a5 100644
--- a/packages/client/src/pages/about.vue
+++ b/packages/client/src/pages/about.vue
@@ -176,6 +176,7 @@
 import { computed, onMounted, ref, watch } from "vue";
 import { Virtual } from "swiper/modules";
 import { Swiper, SwiperSlide } from "swiper/vue";
+import type { Swiper as SwiperType } from "swiper/types";
 import XEmojis from "./about.emojis.vue";
 import XFederation from "./about.federation.vue";
 import { host, version } from "@/config";
@@ -294,19 +295,19 @@ watch(iconSrc, (newValue, oldValue) => {
 	}
 });
 
-let swiperRef = null;
+let swiperRef: SwiperType | null = null;
 
-function setSwiperRef(swiper) {
+function setSwiperRef(swiper: SwiperType) {
 	swiperRef = swiper;
 	syncSlide(tabs.indexOf(tab.value));
 }
 
 function onSlideChange() {
-	tab.value = tabs[swiperRef.activeIndex];
+	tab.value = tabs[swiperRef!.activeIndex];
 }
 
-function syncSlide(index) {
-	swiperRef.slideTo(index);
+function syncSlide(index: number) {
+	swiperRef!.slideTo(index);
 }
 </script>
 
diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue
index 174b4d5713..772e3f6f55 100644
--- a/packages/client/src/pages/admin/abuses.vue
+++ b/packages/client/src/pages/admin/abuses.vue
@@ -93,18 +93,21 @@
 <script lang="ts" setup>
 import { computed, ref } from "vue";
 
+import type { entities } from "firefish-js";
 import MkSelect from "@/components/form/select.vue";
-import MkPagination from "@/components/MkPagination.vue";
+import MkPagination, {
+	type MkPaginationType,
+} from "@/components/MkPagination.vue";
 import XAbuseReport from "@/components/MkAbuseReport.vue";
 import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
 
-const reports = ref<InstanceType<typeof MkPagination>>();
+const reports = ref<MkPaginationType<typeof pagination.endpoint> | null>(null);
 
 const state = ref("unresolved");
-const reporterOrigin = ref("combined");
-const targetUserOrigin = ref("combined");
+const reporterOrigin = ref<entities.OriginType>("combined");
+const targetUserOrigin = ref<entities.OriginType>("combined");
 // const searchUsername = ref("");
 // const searchHost = ref("");
 
diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue
index 7802e5aad6..869979063c 100644
--- a/packages/client/src/pages/admin/emojis.vue
+++ b/packages/client/src/pages/admin/emojis.vue
@@ -153,7 +153,9 @@
 import { computed, defineAsyncComponent, ref } from "vue";
 import MkButton from "@/components/MkButton.vue";
 import MkInput from "@/components/form/input.vue";
-import MkPagination from "@/components/MkPagination.vue";
+import MkPagination, {
+	type MkPaginationType,
+} from "@/components/MkPagination.vue";
 import MkSwitch from "@/components/form/switch.vue";
 import FormSplit from "@/components/form/split.vue";
 import { selectFile, selectFiles } from "@/scripts/select-file";
@@ -162,7 +164,8 @@ import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
 
-const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>();
+const emojisPaginationComponent =
+	ref<MkPaginationType<"admin/emoji/list"> | null>(null);
 
 const tab = ref("local");
 const query = ref(null);
diff --git a/packages/client/src/pages/admin/overview.queue-chart.vue b/packages/client/src/pages/admin/overview.queue-chart.vue
index cfbcbe6a11..f74cbb7e7e 100644
--- a/packages/client/src/pages/admin/overview.queue-chart.vue
+++ b/packages/client/src/pages/admin/overview.queue-chart.vue
@@ -47,9 +47,9 @@ const props = defineProps<{
 
 const alpha = (hex, a) => {
 	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
-	const r = parseInt(result[1], 16);
-	const g = parseInt(result[2], 16);
-	const b = parseInt(result[3], 16);
+	const r = Number.parseInt(result[1], 16);
+	const g = Number.parseInt(result[2], 16);
+	const b = Number.parseInt(result[3], 16);
 	return `rgba(${r}, ${g}, ${b}, ${a})`;
 };
 
diff --git a/packages/client/src/pages/admin/queue.chart.chart.vue b/packages/client/src/pages/admin/queue.chart.chart.vue
index b802457b4a..e8951709f2 100644
--- a/packages/client/src/pages/admin/queue.chart.chart.vue
+++ b/packages/client/src/pages/admin/queue.chart.chart.vue
@@ -47,9 +47,9 @@ const props = defineProps<{
 
 const alpha = (hex, a) => {
 	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
-	const r = parseInt(result[1], 16);
-	const g = parseInt(result[2], 16);
-	const b = parseInt(result[3], 16);
+	const r = Number.parseInt(result[1], 16);
+	const g = Number.parseInt(result[2], 16);
+	const b = Number.parseInt(result[3], 16);
 	return `rgba(${r}, ${g}, ${b}, ${a})`;
 };
 
diff --git a/packages/client/src/pages/admin/users.vue b/packages/client/src/pages/admin/users.vue
index d5028993e5..f72067b84e 100644
--- a/packages/client/src/pages/admin/users.vue
+++ b/packages/client/src/pages/admin/users.vue
@@ -126,7 +126,9 @@
 import { computed, ref } from "vue";
 import MkInput from "@/components/form/input.vue";
 import MkSelect from "@/components/form/select.vue";
-import MkPagination from "@/components/MkPagination.vue";
+import MkPagination, {
+	type MkPaginationType,
+} from "@/components/MkPagination.vue";
 import * as os from "@/os";
 import { lookupUser } from "@/scripts/lookup-user";
 import { i18n } from "@/i18n";
@@ -134,7 +136,9 @@ import { definePageMetadata } from "@/scripts/page-metadata";
 import MkUserCardMini from "@/components/MkUserCardMini.vue";
 import icon from "@/scripts/icon";
 
-const paginationComponent = ref<InstanceType<typeof MkPagination>>();
+const paginationComponent = ref<MkPaginationType<
+	typeof pagination.endpoint
+> | null>(null);
 
 const sort = ref("+createdAt");
 const state = ref("all");
diff --git a/packages/client/src/pages/announcements.vue b/packages/client/src/pages/announcements.vue
index df2a794ba4..59b703a35e 100644
--- a/packages/client/src/pages/announcements.vue
+++ b/packages/client/src/pages/announcements.vue
@@ -54,6 +54,7 @@
 <script lang="ts" setup>
 import { computed, ref } from "vue";
 import MkPagination from "@/components/MkPagination.vue";
+import type { MkPaginationType } from "@/components/MkPagination.vue";
 import MkButton from "@/components/MkButton.vue";
 import * as os from "@/os";
 import { i18n } from "@/i18n";
@@ -66,7 +67,7 @@ const pagination = {
 	limit: 10,
 };
 
-const paginationEl = ref<InstanceType<typeof MkPagination>>();
+const paginationEl = ref<MkPaginationType<"announcements"> | null>(null);
 function read(id: string) {
 	if (!paginationEl.value) return;
 	paginationEl.value.updateItem(id, (announcement) => {
diff --git a/packages/client/src/pages/channel-editor.vue b/packages/client/src/pages/channel-editor.vue
index 60bcfe990e..e776de30e3 100644
--- a/packages/client/src/pages/channel-editor.vue
+++ b/packages/client/src/pages/channel-editor.vue
@@ -41,6 +41,7 @@
 
 <script lang="ts" setup>
 import { computed, ref, watch } from "vue";
+import type { entities } from "firefish-js";
 import MkTextarea from "@/components/form/textarea.vue";
 import MkButton from "@/components/MkButton.vue";
 import MkInput from "@/components/form/input.vue";
@@ -57,26 +58,24 @@ const props = defineProps<{
 	channelId?: string;
 }>();
 
-const channel = ref(null);
-const name = ref(null);
-const description = ref(null);
+const channel = ref<entities.Channel | null>(null);
+const name = ref<string>("");
+const description = ref<string>("");
 const bannerUrl = ref<string | null>(null);
 const bannerId = ref<string | null>(null);
 
-watch(
-	() => bannerId.value,
-	async () => {
-		if (bannerId.value == null) {
-			bannerUrl.value = null;
-		} else {
-			bannerUrl.value = (
-				await os.api("drive/files/show", {
-					fileId: bannerId.value,
-				})
-			).url;
-		}
-	},
-);
+let bannerUrlUpdated = false;
+
+/**
+ * Set banner url and id when we already know the url
+ * Prevent redundant network requests from being sent
+ */
+function setBanner(opt: { bannerId: string | null; bannerUrl: string | null }) {
+	bannerUrlUpdated = true;
+	bannerUrl.value = opt.bannerUrl;
+	bannerId.value = opt.bannerId;
+	bannerUrlUpdated = false;
+}
 
 async function fetchChannel() {
 	if (props.channelId == null) return;
@@ -86,23 +85,44 @@ async function fetchChannel() {
 	});
 
 	name.value = channel.value.name;
-	description.value = channel.value.description;
-	bannerId.value = channel.value.bannerId;
-	bannerUrl.value = channel.value.bannerUrl;
+	description.value = channel.value.description ?? "";
+	setBanner(channel.value);
 }
 
-fetchChannel();
+await fetchChannel();
+
+watch(bannerId, async () => {
+	if (bannerUrlUpdated) {
+		bannerUrlUpdated = false;
+		return;
+	}
+	if (bannerId.value == null) {
+		bannerUrl.value = null;
+	} else {
+		bannerUrl.value = (
+			await os.api("drive/files/show", {
+				fileId: bannerId.value,
+			})
+		).url;
+	}
+});
 
 function save() {
-	const params = {
+	const params: {
+		name: string;
+		description: string;
+		bannerId: string | null;
+	} = {
 		name: name.value,
 		description: description.value,
 		bannerId: bannerId.value,
 	};
 
 	if (props.channelId) {
-		params.channelId = props.channelId;
-		os.api("channels/update", params).then(() => {
+		os.api("channels/update", {
+			...params,
+			channelId: props.channelId,
+		}).then(() => {
 			os.success();
 		});
 	} else {
@@ -113,14 +133,20 @@ function save() {
 	}
 }
 
-function setBannerImage(evt) {
+function setBannerImage(evt: MouseEvent) {
 	selectFile(evt.currentTarget ?? evt.target, null).then((file) => {
-		bannerId.value = file.id;
+		setBanner({
+			bannerId: file.id,
+			bannerUrl: file.url,
+		});
 	});
 }
 
 function removeBannerImage() {
-	bannerId.value = null;
+	setBanner({
+		bannerId: null,
+		bannerUrl: null,
+	});
 }
 
 const headerActions = computed(() => []);
diff --git a/packages/client/src/pages/channel.vue b/packages/client/src/pages/channel.vue
index 15e8b6e256..d3ef5f2b8b 100644
--- a/packages/client/src/pages/channel.vue
+++ b/packages/client/src/pages/channel.vue
@@ -33,7 +33,7 @@
 						:style="{
 							backgroundImage: channel.bannerUrl
 								? `url(${channel.bannerUrl})`
-								: null,
+								: undefined,
 						}"
 						class="banner"
 					>
@@ -88,8 +88,6 @@
 					class="_gap"
 					src="channel"
 					:channel="channelId"
-					@before="before"
-					@after="after"
 				/>
 			</div>
 		</MkSpacer>
@@ -98,6 +96,7 @@
 
 <script lang="ts" setup>
 import { computed, ref, watch } from "vue";
+import type { entities } from "firefish-js";
 import XPostForm from "@/components/MkPostForm.vue";
 import XTimeline from "@/components/MkTimeline.vue";
 import XChannelFollowButton from "@/components/MkChannelFollowButton.vue";
@@ -114,7 +113,11 @@ const props = defineProps<{
 	channelId: string;
 }>();
 
-const channel = ref(null);
+const channel = ref<entities.Channel>(
+	await os.api("channels/show", {
+		channelId: props.channelId,
+	}),
+);
 const showBanner = ref(true);
 
 watch(
@@ -124,7 +127,6 @@ watch(
 			channelId: props.channelId,
 		});
 	},
-	{ immediate: true },
 );
 
 function edit() {
diff --git a/packages/client/src/pages/channels.vue b/packages/client/src/pages/channels.vue
index ec92af54aa..c7789b8787 100644
--- a/packages/client/src/pages/channels.vue
+++ b/packages/client/src/pages/channels.vue
@@ -112,6 +112,7 @@
 import { computed, onMounted, ref, watch } from "vue";
 import { Virtual } from "swiper/modules";
 import { Swiper, SwiperSlide } from "swiper/vue";
+import type { Swiper as SwiperType } from "swiper/types";
 import MkChannelList from "@/components/MkChannelList.vue";
 import MkInput from "@/components/form/input.vue";
 import MkRadios from "@/components/form/radios.vue";
@@ -216,18 +217,18 @@ definePageMetadata(
 	})),
 );
 
-let swiperRef = null;
+let swiperRef: SwiperType | null = null;
 
-function setSwiperRef(swiper) {
+function setSwiperRef(swiper: SwiperType) {
 	swiperRef = swiper;
 	syncSlide(tabs.indexOf(tab.value));
 }
 
 function onSlideChange() {
-	tab.value = tabs[swiperRef.activeIndex];
+	tab.value = tabs[swiperRef!.activeIndex];
 }
 
-function syncSlide(index) {
-	swiperRef.slideTo(index);
+function syncSlide(index: number) {
+	swiperRef!.slideTo(index);
 }
 </script>
diff --git a/packages/client/src/pages/clip.vue b/packages/client/src/pages/clip.vue
index ffe00beeec..8507721519 100644
--- a/packages/client/src/pages/clip.vue
+++ b/packages/client/src/pages/clip.vue
@@ -75,29 +75,29 @@ const headerActions = computed(() =>
 					icon: `${icon("ph-pencil")}`,
 					text: i18n.ts.toEdit,
 					handler: async (): Promise<void> => {
-						const { canceled, result } = await os.form(clip.value.name, {
+						const { canceled, result } = await os.form(clip.value!.name, {
 							name: {
 								type: "string",
 								label: i18n.ts.name,
-								default: clip.value.name,
+								default: clip.value!.name,
 							},
 							description: {
 								type: "string",
 								required: false,
 								multiline: true,
 								label: i18n.ts.description,
-								default: clip.value.description,
+								default: clip.value!.description,
 							},
 							isPublic: {
 								type: "boolean",
 								label: i18n.ts.public,
-								default: clip.value.isPublic,
+								default: clip.value!.isPublic,
 							},
 						});
 						if (canceled) return;
 
 						os.apiWithDialog("clips/update", {
-							clipId: clip.value.id,
+							clipId: clip.value!.id,
 							...result,
 						});
 					},
diff --git a/packages/client/src/pages/favorites.vue b/packages/client/src/pages/favorites.vue
index f9fb36f296..09e857449b 100644
--- a/packages/client/src/pages/favorites.vue
+++ b/packages/client/src/pages/favorites.vue
@@ -37,6 +37,7 @@
 <script lang="ts" setup>
 import { ref } from "vue";
 import MkPagination from "@/components/MkPagination.vue";
+import type { MkPaginationType } from "@/components/MkPagination.vue";
 import XNote from "@/components/MkNote.vue";
 import XList from "@/components/MkDateSeparatedList.vue";
 import { i18n } from "@/i18n";
@@ -48,7 +49,9 @@ const pagination = {
 	limit: 10,
 };
 
-const pagingComponent = ref<InstanceType<typeof MkPagination>>();
+const pagingComponent = ref<MkPaginationType<
+	typeof pagination.endpoint
+> | null>(null);
 
 definePageMetadata({
 	title: i18n.ts.favorites,
diff --git a/packages/client/src/pages/follow-requests-sent.vue b/packages/client/src/pages/follow-requests-sent.vue
index 8ca769848b..b360c704f0 100644
--- a/packages/client/src/pages/follow-requests-sent.vue
+++ b/packages/client/src/pages/follow-requests-sent.vue
@@ -66,14 +66,17 @@
 import { computed, ref } from "vue";
 import { acct } from "firefish-js";
 import MkPagination from "@/components/MkPagination.vue";
+import type { MkPaginationType } from "@/components/MkPagination.vue";
 import { userPage } from "@/filters/user";
-import * as os from "@/os";
+// import * as os from "@/os";
 import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import { me } from "@/me";
 import icon from "@/scripts/icon";
 
-const paginationComponent = ref<InstanceType<typeof MkPagination>>();
+const paginationComponent = ref<MkPaginationType<
+	typeof pagination.endpoint
+> | null>(null);
 
 const pagination = {
 	endpoint: "following/requests/sent" as const,
diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue
index d4999458c8..3abc414c0c 100644
--- a/packages/client/src/pages/follow-requests.vue
+++ b/packages/client/src/pages/follow-requests.vue
@@ -85,6 +85,7 @@
 import { computed, ref } from "vue";
 import { acct } from "firefish-js";
 import MkPagination from "@/components/MkPagination.vue";
+import type { MkPaginationType } from "@/components/MkPagination.vue";
 import { userPage } from "@/filters/user";
 import * as os from "@/os";
 import { i18n } from "@/i18n";
@@ -92,7 +93,9 @@ import { definePageMetadata } from "@/scripts/page-metadata";
 import { me } from "@/me";
 import icon from "@/scripts/icon";
 
-const paginationComponent = ref<InstanceType<typeof MkPagination>>();
+const paginationComponent = ref<MkPaginationType<
+	typeof pagination.endpoint
+> | null>(null);
 
 const pagination = {
 	endpoint: "following/requests/list" as const,
@@ -102,13 +105,13 @@ const pagination = {
 
 function accept(user) {
 	os.api("following/requests/accept", { userId: user.id }).then(() => {
-		paginationComponent.value.reload();
+		paginationComponent.value!.reload();
 	});
 }
 
 function reject(user) {
 	os.api("following/requests/reject", { userId: user.id }).then(() => {
-		paginationComponent.value.reload();
+		paginationComponent.value!.reload();
 	});
 }
 
diff --git a/packages/client/src/pages/gallery/post.vue b/packages/client/src/pages/gallery/post.vue
index 6f5e29ed0b..91b6b764c5 100644
--- a/packages/client/src/pages/gallery/post.vue
+++ b/packages/client/src/pages/gallery/post.vue
@@ -22,7 +22,7 @@
 						<div class="body _block">
 							<div class="title">{{ post.title }}</div>
 							<div class="description">
-								<Mfm :text="post.description" />
+								<Mfm :text="post.description || ''" />
 							</div>
 							<div class="info">
 								<i :class="icon('ph-clock')"></i>
@@ -59,7 +59,7 @@
 								<div class="other">
 									<button
 										v-if="
-											isSignedIn && me.id === post.user.id
+											isSignedIn && me!.id === post.user.id
 										"
 										v-tooltip="i18n.ts.toEdit"
 										v-click-anime
@@ -105,7 +105,7 @@
 									<MkAcct :user="post.user" />
 								</div>
 								<MkFollowButton
-									v-if="!me || me.id != post.user.id"
+									v-if="!isSignedIn || me!.id != post.user.id"
 									:user="post.user"
 									:inline="true"
 									:transparent="false"
@@ -140,7 +140,7 @@
 							</MkPagination>
 						</MkContainer>
 					</div>
-					<MkError v-else-if="error" @retry="fetch()" />
+					<MkError v-else-if="error" @retry="fetchPost()" />
 					<MkLoading v-else />
 				</transition>
 			</div>
@@ -150,6 +150,7 @@
 
 <script lang="ts" setup>
 import { computed, ref, watch } from "vue";
+import type { entities } from "firefish-js";
 import MkButton from "@/components/MkButton.vue";
 import * as os from "@/os";
 import MkContainer from "@/components/MkContainer.vue";
@@ -163,7 +164,7 @@ import { definePageMetadata } from "@/scripts/page-metadata";
 import { shareAvailable } from "@/scripts/share-available";
 import { defaultStore } from "@/store";
 import icon from "@/scripts/icon";
-import { isSignedIn } from "@/me";
+import { isSignedIn, me } from "@/me";
 
 const router = useRouter();
 
@@ -171,18 +172,19 @@ const props = defineProps<{
 	postId: string;
 }>();
 
-const post = ref(null);
+const post = ref<entities.GalleryPost | null>(null);
 const error = ref(null);
 const otherPostsPagination = {
 	endpoint: "users/gallery/posts" as const,
 	limit: 6,
 	params: computed(() => ({
-		userId: post.value.user.id,
+		userId: post.value!.user.id,
 	})),
 };
 
 function fetchPost() {
 	post.value = null;
+	error.value = null;
 	os.api("gallery/posts/show", {
 		postId: props.postId,
 	})
@@ -196,15 +198,15 @@ function fetchPost() {
 
 function share() {
 	navigator.share({
-		title: post.value.title,
-		text: post.value.description,
-		url: `${url}/gallery/${post.value.id}`,
+		title: post.value!.title,
+		text: post.value!.description || undefined,
+		url: `${url}/gallery/${post.value!.id}`,
 	});
 }
 
 function shareWithNote() {
 	os.post({
-		initialText: `${post.value.title} ${url}/gallery/${post.value.id}`,
+		initialText: `${post.value!.title} ${url}/gallery/${post.value!.id}`,
 	});
 }
 
@@ -212,8 +214,8 @@ function like() {
 	os.api("gallery/posts/like", {
 		postId: props.postId,
 	}).then(() => {
-		post.value.isLiked = true;
-		post.value.likedCount++;
+		post.value!.isLiked = true;
+		post.value!.likedCount++;
 	});
 }
 
@@ -221,13 +223,13 @@ async function unlike() {
 	os.api("gallery/posts/unlike", {
 		postId: props.postId,
 	}).then(() => {
-		post.value.isLiked = false;
-		post.value.likedCount--;
+		post.value!.isLiked = false;
+		post.value!.likedCount--;
 	});
 }
 
 function edit() {
-	router.push(`/gallery/${post.value.id}/edit`);
+	router.push(`/gallery/${post.value!.id}/edit`);
 }
 
 watch(() => props.postId, fetchPost, { immediate: true });
diff --git a/packages/client/src/pages/messaging/messaging-room.vue b/packages/client/src/pages/messaging/messaging-room.vue
index fa61ec4bc3..bde0003f2f 100644
--- a/packages/client/src/pages/messaging/messaging-room.vue
+++ b/packages/client/src/pages/messaging/messaging-room.vue
@@ -110,7 +110,7 @@ import { acct } from "firefish-js";
 import XMessage from "./messaging-room.message.vue";
 import XForm from "./messaging-room.form.vue";
 import XList from "@/components/MkDateSeparatedList.vue";
-import type { Paging } from "@/components/MkPagination.vue";
+import type { MkPaginationType, Paging } from "@/components/MkPagination.vue";
 import MkPagination from "@/components/MkPagination.vue";
 import {
 	isBottomVisible,
@@ -136,7 +136,9 @@ const stream = useStream();
 
 const rootEl = ref<HTMLDivElement>();
 const formEl = ref<InstanceType<typeof XForm>>();
-const pagingComponent = ref<InstanceType<typeof MkPagination>>();
+const pagingComponent = ref<MkPaginationType<"messaging/messages"> | null>(
+	null,
+);
 
 const fetching = ref(true);
 const user = ref<entities.UserDetailed | null>(null);
diff --git a/packages/client/src/pages/my-antennas/index.vue b/packages/client/src/pages/my-antennas/index.vue
index a4787f7241..ef45ad75f7 100644
--- a/packages/client/src/pages/my-antennas/index.vue
+++ b/packages/client/src/pages/my-antennas/index.vue
@@ -54,6 +54,7 @@
 
 <script lang="ts" setup>
 import { computed, onActivated, onDeactivated, ref } from "vue";
+import type { MkPaginationType } from "@/components/MkPagination.vue";
 import MkPagination from "@/components/MkPagination.vue";
 import MkButton from "@/components/MkButton.vue";
 import MkInfo from "@/components/MkInfo.vue";
@@ -70,7 +71,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-const list = ref<typeof MkPagination | null>(null);
+const list = ref<MkPaginationType<typeof pagination.endpoint> | null>(null);
 
 let isCached = false;
 let refreshTimer: number | null = null;
diff --git a/packages/client/src/pages/my-clips/index.vue b/packages/client/src/pages/my-clips/index.vue
index daaef6f3a0..d4eeb9e4a9 100644
--- a/packages/client/src/pages/my-clips/index.vue
+++ b/packages/client/src/pages/my-clips/index.vue
@@ -40,7 +40,9 @@
 <script lang="ts" setup>
 import { computed, ref } from "vue";
 
-import MkPagination from "@/components/MkPagination.vue";
+import MkPagination, {
+	type MkPaginationType,
+} from "@/components/MkPagination.vue";
 import MkInfo from "@/components/MkInfo.vue";
 import * as os from "@/os";
 import { i18n } from "@/i18n";
@@ -52,7 +54,9 @@ const pagination = {
 	limit: 10,
 };
 
-const pagingComponent = ref<InstanceType<typeof MkPagination>>();
+const pagingComponent = ref<MkPaginationType<
+	typeof pagination.endpoint
+> | null>(null);
 
 async function create() {
 	const { canceled, result } = await os.form(i18n.ts.createNewClip, {
diff --git a/packages/client/src/pages/my-lists/index.vue b/packages/client/src/pages/my-lists/index.vue
index 990ce45670..5c928ccc28 100644
--- a/packages/client/src/pages/my-lists/index.vue
+++ b/packages/client/src/pages/my-lists/index.vue
@@ -44,7 +44,9 @@
 <script lang="ts" setup>
 import { computed, ref } from "vue";
 
-import MkPagination from "@/components/MkPagination.vue";
+import MkPagination, {
+	type MkPaginationType,
+} from "@/components/MkPagination.vue";
 import MkButton from "@/components/MkButton.vue";
 import MkAvatars from "@/components/MkAvatars.vue";
 import MkInfo from "@/components/MkInfo.vue";
@@ -53,7 +55,9 @@ import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
 
-const pagingComponent = ref<InstanceType<typeof MkPagination>>();
+const pagingComponent = ref<MkPaginationType<
+	typeof pagination.endpoint
+> | null>(null);
 
 const pagination = {
 	endpoint: "users/lists/list" as const,
diff --git a/packages/client/src/pages/note-history.vue b/packages/client/src/pages/note-history.vue
index f6fbf517a3..d0c93899aa 100644
--- a/packages/client/src/pages/note-history.vue
+++ b/packages/client/src/pages/note-history.vue
@@ -35,7 +35,9 @@
 <script lang="ts" setup>
 import { computed, onMounted, ref } from "vue";
 import type { entities } from "firefish-js";
-import MkPagination from "@/components/MkPagination.vue";
+import MkPagination, {
+	type MkPaginationType,
+} from "@/components/MkPagination.vue";
 import { api } from "@/os";
 import XList from "@/components/MkDateSeparatedList.vue";
 import XNote from "@/components/MkNote.vue";
@@ -43,7 +45,9 @@ import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
 
-const pagingComponent = ref<InstanceType<typeof MkPagination>>();
+const pagingComponent = ref<MkPaginationType<
+	typeof pagination.endpoint
+> | null>(null);
 
 const props = defineProps<{
 	noteId: string;
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.image.vue b/packages/client/src/pages/page-editor/els/page-editor.el.image.vue
index 859f0f1112..3820b140df 100644
--- a/packages/client/src/pages/page-editor/els/page-editor.el.image.vue
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.image.vue
@@ -44,9 +44,9 @@ const props = withDefaults(
 const file = ref<any>(null);
 
 async function choose() {
-	os.selectDriveFile(false).then((fileResponse: any) => {
+	os.selectDriveFile(false).then((fileResponse) => {
 		file.value = fileResponse;
-		props.value.fileId = fileResponse.id;
+		props.value.fileId = fileResponse?.id;
 	});
 }
 
diff --git a/packages/client/src/pages/search.vue b/packages/client/src/pages/search.vue
index 972a2d46b2..c7fb067764 100644
--- a/packages/client/src/pages/search.vue
+++ b/packages/client/src/pages/search.vue
@@ -89,8 +89,9 @@ const usersPagination = {
 	endpoint: "users/search" as const,
 	limit: 10,
 	params: computed(() => ({
-		query: props.query,
-		origin: "combined",
+		// FIXME: query is necessary for user search
+		query: props.query!,
+		origin: "combined" as const,
 	})),
 };
 
diff --git a/packages/client/src/pages/settings/preferences-backups.vue b/packages/client/src/pages/settings/preferences-backups.vue
index 202de0a082..73e098c4ea 100644
--- a/packages/client/src/pages/settings/preferences-backups.vue
+++ b/packages/client/src/pages/settings/preferences-backups.vue
@@ -299,12 +299,12 @@ function loadFile(): void {
 		});
 
 		// 一応廃棄
-		(window as any).__misskey_input_ref__ = null;
+		window.__misskey_input_ref__ = null;
 	};
 
 	// https://qiita.com/fukasawah/items/b9dc732d95d99551013d
 	// iOS Safari で正常に動かす為のおまじない
-	(window as any).__misskey_input_ref__ = input;
+	window.__misskey_input_ref__ = input;
 
 	input.click();
 }
diff --git a/packages/client/src/pages/user-list-timeline.vue b/packages/client/src/pages/user-list-timeline.vue
index 69f17c1047..a402e0f576 100644
--- a/packages/client/src/pages/user-list-timeline.vue
+++ b/packages/client/src/pages/user-list-timeline.vue
@@ -3,7 +3,7 @@
 		<template #header
 			><MkPageHeader :actions="headerActions" :tabs="headerTabs"
 		/></template>
-		<div ref="rootEl" class="eqqrhokj">
+		<MkSpacer>
 			<div class="tl _block">
 				<XTimeline
 					ref="tlEl"
@@ -14,12 +14,15 @@
 					:sound="true"
 				/>
 			</div>
-		</div>
+		</MkSpacer>
 	</MkStickyContainer>
 </template>
 
 <script lang="ts" setup>
 import { computed, ref, watch } from "vue";
+import type { entities } from "firefish-js";
+// TODO: disable this rule properly
+// biome-ignore lint/style/useImportType: used in <template>
 import XTimeline from "@/components/MkTimeline.vue";
 import * as os from "@/os";
 import { useRouter } from "@/router";
@@ -33,9 +36,8 @@ const props = defineProps<{
 	listId: string;
 }>();
 
-const list = ref(null);
+const list = ref<entities.UserList>();
 const tlEl = ref<InstanceType<typeof XTimeline>>();
-const rootEl = ref<HTMLElement>();
 
 watch(
 	() => props.listId,
@@ -56,8 +58,8 @@ async function timetravel() {
 		title: i18n.ts.date,
 	});
 	if (canceled) return;
-
-	tlEl.value.timetravel(date);
+	// FIXME:
+	tlEl.value!.timetravel(date);
 }
 
 const headerActions = computed(() =>
@@ -92,13 +94,8 @@ definePageMetadata(
 </script>
 
 <style lang="scss" scoped>
-.eqqrhokj {
-	padding: var(--margin);
-	max-width: 800px;
-	margin: 0 auto;
-	> .tl {
-		background: none;
-		border-radius: var(--radius);
-	}
+.tl {
+	background: none;
+	border-radius: var(--radius);
 }
 </style>
diff --git a/packages/client/src/pages/user/home.vue b/packages/client/src/pages/user/home.vue
index 13846e42d4..463e5cb4a2 100644
--- a/packages/client/src/pages/user/home.vue
+++ b/packages/client/src/pages/user/home.vue
@@ -101,18 +101,6 @@
 										v-tooltip.noDelay="i18n.ts.isBot"
 										><i :class="icon('ph-robot')"></i
 									></span>
-									<span
-										v-if="
-											patrons?.includes(
-												`@${user.username}@${
-													user.host || host
-												}`,
-											)
-										"
-										v-tooltip.noDelay="i18n.ts.isPatron"
-										style="color: var(--badge)"
-										><i :class="icon('ph-hand-coins')"></i
-									></span>
 								</div>
 							</div>
 						</div>
@@ -188,18 +176,6 @@
 									v-tooltip.noDelay="i18n.ts.isBot"
 									><i :class="icon('ph-robot')"></i
 								></span>
-								<span
-									v-if="
-										patrons?.includes(
-											`@${user.username}@${
-												user.host || host
-											}`,
-										)
-									"
-									v-tooltip.noDelay="i18n.ts.isPatron"
-									style="color: var(--badge)"
-									><i :class="icon('ph-hand-coins')"></i
-								></span>
 							</div>
 						</div>
 						<div class="follow-container">
@@ -406,7 +382,6 @@ const parallaxAnimationId = ref<null | number>(null);
 const narrow = ref<null | boolean>(null);
 const rootEl = ref<null | HTMLElement>(null);
 const bannerEl = ref<null | HTMLElement>(null);
-const patrons = ref([]);
 
 const age = computed(() => {
 	return calcAge(props.user.birthday);
@@ -452,9 +427,6 @@ const timeForThem = computed(() => {
 	return "";
 });
 
-const patronsResp = await os.api("patrons");
-patrons.value = patronsResp.patrons;
-
 function parallaxLoop() {
 	parallaxAnimationId.value = window.requestAnimationFrame(parallaxLoop);
 	parallax();
diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts
index a21df2981e..7273919261 100644
--- a/packages/client/src/router.ts
+++ b/packages/client/src/router.ts
@@ -1,18 +1,18 @@
 import type { AsyncComponentLoader } from "vue";
 import { defineAsyncComponent, inject } from "vue";
 import { isEmojiMod, isModerator, me } from "@/me";
-import { Router } from "@/nirax";
+import { type RouteDef, Router } from "@/nirax";
 import MkError from "@/pages/_error_.vue";
 import MkLoading from "@/pages/_loading_.vue";
 
-const page = (loader: AsyncComponentLoader<any>) =>
+const page = (loader: AsyncComponentLoader) =>
 	defineAsyncComponent({
 		loader,
 		loadingComponent: MkLoading,
 		errorComponent: MkError,
 	});
 
-export const routes = [
+export const routes: RouteDef[] = [
 	{
 		path: "/@:initUser/pages/:initPageName/view-source",
 		component: page(() => import("./pages/page-editor/page-editor.vue")),
diff --git a/packages/client/src/scripts/2fa.ts b/packages/client/src/scripts/2fa.ts
index 14d59bebec..9c34c8fb70 100644
--- a/packages/client/src/scripts/2fa.ts
+++ b/packages/client/src/scripts/2fa.ts
@@ -9,7 +9,7 @@ export function byteify(string: string, encoding: "ascii" | "base64" | "hex") {
 			);
 		case "hex":
 			return new Uint8Array(
-				string.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)),
+				string.match(/.{1,2}/g).map((byte) => Number.parseInt(byte, 16)),
 			);
 	}
 }
diff --git a/packages/client/src/scripts/color.ts b/packages/client/src/scripts/color.ts
index 10a99a5a05..10b5ea0e54 100644
--- a/packages/client/src/scripts/color.ts
+++ b/packages/client/src/scripts/color.ts
@@ -1,7 +1,7 @@
 export const alpha = (hex: string, a: number): string => {
 	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
-	const r = parseInt(result[1], 16);
-	const g = parseInt(result[2], 16);
-	const b = parseInt(result[3], 16);
+	const r = Number.parseInt(result[1], 16);
+	const g = Number.parseInt(result[2], 16);
+	const b = Number.parseInt(result[3], 16);
 	return `rgba(${r}, ${g}, ${b}, ${a})`;
 };
diff --git a/packages/client/src/scripts/form.ts b/packages/client/src/scripts/form.ts
index 2cd0ca01dc..dfc1c646dc 100644
--- a/packages/client/src/scripts/form.ts
+++ b/packages/client/src/scripts/form.ts
@@ -1,72 +1,11 @@
-export type FormItem =
-	| {
-			label?: string;
-			type: "string";
-			default: string | null;
-			hidden?: boolean;
-			multiline?: boolean;
-	  }
-	| {
-			label?: string;
-			type: "number";
-			default: number | null;
-			hidden?: boolean;
-			step?: number;
-	  }
-	| {
-			label?: string;
-			type: "boolean";
-			default: boolean | null;
-			hidden?: boolean;
-	  }
-	| {
-			label?: string;
-			type: "enum";
-			default: string | null;
-			hidden?: boolean;
-			enum: string[];
-	  }
-	| {
-			label?: string;
-			type: "radio";
-			default: unknown | null;
-			hidden?: boolean;
-			options: {
-				label: string;
-				value: unknown;
-			}[];
-	  }
-	| {
-			label?: string;
-			type: "object";
-			default: Record<string, unknown> | null;
-			hidden: true;
-	  }
-	| {
-			label?: string;
-			type: "array";
-			default: unknown[] | null;
-			hidden: true;
-	  };
+// TODO: replace this file with @/types/form.ts
+
+import type { FormItemType, GetFormItemResultType } from "@/types/form";
+
+export type FormItem = FormItemType;
 
 export type Form = Record<string, FormItem>;
 
-type GetItemType<Item extends FormItem> = Item["type"] extends "string"
-	? string
-	: Item["type"] extends "number"
-		? number
-		: Item["type"] extends "boolean"
-			? boolean
-			: Item["type"] extends "radio"
-				? unknown
-				: Item["type"] extends "enum"
-					? string
-					: Item["type"] extends "array"
-						? unknown[]
-						: Item["type"] extends "object"
-							? Record<string, unknown>
-							: never;
-
 export type GetFormResultType<F extends Form> = {
-	[P in keyof F]: GetItemType<F[P]>;
+	[P in keyof F]: NonNullable<GetFormItemResultType<F[P]["type"]>>;
 };
diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts
index 450a5885c4..25091f2da2 100644
--- a/packages/client/src/scripts/get-note-menu.ts
+++ b/packages/client/src/scripts/get-note-menu.ts
@@ -13,16 +13,18 @@ import { getUserMenu } from "@/scripts/get-user-menu";
 import icon from "@/scripts/icon";
 import { useRouter } from "@/router";
 import { notePage } from "@/filters/note";
+import type { NoteTranslation } from "@/types/note";
+import type { MenuItem } from "@/types/menu";
 
 const router = useRouter();
 
 export function getNoteMenu(props: {
 	note: entities.Note;
 	menuButton: Ref<HTMLElement | undefined>;
-	translation: Ref<any>;
+	translation: Ref<NoteTranslation | null>;
 	translating: Ref<boolean>;
 	isDeleted: Ref<boolean>;
-	currentClipPage?: Ref<entities.Clip>;
+	currentClipPage?: Ref<entities.Clip> | null;
 }) {
 	const isRenote =
 		props.note.renote != null &&
@@ -290,7 +292,7 @@ export function getNoteMenu(props: {
 		props.translating.value = false;
 	}
 
-	let menu;
+	let menu: MenuItem[];
 	if (isSignedIn) {
 		const statePromise = os.api("notes/state", {
 			noteId: appearNote.id,
@@ -395,7 +397,7 @@ export function getNoteMenu(props: {
 					}
 				: undefined,
 			{
-				type: "parent",
+				type: "parent" as const,
 				icon: `${icon("ph-share-network")}`,
 				text: i18n.ts.share,
 				children: [
@@ -498,7 +500,7 @@ export function getNoteMenu(props: {
 			!isAppearAuthor ? null : undefined,
 			!isAppearAuthor
 				? {
-						type: "parent",
+						type: "parent" as const,
 						icon: `${icon("ph-user")}`,
 						text: i18n.ts.user,
 						children: getUserMenu(appearNote.user),
diff --git a/packages/client/src/scripts/hpml/evaluator.ts b/packages/client/src/scripts/hpml/evaluator.ts
index ba06a87442..65f8f6a9f5 100644
--- a/packages/client/src/scripts/hpml/evaluator.ts
+++ b/packages/client/src/scripts/hpml/evaluator.ts
@@ -183,7 +183,7 @@ export class Hpml {
 			}
 
 			if (expr.type === "number") {
-				return parseInt(expr.value as any, 10);
+				return Number.parseInt(expr.value as any, 10);
 			}
 
 			if (expr.type === "text" || expr.type === "multiLineText") {
diff --git a/packages/client/src/scripts/hpml/lib.ts b/packages/client/src/scripts/hpml/lib.ts
index 0a2226be8a..06bda34655 100644
--- a/packages/client/src/scripts/hpml/lib.ts
+++ b/packages/client/src/scripts/hpml/lib.ts
@@ -505,7 +505,7 @@ export function initHpmlLib(
 		strReplace: (a: string, b: string, c: string) => a.split(b).join(c),
 		strReverse: (a: string) => a.split("").reverse().join(""),
 		join: (texts: string[], separator: string) => texts.join(separator || ""),
-		stringToNumber: (a: string) => parseInt(a),
+		stringToNumber: (a: string) => Number.parseInt(a),
 		numberToString: (a: number) => a.toString(),
 		splitStrByLine: (a: string) => a.split("\n"),
 		pick: (list: any[], i: number) => list[i - 1],
@@ -534,7 +534,7 @@ export function initHpmlLib(
 			let totalFactor = 0;
 			for (const x of list) {
 				const parts = x.split(" ");
-				const factor = parseInt(parts.pop()!, 10);
+				const factor = Number.parseInt(parts.pop()!, 10);
 				const text = parts.join(" ");
 				totalFactor += factor;
 				xs.push({ factor, text });
diff --git a/packages/client/src/scripts/page-metadata.ts b/packages/client/src/scripts/page-metadata.ts
index e2f470d0bb..cf8d7938a7 100644
--- a/packages/client/src/scripts/page-metadata.ts
+++ b/packages/client/src/scripts/page-metadata.ts
@@ -12,6 +12,7 @@ export interface PageMetadata {
 	avatar?: entities.UserDetailed | null;
 	userName?: entities.User | null;
 	bg?: string;
+	hideHeader?: boolean;
 }
 
 export function definePageMetadata(
diff --git a/packages/client/src/scripts/physics.ts b/packages/client/src/scripts/physics.ts
index 5375c01d29..76dd117a79 100644
--- a/packages/client/src/scripts/physics.ts
+++ b/packages/client/src/scripts/physics.ts
@@ -65,10 +65,10 @@ export function physics(container: HTMLElement) {
 	const objs = [];
 	for (const objEl of objEls) {
 		const left = objEl.dataset.physicsX
-			? parseInt(objEl.dataset.physicsX)
+			? Number.parseInt(objEl.dataset.physicsX)
 			: objEl.offsetLeft;
 		const top = objEl.dataset.physicsY
-			? parseInt(objEl.dataset.physicsY)
+			? Number.parseInt(objEl.dataset.physicsY)
 			: objEl.offsetTop;
 
 		let obj;
@@ -90,7 +90,7 @@ export function physics(container: HTMLElement) {
 				objEl.offsetHeight,
 				{
 					chamfer: {
-						radius: parseInt(style.borderRadius || "0", 10),
+						radius: Number.parseInt(style.borderRadius || "0", 10),
 					},
 					restitution: 0.5,
 				},
diff --git a/packages/client/src/scripts/popout.ts b/packages/client/src/scripts/popout.ts
index 54aa422257..9113f4a9a4 100644
--- a/packages/client/src/scripts/popout.ts
+++ b/packages/client/src/scripts/popout.ts
@@ -9,8 +9,8 @@ export function popout(path: string, w?: HTMLElement) {
 	url = appendQuery(url, "zen");
 	if (w) {
 		const position = w.getBoundingClientRect();
-		const width = parseInt(getComputedStyle(w, "").width, 10);
-		const height = parseInt(getComputedStyle(w, "").height, 10);
+		const width = Number.parseInt(getComputedStyle(w, "").width, 10);
+		const height = Number.parseInt(getComputedStyle(w, "").height, 10);
 		const x = window.screenX + position.left;
 		const y = window.screenY + position.top;
 		window.open(
diff --git a/packages/client/src/scripts/reaction-picker.ts b/packages/client/src/scripts/reaction-picker.ts
index 353a032d32..207d582343 100644
--- a/packages/client/src/scripts/reaction-picker.ts
+++ b/packages/client/src/scripts/reaction-picker.ts
@@ -24,14 +24,14 @@ class ReactionPicker {
 			},
 			{
 				done: (reaction) => {
-					this.onChosen!(reaction);
+					this.onChosen?.(reaction);
 				},
 				close: () => {
 					this.manualShowing.value = false;
 				},
 				closed: () => {
 					this.src.value = null;
-					this.onClosed!();
+					this.onClosed?.();
 				},
 			},
 		);
diff --git a/packages/client/src/scripts/select-file.ts b/packages/client/src/scripts/select-file.ts
index adf9f0956a..6b51e5fef9 100644
--- a/packages/client/src/scripts/select-file.ts
+++ b/packages/client/src/scripts/select-file.ts
@@ -9,12 +9,14 @@ import icon from "@/scripts/icon";
 
 const stream = useStream();
 
-function select(
-	src: any,
+function select<Multiple extends boolean>(
+	src: HTMLElement | null | undefined,
 	label: string | null,
-	multiple: boolean,
-): Promise<entities.DriveFile | entities.DriveFile[]> {
-	return new Promise((res, rej) => {
+	multiple: Multiple,
+) {
+	return new Promise<
+		Multiple extends true ? entities.DriveFile[] : entities.DriveFile
+	>((res, rej) => {
 		const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
 
 		const chooseFileFromPc = () => {
@@ -22,6 +24,9 @@ function select(
 			input.type = "file";
 			input.multiple = multiple;
 			input.onchange = () => {
+				if (input.files === null) {
+					return;
+				}
 				const promises = Array.from(input.files).map((file) =>
 					uploadFile(
 						file,
@@ -33,19 +38,19 @@ function select(
 
 				Promise.all(promises)
 					.then((driveFiles) => {
-						res(multiple ? driveFiles : driveFiles[0]);
+						res((multiple ? driveFiles : driveFiles[0]) as never);
 					})
 					.catch((err) => {
 						// アップロードのエラーは uploadFile 内でハンドリングされているためアラートダイアログを出したりはしてはいけない
 					});
 
 				// 一応廃棄
-				(window as any).__misskey_input_ref__ = null;
+				window.__misskey_input_ref__ = null;
 			};
 
 			// https://qiita.com/fukasawah/items/b9dc732d95d99551013d
 			// iOS Safari で正常に動かす為のおまじない
-			(window as any).__misskey_input_ref__ = input;
+			window.__misskey_input_ref__ = input;
 
 			input.click();
 		};
@@ -69,7 +74,7 @@ function select(
 				const connection = stream.useChannel("main");
 				connection.on("urlUploadFinished", (urlResponse) => {
 					if (urlResponse.marker === marker) {
-						res(multiple ? [urlResponse.file] : urlResponse.file);
+						res((multiple ? [urlResponse.file] : urlResponse.file) as never);
 						connection.dispose();
 					}
 				});
@@ -122,15 +127,15 @@ function select(
 }
 
 export function selectFile(
-	src: any,
+	src: HTMLElement | null | undefined,
 	label: string | null = null,
-): Promise<entities.DriveFile> {
-	return select(src, label, false) as Promise<entities.DriveFile>;
+) {
+	return select(src, label, false);
 }
 
 export function selectFiles(
-	src: any,
+	src: HTMLElement | null | undefined,
 	label: string | null = null,
-): Promise<entities.DriveFile[]> {
-	return select(src, label, true) as Promise<entities.DriveFile[]>;
+) {
+	return select(src, label, true);
 }
diff --git a/packages/client/src/scripts/use-chart-tooltip.ts b/packages/client/src/scripts/use-chart-tooltip.ts
index 573b5b4c7c..62347ede8d 100644
--- a/packages/client/src/scripts/use-chart-tooltip.ts
+++ b/packages/client/src/scripts/use-chart-tooltip.ts
@@ -1,16 +1,23 @@
 import { onDeactivated, onUnmounted, ref } from "vue";
+import type { Color, TooltipOptions } from "chart.js";
 import * as os from "@/os";
 import MkChartTooltip from "@/components/MkChartTooltip.vue";
 
+interface ToolTipSerie {
+	backgroundColor: Color;
+	borderColor: Color;
+	text: string;
+}
+
 export function useChartTooltip(
 	opts: { position: "top" | "middle" } = { position: "top" },
 ) {
 	const tooltipShowing = ref(false);
 	const tooltipX = ref(0);
 	const tooltipY = ref(0);
-	const tooltipTitle = ref(null);
-	const tooltipSeries = ref(null);
-	let disposeTooltipComponent;
+	const tooltipTitle = ref<string | null>(null);
+	const tooltipSeries = ref<ToolTipSerie[] | null>(null);
+	let disposeTooltipComponent: () => void;
 
 	os.popup(
 		MkChartTooltip,
@@ -34,7 +41,7 @@ export function useChartTooltip(
 		tooltipShowing.value = false;
 	});
 
-	function handler(context) {
+	const handler: TooltipOptions["external"] = (context) => {
 		if (context.tooltip.opacity === 0) {
 			tooltipShowing.value = false;
 			return;
@@ -56,7 +63,7 @@ export function useChartTooltip(
 		} else if (opts.position === "middle") {
 			tooltipY.value = rect.top + window.scrollY + context.tooltip.caretY;
 		}
-	}
+	};
 
 	return {
 		handler,
diff --git a/packages/client/src/scripts/use-note-capture.ts b/packages/client/src/scripts/use-note-capture.ts
index def3baf8f8..1bc32d5246 100644
--- a/packages/client/src/scripts/use-note-capture.ts
+++ b/packages/client/src/scripts/use-note-capture.ts
@@ -6,7 +6,7 @@ import { isSignedIn, me } from "@/me";
 import * as os from "@/os";
 
 export function useNoteCapture(props: {
-	rootEl: Ref<HTMLElement>;
+	rootEl: Ref<HTMLElement | null>;
 	note: Ref<entities.Note>;
 	isDeletedRef: Ref<boolean>;
 }) {
diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts
index deba55d285..29393123c0 100644
--- a/packages/client/src/store.ts
+++ b/packages/client/src/store.ts
@@ -1,13 +1,27 @@
 import { markRaw, ref } from "vue";
+import type { ApiTypes, entities } from "firefish-js";
 import { isSignedIn } from "./me";
 import { Storage } from "./pizzax";
 import type { NoteVisibility } from "@/types/note";
 
-export const postFormActions = [];
-export const userActions = [];
-export const noteActions = [];
-export const noteViewInterruptors = [];
-export const notePostInterruptors = [];
+export const postFormActions: {
+	title: string;
+	handler: (from, update) => void | Promise<void>;
+}[] = [];
+export const userActions: {
+	title: string;
+	handler: (user: entities.User) => void | Promise<void>;
+}[] = [];
+export const noteActions: {
+	title: string;
+	handler: (note: entities.Note) => void | Promise<void>;
+}[] = [];
+export const noteViewInterruptors: {
+	handler: (note: entities.Note) => Promise<entities.Note>;
+}[] = [];
+export const notePostInterruptors: {
+	handler: (note: ApiTypes.NoteSubmitReq) => Promise<ApiTypes.NoteSubmitReq>;
+}[] = [];
 
 const menuOptions = [
 	"notifications",
diff --git a/packages/client/src/types/form.ts b/packages/client/src/types/form.ts
new file mode 100644
index 0000000000..c5e169c465
--- /dev/null
+++ b/packages/client/src/types/form.ts
@@ -0,0 +1,138 @@
+export interface BaseFormItem {
+	hidden?: boolean;
+	label?: string;
+	description?: string;
+	required?: boolean;
+}
+
+export type FormItemTextInput = BaseFormItem & {
+	type: "string";
+	default?: string | null;
+	multiline?: false;
+};
+export type FormItemTextarea = BaseFormItem & {
+	type: "string";
+	default?: string | null;
+	multiline: true;
+};
+
+export type FormItemText = FormItemTextInput | FormItemTextarea;
+
+export type FormItemNumber = BaseFormItem & {
+	type: "number";
+	default?: number | null;
+	step?: number | null;
+};
+export type FormItemEmail = BaseFormItem & {
+	type: "email";
+	default?: string | null;
+};
+export type FormItemPassword = BaseFormItem & {
+	type: "password";
+	default?: never;
+	__result_typedef?: string;
+};
+export type FormItemUrl = BaseFormItem & {
+	type: "url";
+	default?: string | null;
+};
+export type FormItemDate = BaseFormItem & {
+	type: "date";
+	default?: Date | null;
+};
+export type FormItemTime = BaseFormItem & {
+	type: "time";
+	default?: number | Date | null;
+};
+export type FormItemSearch = BaseFormItem & {
+	type: "search";
+	default?: string | null;
+};
+export type FormItemSwitch = BaseFormItem & {
+	type: "boolean";
+	default?: boolean | null;
+};
+export type FormItemSelect = BaseFormItem & {
+	type: "enum";
+	default?: string | number | symbol | null;
+	enum: {
+		value: string | number | symbol;
+		label: string;
+	}[];
+};
+export type FormItemRadios = BaseFormItem & {
+	type: "radio";
+	default?: string | number | symbol | null;
+	options: {
+		label: string;
+		value: string | number | symbol;
+	}[];
+};
+export type FormItemRange = BaseFormItem & {
+	type: "range";
+	default?: number | null;
+	min: number;
+	max: number;
+	step?: number;
+	textConverter?: (value: number) => string;
+};
+export type FormItemButton = BaseFormItem & {
+	type: "button";
+	content?: string;
+	action: (event, values) => unknown;
+	default?: never;
+};
+export type FormItemObject = BaseFormItem & {
+	type: "object";
+	default: Record<string, unknown> | null;
+	hidden: true;
+};
+
+export type FormItemInputArray = [
+	FormItemTextInput,
+	FormItemNumber,
+	FormItemEmail,
+	FormItemPassword,
+	FormItemUrl,
+	FormItemDate,
+	FormItemTime,
+	FormItemSearch,
+];
+
+export type FormItemTypeArray = [
+	...FormItemInputArray,
+	FormItemTextarea,
+	FormItemSwitch,
+	FormItemSelect,
+	FormItemButton,
+	FormItemRadios,
+	FormItemRange,
+	FormItemObject,
+];
+
+export type FormItemInput = FormItemInputArray[number];
+
+export type FormItemType = FormItemTypeArray[number];
+
+export type Form = Record<string, FormItemType>;
+
+export type GetFormItemByType<
+	T extends FormItemType["type"],
+	F extends FormItemType = FormItemType,
+> = F extends { type: T } ? F : never;
+
+type NonUndefindAble<T> = T extends undefined ? never : T;
+type NonNullAble<T> = T extends null ? never : T;
+
+export type GetFormItemResultType<
+	T extends FormItemType["type"],
+	I extends FormItemType = GetFormItemByType<T>,
+> = NonUndefindAble<
+	"__result_typedef" extends keyof I ? I["__result_typedef"] : I["default"]
+>;
+
+export type GetFormResultType<F extends Form> = {
+	[K in keyof F]: F[K]["required"] extends false
+		? GetFormItemResultType<F[K]["type"]>
+		: NonNullAble<GetFormItemResultType<F[K]["type"]>>;
+};
diff --git a/packages/client/src/types/note.ts b/packages/client/src/types/note.ts
index 559ce0c793..17338c02b8 100644
--- a/packages/client/src/types/note.ts
+++ b/packages/client/src/types/note.ts
@@ -1,3 +1,8 @@
 import type { noteVisibilities } from "firefish-js";
 
 export type NoteVisibility = (typeof noteVisibilities)[number] | "private";
+
+export interface NoteTranslation {
+	sourceLang: string;
+	text: string;
+}
diff --git a/packages/client/src/types/post-form.ts b/packages/client/src/types/post-form.ts
new file mode 100644
index 0000000000..c8d92f4636
--- /dev/null
+++ b/packages/client/src/types/post-form.ts
@@ -0,0 +1,12 @@
+import type { entities } from "firefish-js";
+
+export interface PollType {
+	choices: string[];
+	multiple: boolean;
+	expiresAt: string | null;
+	expiredAfter: number | null;
+}
+
+export type NoteDraft = entities.Note & {
+	poll?: PollType;
+};
diff --git a/packages/client/src/widgets/widget.ts b/packages/client/src/widgets/widget.ts
index 9f33cecfa2..1fdcca86f1 100644
--- a/packages/client/src/widgets/widget.ts
+++ b/packages/client/src/widgets/widget.ts
@@ -23,9 +23,7 @@ export interface WidgetComponentExpose {
 	configure: () => void;
 }
 
-export const useWidgetPropsManager = <
-	F extends Form & Record<string, { default: any }>,
->(
+export const useWidgetPropsManager = <F extends Form>(
 	name: string,
 	propsDef: F,
 	props: Readonly<WidgetComponentProps<GetFormResultType<F>>>,
diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json
index 4d62c784dd..51f6e8dd1e 100644
--- a/packages/client/tsconfig.json
+++ b/packages/client/tsconfig.json
@@ -30,5 +30,5 @@
 		"jsx": "preserve"
 	},
 	"compileOnSave": false,
-	"include": ["./src/**/*.ts", "./src/**/*.vue"]
+	"include": ["./src/**/*.ts", "./src/**/*.vue", "./@types"]
 }
diff --git a/packages/firefish-js/bun.lockb b/packages/firefish-js/bun.lockb
deleted file mode 100755
index 96788d2325..0000000000
Binary files a/packages/firefish-js/bun.lockb and /dev/null differ
diff --git a/packages/firefish-js/src/api.ts b/packages/firefish-js/src/api.ts
index 2639712a3f..4a7e1f0b64 100644
--- a/packages/firefish-js/src/api.ts
+++ b/packages/firefish-js/src/api.ts
@@ -122,6 +122,6 @@ export class APIClient {
 				.catch(reject);
 		});
 
-		return promise as any;
+		return promise;
 	}
 }
diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts
index d3564573f0..c0a7c9019b 100644
--- a/packages/firefish-js/src/api.types.ts
+++ b/packages/firefish-js/src/api.types.ts
@@ -1,4 +1,5 @@
 import type {
+	AbuseUserReport,
 	Ad,
 	Announcement,
 	Antenna,
@@ -35,6 +36,7 @@ import type {
 	UserDetailed,
 	UserGroup,
 	UserList,
+	UserLite,
 	UserSorting,
 } from "./entities";
 
@@ -44,11 +46,13 @@ type TODO = Record<string, any> | null;
 
 type NoParams = Record<string, never>;
 
-type ShowUserReq = { username: string; host?: string } | { userId: User["id"] };
+type ShowUserReq =
+	| { username: string; host?: string | null }
+	| { userId: User["id"] };
 
-type NoteSubmitReq = {
+export type NoteSubmitReq = {
 	editId?: null | Note["id"];
-	visibility?: "public" | "home" | "followers" | "specified";
+	visibility?: (typeof consts.noteVisibilities)[number];
 	visibleUserIds?: User["id"][];
 	text?: null | string;
 	cw?: null | string;
@@ -60,15 +64,27 @@ type NoteSubmitReq = {
 	channelId?: null | Channel["id"];
 	poll?: null | {
 		choices: string[];
-		multiple?: boolean;
-		expiresAt?: null | number;
-		expiredAfter?: null | number;
+		multiple: boolean;
+		expiresAt: string | null;
+		expiredAfter: number | null;
 	};
+	lang?: string;
 };
 
 export type Endpoints = {
 	// admin
-	"admin/abuse-user-reports": { req: TODO; res: TODO };
+	"admin/abuse-user-reports": {
+		req: {
+			limit?: number;
+			sinceId?: AbuseUserReport["id"];
+			untilId?: AbuseUserReport["id"];
+			state?: string;
+			reporterOrigin?: OriginType;
+			targetUserOrigin?: OriginType;
+			forwarded?: boolean;
+		};
+		res: AbuseUserReport[];
+	};
 	"admin/delete-all-files-of-a-user": {
 		req: { userId: User["id"] };
 		res: null;
@@ -206,16 +222,31 @@ export type Endpoints = {
 	};
 
 	// channels
-	"channels/create": { req: TODO; res: TODO };
-	"channels/featured": { req: TODO; res: TODO };
+	"channels/create": {
+		req: {
+			name: string;
+			description?: string;
+			bannerId: DriveFile["id"] | null;
+		};
+		res: Channel;
+	};
+	"channels/featured": { req: TODO; res: Channel[] };
 	"channels/follow": { req: TODO; res: TODO };
-	"channels/followed": { req: TODO; res: TODO };
-	"channels/owned": { req: TODO; res: TODO };
+	"channels/followed": { req: TODO; res: Channel[] };
+	"channels/owned": { req: TODO; res: Channel[] };
 	"channels/pin-note": { req: TODO; res: TODO };
-	"channels/show": { req: TODO; res: TODO };
+	"channels/show": { req: TODO; res: Channel };
 	"channels/timeline": { req: TODO; res: Note[] };
 	"channels/unfollow": { req: TODO; res: TODO };
-	"channels/update": { req: TODO; res: TODO };
+	"channels/update": {
+		req: {
+			channelId: Channel["id"];
+			name: string;
+			description?: string;
+			bannerId: DriveFile["id"] | null;
+		};
+		res: Channel;
+	};
 
 	// charts
 	"charts/active-users": {
@@ -252,6 +283,12 @@ export type Endpoints = {
 		res: DriveFile[];
 	};
 	"drive/files/attached-notes": { req: TODO; res: Note[] };
+	"drive/files/caption-image": {
+		req: {
+			url: string;
+		};
+		res: string;
+	};
 	"drive/files/check-existence": { req: TODO; res: TODO };
 	"drive/files/create": { req: TODO; res: TODO };
 	"drive/files/delete": { req: { fileId: DriveFile["id"] }; res: null };
@@ -378,6 +415,17 @@ export type Endpoints = {
 		res: Instance[];
 	};
 	"federation/show-instance": { req: { host: string }; res: Instance };
+	"federation/stats": {
+		req: {
+			limit?: number;
+		};
+		res: {
+			topSubInstances: Instance[];
+			otherFollowersCount: number;
+			topPubInstances: Instance[];
+			otherFollowingCount: number;
+		};
+	};
 	"federation/update-remote-user": { req: { userId: User["id"] }; res: null };
 	"federation/users": {
 		req: {
@@ -405,7 +453,12 @@ export type Endpoints = {
 	"gallery/posts/create": { req: TODO; res: TODO };
 	"gallery/posts/delete": { req: { postId: GalleryPost["id"] }; res: null };
 	"gallery/posts/like": { req: TODO; res: TODO };
-	"gallery/posts/show": { req: TODO; res: TODO };
+	"gallery/posts/show": {
+		req: {
+			postId: GalleryPost["id"];
+		};
+		res: GalleryPost;
+	};
 	"gallery/posts/unlike": { req: TODO; res: TODO };
 	"gallery/posts/update": { req: TODO; res: TODO };
 
@@ -447,7 +500,14 @@ export type Endpoints = {
 		res: NoteFavorite[];
 	};
 	"i/gallery/likes": { req: TODO; res: TODO };
-	"i/gallery/posts": { req: TODO; res: TODO };
+	"i/gallery/posts": {
+		req: {
+			limit?: number;
+			sinceId?: NoteFavorite["id"];
+			untilId?: NoteFavorite["id"];
+		};
+		res: GalleryPost[];
+	};
 	"i/get-word-muted-notes-count": { req: TODO; res: TODO };
 	"i/import-following": { req: TODO; res: TODO };
 	"i/import-user-lists": { req: TODO; res: TODO };
@@ -630,7 +690,14 @@ export type Endpoints = {
 		res: Note[];
 	};
 	"notes/clips": { req: TODO; res: TODO };
-	"notes/conversation": { req: TODO; res: TODO };
+	"notes/conversation": {
+		req: {
+			noteId: string;
+			limit?: number;
+			offset?: number;
+		};
+		res: Note[];
+	};
 	"notes/create": {
 		req: NoteSubmitReq;
 		res: { createdNote: Note };
@@ -733,7 +800,24 @@ export type Endpoints = {
 		res: Note[];
 	};
 	"notes/search-by-tag": { req: TODO; res: TODO };
-	"notes/search": { req: TODO; res: TODO };
+	"notes/search": {
+		req: {
+			query: string;
+			sinceId?: string;
+			untilId?: string;
+			sinceDate?: number;
+			untilDate?: number;
+			limit?: number;
+			offset?: number;
+			host?: string;
+			userId?: string;
+			withFiles?: boolean;
+			searchCwAndAlt?: boolean;
+			channelId?: string;
+			order?: "chronological" | "relevancy";
+		};
+		res: Note[];
+	};
 	"notes/show": { req: { noteId: Note["id"] }; res: Note };
 	"notes/state": { req: TODO; res: TODO };
 	"notes/timeline": {
@@ -746,6 +830,16 @@ export type Endpoints = {
 		};
 		res: Note[];
 	};
+	"notes/translate": {
+		req: {
+			noteId: string;
+			targetLang: string;
+		};
+		res: {
+			sourceLang: string;
+			text: string;
+		};
+	};
 	"notes/unrenote": { req: { noteId: Note["id"] }; res: null };
 	"notes/user-list-timeline": {
 		req: {
@@ -863,7 +957,15 @@ export type Endpoints = {
 		};
 		res: FollowingFolloweePopulated[];
 	};
-	"users/gallery/posts": { req: TODO; res: TODO };
+	"users/gallery/posts": {
+		req: {
+			userId: User["id"];
+			limit?: number;
+			sinceId?: NoteFavorite["id"];
+			untilId?: NoteFavorite["id"];
+		};
+		res: GalleryPost[];
+	};
 	"users/get-frequently-replied-users": { req: TODO; res: TODO };
 	"users/groups/create": { req: TODO; res: TODO };
 	"users/groups/delete": { req: { groupId: UserGroup["id"] }; res: null };
@@ -908,7 +1010,16 @@ export type Endpoints = {
 	"users/relation": { req: TODO; res: TODO };
 	"users/report-abuse": { req: TODO; res: TODO };
 	"users/search-by-username-and-host": { req: TODO; res: TODO };
-	"users/search": { req: TODO; res: TODO };
+	"users/search": {
+		req: {
+			query: string;
+			offset?: number;
+			limit?: number;
+			origin?: "local" | "remote" | "combined";
+			detail?: true; // FIXME: when false, returns UserLite
+		};
+		res: UserDetailed[];
+	};
 	"users/show": {
 		req: ShowUserReq | { userIds: User["id"][] };
 		res: {
diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts
index 6f9d6b400b..67fb41988d 100644
--- a/packages/firefish-js/src/entities.ts
+++ b/packages/firefish-js/src/entities.ts
@@ -18,18 +18,18 @@ export type UserLite = {
 	avatarBlurhash: string;
 	alsoKnownAs: string[];
 	movedToUri: any;
-	emojis: {
-		name: string;
-		url: string;
-	}[];
-	instance?: {
-		name: Instance["name"];
-		softwareName: Instance["softwareName"];
-		softwareVersion: Instance["softwareVersion"];
-		iconUrl: Instance["iconUrl"];
-		faviconUrl: Instance["faviconUrl"];
-		themeColor: Instance["themeColor"];
-	};
+	emojis: EmojiLite[];
+	instance?: InstanceLite;
+	avatarColor: null;
+	emojiModPerm: "unauthorized" | "add" | "mod" | "full";
+	isAdmin?: boolean;
+	isModerator?: boolean;
+	isBot?: boolean;
+	isLocked: boolean;
+	isIndexable: boolean;
+	isCat?: boolean;
+	speakAsCat?: boolean;
+	driveCapacityOverrideMb: number | null;
 };
 
 export type UserDetailed = UserLite & {
@@ -56,7 +56,6 @@ export type UserDetailed = UserLite & {
 	isCat: boolean;
 	isFollowed: boolean;
 	isFollowing: boolean;
-	isLocked: boolean;
 	isModerator: boolean;
 	isMuted: boolean;
 	isRenoteMuted: boolean;
@@ -79,7 +78,9 @@ export type UserDetailed = UserLite & {
 	url: string | null;
 };
 
-export type UserGroup = TODO;
+export type UserGroup = {
+	id: ID;
+} & Record<string, TODO>;
 
 export type UserList = {
 	id: ID;
@@ -137,7 +138,21 @@ export type DriveFile = {
 
 export type DriveFolder = TODO;
 
-export type GalleryPost = TODO;
+export type GalleryPost = {
+	id: ID;
+	createdAt: DateString;
+	updatedAt: DateString;
+	title: string;
+	description: string | null;
+	userId: User["id"];
+	user: UserDetailed;
+	fileIds?: DriveFile["id"][];
+	files?: DriveFile[];
+	tags?: string[];
+	isSensitive: boolean;
+	isLiked?: boolean;
+	likedCount: number;
+};
 
 export type Note = {
 	id: ID;
@@ -171,10 +186,7 @@ export type Note = {
 			votes: number;
 		}[];
 	};
-	emojis: {
-		name: string;
-		url: string;
-	}[];
+	emojis: EmojiLite[];
 	uri?: string;
 	url?: string;
 	updatedAt?: DateString;
@@ -191,10 +203,7 @@ export type NoteEdit = {
 	updatedAt: string;
 	fileIds: DriveFile["id"][];
 	files: DriveFile[];
-	emojis: {
-		name: string;
-		url: string;
-	}[];
+	emojis: EmojiLite[];
 };
 
 export type NoteReaction = {
@@ -228,7 +237,10 @@ export interface RenoteNotification extends BaseNotification {
 	type: "renote";
 	user: User;
 	userId: User["id"];
-	note: Note;
+	note: Note & {
+		renote: Note;
+		renoteId: string;
+	};
 }
 export interface QuoteNotification extends BaseNotification {
 	type: "quote";
@@ -325,6 +337,8 @@ export type EmojiLite = {
 	id: string;
 	name: string;
 	url: string;
+	width: number | null;
+	height: number | null;
 };
 
 export type LiteInstanceMetadata = {
@@ -334,6 +348,7 @@ export type LiteInstanceMetadata = {
 	name: string | null;
 	uri: string;
 	description: string | null;
+	donationLink?: string;
 	tosUrl: string | null;
 	disableRegistration: boolean;
 	disableLocalTimeline: boolean;
@@ -362,7 +377,40 @@ export type LiteInstanceMetadata = {
 };
 
 export type DetailedInstanceMetadata = LiteInstanceMetadata & {
-	features: Record<string, any>;
+	features: {
+		registration: boolean;
+		localTimeLine: boolean;
+		recommendedTimeLine: boolean;
+		globalTimeLine: boolean;
+		searchFilters: boolean;
+		hcaptcha: boolean;
+		recaptcha: boolean;
+		objectStorage: boolean;
+		serviceWorker: boolean;
+		miauth?: boolean;
+	};
+	langs: string[];
+	moreUrls: object;
+	repositoryUrl: string;
+	feedbackUrl: string;
+	defaultDarkTheme: string | null;
+	defaultLightTheme: string | null;
+	enableGuestTimeline: boolean;
+	cacheRemoteFiles: boolean;
+	emailRequiredForSignup: boolean;
+	mascotImageUrl: string;
+	bannerUrl: string;
+	errorImageUrl: string;
+	iconUrl: string | null;
+	maxCaptionTextLength: number;
+	requireSetup: boolean;
+	translatorAvailable: boolean;
+	proxyAccountName: string | null;
+	secureMode?: boolean;
+	privateMode?: boolean;
+	defaultReaction: string;
+	donationLink?: string | null;
+	enableServerMachineStats?: boolean;
 };
 
 export type InstanceMetadata = LiteInstanceMetadata | DetailedInstanceMetadata;
@@ -430,6 +478,8 @@ export type Announcement = {
 	title: string;
 	imageUrl: string | null;
 	isRead?: boolean;
+	isGoodNews: boolean;
+	showPopUp: boolean;
 };
 
 export type Antenna = {
@@ -477,8 +527,17 @@ export type FollowRequest = {
 
 export type Channel = {
 	id: ID;
+	createdAt: DateString;
+	lastNotedAt: DateString | null;
 	name: string;
-	// TODO
+	description: string | null;
+	bannerId: DriveFile["id"];
+	bannerUrl: string | null;
+	notesCount: number;
+	usersCount: number;
+	isFollowing?: boolean;
+	userId: User["id"] | null;
+	hasUnreadNote?: boolean;
 };
 
 export type Following = {
@@ -503,6 +562,15 @@ export type Blocking = {
 	blockee: UserDetailed;
 };
 
+export type InstanceLite = {
+	name: Instance["name"];
+	softwareName: Instance["softwareName"];
+	softwareVersion: Instance["softwareVersion"];
+	iconUrl: Instance["iconUrl"];
+	faviconUrl: Instance["faviconUrl"];
+	themeColor: Instance["themeColor"];
+};
+
 export type Instance = {
 	id: ID;
 	caughtAt: DateString;
@@ -519,6 +587,8 @@ export type Instance = {
 	lastCommunicatedAt: DateString;
 	isNotResponding: boolean;
 	isSuspended: boolean;
+	isBlocked: boolean;
+	isSilenced: boolean;
 	softwareName: string | null;
 	softwareVersion: string | null;
 	openRegistrations: boolean | null;
@@ -548,3 +618,17 @@ export type UserSorting =
 	| "+updatedAt"
 	| "-updatedAt";
 export type OriginType = "combined" | "local" | "remote";
+
+export type AbuseUserReport = {
+	id: string;
+	createdAt: DateString;
+	comment: string;
+	resolved: boolean;
+	reporterId: User["id"];
+	targetUserId: User["id"];
+	assigneeId: User["id"] | null;
+	reporter: UserDetailed;
+	targetUser: UserDetailed;
+	assignee?: UserDetailed | null;
+	forwarded: boolean;
+};
diff --git a/packages/firefish-js/src/index.ts b/packages/firefish-js/src/index.ts
index 6639985481..3398ed8a2e 100644
--- a/packages/firefish-js/src/index.ts
+++ b/packages/firefish-js/src/index.ts
@@ -1,6 +1,7 @@
 import * as acct from "./acct";
 import type { Acct } from "./acct";
 import { Endpoints } from "./api.types";
+import type * as ApiTypes from "./api.types";
 import * as consts from "./consts";
 import Stream, { Connection } from "./streaming";
 import * as StreamTypes from "./streaming.types";
@@ -8,6 +9,7 @@ import type * as TypeUtils from "./type-utils";
 
 export {
 	Endpoints,
+	type ApiTypes,
 	Stream,
 	Connection as ChannelConnection,
 	StreamTypes,
diff --git a/packages/firefish-js/src/streaming.types.ts b/packages/firefish-js/src/streaming.types.ts
index 18c7682608..5b81780271 100644
--- a/packages/firefish-js/src/streaming.types.ts
+++ b/packages/firefish-js/src/streaming.types.ts
@@ -8,7 +8,9 @@ import type {
 	Notification,
 	PageEvent,
 	User,
+	UserDetailed,
 	UserGroup,
+	UserLite,
 } from "./entities";
 import type { Connection } from "./streaming";
 
@@ -26,9 +28,9 @@ export type Channels = {
 			mention: (payload: Note) => void;
 			reply: (payload: Note) => void;
 			renote: (payload: Note) => void;
-			follow: (payload: User) => void; // 自分が他人をフォローしたとき
-			followed: (payload: User) => void; // 他人が自分をフォローしたとき
-			unfollow: (payload: User) => void; // 自分が他人をフォロー解除したとき
+			follow: (payload: UserDetailed) => void; // 自分が他人をフォローしたとき
+			followed: (payload: UserLite) => void; // 他人が自分をフォローしたとき
+			unfollow: (payload: UserDetailed) => void; // 自分が他人をフォロー解除したとき
 			meUpdated: (payload: MeDetailed) => void;
 			pageEvent: (payload: PageEvent) => void;
 			urlUploadFinished: (payload: { marker: string; file: DriveFile }) => void;
diff --git a/patrons.json b/patrons.json
index 738083ec98..cbbeacfdc9 100644
--- a/patrons.json
+++ b/patrons.json
@@ -1,118 +1,4 @@
 {
-	"patrons": [
-		"@atomicpoet@firefish.social",
-		"@shoq@mastodon.social",
-		"@pikadude@erisly.social",
-		"@sage@stop.voring.me",
-		"@sky@therian.club",
-		"@panos@electricrequiem.com",
-		"@redhunt07@www.foxyhole.io",
-		"@griff@firefish.social",
-		"@cafkafk@ck.cafkafk.com",
-		"@privateger@plasmatrap.com",
-		"@effye@toot.thoughtworks.com",
-		"@Kio@kitsunes.club",
-		"@twann@tech.lgbt",
-		"@surfbum@firefish.nz",
-		"@topher@mastodon.online",
-		"@hanicef@stop.voring.me",
-		"@nmkj@calckey.jp",
-		"@unattributed@firefish.social",
-		"@cody@misskey.codingneko.com",
-		"@kate@blahaj.zone",
-		"@emtk@mkkey.net",
-		"@jovikowi@firefish.social",
-		"@padraig@firefish.social",
-		"@pancakes@cats.city",
-		"@theresmiling@firefish.social",
-		"@kristian@firefish.social",
-		"@jo@blahaj.zone",
-		"@narF@firefish.social",
-		"@AlderForrest@raining.anvil.top",
-		"@box464@firefish.social",
-		"@MariaTheMartian@firefish.social",
-		"@nisemikol@firefish.social",
-		"@smallpatatas@blahaj.zone",
-		"@bayra@stop.voring.me",
-		"@frost@wolfdo.gg",
-		"@joebiden@fuckgov.org",
-		"@nyaa@firefish.social",
-		"@Dan@firefish.social",
-		"@dana@firefish.social",
-		"@Jdreben@firefish.social",
-		"@natalie@prismst.one",
-		"@KelsonV@wandering.shop",
-		"@breakfastmtn@firefish.social",
-		"@richardazia@mastodon.social",
-		"@joestone@firefish.social",
-		"@aj@firefish.social",
-		"@zepfanman@ramblingreaders.org",
-		"@kimby@stop.voring.me",
-		"@fyrfli@fyrfli.social",
-		"@riversidebryan@firefish.lgbt",
-		"@aRubes@sloth.run",
-		"@andreasdotorg@firefish.social",
-		"@ozzy@calckey.online",
-		"@leni@windycity.style",
-		"@mhzmodels@calckey.art",
-		"@ReflexVE@firefish.social",
-		"@mark@firefish.social",
-		"@skyizwhite@himagine.club",
-		"@Uwu@firefish.social",
-		"@jGoose@firefish.social",
-		"@kunev@blewsky.social",
-		"@Simoto@electricrequiem.com",
-		"@Evoterra@firefish.social",
-		"@LauraLangdon@procial.tchncs.de",
-		"@mho@social.heise.de",
-		"@richardazia@firefish.social",
-		"@blues653@firefish.social",
-		"@rafale_blue@calc.04.si",
-		"@esm@lethallava.land",
-		"@vmstan@vmst.io",
-		"@jtbennett@noc.social",
-		"@renere@distance.blue",
-		"@theking@kitsunes.club",
-		"@toof@fedi.toofie.net",
-		"@Punko@firefish.social",
-		"@joesbrat67@firefish.social",
-		"@arth@firefish.social",
-		"@octofloofy@ck.octofloofy.ink",
-		"@pauliehedron@infosec.town",
-		"@soulthunk@lethallava.land",
-		"@bumble@ibe.social",
-		"@DarrenNevares@firefish.social",
-		"@irfan@firefish.social",
-		"@dvd@dvd.chat",
-		"@charlie2alpha@electricrequiem.com",
-		"@arndot@layer8.space",
-		"@ryan@c.ryanccn.dev",
-		"@lapastora_deprova@firefish.social",
-		"@rameez@firefish.social",
-		"@dracoling@firetribe.org",
-		"@Space6host@firefish.social",
-		"@zakalwe@plasmatrap.com",
-		"@seasicksailor@firefish.social",
-		"@geerue@firefish.social",
-		"@WXFanatic@m.ai6yr.org",
-		"@Hunkabilly@calckey.world",
-		"@samleegray@firefish.social",
-		"@schwarzewald@kodow.net",
-		"@Conatusprinciple@firefish.social",
-		"@183231bcb@firefish.lgbt",
-		"@wiase@firefish.social",
-		"@leonieke@vitaulium.nl",
-		"@soulfire@wackywolf.xyz",
-		"@elbullazul@pub.elbullazul.com",
-		"@rafale_blue@calc.04.si",
-		"@firnin@federation.network",
-		"@clement@ck.villisek.fr",
-		"@hryggrbyr@ibe.social"
-	],
-	"sponsors": [
-		"@atomicpoet@firefish.social",
-		"@unattributed@firefish.social",
-		"@jtbennett@noc.social",
-		"\nInterkosmos Link"
-	]
+  "patrons": [],
+  "sponsors": []
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 192b5f95e5..b591ffe9c1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -46,8 +46,8 @@ importers:
         specifier: 8.0.1
         version: 8.0.1
       pnpm:
-        specifier: 8.15.6
-        version: 8.15.6
+        specifier: 8.15.7
+        version: 8.15.7
       typescript:
         specifier: 5.4.5
         version: 5.4.5
@@ -55,14 +55,14 @@ importers:
   packages/backend:
     dependencies:
       '@bull-board/api':
-        specifier: 5.15.3
-        version: 5.15.3(@bull-board/ui@5.15.3)
+        specifier: 5.15.5
+        version: 5.15.5(@bull-board/ui@5.15.5)
       '@bull-board/koa':
-        specifier: 5.15.3
-        version: 5.15.3(@types/koa@2.15.0)(pug@3.0.2)
+        specifier: 5.15.5
+        version: 5.15.5(@types/koa@2.15.0)(pug@3.0.2)
       '@bull-board/ui':
-        specifier: 5.15.3
-        version: 5.15.3
+        specifier: 5.15.5
+        version: 5.15.5
       '@discordapp/twemoji':
         specifier: ^15.0.3
         version: 15.0.3
@@ -87,9 +87,6 @@ importers:
       '@sinonjs/fake-timers':
         specifier: 11.2.2
         version: 11.2.2
-      '@twemoji/parser':
-        specifier: ^15.1.1
-        version: 15.1.1
       adm-zip:
         specifier: 0.5.10
         version: 0.5.10
@@ -99,21 +96,15 @@ importers:
       archiver:
         specifier: 7.0.1
         version: 7.0.1
-      argon2:
-        specifier: ^0.40.1
-        version: 0.40.1
       aws-sdk:
-        specifier: 2.1597.0
-        version: 2.1597.0
+        specifier: 2.1599.0
+        version: 2.1599.0
       axios:
         specifier: ^1.6.8
         version: 1.6.8
       backend-rs:
         specifier: workspace:*
         version: link:../backend-rs
-      bcryptjs:
-        specifier: 2.4.3
-        version: 2.4.3
       blurhash:
         specifier: 2.0.5
         version: 2.0.5
@@ -151,8 +142,8 @@ importers:
         specifier: 0.1.21
         version: 0.1.21
       deepl-node:
-        specifier: 1.12.0
-        version: 1.12.0
+        specifier: 1.13.0
+        version: 1.13.0
       escape-regexp:
         specifier: 0.0.1
         version: 0.0.1
@@ -289,8 +280,8 @@ importers:
         specifier: 1.5.3
         version: 1.5.3
       qs:
-        specifier: 6.12.0
-        version: 6.12.0
+        specifier: 6.12.1
+        version: 6.12.1
       random-seed:
         specifier: 0.3.0
         version: 0.3.0
@@ -377,9 +368,6 @@ importers:
       '@types/adm-zip':
         specifier: ^0.5.5
         version: 0.5.5
-      '@types/bcryptjs':
-        specifier: 2.4.6
-        version: 2.4.6
       '@types/color-convert':
         specifier: ^2.0.3
         version: 2.0.3
@@ -556,22 +544,22 @@ importers:
     devDependencies:
       '@eslint-sets/eslint-config-vue3':
         specifier: ^5.12.0
-        version: 5.12.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5)
+        version: 5.12.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5)
       '@eslint-sets/eslint-config-vue3-ts':
         specifier: ^3.3.0
-        version: 3.3.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5)
+        version: 3.3.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5)
       '@phosphor-icons/web':
         specifier: ^2.1.1
         version: 2.1.1
       '@rollup/plugin-alias':
         specifier: 5.1.0
-        version: 5.1.0(rollup@4.14.1)
+        version: 5.1.0(rollup@4.14.2)
       '@rollup/plugin-json':
         specifier: 6.1.0
-        version: 6.1.0(rollup@4.14.1)
+        version: 6.1.0(rollup@4.14.2)
       '@rollup/pluginutils':
         specifier: ^5.1.0
-        version: 5.1.0(rollup@4.14.1)
+        version: 5.1.0(rollup@4.14.2)
       '@syuilo/aiscript':
         specifier: 0.17.0
         version: 0.17.0
@@ -667,7 +655,7 @@ importers:
         version: 3.0.12
       eslint-plugin-file-progress:
         specifier: ^1.3.0
-        version: 1.3.0(eslint@8.57.0)
+        version: 1.3.0(eslint@8.46.0)
       eventemitter3:
         specifier: 5.0.1
         version: 5.0.1
@@ -720,14 +708,14 @@ importers:
         specifier: 2.3.1
         version: 2.3.1
       rollup:
-        specifier: 4.14.1
-        version: 4.14.1
+        specifier: 4.14.2
+        version: 4.14.2
       s-age:
         specifier: 1.1.2
         version: 1.1.2
       sass:
-        specifier: 1.74.1
-        version: 1.74.1
+        specifier: 1.75.0
+        version: 1.75.0
       seedrandom:
         specifier: 3.0.5
         version: 3.0.5
@@ -766,7 +754,7 @@ importers:
         version: 9.0.1
       vite:
         specifier: 5.2.8
-        version: 5.2.8(@types/node@20.12.7)(sass@1.74.1)
+        version: 5.2.8(@types/node@20.12.7)(sass@1.75.0)
       vite-plugin-compression:
         specifier: ^0.5.1
         version: 0.5.1(vite@5.2.8)
@@ -775,7 +763,7 @@ importers:
         version: 3.4.21(typescript@5.4.5)
       vue-draggable-plus:
         specifier: ^0.4.0
-        version: 0.4.0(@types/sortablejs@1.15.4)
+        version: 0.4.0(@types/sortablejs@1.15.8)
       vue-plyr:
         specifier: ^7.0.0
         version: 7.0.0
@@ -783,8 +771,8 @@ importers:
         specifier: 2.0.0-alpha.2
         version: 2.0.0-alpha.2(vue@3.4.21)
       vue-tsc:
-        specifier: 2.0.12
-        version: 2.0.12(typescript@5.4.5)
+        specifier: 2.0.13
+        version: 2.0.13(typescript@5.4.5)
 
   packages/firefish-js:
     dependencies:
@@ -940,7 +928,7 @@ importers:
         version: 4.17.21
       ts-jest:
         specifier: ^29.0.5
-        version: 29.1.1(@babel/core@7.23.2)(jest@29.7.0)(typescript@4.9.4)
+        version: 29.1.1(@babel/core@7.24.4)(jest@29.7.0)(typescript@4.9.4)
       typedoc:
         specifier: ^0.23.24
         version: 0.23.28(typescript@4.9.4)
@@ -958,7 +946,7 @@ importers:
         version: 6.2.1
       vite:
         specifier: 5.2.8
-        version: 5.2.8(@types/node@20.12.7)(sass@1.74.1)
+        version: 5.2.8(@types/node@20.12.7)(sass@1.75.0)
       vite-plugin-compression:
         specifier: ^0.5.1
         version: 0.5.1(vite@5.2.8)
@@ -977,6 +965,14 @@ packages:
       '@jridgewell/gen-mapping': 0.3.3
       '@jridgewell/trace-mapping': 0.3.20
 
+  /@ampproject/remapping@2.3.0:
+    resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
+    engines: {node: '>=6.0.0'}
+    dependencies:
+      '@jridgewell/gen-mapping': 0.3.5
+      '@jridgewell/trace-mapping': 0.3.25
+    dev: true
+
   /@babel/code-frame@7.22.10:
     resolution: {integrity: sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==}
     engines: {node: '>=6.9.0'}
@@ -992,6 +988,14 @@ packages:
       chalk: 2.4.2
     dev: true
 
+  /@babel/code-frame@7.24.2:
+    resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/highlight': 7.24.2
+      picocolors: 1.0.0
+    dev: true
+
   /@babel/compat-data@7.22.9:
     resolution: {integrity: sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==}
     engines: {node: '>=6.9.0'}
@@ -1001,6 +1005,11 @@ packages:
     engines: {node: '>=6.9.0'}
     dev: true
 
+  /@babel/compat-data@7.24.4:
+    resolution: {integrity: sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==}
+    engines: {node: '>=6.9.0'}
+    dev: true
+
   /@babel/core@7.22.10:
     resolution: {integrity: sha512-fTmqbbUBAwCcre6zPzNngvsI0aNrPZe77AeqvDxWM9Nm+04RrJ3CAmGHA9f7lJQY6ZMhRztNemy4uslDxTX4Qw==}
     engines: {node: '>=6.9.0'}
@@ -1046,30 +1055,53 @@ packages:
       - supports-color
     dev: true
 
-  /@babel/eslint-parser@7.23.10(@babel/core@7.23.2)(eslint@8.57.0):
+  /@babel/core@7.24.4:
+    resolution: {integrity: sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@ampproject/remapping': 2.3.0
+      '@babel/code-frame': 7.24.2
+      '@babel/generator': 7.24.4
+      '@babel/helper-compilation-targets': 7.23.6
+      '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.4)
+      '@babel/helpers': 7.24.4
+      '@babel/parser': 7.24.4
+      '@babel/template': 7.24.0
+      '@babel/traverse': 7.24.1
+      '@babel/types': 7.24.0
+      convert-source-map: 2.0.0
+      debug: 4.3.4(supports-color@8.1.1)
+      gensync: 1.0.0-beta.2
+      json5: 2.2.3
+      semver: 6.3.1
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
+  /@babel/eslint-parser@7.23.10(@babel/core@7.24.4)(eslint@8.46.0):
     resolution: {integrity: sha512-3wSYDPZVnhseRnxRJH6ZVTNknBz76AEnyC+AYYhasjP3Yy23qz0ERR7Fcd2SHmYuSFJ2kY9gaaDd3vyqU09eSw==}
     engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0}
     peerDependencies:
       '@babel/core': ^7.11.0
       eslint: ^7.5.0 || ^8.0.0
     dependencies:
-      '@babel/core': 7.23.2
+      '@babel/core': 7.24.4
       '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1
-      eslint: 8.57.0
+      eslint: 8.46.0
       eslint-visitor-keys: 2.1.0
       semver: 6.3.1
     dev: true
 
-  /@babel/eslint-parser@7.23.3(@babel/core@7.23.2)(eslint@8.57.0):
+  /@babel/eslint-parser@7.23.3(@babel/core@7.24.4)(eslint@8.46.0):
     resolution: {integrity: sha512-9bTuNlyx7oSstodm1cR1bECj4fkiknsDa1YniISkJemMY3DGhJNYBECbe6QD/q54mp2J8VO66jW3/7uP//iFCw==}
     engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0}
     peerDependencies:
       '@babel/core': ^7.11.0
       eslint: ^7.5.0 || ^8.0.0
     dependencies:
-      '@babel/core': 7.23.2
+      '@babel/core': 7.24.4
       '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1
-      eslint: 8.57.0
+      eslint: 8.46.0
       eslint-visitor-keys: 2.1.0
       semver: 6.3.1
     dev: true
@@ -1093,6 +1125,16 @@ packages:
       jsesc: 2.5.2
     dev: true
 
+  /@babel/generator@7.24.4:
+    resolution: {integrity: sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/types': 7.24.0
+      '@jridgewell/gen-mapping': 0.3.5
+      '@jridgewell/trace-mapping': 0.3.25
+      jsesc: 2.5.2
+    dev: true
+
   /@babel/helper-compilation-targets@7.22.10:
     resolution: {integrity: sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==}
     engines: {node: '>=6.9.0'}
@@ -1114,6 +1156,17 @@ packages:
       semver: 6.3.1
     dev: true
 
+  /@babel/helper-compilation-targets@7.23.6:
+    resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/compat-data': 7.24.4
+      '@babel/helper-validator-option': 7.23.5
+      browserslist: 4.23.0
+      lru-cache: 5.1.1
+      semver: 6.3.1
+    dev: true
+
   /@babel/helper-environment-visitor@7.22.20:
     resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==}
     engines: {node: '>=6.9.0'}
@@ -1157,6 +1210,13 @@ packages:
     dependencies:
       '@babel/types': 7.23.0
 
+  /@babel/helper-module-imports@7.24.3:
+    resolution: {integrity: sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/types': 7.24.0
+    dev: true
+
   /@babel/helper-module-transforms@7.22.9(@babel/core@7.22.10):
     resolution: {integrity: sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==}
     engines: {node: '>=6.9.0'}
@@ -1184,6 +1244,20 @@ packages:
       '@babel/helper-validator-identifier': 7.22.20
     dev: true
 
+  /@babel/helper-module-transforms@7.23.3(@babel/core@7.24.4):
+    resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==}
+    engines: {node: '>=6.9.0'}
+    peerDependencies:
+      '@babel/core': ^7.0.0
+    dependencies:
+      '@babel/core': 7.24.4
+      '@babel/helper-environment-visitor': 7.22.20
+      '@babel/helper-module-imports': 7.24.3
+      '@babel/helper-simple-access': 7.22.5
+      '@babel/helper-split-export-declaration': 7.22.6
+      '@babel/helper-validator-identifier': 7.22.20
+    dev: true
+
   /@babel/helper-plugin-utils@7.22.5:
     resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==}
     engines: {node: '>=6.9.0'}
@@ -1204,6 +1278,11 @@ packages:
     resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==}
     engines: {node: '>=6.9.0'}
 
+  /@babel/helper-string-parser@7.24.1:
+    resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==}
+    engines: {node: '>=6.9.0'}
+    dev: true
+
   /@babel/helper-validator-identifier@7.22.20:
     resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
     engines: {node: '>=6.9.0'}
@@ -1221,6 +1300,11 @@ packages:
     resolution: {integrity: sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==}
     engines: {node: '>=6.9.0'}
 
+  /@babel/helper-validator-option@7.23.5:
+    resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==}
+    engines: {node: '>=6.9.0'}
+    dev: true
+
   /@babel/helpers@7.22.10:
     resolution: {integrity: sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw==}
     engines: {node: '>=6.9.0'}
@@ -1242,6 +1326,17 @@ packages:
       - supports-color
     dev: true
 
+  /@babel/helpers@7.24.4:
+    resolution: {integrity: sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/template': 7.24.0
+      '@babel/traverse': 7.24.1
+      '@babel/types': 7.24.0
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
   /@babel/highlight@7.22.10:
     resolution: {integrity: sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==}
     engines: {node: '>=6.9.0'}
@@ -1259,6 +1354,16 @@ packages:
       js-tokens: 4.0.0
     dev: true
 
+  /@babel/highlight@7.24.2:
+    resolution: {integrity: sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/helper-validator-identifier': 7.22.20
+      chalk: 2.4.2
+      js-tokens: 4.0.0
+      picocolors: 1.0.0
+    dev: true
+
   /@babel/parser@7.22.10:
     resolution: {integrity: sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==}
     engines: {node: '>=6.0.0'}
@@ -1281,6 +1386,14 @@ packages:
       '@babel/types': 7.23.0
     dev: true
 
+  /@babel/parser@7.24.4:
+    resolution: {integrity: sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==}
+    engines: {node: '>=6.0.0'}
+    hasBin: true
+    dependencies:
+      '@babel/types': 7.24.0
+    dev: true
+
   /@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.22.10):
     resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==}
     engines: {node: '>=6.9.0'}
@@ -1466,6 +1579,15 @@ packages:
       '@babel/parser': 7.23.0
       '@babel/types': 7.23.0
 
+  /@babel/template@7.24.0:
+    resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/code-frame': 7.24.2
+      '@babel/parser': 7.24.4
+      '@babel/types': 7.24.0
+    dev: true
+
   /@babel/traverse@7.22.10:
     resolution: {integrity: sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig==}
     engines: {node: '>=6.9.0'}
@@ -1501,6 +1623,24 @@ packages:
       - supports-color
     dev: true
 
+  /@babel/traverse@7.24.1:
+    resolution: {integrity: sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/code-frame': 7.24.2
+      '@babel/generator': 7.24.4
+      '@babel/helper-environment-visitor': 7.22.20
+      '@babel/helper-function-name': 7.23.0
+      '@babel/helper-hoist-variables': 7.22.5
+      '@babel/helper-split-export-declaration': 7.22.6
+      '@babel/parser': 7.24.4
+      '@babel/types': 7.24.0
+      debug: 4.3.4(supports-color@8.1.1)
+      globals: 11.12.0
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
   /@babel/types@7.22.10:
     resolution: {integrity: sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==}
     engines: {node: '>=6.9.0'}
@@ -1517,6 +1657,15 @@ packages:
       '@babel/helper-validator-identifier': 7.22.20
       to-fast-properties: 2.0.0
 
+  /@babel/types@7.24.0:
+    resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/helper-string-parser': 7.24.1
+      '@babel/helper-validator-identifier': 7.22.20
+      to-fast-properties: 2.0.0
+    dev: true
+
   /@bcoe/v8-coverage@0.2.3:
     resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
     dev: true
@@ -1601,26 +1750,26 @@ packages:
     dev: true
     optional: true
 
-  /@bull-board/api@5.15.3(@bull-board/ui@5.15.3):
-    resolution: {integrity: sha512-tlEYOI6Hp0ZGozDCtKQEFgvzTKXj+drKStHJm86s1TcUZlsnMjzR0BUxu5CW6EB3tS3MtPLJH5RQCmUq0UEiiQ==}
+  /@bull-board/api@5.15.5(@bull-board/ui@5.15.5):
+    resolution: {integrity: sha512-s3x0f+0s4nwndBM+QSROMVKiDyE/vaaouQCsxRWOFqneLCkM+Ro2wF6fkhmFkZMjouoBbS8rCFGaIZ+8uttYtg==}
     peerDependencies:
-      '@bull-board/ui': 5.15.3
+      '@bull-board/ui': 5.15.5
     dependencies:
-      '@bull-board/ui': 5.15.3
+      '@bull-board/ui': 5.15.5
       redis-info: 3.1.0
     dev: false
 
-  /@bull-board/koa@5.15.3(@types/koa@2.15.0)(pug@3.0.2):
-    resolution: {integrity: sha512-pgHdRcre8RJKwWqlMLFY1oj742hLtxVrHsT2s4k+Ribzmoj3bq1tgRHtu6m9TX7AyABBtcTfTo30NNbPPrYR7A==}
+  /@bull-board/koa@5.15.5(@types/koa@2.15.0)(pug@3.0.2):
+    resolution: {integrity: sha512-Kbmca8hKNW5wLpGM/H1RBm09bcdK+KWCsINUyDtp91bGwMRK0mhiqvjJLJpRohXXmtPTnnJDuVO9p1gYsbed3Q==}
     dependencies:
-      '@bull-board/api': 5.15.3(@bull-board/ui@5.15.3)
-      '@bull-board/ui': 5.15.3
-      ejs: 3.1.9
+      '@bull-board/api': 5.15.5(@bull-board/ui@5.15.5)
+      '@bull-board/ui': 5.15.5
+      ejs: 3.1.10
       koa: 2.15.3
       koa-mount: 4.0.0
       koa-router: 10.1.1
       koa-static: 5.0.0
-      koa-views: 7.0.2(@types/koa@2.15.0)(ejs@3.1.9)(pug@3.0.2)
+      koa-views: 7.0.2(@types/koa@2.15.0)(ejs@3.1.10)(pug@3.0.2)
     transitivePeerDependencies:
       - '@types/koa'
       - arc-templates
@@ -1678,10 +1827,10 @@ packages:
       - whiskers
     dev: false
 
-  /@bull-board/ui@5.15.3:
-    resolution: {integrity: sha512-wCXk+s4cSszZe0p0sYYxZPLSKafFQNPsUypTvpAh3IC2p4fr6F/wUBGb1kBMspRkFC19l5yFCD5qPHVlAR0QKw==}
+  /@bull-board/ui@5.15.5:
+    resolution: {integrity: sha512-TSXgqBDI3ig6ez6yHArGzpwCuA/rhQewv0KOUAvPzssgX4HqfkatrV7gTuTM+XJe7/sLiXnBiryV7SRV0hgRMg==}
     dependencies:
-      '@bull-board/api': 5.15.3(@bull-board/ui@5.15.3)
+      '@bull-board/api': 5.15.5(@bull-board/ui@5.15.5)
     dev: false
 
   /@cbor-extract/cbor-extract-darwin-arm64@2.2.0:
@@ -1850,8 +1999,8 @@ packages:
       universalify: 0.1.2
     dev: false
 
-  /@emnapi/runtime@1.1.0:
-    resolution: {integrity: sha512-gCGlE0fJGWalfy+wbFApjhKn6uoSVvopru77IPyxNKkjkaiSx2HxDS7eOYSmo9dcMIhmmIvoxiC3N9TM1c3EaA==}
+  /@emnapi/runtime@1.1.1:
+    resolution: {integrity: sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==}
     requiresBuild: true
     dependencies:
       tslib: 2.6.2
@@ -2084,16 +2233,6 @@ packages:
       eslint-visitor-keys: 3.4.3
     dev: true
 
-  /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0):
-    resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-    peerDependencies:
-      eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
-    dependencies:
-      eslint: 8.57.0
-      eslint-visitor-keys: 3.4.3
-    dev: true
-
   /@eslint-community/eslint-utils@4.4.0(eslint@9.0.0):
     resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -2114,29 +2253,29 @@ packages:
     engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
     dev: true
 
-  /@eslint-sets/eslint-config-basic@3.3.0(@babel/core@7.23.2)(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(prettier@3.2.5):
+  /@eslint-sets/eslint-config-basic@3.3.0(@babel/core@7.24.4)(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)(prettier@3.2.5):
     resolution: {integrity: sha512-x5YH0CvZJxn19/5ehu188XaoLQpxOGlFiIuPHCN6FyONgrmriakT/cmIIBOJg2Vi/y1bn2xbhsgVNb00J3HyTg==}
     peerDependencies:
       eslint: '>=8.0.0'
       prettier: '>=2.0.0'
     dependencies:
-      '@babel/eslint-parser': 7.23.3(@babel/core@7.23.2)(eslint@8.57.0)
-      eslint: 8.57.0
-      eslint-config-prettier: 8.9.0(eslint@8.57.0)
-      eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0)
+      '@babel/eslint-parser': 7.23.3(@babel/core@7.24.4)(eslint@8.46.0)
+      eslint: 8.46.0
+      eslint-config-prettier: 8.9.0(eslint@8.46.0)
+      eslint-plugin-eslint-comments: 3.2.0(eslint@8.46.0)
       eslint-plugin-html: 7.1.0
-      eslint-plugin-import: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)
-      eslint-plugin-jsonc: 2.10.0(eslint@8.57.0)
-      eslint-plugin-markdown: 3.0.1(eslint@8.57.0)
-      eslint-plugin-n: 15.7.0(eslint@8.57.0)
-      eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@8.57.0)(prettier@3.2.5)
-      eslint-plugin-promise: 5.2.0(eslint@8.57.0)
+      eslint-plugin-import: 2.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)
+      eslint-plugin-jsonc: 2.10.0(eslint@8.46.0)
+      eslint-plugin-markdown: 3.0.1(eslint@8.46.0)
+      eslint-plugin-n: 15.7.0(eslint@8.46.0)
+      eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@8.46.0)(prettier@3.2.5)
+      eslint-plugin-promise: 5.2.0(eslint@8.46.0)
       eslint-plugin-tsdoc: 0.2.17
-      eslint-plugin-unicorn: 45.0.2(eslint@8.57.0)
-      eslint-plugin-yml: 1.10.0(eslint@8.57.0)
+      eslint-plugin-unicorn: 45.0.2(eslint@8.46.0)
+      eslint-plugin-yml: 1.10.0(eslint@8.46.0)
       jsonc-eslint-parser: 2.3.0
       prettier: 3.2.5
-      vue-eslint-parser: 9.3.2(eslint@8.57.0)
+      vue-eslint-parser: 9.3.2(eslint@8.46.0)
       yaml-eslint-parser: 1.2.2
     transitivePeerDependencies:
       - '@babel/core'
@@ -2146,7 +2285,7 @@ packages:
       - supports-color
     dev: true
 
-  /@eslint-sets/eslint-config-basic@5.12.0(@babel/core@7.23.2)(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5):
+  /@eslint-sets/eslint-config-basic@5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5):
     resolution: {integrity: sha512-AgECfmJsiVOWKmvgjv780VuuoT9SE6PRgxGTtytHSfE9b9MAJjHxToVTKtD4UEKvocEGbg2EcwqGbff8cxDWKw==}
     peerDependencies:
       eslint: '>=7.4.0'
@@ -2156,24 +2295,24 @@ packages:
       typescript:
         optional: true
     dependencies:
-      '@babel/eslint-parser': 7.23.10(@babel/core@7.23.2)(eslint@8.57.0)
-      eslint: 8.57.0
-      eslint-config-prettier: 9.1.0(eslint@8.57.0)
-      eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0)
+      '@babel/eslint-parser': 7.23.10(@babel/core@7.24.4)(eslint@8.46.0)
+      eslint: 8.46.0
+      eslint-config-prettier: 9.1.0(eslint@8.46.0)
+      eslint-plugin-eslint-comments: 3.2.0(eslint@8.46.0)
       eslint-plugin-html: 7.1.0
-      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)
-      eslint-plugin-jsonc: 2.13.0(eslint@8.57.0)
-      eslint-plugin-markdown: 3.0.1(eslint@8.57.0)
-      eslint-plugin-n: 16.6.2(eslint@8.57.0)
-      eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5)
-      eslint-plugin-promise: 6.1.1(eslint@8.57.0)
+      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint@8.46.0)
+      eslint-plugin-jsonc: 2.13.0(eslint@8.46.0)
+      eslint-plugin-markdown: 3.0.1(eslint@8.46.0)
+      eslint-plugin-n: 16.6.2(eslint@8.46.0)
+      eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.46.0)(prettier@3.2.5)
+      eslint-plugin-promise: 6.1.1(eslint@8.46.0)
       eslint-plugin-tsdoc: 0.2.17
-      eslint-plugin-unicorn: 40.1.0(eslint@8.57.0)
-      eslint-plugin-yml: 1.12.2(eslint@8.57.0)
+      eslint-plugin-unicorn: 40.1.0(eslint@8.46.0)
+      eslint-plugin-yml: 1.12.2(eslint@8.46.0)
       jsonc-eslint-parser: 2.4.0
       prettier: 3.2.5
       typescript: 5.4.5
-      vue-eslint-parser: 9.4.2(eslint@8.57.0)
+      vue-eslint-parser: 9.4.2(eslint@8.46.0)
       yaml-eslint-parser: 1.2.2
     transitivePeerDependencies:
       - '@babel/core'
@@ -2184,19 +2323,19 @@ packages:
       - supports-color
     dev: true
 
-  /@eslint-sets/eslint-config-ts@3.3.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5):
+  /@eslint-sets/eslint-config-ts@3.3.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5):
     resolution: {integrity: sha512-4Vj3KxYx16hmW6AyEv1mil0gVN8H3rdJt8TRWufbAj0ZN+EjwOPf3TqE7ASCYto/NpA8xWQY3NGm/og9Or/dDQ==}
     peerDependencies:
       eslint: '>=8.0.0'
       prettier: '>=2.0.0'
       typescript: '>=4.0.0'
     dependencies:
-      '@eslint-sets/eslint-config-basic': 3.3.0(@babel/core@7.23.2)(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(prettier@3.2.5)
-      '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@5.4.5)
-      '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5)
-      eslint: 8.57.0
-      eslint-config-prettier: 8.9.0(eslint@8.57.0)
-      eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@8.57.0)(prettier@3.2.5)
+      '@eslint-sets/eslint-config-basic': 3.3.0(@babel/core@7.24.4)(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)(prettier@3.2.5)
+      '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@5.4.5)
+      eslint: 8.46.0
+      eslint-config-prettier: 8.9.0(eslint@8.46.0)
+      eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@8.46.0)(prettier@3.2.5)
       eslint-plugin-tsdoc: 0.2.17
       prettier: 3.2.5
       typescript: 5.4.5
@@ -2207,7 +2346,7 @@ packages:
       - supports-color
     dev: true
 
-  /@eslint-sets/eslint-config-ts@5.12.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5):
+  /@eslint-sets/eslint-config-ts@5.12.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5):
     resolution: {integrity: sha512-7vOzV6qYv0SbA9W17m9lkG/Zv+qVeCcAbWEY1d9hUbBHx9Ip48kNMNVDrnh97zUORXGcmjxsZ81W2lC36Ox2pw==}
     peerDependencies:
       eslint: '>=7.4.0'
@@ -2217,12 +2356,12 @@ packages:
       typescript:
         optional: true
     dependencies:
-      '@eslint-sets/eslint-config-basic': 5.12.0(@babel/core@7.23.2)(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5)
-      '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.5)
-      '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5)
-      eslint: 8.57.0
-      eslint-config-prettier: 9.1.0(eslint@8.57.0)
-      eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5)
+      '@eslint-sets/eslint-config-basic': 5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5)
+      '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.46.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 6.21.0(eslint@8.46.0)(typescript@5.4.5)
+      eslint: 8.46.0
+      eslint-config-prettier: 9.1.0(eslint@8.46.0)
+      eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.46.0)(prettier@3.2.5)
       eslint-plugin-tsdoc: 0.2.17
       prettier: 3.2.5
       typescript: 5.4.5
@@ -2234,26 +2373,26 @@ packages:
       - supports-color
     dev: true
 
-  /@eslint-sets/eslint-config-vue3-ts@3.3.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5):
+  /@eslint-sets/eslint-config-vue3-ts@3.3.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5):
     resolution: {integrity: sha512-KX3VFuS5U4FYKfZ6PABQjl54BMpNapNjYYe103Nm2Zy8y9zphDCBAARbhU97XNSvzkurve7HhJcsi9gXrWlGFA==}
     peerDependencies:
       eslint: '>=8.0.0'
       prettier: '>=2.0.0'
       typescript: '>=4.0.0'
     dependencies:
-      '@eslint-sets/eslint-config-ts': 3.3.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5)
-      '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@5.4.5)
-      '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5)
-      eslint: 8.57.0
-      eslint-config-prettier: 8.9.0(eslint@8.57.0)
-      eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@8.57.0)(prettier@3.2.5)
+      '@eslint-sets/eslint-config-ts': 3.3.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5)
+      '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@5.4.5)
+      eslint: 8.46.0
+      eslint-config-prettier: 8.9.0(eslint@8.46.0)
+      eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.9.0)(eslint@8.46.0)(prettier@3.2.5)
       eslint-plugin-tsdoc: 0.2.17
       eslint-plugin-vitest-globals: 1.4.0
-      eslint-plugin-vue: 9.16.1(eslint@8.57.0)
-      eslint-plugin-vue-scoped-css: 2.5.0(eslint@8.57.0)(vue-eslint-parser@9.3.1)
+      eslint-plugin-vue: 9.16.1(eslint@8.46.0)
+      eslint-plugin-vue-scoped-css: 2.5.0(eslint@8.46.0)(vue-eslint-parser@9.3.1)
       prettier: 3.2.5
       typescript: 5.4.5
-      vue-eslint-parser: 9.3.1(eslint@8.57.0)
+      vue-eslint-parser: 9.3.1(eslint@8.46.0)
     transitivePeerDependencies:
       - '@babel/core'
       - eslint-import-resolver-typescript
@@ -2261,7 +2400,7 @@ packages:
       - supports-color
     dev: true
 
-  /@eslint-sets/eslint-config-vue3@5.12.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5):
+  /@eslint-sets/eslint-config-vue3@5.12.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5):
     resolution: {integrity: sha512-gQBmQicZihPcxncIdkKagQGZ2dH+97ioAlUpsaczEdgY9pLrLOU5oGTetjbaxAp6zGS2sXm1n0i2BnwRIlt4Bg==}
     peerDependencies:
       eslint: '>=7.4.0'
@@ -2271,22 +2410,22 @@ packages:
       typescript:
         optional: true
     dependencies:
-      '@eslint-sets/eslint-config-basic': 5.12.0(@babel/core@7.23.2)(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5)
-      '@eslint-sets/eslint-config-ts': 5.12.0(@babel/core@7.23.2)(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5)
-      '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.5)
-      '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5)
-      eslint: 8.57.0
-      eslint-config-prettier: 9.1.0(eslint@8.57.0)
-      eslint-plugin-jsdoc: 48.0.6(eslint@8.57.0)
-      eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5)
+      '@eslint-sets/eslint-config-basic': 5.12.0(@babel/core@7.24.4)(@typescript-eslint/parser@6.21.0)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5)
+      '@eslint-sets/eslint-config-ts': 5.12.0(@babel/core@7.24.4)(eslint@8.46.0)(prettier@3.2.5)(typescript@5.4.5)
+      '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.46.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 6.21.0(eslint@8.46.0)(typescript@5.4.5)
+      eslint: 8.46.0
+      eslint-config-prettier: 9.1.0(eslint@8.46.0)
+      eslint-plugin-jsdoc: 48.0.6(eslint@8.46.0)
+      eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.46.0)(prettier@3.2.5)
       eslint-plugin-tsdoc: 0.2.17
       eslint-plugin-vitest-globals: 1.4.0
-      eslint-plugin-vue: 9.21.1(eslint@8.57.0)
-      eslint-plugin-vue-scoped-css: 2.7.2(eslint@8.57.0)(vue-eslint-parser@9.4.2)
+      eslint-plugin-vue: 9.21.1(eslint@8.46.0)
+      eslint-plugin-vue-scoped-css: 2.7.2(eslint@8.46.0)(vue-eslint-parser@9.4.2)
       local-pkg: 0.5.0
       prettier: 3.2.5
       typescript: 5.4.5
-      vue-eslint-parser: 9.4.2(eslint@8.57.0)
+      vue-eslint-parser: 9.4.2(eslint@8.46.0)
     transitivePeerDependencies:
       - '@babel/core'
       - '@types/eslint'
@@ -2312,23 +2451,6 @@ packages:
       - supports-color
     dev: true
 
-  /@eslint/eslintrc@2.1.4:
-    resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-    dependencies:
-      ajv: 6.12.6
-      debug: 4.3.4(supports-color@8.1.1)
-      espree: 9.6.1
-      globals: 13.24.0
-      ignore: 5.2.4
-      import-fresh: 3.3.0
-      js-yaml: 4.1.0
-      minimatch: 3.1.2
-      strip-json-comments: 3.1.1
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
   /@eslint/eslintrc@3.0.2:
     resolution: {integrity: sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -2351,11 +2473,6 @@ packages:
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     dev: true
 
-  /@eslint/js@8.57.0:
-    resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-    dev: true
-
   /@eslint/js@9.0.0:
     resolution: {integrity: sha512-RThY/MnKrhubF6+s1JflwUjPEsnCEmYCWwqa/aRISKWNXGZ9epUwft4bUMM35SdKF9xvBrLydAM1RDHd1Z//ZQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -2372,17 +2489,6 @@ packages:
       - supports-color
     dev: true
 
-  /@humanwhocodes/config-array@0.11.14:
-    resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
-    engines: {node: '>=10.10.0'}
-    dependencies:
-      '@humanwhocodes/object-schema': 2.0.3
-      debug: 4.3.4(supports-color@8.1.1)
-      minimatch: 3.1.2
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
   /@humanwhocodes/config-array@0.12.3:
     resolution: {integrity: sha512-jsNnTBlMWuTpDkeE3on7+dWJi0D6fdDfeANj/w7MpS8ztROCoLvIO2nG0CcFj+E4k8j4QrSTh4Oryi3i2G669g==}
     engines: {node: '>=10.10.0'}
@@ -2585,7 +2691,7 @@ packages:
     cpu: [wasm32]
     requiresBuild: true
     dependencies:
-      '@emnapi/runtime': 1.1.0
+      '@emnapi/runtime': 1.1.1
     dev: false
     optional: true
 
@@ -2861,14 +2967,33 @@ packages:
       '@jridgewell/sourcemap-codec': 1.4.15
       '@jridgewell/trace-mapping': 0.3.20
 
+  /@jridgewell/gen-mapping@0.3.5:
+    resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
+    engines: {node: '>=6.0.0'}
+    dependencies:
+      '@jridgewell/set-array': 1.2.1
+      '@jridgewell/sourcemap-codec': 1.4.15
+      '@jridgewell/trace-mapping': 0.3.25
+    dev: true
+
   /@jridgewell/resolve-uri@3.1.1:
     resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==}
     engines: {node: '>=6.0.0'}
 
+  /@jridgewell/resolve-uri@3.1.2:
+    resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+    engines: {node: '>=6.0.0'}
+    dev: true
+
   /@jridgewell/set-array@1.1.2:
     resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
     engines: {node: '>=6.0.0'}
 
+  /@jridgewell/set-array@1.2.1:
+    resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
+    engines: {node: '>=6.0.0'}
+    dev: true
+
   /@jridgewell/source-map@0.3.5:
     resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==}
     dependencies:
@@ -2890,6 +3015,13 @@ packages:
       '@jridgewell/resolve-uri': 3.1.1
       '@jridgewell/sourcemap-codec': 1.4.15
 
+  /@jridgewell/trace-mapping@0.3.25:
+    resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
+    dependencies:
+      '@jridgewell/resolve-uri': 3.1.2
+      '@jridgewell/sourcemap-codec': 1.4.15
+    dev: true
+
   /@jridgewell/trace-mapping@0.3.9:
     resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
     dependencies:
@@ -3294,11 +3426,6 @@ packages:
       sshpk: 1.17.0
     dev: false
 
-  /@phc/format@1.0.0:
-    resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
-    engines: {node: '>=10'}
-    dev: false
-
   /@phosphor-icons/web@2.1.1:
     resolution: {integrity: sha512-QjrfbItu5Rb2i37GzsKxmrRHfZPTVk3oXSPBnQ2+oACDbQRWGAeB0AsvZw263n1nFouQuff+khOCtRbrc6+k+A==}
     dev: true
@@ -3346,7 +3473,7 @@ packages:
       - encoding
     dev: false
 
-  /@rollup/plugin-alias@5.1.0(rollup@4.14.1):
+  /@rollup/plugin-alias@5.1.0(rollup@4.14.2):
     resolution: {integrity: sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ==}
     engines: {node: '>=14.0.0'}
     peerDependencies:
@@ -3355,11 +3482,11 @@ packages:
       rollup:
         optional: true
     dependencies:
-      rollup: 4.14.1
+      rollup: 4.14.2
       slash: 4.0.0
     dev: true
 
-  /@rollup/plugin-json@6.1.0(rollup@4.14.1):
+  /@rollup/plugin-json@6.1.0(rollup@4.14.2):
     resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==}
     engines: {node: '>=14.0.0'}
     peerDependencies:
@@ -3368,8 +3495,8 @@ packages:
       rollup:
         optional: true
     dependencies:
-      '@rollup/pluginutils': 5.1.0(rollup@4.14.1)
-      rollup: 4.14.1
+      '@rollup/pluginutils': 5.1.0(rollup@4.14.2)
+      rollup: 4.14.2
     dev: true
 
   /@rollup/pluginutils@4.2.1:
@@ -3380,7 +3507,7 @@ packages:
       picomatch: 2.3.1
     dev: true
 
-  /@rollup/pluginutils@5.1.0(rollup@4.14.1):
+  /@rollup/pluginutils@5.1.0(rollup@4.14.2):
     resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==}
     engines: {node: '>=14.0.0'}
     peerDependencies:
@@ -3392,123 +3519,123 @@ packages:
       '@types/estree': 1.0.3
       estree-walker: 2.0.2
       picomatch: 2.3.1
-      rollup: 4.14.1
+      rollup: 4.14.2
     dev: true
 
-  /@rollup/rollup-android-arm-eabi@4.14.1:
-    resolution: {integrity: sha512-fH8/o8nSUek8ceQnT7K4EQbSiV7jgkHq81m9lWZFIXjJ7lJzpWXbQFpT/Zh6OZYnpFykvzC3fbEvEAFZu03dPA==}
+  /@rollup/rollup-android-arm-eabi@4.14.2:
+    resolution: {integrity: sha512-ahxSgCkAEk+P/AVO0vYr7DxOD3CwAQrT0Go9BJyGQ9Ef0QxVOfjDZMiF4Y2s3mLyPrjonchIMH/tbWHucJMykQ==}
     cpu: [arm]
     os: [android]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-android-arm64@4.14.1:
-    resolution: {integrity: sha512-Y/9OHLjzkunF+KGEoJr3heiD5X9OLa8sbT1lm0NYeKyaM3oMhhQFvPB0bNZYJwlq93j8Z6wSxh9+cyKQaxS7PQ==}
+  /@rollup/rollup-android-arm64@4.14.2:
+    resolution: {integrity: sha512-lAarIdxZWbFSHFSDao9+I/F5jDaKyCqAPMq5HqnfpBw8dKDiCaaqM0lq5h1pQTLeIqueeay4PieGR5jGZMWprw==}
     cpu: [arm64]
     os: [android]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-darwin-arm64@4.14.1:
-    resolution: {integrity: sha512-+kecg3FY84WadgcuSVm6llrABOdQAEbNdnpi5X3UwWiFVhZIZvKgGrF7kmLguvxHNQy+UuRV66cLVl3S+Rkt+Q==}
+  /@rollup/rollup-darwin-arm64@4.14.2:
+    resolution: {integrity: sha512-SWsr8zEUk82KSqquIMgZEg2GE5mCSfr9sE/thDROkX6pb3QQWPp8Vw8zOq2GyxZ2t0XoSIUlvHDkrf5Gmf7x3Q==}
     cpu: [arm64]
     os: [darwin]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-darwin-x64@4.14.1:
-    resolution: {integrity: sha512-2pYRzEjVqq2TB/UNv47BV/8vQiXkFGVmPFwJb+1E0IFFZbIX8/jo1olxqqMbo6xCXf8kabANhp5bzCij2tFLUA==}
+  /@rollup/rollup-darwin-x64@4.14.2:
+    resolution: {integrity: sha512-o/HAIrQq0jIxJAhgtIvV5FWviYK4WB0WwV91SLUnsliw1lSAoLsmgEEgRWzDguAFeUEUUoIWXiJrPqU7vGiVkA==}
     cpu: [x64]
     os: [darwin]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-linux-arm-gnueabihf@4.14.1:
-    resolution: {integrity: sha512-mS6wQ6Do6/wmrF9aTFVpIJ3/IDXhg1EZcQFYHZLHqw6AzMBjTHWnCG35HxSqUNphh0EHqSM6wRTT8HsL1C0x5g==}
+  /@rollup/rollup-linux-arm-gnueabihf@4.14.2:
+    resolution: {integrity: sha512-nwlJ65UY9eGq91cBi6VyDfArUJSKOYt5dJQBq8xyLhvS23qO+4Nr/RreibFHjP6t+5ap2ohZrUJcHv5zk5ju/g==}
     cpu: [arm]
     os: [linux]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-linux-arm64-gnu@4.14.1:
-    resolution: {integrity: sha512-p9rGKYkHdFMzhckOTFubfxgyIO1vw//7IIjBBRVzyZebWlzRLeNhqxuSaZ7kCEKVkm/kuC9fVRW9HkC/zNRG2w==}
+  /@rollup/rollup-linux-arm64-gnu@4.14.2:
+    resolution: {integrity: sha512-Pg5TxxO2IVlMj79+c/9G0LREC9SY3HM+pfAwX7zj5/cAuwrbfj2Wv9JbMHIdPCfQpYsI4g9mE+2Bw/3aeSs2rQ==}
     cpu: [arm64]
     os: [linux]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-linux-arm64-musl@4.14.1:
-    resolution: {integrity: sha512-nDY6Yz5xS/Y4M2i9JLQd3Rofh5OR8Bn8qe3Mv/qCVpHFlwtZSBYSPaU4mrGazWkXrdQ98GB//H0BirGR/SKFSw==}
+  /@rollup/rollup-linux-arm64-musl@4.14.2:
+    resolution: {integrity: sha512-cAOTjGNm84gc6tS02D1EXtG7tDRsVSDTBVXOLbj31DkwfZwgTPYZ6aafSU7rD/4R2a34JOwlF9fQayuTSkoclA==}
     cpu: [arm64]
     os: [linux]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-linux-powerpc64le-gnu@4.14.1:
-    resolution: {integrity: sha512-im7HE4VBL+aDswvcmfx88Mp1soqL9OBsdDBU8NqDEYtkri0qV0THhQsvZtZeNNlLeCUQ16PZyv7cqutjDF35qw==}
-    cpu: [ppc64le]
+  /@rollup/rollup-linux-powerpc64le-gnu@4.14.2:
+    resolution: {integrity: sha512-4RyT6v1kXb7C0fn6zV33rvaX05P0zHoNzaXI/5oFHklfKm602j+N4mn2YvoezQViRLPnxP8M1NaY4s/5kXO5cw==}
+    cpu: [ppc64]
     os: [linux]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-linux-riscv64-gnu@4.14.1:
-    resolution: {integrity: sha512-RWdiHuAxWmzPJgaHJdpvUUlDz8sdQz4P2uv367T2JocdDa98iRw2UjIJ4QxSyt077mXZT2X6pKfT2iYtVEvOFw==}
+  /@rollup/rollup-linux-riscv64-gnu@4.14.2:
+    resolution: {integrity: sha512-KNUH6jC/vRGAKSorySTyc/yRYlCwN/5pnMjXylfBniwtJx5O7X17KG/0efj8XM3TZU7raYRXJFFReOzNmL1n1w==}
     cpu: [riscv64]
     os: [linux]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-linux-s390x-gnu@4.14.1:
-    resolution: {integrity: sha512-VMgaGQ5zRX6ZqV/fas65/sUGc9cPmsntq2FiGmayW9KMNfWVG/j0BAqImvU4KTeOOgYSf1F+k6at1UfNONuNjA==}
+  /@rollup/rollup-linux-s390x-gnu@4.14.2:
+    resolution: {integrity: sha512-xPV4y73IBEXToNPa3h5lbgXOi/v0NcvKxU0xejiFw6DtIYQqOTMhZ2DN18/HrrP0PmiL3rGtRG9gz1QE8vFKXQ==}
     cpu: [s390x]
     os: [linux]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-linux-x64-gnu@4.14.1:
-    resolution: {integrity: sha512-9Q7DGjZN+hTdJomaQ3Iub4m6VPu1r94bmK2z3UeWP3dGUecRC54tmVu9vKHTm1bOt3ASoYtEz6JSRLFzrysKlA==}
+  /@rollup/rollup-linux-x64-gnu@4.14.2:
+    resolution: {integrity: sha512-QBhtr07iFGmF9egrPOWyO5wciwgtzKkYPNLVCFZTmr4TWmY0oY2Dm/bmhHjKRwZoGiaKdNcKhFtUMBKvlchH+Q==}
     cpu: [x64]
     os: [linux]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-linux-x64-musl@4.14.1:
-    resolution: {integrity: sha512-JNEG/Ti55413SsreTguSx0LOVKX902OfXIKVg+TCXO6Gjans/k9O6ww9q3oLGjNDaTLxM+IHFMeXy/0RXL5R/g==}
+  /@rollup/rollup-linux-x64-musl@4.14.2:
+    resolution: {integrity: sha512-8zfsQRQGH23O6qazZSFY5jP5gt4cFvRuKTpuBsC1ZnSWxV8ZKQpPqOZIUtdfMOugCcBvFGRa1pDC/tkf19EgBw==}
     cpu: [x64]
     os: [linux]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-win32-arm64-msvc@4.14.1:
-    resolution: {integrity: sha512-ryS22I9y0mumlLNwDFYZRDFLwWh3aKaC72CWjFcFvxK0U6v/mOkM5Up1bTbCRAhv3kEIwW2ajROegCIQViUCeA==}
+  /@rollup/rollup-win32-arm64-msvc@4.14.2:
+    resolution: {integrity: sha512-H4s8UjgkPnlChl6JF5empNvFHp77Jx+Wfy2EtmYPe9G22XV+PMuCinZVHurNe8ggtwoaohxARJZbaH/3xjB/FA==}
     cpu: [arm64]
     os: [win32]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-win32-ia32-msvc@4.14.1:
-    resolution: {integrity: sha512-TdloItiGk+T0mTxKx7Hp279xy30LspMso+GzQvV2maYePMAWdmrzqSNZhUpPj3CGw12aGj57I026PgLCTu8CGg==}
+  /@rollup/rollup-win32-ia32-msvc@4.14.2:
+    resolution: {integrity: sha512-djqpAjm/i8erWYF0K6UY4kRO3X5+T4TypIqw60Q8MTqSBaQNpNXDhxdjpZ3ikgb+wn99svA7jxcXpiyg9MUsdw==}
     cpu: [ia32]
     os: [win32]
     requiresBuild: true
     dev: true
     optional: true
 
-  /@rollup/rollup-win32-x64-msvc@4.14.1:
-    resolution: {integrity: sha512-wQGI+LY/Py20zdUPq+XCem7JcPOyzIJBm3dli+56DJsQOHbnXZFEwgmnC6el1TPAfC8lBT3m+z69RmLykNUbew==}
+  /@rollup/rollup-win32-x64-msvc@4.14.2:
+    resolution: {integrity: sha512-teAqzLT0yTYZa8ZP7zhFKEx4cotS8Tkk5XiqNMJhD4CpaWB1BHARE4Qy+RzwnXvSAYv+Q3jAqCVBS+PS+Yee8Q==}
     cpu: [x64]
     os: [win32]
     requiresBuild: true
@@ -3750,10 +3877,6 @@ packages:
   /@twemoji/parser@15.0.0:
     resolution: {integrity: sha512-lh9515BNsvKSNvyUqbj5yFu83iIDQ77SwVcsN/SnEGawczhsKU6qWuogewN1GweTi5Imo5ToQ9s+nNTf97IXvg==}
 
-  /@twemoji/parser@15.1.1:
-    resolution: {integrity: sha512-CChRzIu6ngkCJOmURBlYEdX5DZSu+bBTtqR60XjBkFrmvplKW7OQsea+i8XwF4bLVlUXBO7ZmHhRPDzfQyLwwg==}
-    dev: false
-
   /@types/accepts@1.3.5:
     resolution: {integrity: sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==}
     dependencies:
@@ -3831,10 +3954,6 @@ packages:
       '@babel/types': 7.23.0
     dev: true
 
-  /@types/bcryptjs@2.4.6:
-    resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==}
-    dev: true
-
   /@types/body-parser@1.19.2:
     resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==}
     dependencies:
@@ -4319,8 +4438,8 @@ packages:
     resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==}
     dev: true
 
-  /@types/sortablejs@1.15.4:
-    resolution: {integrity: sha512-7oL7CcPSfoyoNx3Ba1+79ykJzpEKVhHUyfAiN5eT/FoeDXOR3eBDLXf9ndDNuxaExmjpI+zVi2dMMuaoXUOzNA==}
+  /@types/sortablejs@1.15.8:
+    resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==}
     dev: true
 
   /@types/stack-utils@2.0.2:
@@ -4453,7 +4572,7 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@5.4.5):
+  /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0)(typescript@5.4.5):
     resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
@@ -4465,12 +4584,12 @@ packages:
         optional: true
     dependencies:
       '@eslint-community/regexpp': 4.9.1
-      '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@5.4.5)
       '@typescript-eslint/scope-manager': 5.62.0
-      '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.0)(typescript@5.4.5)
-      '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/type-utils': 5.62.0(eslint@8.46.0)(typescript@5.4.5)
+      '@typescript-eslint/utils': 5.62.0(eslint@8.46.0)(typescript@5.4.5)
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      eslint: 8.46.0
       graphemer: 1.4.0
       ignore: 5.2.4
       natural-compare-lite: 1.4.0
@@ -4481,7 +4600,7 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.5):
+  /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.46.0)(typescript@5.4.5):
     resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==}
     engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
@@ -4493,13 +4612,13 @@ packages:
         optional: true
     dependencies:
       '@eslint-community/regexpp': 4.9.1
-      '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 6.21.0(eslint@8.46.0)(typescript@5.4.5)
       '@typescript-eslint/scope-manager': 6.21.0
-      '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.4.5)
-      '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/type-utils': 6.21.0(eslint@8.46.0)(typescript@5.4.5)
+      '@typescript-eslint/utils': 6.21.0(eslint@8.46.0)(typescript@5.4.5)
       '@typescript-eslint/visitor-keys': 6.21.0
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      eslint: 8.46.0
       graphemer: 1.4.0
       ignore: 5.2.4
       natural-compare: 1.4.0
@@ -4530,7 +4649,7 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.4.5):
+  /@typescript-eslint/parser@5.62.0(eslint@8.46.0)(typescript@5.4.5):
     resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
@@ -4544,13 +4663,13 @@ packages:
       '@typescript-eslint/types': 5.62.0
       '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5)
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      eslint: 8.46.0
       typescript: 5.4.5
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5):
+  /@typescript-eslint/parser@6.21.0(eslint@8.46.0)(typescript@5.4.5):
     resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==}
     engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
@@ -4565,7 +4684,7 @@ packages:
       '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5)
       '@typescript-eslint/visitor-keys': 6.21.0
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      eslint: 8.46.0
       typescript: 5.4.5
     transitivePeerDependencies:
       - supports-color
@@ -4607,7 +4726,7 @@ packages:
       - supports-color
     dev: true
 
-  /@typescript-eslint/type-utils@5.62.0(eslint@8.57.0)(typescript@5.4.5):
+  /@typescript-eslint/type-utils@5.62.0(eslint@8.46.0)(typescript@5.4.5):
     resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
@@ -4618,16 +4737,16 @@ packages:
         optional: true
     dependencies:
       '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5)
-      '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/utils': 5.62.0(eslint@8.46.0)(typescript@5.4.5)
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      eslint: 8.46.0
       tsutils: 3.21.0(typescript@5.4.5)
       typescript: 5.4.5
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /@typescript-eslint/type-utils@6.21.0(eslint@8.57.0)(typescript@5.4.5):
+  /@typescript-eslint/type-utils@6.21.0(eslint@8.46.0)(typescript@5.4.5):
     resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==}
     engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
@@ -4638,9 +4757,9 @@ packages:
         optional: true
     dependencies:
       '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5)
-      '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/utils': 6.21.0(eslint@8.46.0)(typescript@5.4.5)
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      eslint: 8.46.0
       ts-api-utils: 1.0.1(typescript@5.4.5)
       typescript: 5.4.5
     transitivePeerDependencies:
@@ -4741,19 +4860,19 @@ packages:
       - typescript
     dev: true
 
-  /@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.4.5):
+  /@typescript-eslint/utils@5.62.0(eslint@8.46.0)(typescript@5.4.5):
     resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
       '@types/json-schema': 7.0.12
       '@types/semver': 7.5.8
       '@typescript-eslint/scope-manager': 5.62.0
       '@typescript-eslint/types': 5.62.0
       '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5)
-      eslint: 8.57.0
+      eslint: 8.46.0
       eslint-scope: 5.1.1
       semver: 7.6.0
     transitivePeerDependencies:
@@ -4761,19 +4880,19 @@ packages:
       - typescript
     dev: true
 
-  /@typescript-eslint/utils@6.21.0(eslint@8.57.0)(typescript@5.4.5):
+  /@typescript-eslint/utils@6.21.0(eslint@8.46.0)(typescript@5.4.5):
     resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==}
     engines: {node: ^16.0.0 || >=18.0.0}
     peerDependencies:
       eslint: ^7.0.0 || ^8.0.0
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
       '@types/json-schema': 7.0.14
       '@types/semver': 7.5.8
       '@typescript-eslint/scope-manager': 6.21.0
       '@typescript-eslint/types': 6.21.0
       '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5)
-      eslint: 8.57.0
+      eslint: 8.46.0
       semver: 7.6.0
     transitivePeerDependencies:
       - supports-color
@@ -4796,10 +4915,6 @@ packages:
       eslint-visitor-keys: 3.4.3
     dev: true
 
-  /@ungap/structured-clone@1.2.0:
-    resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
-    dev: true
-
   /@vercel/nft@0.26.3:
     resolution: {integrity: sha512-h1z/NN9ppS4YOKwSgBoopJlhm7tS2Qb/9Ld1HXjDpvvTE7mY0xVD8nllXs+RihD9uTGJISOIMzp18Eg0EApaMA==}
     engines: {node: '>=16'}
@@ -4829,26 +4944,26 @@ packages:
       vite: ^5.0.0
       vue: ^3.2.25
     dependencies:
-      vite: 5.2.8(@types/node@20.12.7)(sass@1.74.1)
+      vite: 5.2.8(@types/node@20.12.7)(sass@1.75.0)
       vue: 3.4.21(typescript@5.4.5)
     dev: true
 
-  /@volar/language-core@2.2.0-alpha.7:
-    resolution: {integrity: sha512-igpp+nTkyl8faVzRJMpSCeA4XlBJ5UVSyc/WGyksmUmP10YbfufbcQCFlxEXv2uMBV+a3L4JVCj+Vju+08FOSA==}
+  /@volar/language-core@2.2.0-alpha.8:
+    resolution: {integrity: sha512-Ew1Iw7/RIRNuDLn60fWJdOLApAlfTVPxbPiSLzc434PReC9kleYtaa//Wo2WlN1oiRqneW0pWQQV0CwYqaimLQ==}
     dependencies:
-      '@volar/source-map': 2.2.0-alpha.7
+      '@volar/source-map': 2.2.0-alpha.8
     dev: true
 
-  /@volar/source-map@2.2.0-alpha.7:
-    resolution: {integrity: sha512-iIZM2EovdEnr6mMwlsnt4ciix4xz7HSGHyUSviRaY5cii5PMXGHeUU9UDeb+xzLCx8kdk3L5J4z+ts50AhkYcg==}
+  /@volar/source-map@2.2.0-alpha.8:
+    resolution: {integrity: sha512-E1ZVmXFJ5DU4fWDcWHzi8OLqqReqIDwhXvIMhVdk6+VipfMVv4SkryXu7/rs4GA/GsebcRyJdaSkKBB3OAkIcA==}
     dependencies:
       muggle-string: 0.4.1
     dev: true
 
-  /@volar/typescript@2.2.0-alpha.7:
-    resolution: {integrity: sha512-qy04/hx4UbW1BdPlzaxlH60D4plubcyqdbYM6Y5vZiascZxFowtd6vE39Td9FYzDxwcKgzb/Crvf/ABhdHnuBA==}
+  /@volar/typescript@2.2.0-alpha.8:
+    resolution: {integrity: sha512-RLbRDI+17CiayHZs9HhSzlH0FhLl/+XK6o2qoiw2o2GGKcyD1aDoY6AcMd44acYncTOrqoTNoY6LuCiRyiJiGg==}
     dependencies:
-      '@volar/language-core': 2.2.0-alpha.7
+      '@volar/language-core': 2.2.0-alpha.8
       path-browserify: 1.0.1
     dev: true
 
@@ -4898,19 +5013,19 @@ packages:
       '@vue/shared': 3.4.21
     dev: true
 
-  /@vue/language-core@2.0.12(typescript@5.4.5):
-    resolution: {integrity: sha512-aIStDPt69SHOpiIckGTIIjEz/sXc6ZfCMS5uWYL1AcbcRMhzFCLZscGAVte1+ad+RRFepSpKBjGttyPcgKJ7ww==}
+  /@vue/language-core@2.0.13(typescript@5.4.5):
+    resolution: {integrity: sha512-oQgM+BM66SU5GKtUMLQSQN0bxHFkFpLSSAiY87wVziPaiNQZuKVDt/3yA7GB9PiQw0y/bTNL0bOc0jM/siYjKg==}
     peerDependencies:
       typescript: '*'
     peerDependenciesMeta:
       typescript:
         optional: true
     dependencies:
-      '@volar/language-core': 2.2.0-alpha.7
+      '@volar/language-core': 2.2.0-alpha.8
       '@vue/compiler-dom': 3.4.21
       '@vue/shared': 3.4.21
       computeds: 0.0.1
-      minimatch: 9.0.3
+      minimatch: 9.0.4
       path-browserify: 1.0.1
       typescript: 5.4.5
       vue-template-compiler: 2.7.16
@@ -5358,16 +5473,6 @@ packages:
   /arg@4.1.3:
     resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
 
-  /argon2@0.40.1:
-    resolution: {integrity: sha512-DjtHDwd7pm12qeWyfihHoM8Bn5vGcgH6sKwgPqwNYroRmxlrzadHEvMyuvQxN/V8YSyRRKD5x6ito09q1e9OyA==}
-    engines: {node: '>=16.17.0'}
-    requiresBuild: true
-    dependencies:
-      '@phc/format': 1.0.0
-      node-addon-api: 7.1.0
-      node-gyp-build: 4.8.0
-    dev: false
-
   /argparse@1.0.10:
     resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
     dependencies:
@@ -5635,6 +5740,10 @@ packages:
     resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==}
     dev: false
 
+  /async@3.2.5:
+    resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==}
+    dev: false
+
   /asynckit@0.4.0:
     resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
 
@@ -5721,9 +5830,17 @@ packages:
   /available-typed-arrays@1.0.5:
     resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==}
     engines: {node: '>= 0.4'}
+    dev: true
 
-  /aws-sdk@2.1597.0:
-    resolution: {integrity: sha512-YvApP9p5a5TD870mvQRrcUyJz3nKFrtlnDLaA4yrmAaidMDGzdNJ+AZlW0+onRCB4llzKD4Hos56zea0ulR+zQ==}
+  /available-typed-arrays@1.0.7:
+    resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      possible-typed-array-names: 1.0.0
+    dev: false
+
+  /aws-sdk@2.1599.0:
+    resolution: {integrity: sha512-jPb1LAN+s1TLTK+VR3TTJLr//sb3AhhT60Bm9jxB5G/fVeeRczXtBtixNpQ00gksQdkstILYLc9S6MuKMsksxA==}
     engines: {node: '>= 10.0.0'}
     requiresBuild: true
     dependencies:
@@ -5893,10 +6010,6 @@ packages:
       tweetnacl: 0.14.5
     dev: false
 
-  /bcryptjs@2.4.3:
-    resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==}
-    dev: false
-
   /bin-check@4.1.0:
     resolution: {integrity: sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==}
     engines: {node: '>=4'}
@@ -6054,6 +6167,17 @@ packages:
       node-releases: 2.0.13
       update-browserslist-db: 1.0.13(browserslist@4.22.1)
 
+  /browserslist@4.23.0:
+    resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==}
+    engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+    hasBin: true
+    dependencies:
+      caniuse-lite: 1.0.30001609
+      electron-to-chromium: 1.4.736
+      node-releases: 2.0.14
+      update-browserslist-db: 1.0.13(browserslist@4.23.0)
+    dev: true
+
   /bs-logger@0.2.6:
     resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==}
     engines: {node: '>= 6'}
@@ -6107,7 +6231,7 @@ packages:
     resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==}
     dependencies:
       base64-js: 1.5.1
-      ieee754: 1.2.1
+      ieee754: 1.1.13
       isarray: 1.0.0
     dev: false
 
@@ -6292,6 +6416,10 @@ packages:
   /caniuse-lite@1.0.30001551:
     resolution: {integrity: sha512-vtBAez47BoGMMzlbYhfXrMV1kvRF2WP/lqiMuDu1Sb4EE4LKEgjopFDSRtZfdVnslNRpOqV/woE+Xgrwj6VQlg==}
 
+  /caniuse-lite@1.0.30001609:
+    resolution: {integrity: sha512-JFPQs34lHKx1B5t1EpQpWH4c+29zIyn/haGsbpfq3suuV9v56enjFt23zqijxGTMwy1p/4H2tjnQMY+p1WoAyA==}
+    dev: true
+
   /canonicalize@1.0.8:
     resolution: {integrity: sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A==}
     dev: false
@@ -6483,6 +6611,21 @@ packages:
       fsevents: 2.3.3
     dev: true
 
+  /chokidar@3.6.0:
+    resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
+    engines: {node: '>= 8.10.0'}
+    dependencies:
+      anymatch: 3.1.3
+      braces: 3.0.2
+      glob-parent: 5.1.2
+      is-binary-path: 2.1.0
+      is-glob: 4.0.3
+      normalize-path: 3.0.0
+      readdirp: 3.6.0
+    optionalDependencies:
+      fsevents: 2.3.3
+    dev: true
+
   /chownr@2.0.0:
     resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
     engines: {node: '>=10'}
@@ -6655,7 +6798,7 @@ packages:
     resolution: {integrity: sha512-sX/LQ7LqUhgyaxzbe7IqwPeTr2yfpfUIQ/dgpKo6ZI4y4lpQA0YxAomWIY+7I7rHWcG02PG+OuPREzMW/5tszQ==}
     dependencies:
       inflation: 2.0.0
-      qs: 6.12.0
+      qs: 6.12.1
       raw-body: 2.5.2
       type-is: 1.6.18
     dev: false
@@ -6664,7 +6807,7 @@ packages:
     resolution: {integrity: sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==}
     dependencies:
       inflation: 2.0.0
-      qs: 6.12.0
+      qs: 6.12.1
       raw-body: 2.5.2
       type-is: 1.6.18
     dev: false
@@ -6877,7 +7020,7 @@ packages:
     resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
     dev: true
 
-  /consolidate@0.16.0(ejs@3.1.9)(pug@3.0.2):
+  /consolidate@0.16.0(ejs@3.1.10)(pug@3.0.2):
     resolution: {integrity: sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ==}
     engines: {node: '>= 0.10.0'}
     deprecated: Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog
@@ -7044,7 +7187,7 @@ packages:
         optional: true
     dependencies:
       bluebird: 3.7.2
-      ejs: 3.1.9
+      ejs: 3.1.10
       pug: 3.0.2
     dev: false
 
@@ -7109,8 +7252,8 @@ packages:
     resolution: {integrity: sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==}
     dev: false
 
-  /core-js@3.33.0:
-    resolution: {integrity: sha512-HoZr92+ZjFEKar5HS6MC776gYslNOKHt75mEBKWKnPeFDpZ6nH5OeF3S6HFT1mUAUZKrzkez05VboaX8myjSuw==}
+  /core-js@3.36.1:
+    resolution: {integrity: sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==}
     requiresBuild: true
     dev: true
 
@@ -7509,8 +7652,8 @@ packages:
     resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
     dev: true
 
-  /deepl-node@1.12.0:
-    resolution: {integrity: sha512-c/8x1R0dXPL7NSDdQ94lYPou/A+I6cbo6b7gFb/28HbjcHnKB4RtWXWLgdv7n51GEXL7OE2eoRZQcAu4ZI+vGg==}
+  /deepl-node@1.13.0:
+    resolution: {integrity: sha512-pm8Al5B+/fRHiIKoreoSmv2RlXidF18+CznhtLILiYcj3EbxZpIhxWO8cgXCCsCTrUDMAbScIl8CuH3AqLPpGg==}
     engines: {node: '>=12.0'}
     dependencies:
       '@types/node': 20.12.7
@@ -7795,8 +7938,8 @@ packages:
     resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
     dev: false
 
-  /ejs@3.1.9:
-    resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==}
+  /ejs@3.1.10:
+    resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
     engines: {node: '>=0.10.0'}
     hasBin: true
     dependencies:
@@ -7810,6 +7953,10 @@ packages:
   /electron-to-chromium@1.4.561:
     resolution: {integrity: sha512-eS5t4ulWOBfVHdq9SW2dxEaFarj1lPjvJ8PaYMOjY0DecBaj/t4ARziL2IPpDr4atyWwjLFGQ2vo/VCgQFezVQ==}
 
+  /electron-to-chromium@1.4.736:
+    resolution: {integrity: sha512-Rer6wc3ynLelKNM4lOCg7/zPQj8tPOCB2hzD32PX9wd3hgRRi9MxEbmkFCokzcEhRVMiOVLjnL9ig9cefJ+6+Q==}
+    dev: true
+
   /emittery@0.13.1:
     resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==}
     engines: {node: '>=12'}
@@ -8060,41 +8207,41 @@ packages:
     engines: {node: '>=12'}
     dev: true
 
-  /eslint-compat-utils@0.1.2(eslint@8.57.0):
+  /eslint-compat-utils@0.1.2(eslint@8.46.0):
     resolution: {integrity: sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==}
     engines: {node: '>=12'}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
-      eslint: 8.57.0
+      eslint: 8.46.0
     dev: true
 
-  /eslint-compat-utils@0.4.1(eslint@8.57.0):
+  /eslint-compat-utils@0.4.1(eslint@8.46.0):
     resolution: {integrity: sha512-5N7ZaJG5pZxUeNNJfUchurLVrunD1xJvyg5kYOIVF8kg1f3ajTikmAu/5fZ9w100omNPOoMjngRszh/Q/uFGMg==}
     engines: {node: '>=12'}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
-      eslint: 8.57.0
+      eslint: 8.46.0
       semver: 7.6.0
     dev: true
 
-  /eslint-config-prettier@8.9.0(eslint@8.57.0):
+  /eslint-config-prettier@8.9.0(eslint@8.46.0):
     resolution: {integrity: sha512-+sbni7NfVXnOpnRadUA8S28AUlsZt9GjgFvABIRL9Hkn8KqNzOp+7Lw4QWtrwn20KzU3wqu1QoOj2m+7rKRqkA==}
     hasBin: true
     peerDependencies:
       eslint: '>=7.0.0'
     dependencies:
-      eslint: 8.57.0
+      eslint: 8.46.0
     dev: true
 
-  /eslint-config-prettier@9.1.0(eslint@8.57.0):
+  /eslint-config-prettier@9.1.0(eslint@8.46.0):
     resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==}
     hasBin: true
     peerDependencies:
       eslint: '>=7.0.0'
     dependencies:
-      eslint: 8.57.0
+      eslint: 8.46.0
     dev: true
 
   /eslint-config-standard@16.0.3(eslint-plugin-import@2.28.0)(eslint-plugin-node@11.1.0)(eslint-plugin-promise@6.1.1)(eslint@8.46.0):
@@ -8164,7 +8311,7 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0):
+  /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@8.46.0):
     resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==}
     engines: {node: '>=4'}
     peerDependencies:
@@ -8185,53 +8332,24 @@ packages:
       eslint-import-resolver-webpack:
         optional: true
     dependencies:
-      '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 6.21.0(eslint@8.46.0)(typescript@5.4.5)
       debug: 3.2.7
-      eslint: 8.57.0
+      eslint: 8.46.0
       eslint-import-resolver-node: 0.3.9
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0):
-    resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==}
-    engines: {node: '>=4'}
-    peerDependencies:
-      '@typescript-eslint/parser': '*'
-      eslint: '*'
-      eslint-import-resolver-node: '*'
-      eslint-import-resolver-typescript: '*'
-      eslint-import-resolver-webpack: '*'
-    peerDependenciesMeta:
-      '@typescript-eslint/parser':
-        optional: true
-      eslint:
-        optional: true
-      eslint-import-resolver-node:
-        optional: true
-      eslint-import-resolver-typescript:
-        optional: true
-      eslint-import-resolver-webpack:
-        optional: true
-    dependencies:
-      '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5)
-      debug: 3.2.7
-      eslint: 8.57.0
-      eslint-import-resolver-node: 0.3.9
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
-  /eslint-plugin-es-x@7.5.0(eslint@8.57.0):
+  /eslint-plugin-es-x@7.5.0(eslint@8.46.0):
     resolution: {integrity: sha512-ODswlDSO0HJDzXU0XvgZ3lF3lS3XAZEossh15Q2UHjwrJggWeBoKqqEsLTZLXl+dh5eOAozG0zRcYtuE35oTuQ==}
     engines: {node: ^14.18.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=8'
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
       '@eslint-community/regexpp': 4.9.1
-      eslint: 8.57.0
-      eslint-compat-utils: 0.1.2(eslint@8.57.0)
+      eslint: 8.46.0
+      eslint-compat-utils: 0.1.2(eslint@8.46.0)
     dev: true
 
   /eslint-plugin-es@3.0.1(eslint@8.46.0):
@@ -8245,35 +8363,35 @@ packages:
       regexpp: 3.2.0
     dev: true
 
-  /eslint-plugin-es@4.1.0(eslint@8.57.0):
+  /eslint-plugin-es@4.1.0(eslint@8.46.0):
     resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==}
     engines: {node: '>=8.10.0'}
     peerDependencies:
       eslint: '>=4.19.1'
     dependencies:
-      eslint: 8.57.0
+      eslint: 8.46.0
       eslint-utils: 2.1.0
       regexpp: 3.2.0
     dev: true
 
-  /eslint-plugin-eslint-comments@3.2.0(eslint@8.57.0):
+  /eslint-plugin-eslint-comments@3.2.0(eslint@8.46.0):
     resolution: {integrity: sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==}
     engines: {node: '>=6.5.0'}
     peerDependencies:
       eslint: '>=4.19.1'
     dependencies:
       escape-string-regexp: 1.0.5
-      eslint: 8.57.0
+      eslint: 8.46.0
       ignore: 5.2.4
     dev: true
 
-  /eslint-plugin-file-progress@1.3.0(eslint@8.57.0):
+  /eslint-plugin-file-progress@1.3.0(eslint@8.46.0):
     resolution: {integrity: sha512-LncpnGHU26KPvCrvDC2Sl9PfjdrsG8qltgiK6BR7KybWtfqrdlsu1ax3+hyPMn5OkKBTF3Wki3oqK1MSMeOtQw==}
     peerDependencies:
       eslint: ^7.0.0 || ^8.0.0
     dependencies:
       chalk: 4.1.2
-      eslint: 8.57.0
+      eslint: 8.46.0
       ora: 5.4.1
     dev: true
 
@@ -8319,7 +8437,7 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-plugin-import@2.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0):
+  /eslint-plugin-import@2.29.0(@typescript-eslint/parser@5.62.0)(eslint@8.46.0):
     resolution: {integrity: sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==}
     engines: {node: '>=4'}
     peerDependencies:
@@ -8329,16 +8447,16 @@ packages:
       '@typescript-eslint/parser':
         optional: true
     dependencies:
-      '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 5.62.0(eslint@8.46.0)(typescript@5.4.5)
       array-includes: 3.1.7
       array.prototype.findlastindex: 1.2.3
       array.prototype.flat: 1.3.2
       array.prototype.flatmap: 1.3.2
       debug: 3.2.7
       doctrine: 2.1.0
-      eslint: 8.57.0
+      eslint: 8.46.0
       eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0)
+      eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@8.46.0)
       hasown: 2.0.0
       is-core-module: 2.13.1
       is-glob: 4.0.3
@@ -8354,7 +8472,7 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0)(eslint@8.57.0):
+  /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0)(eslint@8.46.0):
     resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==}
     engines: {node: '>=4'}
     peerDependencies:
@@ -8364,16 +8482,16 @@ packages:
       '@typescript-eslint/parser':
         optional: true
     dependencies:
-      '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 6.21.0(eslint@8.46.0)(typescript@5.4.5)
       array-includes: 3.1.7
       array.prototype.findlastindex: 1.2.3
       array.prototype.flat: 1.3.2
       array.prototype.flatmap: 1.3.2
       debug: 3.2.7
       doctrine: 2.1.0
-      eslint: 8.57.0
+      eslint: 8.46.0
       eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0)
+      eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@8.46.0)
       hasown: 2.0.0
       is-core-module: 2.13.1
       is-glob: 4.0.3
@@ -8389,7 +8507,7 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-plugin-jsdoc@48.0.6(eslint@8.57.0):
+  /eslint-plugin-jsdoc@48.0.6(eslint@8.46.0):
     resolution: {integrity: sha512-LgwXOX6TWxxFYcbdVe+BJ94Kl/pgjSPYHLzqEdAMXTA1BH9WDx7iJ+9/iDajPF64LtzWX8C1mCfpbMZjJGhAOw==}
     engines: {node: '>=18'}
     peerDependencies:
@@ -8400,7 +8518,7 @@ packages:
       comment-parser: 1.4.1
       debug: 4.3.4(supports-color@8.1.1)
       escape-string-regexp: 4.0.0
-      eslint: 8.57.0
+      eslint: 8.46.0
       esquery: 1.5.0
       is-builtin-module: 3.2.1
       semver: 7.6.0
@@ -8409,28 +8527,28 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-plugin-jsonc@2.10.0(eslint@8.57.0):
+  /eslint-plugin-jsonc@2.10.0(eslint@8.46.0):
     resolution: {integrity: sha512-9d//o6Jyh4s1RxC9fNSt1+MMaFN2ruFdXPG9XZcb/mR2KkfjADYiNL/hbU6W0Cyxfg3tS/XSFuhl5LgtMD8hmw==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
-      eslint: 8.57.0
-      eslint-compat-utils: 0.1.2(eslint@8.57.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
+      eslint: 8.46.0
+      eslint-compat-utils: 0.1.2(eslint@8.46.0)
       jsonc-eslint-parser: 2.3.0
       natural-compare: 1.4.0
     dev: true
 
-  /eslint-plugin-jsonc@2.13.0(eslint@8.57.0):
+  /eslint-plugin-jsonc@2.13.0(eslint@8.46.0):
     resolution: {integrity: sha512-2wWdJfpO/UbZzPDABuUVvlUQjfMJa2p2iQfYt/oWxOMpXCcjuiMUSaA02gtY/Dbu82vpaSqc+O7Xq6ECHwtIxA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
-      eslint: 8.57.0
-      eslint-compat-utils: 0.4.1(eslint@8.57.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
+      eslint: 8.46.0
+      eslint-compat-utils: 0.4.1(eslint@8.46.0)
       espree: 9.6.1
       graphemer: 1.4.0
       jsonc-eslint-parser: 2.4.0
@@ -8438,28 +8556,28 @@ packages:
       synckit: 0.6.2
     dev: true
 
-  /eslint-plugin-markdown@3.0.1(eslint@8.57.0):
+  /eslint-plugin-markdown@3.0.1(eslint@8.46.0):
     resolution: {integrity: sha512-8rqoc148DWdGdmYF6WSQFT3uQ6PO7zXYgeBpHAOAakX/zpq+NvFYbDA/H7PYzHajwtmaOzAwfxyl++x0g1/N9A==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
     dependencies:
-      eslint: 8.57.0
+      eslint: 8.46.0
       mdast-util-from-markdown: 0.8.5
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /eslint-plugin-n@15.7.0(eslint@8.57.0):
+  /eslint-plugin-n@15.7.0(eslint@8.46.0):
     resolution: {integrity: sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==}
     engines: {node: '>=12.22.0'}
     peerDependencies:
       eslint: '>=7.0.0'
     dependencies:
       builtins: 5.0.1
-      eslint: 8.57.0
-      eslint-plugin-es: 4.1.0(eslint@8.57.0)
-      eslint-utils: 3.0.0(eslint@8.57.0)
+      eslint: 8.46.0
+      eslint-plugin-es: 4.1.0(eslint@8.46.0)
+      eslint-utils: 3.0.0(eslint@8.46.0)
       ignore: 5.2.4
       is-core-module: 2.13.1
       minimatch: 3.1.2
@@ -8467,16 +8585,16 @@ packages:
       semver: 7.6.0
     dev: true
 
-  /eslint-plugin-n@16.6.2(eslint@8.57.0):
+  /eslint-plugin-n@16.6.2(eslint@8.46.0):
     resolution: {integrity: sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==}
     engines: {node: '>=16.0.0'}
     peerDependencies:
       eslint: '>=7.0.0'
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
       builtins: 5.0.1
-      eslint: 8.57.0
-      eslint-plugin-es-x: 7.5.0(eslint@8.57.0)
+      eslint: 8.46.0
+      eslint-plugin-es-x: 7.5.0(eslint@8.46.0)
       get-tsconfig: 4.7.2
       globals: 13.24.0
       ignore: 5.2.4
@@ -8502,7 +8620,7 @@ packages:
       semver: 6.3.1
     dev: true
 
-  /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.9.0)(eslint@8.57.0)(prettier@3.2.5):
+  /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.9.0)(eslint@8.46.0)(prettier@3.2.5):
     resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==}
     engines: {node: '>=12.0.0'}
     peerDependencies:
@@ -8513,13 +8631,13 @@ packages:
       eslint-config-prettier:
         optional: true
     dependencies:
-      eslint: 8.57.0
-      eslint-config-prettier: 8.9.0(eslint@8.57.0)
+      eslint: 8.46.0
+      eslint-config-prettier: 8.9.0(eslint@8.46.0)
       prettier: 3.2.5
       prettier-linter-helpers: 1.0.0
     dev: true
 
-  /eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5):
+  /eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@8.46.0)(prettier@3.2.5):
     resolution: {integrity: sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==}
     engines: {node: ^14.18.0 || >=16.0.0}
     peerDependencies:
@@ -8533,20 +8651,20 @@ packages:
       eslint-config-prettier:
         optional: true
     dependencies:
-      eslint: 8.57.0
-      eslint-config-prettier: 9.1.0(eslint@8.57.0)
+      eslint: 8.46.0
+      eslint-config-prettier: 9.1.0(eslint@8.46.0)
       prettier: 3.2.5
       prettier-linter-helpers: 1.0.0
       synckit: 0.8.8
     dev: true
 
-  /eslint-plugin-promise@5.2.0(eslint@8.57.0):
+  /eslint-plugin-promise@5.2.0(eslint@8.46.0):
     resolution: {integrity: sha512-SftLb1pUG01QYq2A/hGAWfDRXqYD82zE7j7TopDOyNdU+7SvvoXREls/+PRTY17vUXzXnZA/zfnyKgRH6x4JJw==}
     engines: {node: ^10.12.0 || >=12.0.0}
     peerDependencies:
       eslint: ^7.0.0
     dependencies:
-      eslint: 8.57.0
+      eslint: 8.46.0
     dev: true
 
   /eslint-plugin-promise@6.1.1(eslint@8.46.0):
@@ -8558,15 +8676,6 @@ packages:
       eslint: 8.46.0
     dev: true
 
-  /eslint-plugin-promise@6.1.1(eslint@8.57.0):
-    resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-    peerDependencies:
-      eslint: ^7.0.0 || ^8.0.0
-    dependencies:
-      eslint: 8.57.0
-    dev: true
-
   /eslint-plugin-standard@5.0.0(eslint@8.46.0):
     resolution: {integrity: sha512-eSIXPc9wBM4BrniMzJRBm2uoVuXz2EPa+NXPk2+itrVt+r5SbKFERx/IgrK/HmfjddyKVz2f+j+7gBRvu19xLg==}
     deprecated: 'standard 16.0.0 and eslint-config-standard 16.0.0 no longer require the eslint-plugin-standard package. You can remove it from your dependencies with ''npm rm eslint-plugin-standard''. More info here: https://github.com/standard/standard/issues/1316'
@@ -8583,7 +8692,7 @@ packages:
       '@microsoft/tsdoc-config': 0.16.2
     dev: true
 
-  /eslint-plugin-unicorn@40.1.0(eslint@8.57.0):
+  /eslint-plugin-unicorn@40.1.0(eslint@8.46.0):
     resolution: {integrity: sha512-y5doK2DF9Sr5AqKEHbHxjFllJ167nKDRU01HDcWyv4Tnmaoe9iNxMrBnaybZvWZUaE3OC5Unu0lNIevYamloig==}
     engines: {node: '>=12'}
     peerDependencies:
@@ -8592,8 +8701,8 @@ packages:
       '@babel/helper-validator-identifier': 7.22.20
       ci-info: 3.9.0
       clean-regexp: 1.0.0
-      eslint: 8.57.0
-      eslint-utils: 3.0.0(eslint@8.57.0)
+      eslint: 8.46.0
+      eslint-utils: 3.0.0(eslint@8.46.0)
       esquery: 1.5.0
       indent-string: 4.0.0
       is-builtin-module: 3.2.1
@@ -8606,17 +8715,17 @@ packages:
       strip-indent: 3.0.0
     dev: true
 
-  /eslint-plugin-unicorn@45.0.2(eslint@8.57.0):
+  /eslint-plugin-unicorn@45.0.2(eslint@8.46.0):
     resolution: {integrity: sha512-Y0WUDXRyGDMcKLiwgL3zSMpHrXI00xmdyixEGIg90gHnj0PcHY4moNv3Ppje/kDivdAy5vUeUr7z211ImPv2gw==}
     engines: {node: '>=14.18'}
     peerDependencies:
       eslint: '>=8.28.0'
     dependencies:
       '@babel/helper-validator-identifier': 7.22.5
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
       ci-info: 3.9.0
       clean-regexp: 1.0.0
-      eslint: 8.57.0
+      eslint: 8.46.0
       esquery: 1.5.0
       indent-string: 4.0.0
       is-builtin-module: 3.2.1
@@ -8635,92 +8744,92 @@ packages:
     resolution: {integrity: sha512-WE+YlK9X9s4vf5EaYRU0Scw7WItDZStm+PapFSYlg2ABNtaQ4zIG7wEqpoUB3SlfM+SgkhgmzR0TeJOO5k3/Nw==}
     dev: true
 
-  /eslint-plugin-vue-scoped-css@2.5.0(eslint@8.57.0)(vue-eslint-parser@9.3.1):
+  /eslint-plugin-vue-scoped-css@2.5.0(eslint@8.46.0)(vue-eslint-parser@9.3.1):
     resolution: {integrity: sha512-vR+raYNE1aQ69lS1lZGiKoz8rXFI3MWf2fxrfns/XCQ0XT5sIguhDtQS+9JmUQJClenLDEe2CQx7P+eeSdF4cA==}
     engines: {node: ^12.22 || ^14.17 || >=16}
     peerDependencies:
       eslint: '>=5.0.0'
       vue-eslint-parser: '>=7.1.0'
     dependencies:
-      eslint: 8.57.0
-      eslint-utils: 3.0.0(eslint@8.57.0)
+      eslint: 8.46.0
+      eslint-utils: 3.0.0(eslint@8.46.0)
       lodash: 4.17.21
       postcss: 8.4.31
       postcss-safe-parser: 6.0.0(postcss@8.4.31)
       postcss-scss: 4.0.6(postcss@8.4.31)
       postcss-selector-parser: 6.0.13
       postcss-styl: 0.12.3
-      vue-eslint-parser: 9.3.1(eslint@8.57.0)
+      vue-eslint-parser: 9.3.1(eslint@8.46.0)
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /eslint-plugin-vue-scoped-css@2.7.2(eslint@8.57.0)(vue-eslint-parser@9.4.2):
+  /eslint-plugin-vue-scoped-css@2.7.2(eslint@8.46.0)(vue-eslint-parser@9.4.2):
     resolution: {integrity: sha512-myJ99CJuwmAx5kq1WjgIeaUkxeU6PIEUh7age79Alm30bhN4fVTapOQLSMlvVTgxr36Y3igsZ3BCJM32LbHHig==}
     engines: {node: ^12.22 || ^14.17 || >=16}
     peerDependencies:
       eslint: '>=5.0.0'
       vue-eslint-parser: '>=7.1.0'
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
-      eslint: 8.57.0
-      eslint-compat-utils: 0.4.1(eslint@8.57.0)
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
+      eslint: 8.46.0
+      eslint-compat-utils: 0.4.1(eslint@8.46.0)
       lodash: 4.17.21
       postcss: 8.4.31
       postcss-safe-parser: 6.0.0(postcss@8.4.31)
       postcss-scss: 4.0.6(postcss@8.4.31)
       postcss-selector-parser: 6.0.13
       postcss-styl: 0.12.3
-      vue-eslint-parser: 9.4.2(eslint@8.57.0)
+      vue-eslint-parser: 9.4.2(eslint@8.46.0)
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /eslint-plugin-vue@9.16.1(eslint@8.57.0):
+  /eslint-plugin-vue@9.16.1(eslint@8.46.0):
     resolution: {integrity: sha512-2FtnTqazA6aYONfDuOZTk0QzwhAwi7Z4+uJ7+GHeGxcKapjqWlDsRWDenvyG/utyOfAS5bVRmAG3cEWiYEz2bA==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.2.0 || ^7.0.0 || ^8.0.0
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
-      eslint: 8.57.0
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
+      eslint: 8.46.0
       natural-compare: 1.4.0
       nth-check: 2.1.1
       postcss-selector-parser: 6.0.13
       semver: 7.6.0
-      vue-eslint-parser: 9.3.2(eslint@8.57.0)
+      vue-eslint-parser: 9.3.2(eslint@8.46.0)
       xml-name-validator: 4.0.0
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /eslint-plugin-vue@9.21.1(eslint@8.57.0):
+  /eslint-plugin-vue@9.21.1(eslint@8.46.0):
     resolution: {integrity: sha512-XVtI7z39yOVBFJyi8Ljbn7kY9yHzznKXL02qQYn+ta63Iy4A9JFBw6o4OSB9hyD2++tVT+su9kQqetUyCCwhjw==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: ^6.2.0 || ^7.0.0 || ^8.0.0
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
-      eslint: 8.57.0
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0)
+      eslint: 8.46.0
       natural-compare: 1.4.0
       nth-check: 2.1.1
       postcss-selector-parser: 6.0.13
       semver: 7.6.0
-      vue-eslint-parser: 9.4.2(eslint@8.57.0)
+      vue-eslint-parser: 9.4.2(eslint@8.46.0)
       xml-name-validator: 4.0.0
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /eslint-plugin-yml@1.10.0(eslint@8.57.0):
+  /eslint-plugin-yml@1.10.0(eslint@8.46.0):
     resolution: {integrity: sha512-53SUwuNDna97lVk38hL/5++WXDuugPM9SUQ1T645R0EHMRCdBIIxGye/oOX2qO3FQ7aImxaUZJU/ju+NMUBrLQ==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
-      eslint-compat-utils: 0.1.2(eslint@8.57.0)
+      eslint: 8.46.0
+      eslint-compat-utils: 0.1.2(eslint@8.46.0)
       lodash: 4.17.21
       natural-compare: 1.4.0
       yaml-eslint-parser: 1.2.2
@@ -8728,15 +8837,15 @@ packages:
       - supports-color
     dev: true
 
-  /eslint-plugin-yml@1.12.2(eslint@8.57.0):
+  /eslint-plugin-yml@1.12.2(eslint@8.46.0):
     resolution: {integrity: sha512-hvS9p08FhPT7i/ynwl7/Wt7ke7Rf4P2D6fT8lZlL43peZDTsHtH2A0SIFQ7Kt7+mJ6if6P+FX3iJhMkdnxQwpg==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
-      eslint-compat-utils: 0.4.1(eslint@8.57.0)
+      eslint: 8.46.0
+      eslint-compat-utils: 0.4.1(eslint@8.46.0)
       lodash: 4.17.21
       natural-compare: 1.4.0
       yaml-eslint-parser: 1.2.2
@@ -8779,13 +8888,13 @@ packages:
       eslint-visitor-keys: 1.3.0
     dev: true
 
-  /eslint-utils@3.0.0(eslint@8.57.0):
+  /eslint-utils@3.0.0(eslint@8.46.0):
     resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==}
     engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0}
     peerDependencies:
       eslint: '>=5'
     dependencies:
-      eslint: 8.57.0
+      eslint: 8.46.0
       eslint-visitor-keys: 2.1.0
     dev: true
 
@@ -8860,53 +8969,6 @@ packages:
       - supports-color
     dev: true
 
-  /eslint@8.57.0:
-    resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==}
-    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
-    hasBin: true
-    dependencies:
-      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
-      '@eslint-community/regexpp': 4.9.1
-      '@eslint/eslintrc': 2.1.4
-      '@eslint/js': 8.57.0
-      '@humanwhocodes/config-array': 0.11.14
-      '@humanwhocodes/module-importer': 1.0.1
-      '@nodelib/fs.walk': 1.2.8
-      '@ungap/structured-clone': 1.2.0
-      ajv: 6.12.6
-      chalk: 4.1.2
-      cross-spawn: 7.0.3
-      debug: 4.3.4(supports-color@8.1.1)
-      doctrine: 3.0.0
-      escape-string-regexp: 4.0.0
-      eslint-scope: 7.2.2
-      eslint-visitor-keys: 3.4.3
-      espree: 9.6.1
-      esquery: 1.5.0
-      esutils: 2.0.3
-      fast-deep-equal: 3.1.3
-      file-entry-cache: 6.0.1
-      find-up: 5.0.0
-      glob-parent: 6.0.2
-      globals: 13.24.0
-      graphemer: 1.4.0
-      ignore: 5.2.4
-      imurmurhash: 0.1.4
-      is-glob: 4.0.3
-      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
-    transitivePeerDependencies:
-      - supports-color
-    dev: true
-
   /eslint@9.0.0:
     resolution: {integrity: sha512-IMryZ5SudxzQvuod6rUdIUz29qFItWx281VhtFVc2Psy/ZhlCeD/5DT6lBIJ4H3G+iamGJoTln1v+QSuPw0p7Q==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -9616,7 +9678,7 @@ packages:
       dezalgo: 1.0.4
       hexoid: 1.0.0
       once: 1.4.0
-      qs: 6.12.0
+      qs: 6.12.1
     dev: false
 
   /fragment-cache@0.2.1:
@@ -9680,7 +9742,7 @@ packages:
     requiresBuild: true
     dependencies:
       bindings: 1.5.0
-      nan: 2.18.0
+      nan: 2.19.0
     dev: false
     optional: true
 
@@ -9759,9 +9821,9 @@ packages:
     dependencies:
       es-errors: 1.3.0
       function-bind: 1.1.2
-      has-proto: 1.0.1
+      has-proto: 1.0.3
       has-symbols: 1.0.3
-      hasown: 2.0.0
+      hasown: 2.0.2
 
   /get-package-type@0.1.0:
     resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==}
@@ -10215,6 +10277,10 @@ packages:
     resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==}
     engines: {node: '>= 0.4'}
 
+  /has-proto@1.0.3:
+    resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==}
+    engines: {node: '>= 0.4'}
+
   /has-symbols@1.0.3:
     resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
     engines: {node: '>= 0.4'}
@@ -10225,6 +10291,12 @@ packages:
     dependencies:
       has-symbols: 1.0.3
 
+  /has-tostringtag@1.0.2:
+    resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      has-symbols: 1.0.3
+
   /has-unicode@2.0.1:
     resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==}
     dev: true
@@ -10272,6 +10344,12 @@ packages:
     dependencies:
       function-bind: 1.1.2
 
+  /hasown@2.0.2:
+    resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      function-bind: 1.1.2
+
   /he@1.2.0:
     resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
     hasBin: true
@@ -10469,8 +10547,8 @@ packages:
     engines: {node: '>= 4'}
     dev: true
 
-  /immutable@4.3.2:
-    resolution: {integrity: sha512-oGXzbEDem9OOpDWZu88jGiYCvIsLHMvGw+8OXlpsvTFvIQplQbjg1B1cvKg8f7Hoch6+NGjpPsH1Fr+Mc2D1aA==}
+  /immutable@4.3.5:
+    resolution: {integrity: sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==}
     dev: true
 
   /import-fresh@3.3.0:
@@ -10627,7 +10705,7 @@ packages:
     resolution: {integrity: sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==}
     engines: {node: '>= 0.10'}
     dependencies:
-      hasown: 2.0.0
+      hasown: 2.0.2
     dev: false
 
   /is-alphabetical@1.0.4:
@@ -10646,7 +10724,7 @@ packages:
     engines: {node: '>= 0.4'}
     dependencies:
       call-bind: 1.0.7
-      has-tostringtag: 1.0.0
+      has-tostringtag: 1.0.2
     dev: false
 
   /is-array-buffer@3.0.2:
@@ -10689,7 +10767,7 @@ packages:
     engines: {node: '>= 0.4'}
     dependencies:
       call-bind: 1.0.2
-      has-tostringtag: 1.0.0
+      has-tostringtag: 1.0.2
     dev: true
 
   /is-buffer@1.1.6:
@@ -10721,14 +10799,14 @@ packages:
     resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==}
     engines: {node: '>= 0.4'}
     dependencies:
-      hasown: 2.0.0
+      hasown: 2.0.2
     dev: false
 
   /is-date-object@1.0.5:
     resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==}
     engines: {node: '>= 0.4'}
     dependencies:
-      has-tostringtag: 1.0.0
+      has-tostringtag: 1.0.2
     dev: true
 
   /is-decimal@1.0.4:
@@ -10861,7 +10939,7 @@ packages:
     resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==}
     engines: {node: '>= 0.4'}
     dependencies:
-      has-tostringtag: 1.0.0
+      has-tostringtag: 1.0.2
     dev: true
 
   /is-number@3.0.0:
@@ -10979,6 +11057,14 @@ packages:
     engines: {node: '>= 0.4'}
     dependencies:
       which-typed-array: 1.1.11
+    dev: true
+
+  /is-typed-array@1.1.13:
+    resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      which-typed-array: 1.1.15
+    dev: false
 
   /is-typedarray@1.0.0:
     resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==}
@@ -11134,7 +11220,7 @@ packages:
     engines: {node: '>=10'}
     hasBin: true
     dependencies:
-      async: 3.2.4
+      async: 3.2.5
       chalk: 4.1.2
       filelist: 1.0.4
       minimatch: 3.1.2
@@ -12034,7 +12120,7 @@ packages:
       http-errors: 1.8.1
       koa-compose: 4.1.0
       methods: 1.1.2
-      path-to-regexp: 6.2.1
+      path-to-regexp: 6.2.2
     transitivePeerDependencies:
       - supports-color
     dev: false
@@ -12068,7 +12154,7 @@ packages:
       - supports-color
     dev: false
 
-  /koa-views@7.0.2(@types/koa@2.15.0)(ejs@3.1.9)(pug@3.0.2):
+  /koa-views@7.0.2(@types/koa@2.15.0)(ejs@3.1.10)(pug@3.0.2):
     resolution: {integrity: sha512-dvx3mdVeSVuIPEaKAoGbxLcenudvhl821xxyuRbcoA+bOJ2dvN8wlGjkLu0ZFMlkCscXZV6lzxy28rafeazI/w==}
     deprecated: This package is deprecated, please use the new fork @ladjs/koa-views. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/koa-views for updates and release changelog
     peerDependencies:
@@ -12078,7 +12164,7 @@ packages:
         optional: true
     dependencies:
       '@types/koa': 2.15.0
-      consolidate: 0.16.0(ejs@3.1.9)(pug@3.0.2)
+      consolidate: 0.16.0(ejs@3.1.10)(pug@3.0.2)
       debug: 4.3.4(supports-color@8.1.1)
       get-paths: 0.0.7
       koa-send: 5.0.1
@@ -12307,8 +12393,8 @@ packages:
     engines: {node: '>=6.11.5'}
     dev: true
 
-  /loadjs@4.2.0:
-    resolution: {integrity: sha512-AgQGZisAlTPbTEzrHPb6q+NYBMD+DP9uvGSIjSUM5uG+0jG15cb8axWpxuOIqrmQjn6scaaH8JwloiP27b2KXA==}
+  /loadjs@4.3.0:
+    resolution: {integrity: sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ==}
     dev: true
 
   /local-pkg@0.5.0:
@@ -12759,6 +12845,13 @@ packages:
     dependencies:
       brace-expansion: 2.0.1
 
+  /minimatch@9.0.4:
+    resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==}
+    engines: {node: '>=16 || 14 >=14.17'}
+    dependencies:
+      brace-expansion: 2.0.1
+    dev: true
+
   /minimist-options@4.1.0:
     resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==}
     engines: {node: '>= 6'}
@@ -12935,8 +13028,8 @@ packages:
       thenify-all: 1.6.0
     dev: false
 
-  /nan@2.18.0:
-    resolution: {integrity: sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==}
+  /nan@2.19.0:
+    resolution: {integrity: sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==}
     requiresBuild: true
     dev: false
     optional: true
@@ -13029,11 +13122,6 @@ packages:
     dev: true
     optional: true
 
-  /node-addon-api@7.1.0:
-    resolution: {integrity: sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==}
-    engines: {node: ^16 || ^18 || >= 20}
-    dev: false
-
   /node-domexception@1.0.0:
     resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
     engines: {node: '>=10.5.0'}
@@ -13083,6 +13171,7 @@ packages:
   /node-gyp-build@4.8.0:
     resolution: {integrity: sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==}
     hasBin: true
+    dev: true
 
   /node-int64@0.4.0:
     resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
@@ -13091,6 +13180,10 @@ packages:
   /node-releases@2.0.13:
     resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
 
+  /node-releases@2.0.14:
+    resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
+    dev: true
+
   /nodemailer@6.9.13:
     resolution: {integrity: sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==}
     engines: {node: '>=6.0.0'}
@@ -13718,6 +13811,10 @@ packages:
     resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==}
     dev: false
 
+  /path-to-regexp@6.2.2:
+    resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==}
+    dev: false
+
   /path-type@1.1.0:
     resolution: {integrity: sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==}
     engines: {node: '>=0.10.0'}
@@ -13943,8 +14040,8 @@ packages:
     engines: {node: '>=14.19.0'}
     dev: false
 
-  /pnpm@8.15.6:
-    resolution: {integrity: sha512-d7iem+d6Kwatj0A6Gcrl4il29hAj+YrTI9XDAZSVjrwC7gpq5dE+5FT2E05OjK8poF8LGg4dKxe8prah8RWfhg==}
+  /pnpm@8.15.7:
+    resolution: {integrity: sha512-yFzSG22hAzIVaxyiqnnAph7nrS6wRTuIqymSienoypPmCRIyslwHy/YfbfdxKNnISeXJrG5EhU29IRxJ86Z63A==}
     engines: {node: '>=16.14'}
     hasBin: true
     dev: true
@@ -13954,6 +14051,11 @@ packages:
     engines: {node: '>=0.10.0'}
     dev: false
 
+  /possible-typed-array-names@1.0.0:
+    resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==}
+    engines: {node: '>= 0.4'}
+    dev: false
+
   /postcss-calc@5.3.1:
     resolution: {integrity: sha512-iBcptYFq+QUh9gzP7ta2btw50o40s4uLI4UDVgd5yRAZtUDWc5APdl5yQDd2h/TyiZNbJrv0HiYhT102CMgN7Q==}
     dependencies:
@@ -14548,8 +14650,8 @@ packages:
       yargs: 15.4.1
     dev: false
 
-  /qs@6.12.0:
-    resolution: {integrity: sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==}
+  /qs@6.12.1:
+    resolution: {integrity: sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==}
     engines: {node: '>=0.6'}
     dependencies:
       side-channel: 1.0.6
@@ -15045,28 +15147,28 @@ packages:
       seedrandom: 2.4.2
     dev: false
 
-  /rollup@4.14.1:
-    resolution: {integrity: sha512-4LnHSdd3QK2pa1J6dFbfm1HN0D7vSK/ZuZTsdyUAlA6Rr1yTouUTL13HaDOGJVgby461AhrNGBS7sCGXXtT+SA==}
+  /rollup@4.14.2:
+    resolution: {integrity: sha512-WkeoTWvuBoFjFAhsEOHKRoZ3r9GfTyhh7Vff1zwebEFLEFjT1lG3784xEgKiTa7E+e70vsC81roVL2MP4tgEEQ==}
     engines: {node: '>=18.0.0', npm: '>=8.0.0'}
     hasBin: true
     dependencies:
       '@types/estree': 1.0.5
     optionalDependencies:
-      '@rollup/rollup-android-arm-eabi': 4.14.1
-      '@rollup/rollup-android-arm64': 4.14.1
-      '@rollup/rollup-darwin-arm64': 4.14.1
-      '@rollup/rollup-darwin-x64': 4.14.1
-      '@rollup/rollup-linux-arm-gnueabihf': 4.14.1
-      '@rollup/rollup-linux-arm64-gnu': 4.14.1
-      '@rollup/rollup-linux-arm64-musl': 4.14.1
-      '@rollup/rollup-linux-powerpc64le-gnu': 4.14.1
-      '@rollup/rollup-linux-riscv64-gnu': 4.14.1
-      '@rollup/rollup-linux-s390x-gnu': 4.14.1
-      '@rollup/rollup-linux-x64-gnu': 4.14.1
-      '@rollup/rollup-linux-x64-musl': 4.14.1
-      '@rollup/rollup-win32-arm64-msvc': 4.14.1
-      '@rollup/rollup-win32-ia32-msvc': 4.14.1
-      '@rollup/rollup-win32-x64-msvc': 4.14.1
+      '@rollup/rollup-android-arm-eabi': 4.14.2
+      '@rollup/rollup-android-arm64': 4.14.2
+      '@rollup/rollup-darwin-arm64': 4.14.2
+      '@rollup/rollup-darwin-x64': 4.14.2
+      '@rollup/rollup-linux-arm-gnueabihf': 4.14.2
+      '@rollup/rollup-linux-arm64-gnu': 4.14.2
+      '@rollup/rollup-linux-arm64-musl': 4.14.2
+      '@rollup/rollup-linux-powerpc64le-gnu': 4.14.2
+      '@rollup/rollup-linux-riscv64-gnu': 4.14.2
+      '@rollup/rollup-linux-s390x-gnu': 4.14.2
+      '@rollup/rollup-linux-x64-gnu': 4.14.2
+      '@rollup/rollup-linux-x64-musl': 4.14.2
+      '@rollup/rollup-win32-arm64-msvc': 4.14.2
+      '@rollup/rollup-win32-ia32-msvc': 4.14.2
+      '@rollup/rollup-win32-x64-msvc': 4.14.2
       fsevents: 2.3.3
     dev: true
 
@@ -15138,13 +15240,13 @@ packages:
       postcss: 8.4.35
     dev: false
 
-  /sass@1.74.1:
-    resolution: {integrity: sha512-w0Z9p/rWZWelb88ISOLyvqTWGmtmu2QJICqDBGyNnfG4OUnPX9BBjjYIXUpXCMOOg5MQWNpqzt876la1fsTvUA==}
+  /sass@1.75.0:
+    resolution: {integrity: sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw==}
     engines: {node: '>=14.0.0'}
     hasBin: true
     dependencies:
-      chokidar: 3.5.3
-      immutable: 4.3.2
+      chokidar: 3.6.0
+      immutable: 4.3.5
       source-map-js: 1.2.0
     dev: true
 
@@ -16293,7 +16395,7 @@ packages:
       typescript: 5.4.5
     dev: true
 
-  /ts-jest@29.1.1(@babel/core@7.23.2)(jest@29.7.0)(typescript@4.9.4):
+  /ts-jest@29.1.1(@babel/core@7.24.4)(jest@29.7.0)(typescript@4.9.4):
     resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     hasBin: true
@@ -16314,7 +16416,7 @@ packages:
       esbuild:
         optional: true
     dependencies:
-      '@babel/core': 7.23.2
+      '@babel/core': 7.24.4
       bs-logger: 0.2.6
       fast-json-stable-stringify: 2.1.0
       jest: 29.7.0(@types/node@18.11.18)
@@ -16842,6 +16944,17 @@ packages:
       escalade: 3.1.1
       picocolors: 1.0.0
 
+  /update-browserslist-db@1.0.13(browserslist@4.23.0):
+    resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
+    hasBin: true
+    peerDependencies:
+      browserslist: '>= 4.21.0'
+    dependencies:
+      browserslist: 4.23.0
+      escalade: 3.1.1
+      picocolors: 1.0.0
+    dev: true
+
   /uri-js@4.4.1:
     resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
     dependencies:
@@ -16885,8 +16998,8 @@ packages:
       inherits: 2.0.4
       is-arguments: 1.1.1
       is-generator-function: 1.0.10
-      is-typed-array: 1.1.12
-      which-typed-array: 1.1.11
+      is-typed-array: 1.1.13
+      which-typed-array: 1.1.15
     dev: false
 
   /uuid@8.0.0:
@@ -17018,12 +17131,12 @@ packages:
       chalk: 4.1.2
       debug: 4.3.4(supports-color@8.1.1)
       fs-extra: 10.1.0
-      vite: 5.2.8(@types/node@20.12.7)(sass@1.74.1)
+      vite: 5.2.8(@types/node@20.12.7)(sass@1.75.0)
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /vite@5.2.8(@types/node@20.12.7)(sass@1.74.1):
+  /vite@5.2.8(@types/node@20.12.7)(sass@1.75.0):
     resolution: {integrity: sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==}
     engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
@@ -17054,8 +17167,8 @@ packages:
       '@types/node': 20.12.7
       esbuild: 0.20.2
       postcss: 8.4.38
-      rollup: 4.14.1
-      sass: 1.74.1
+      rollup: 4.14.2
+      sass: 1.75.0
     optionalDependencies:
       fsevents: 2.3.3
     dev: true
@@ -17072,7 +17185,7 @@ packages:
     resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==}
     dev: true
 
-  /vue-draggable-plus@0.4.0(@types/sortablejs@1.15.4):
+  /vue-draggable-plus@0.4.0(@types/sortablejs@1.15.8):
     resolution: {integrity: sha512-CcvSopMmSZY9McCdQ56QsAydT5cZilx9LzLU0UIz6KDYe9ll6QnmNHN80t6wnxN402ZECSXc5X1dm/myiMFi+A==}
     peerDependencies:
       '@types/sortablejs': ^1.15.0
@@ -17081,17 +17194,17 @@ packages:
       '@vue/composition-api':
         optional: true
     dependencies:
-      '@types/sortablejs': 1.15.4
+      '@types/sortablejs': 1.15.8
     dev: true
 
-  /vue-eslint-parser@9.3.1(eslint@8.57.0):
+  /vue-eslint-parser@9.3.1(eslint@8.46.0):
     resolution: {integrity: sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      eslint: 8.46.0
       eslint-scope: 7.2.2
       eslint-visitor-keys: 3.4.3
       espree: 9.6.1
@@ -17102,14 +17215,14 @@ packages:
       - supports-color
     dev: true
 
-  /vue-eslint-parser@9.3.2(eslint@8.57.0):
+  /vue-eslint-parser@9.3.2(eslint@8.46.0):
     resolution: {integrity: sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      eslint: 8.46.0
       eslint-scope: 7.2.2
       eslint-visitor-keys: 3.4.3
       espree: 9.6.1
@@ -17120,14 +17233,14 @@ packages:
       - supports-color
     dev: true
 
-  /vue-eslint-parser@9.4.2(eslint@8.57.0):
+  /vue-eslint-parser@9.4.2(eslint@8.46.0):
     resolution: {integrity: sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
     dependencies:
       debug: 4.3.4(supports-color@8.1.1)
-      eslint: 8.57.0
+      eslint: 8.46.0
       eslint-scope: 7.2.2
       eslint-visitor-keys: 3.4.3
       espree: 9.6.1
@@ -17161,14 +17274,14 @@ packages:
       he: 1.2.0
     dev: true
 
-  /vue-tsc@2.0.12(typescript@5.4.5):
-    resolution: {integrity: sha512-thlBBWlPYrNdba535oDdxz7PRUufZgRZRVP5Aql5wBVpGSWSeqou4EzFXeKVoZr59lp9hJROubDVzlhACmcEhg==}
+  /vue-tsc@2.0.13(typescript@5.4.5):
+    resolution: {integrity: sha512-a3nL3FvguCWVJUQW/jFrUxdeUtiEkbZoQjidqvMeBK//tuE2w6NWQAbdrEpY2+6nSa4kZoKZp8TZUMtHpjt4mQ==}
     hasBin: true
     peerDependencies:
       typescript: '*'
     dependencies:
-      '@volar/typescript': 2.2.0-alpha.7
-      '@vue/language-core': 2.0.12(typescript@5.4.5)
+      '@volar/typescript': 2.2.0-alpha.8
+      '@vue/language-core': 2.0.13(typescript@5.4.5)
       semver: 7.6.0
       typescript: 5.4.5
     dev: true
@@ -17355,6 +17468,18 @@ packages:
       for-each: 0.3.3
       gopd: 1.0.1
       has-tostringtag: 1.0.0
+    dev: true
+
+  /which-typed-array@1.1.15:
+    resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==}
+    engines: {node: '>= 0.4'}
+    dependencies:
+      available-typed-arrays: 1.0.7
+      call-bind: 1.0.7
+      for-each: 0.3.3
+      gopd: 1.0.1
+      has-tostringtag: 1.0.2
+    dev: false
 
   /which@1.3.1:
     resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
@@ -17495,7 +17620,7 @@ packages:
     resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
     engines: {node: '>=4.0.0'}
     dependencies:
-      sax: 1.2.4
+      sax: 1.2.1
       xmlbuilder: 11.0.1
     dev: false
 
@@ -17713,9 +17838,9 @@ packages:
     name: plyr
     version: 3.7.0
     dependencies:
-      core-js: 3.33.0
+      core-js: 3.36.1
       custom-event-polyfill: 1.0.7
-      loadjs: 4.2.0
+      loadjs: 4.3.0
       rangetouch: 2.0.1
       url-polyfill: 1.1.12
     dev: true