diff --git a/composer.json b/composer.json
index 15572a9..8475536 100644
--- a/composer.json
+++ b/composer.json
@@ -1,13 +1,13 @@
 {
     "require": {
-        "flashwave/index": "^0.2410",
-        "flashii/rpcii": "^3.0",
-        "flashii/apii": "^0.3",
+        "flashwave/index": "^0.2503",
+        "flashii/rpcii": "^5.0",
+        "flashii/apii": "^0.4",
         "sentry/sdk": "^4.0",
-        "nesbot/carbon": "^3.7"
+        "nesbot/carbon": "^3.8"
     },
     "require-dev": {
-        "phpstan/phpstan": "^2.0"
+        "phpstan/phpstan": "^2.1"
     },
     "autoload": {
         "psr-4": {
diff --git a/composer.lock b/composer.lock
index 4bd50d2..db07489 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "7009c2a98d8882c938653f4455f52a82",
+    "content-hash": "2d07dcd636742c34625c576e67eaaf0b",
     "packages": [
         {
             "name": "carbonphp/carbon-doctrine-types",
@@ -77,18 +77,20 @@
         },
         {
             "name": "flashii/apii",
-            "version": "v0.3.0",
+            "version": "v0.4.0",
             "source": {
                 "type": "git",
                 "url": "https://patchii.net/flashii/apii-php.git",
-                "reference": "2d6c135faddd359341762afcb9c429e279d87059"
+                "reference": "d330e0792515dbae2832bd8e2ac761cc685175e9"
             },
             "require": {
-                "php": ">=8.1"
+                "guzzlehttp/guzzle": "~7.9",
+                "php": ">=8.1",
+                "psr/http-client": "^1.0"
             },
             "require-dev": {
-                "phpstan/phpstan": "^1.12",
-                "phpunit/phpunit": "^10.5"
+                "phpstan/phpstan": "~2.1",
+                "phpunit/phpunit": "~10.5"
             },
             "type": "library",
             "autoload": {
@@ -110,24 +112,26 @@
             ],
             "description": "Client library for the Flashii.net API.",
             "homepage": "https://api.flashii.net",
-            "time": "2024-11-22T21:36:01+00:00"
+            "time": "2025-03-20T17:05:52+00:00"
         },
         {
             "name": "flashii/rpcii",
-            "version": "v3.0.0",
+            "version": "v5.0.1",
             "source": {
                 "type": "git",
                 "url": "https://patchii.net/flashii/rpcii-php.git",
-                "reference": "25ac46d5dee60027032e175107e638dfb0b8f7f9"
+                "reference": "28c25e0a342173524f8894d03158663842e03252"
             },
             "require": {
                 "ext-msgpack": ">=2.2",
-                "flashwave/index": "^0.2410",
-                "php": ">=8.4"
+                "flashwave/index": "^0.2503",
+                "guzzlehttp/guzzle": "~7.0",
+                "php": ">=8.4",
+                "psr/http-client": "^1.0"
             },
             "require-dev": {
                 "phpstan/phpstan": "^2.1",
-                "phpunit/phpunit": "^11.5"
+                "phpunit/phpunit": "^12.0"
             },
             "type": "library",
             "autoload": {
@@ -149,25 +153,27 @@
             ],
             "description": "HTTP RPC client/server library.",
             "homepage": "https://railgun.sh/rpcii",
-            "time": "2025-01-17T00:05:22+00:00"
+            "time": "2025-03-21T19:38:29+00:00"
         },
         {
             "name": "flashwave/index",
-            "version": "v0.2410.830205",
+            "version": "v0.2503.221959",
             "source": {
                 "type": "git",
                 "url": "https://patchii.net/flash/index.git",
-                "reference": "416c716b2efab9619d14be02a20cd6540e18a83f"
+                "reference": "d99562db163589cc51e32e10e851085230fb3181"
             },
             "require": {
                 "ext-mbstring": "*",
-                "php": ">=8.3",
-                "twig/html-extra": "^3.13",
-                "twig/twig": "^3.14"
+                "php": ">=8.4",
+                "psr/http-message": "^2.0",
+                "psr/http-server-handler": "^1.0",
+                "twig/html-extra": "^3.20",
+                "twig/twig": "^3.20"
             },
             "require-dev": {
-                "phpstan/phpstan": "^2.0",
-                "phpunit/phpunit": "^11.4"
+                "phpstan/phpstan": "^2.1",
+                "phpunit/phpunit": "^12.0"
             },
             "suggest": {
                 "ext-memcache": "Support for the Index\\Cache\\Memcached namespace (only if you can't use ext-memcached for some reason).",
@@ -204,7 +210,216 @@
             ],
             "description": "Composer package for the common library for my projects.",
             "homepage": "https://railgun.sh/index",
-            "time": "2024-12-22T02:05:46+00:00"
+            "time": "2025-03-22T19:59:44+00:00"
+        },
+        {
+            "name": "guzzlehttp/guzzle",
+            "version": "7.9.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/guzzle.git",
+                "reference": "d281ed313b989f213357e3be1a179f02196ac99b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b",
+                "reference": "d281ed313b989f213357e3be1a179f02196ac99b",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "guzzlehttp/promises": "^1.5.3 || ^2.0.3",
+                "guzzlehttp/psr7": "^2.7.0",
+                "php": "^7.2.5 || ^8.0",
+                "psr/http-client": "^1.0",
+                "symfony/deprecation-contracts": "^2.2 || ^3.0"
+            },
+            "provide": {
+                "psr/http-client-implementation": "1.0"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.8.2",
+                "ext-curl": "*",
+                "guzzle/client-integration-tests": "3.0.2",
+                "php-http/message-factory": "^1.1",
+                "phpunit/phpunit": "^8.5.39 || ^9.6.20",
+                "psr/log": "^1.1 || ^2.0 || ^3.0"
+            },
+            "suggest": {
+                "ext-curl": "Required for CURL handler support",
+                "ext-intl": "Required for Internationalized Domain Name (IDN) support",
+                "psr/log": "Required for using the Log middleware"
+            },
+            "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "bin-links": true,
+                    "forward-command": false
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/functions_include.php"
+                ],
+                "psr-4": {
+                    "GuzzleHttp\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Jeremy Lindblom",
+                    "email": "jeremeamia@gmail.com",
+                    "homepage": "https://github.com/jeremeamia"
+                },
+                {
+                    "name": "George Mponos",
+                    "email": "gmponos@gmail.com",
+                    "homepage": "https://github.com/gmponos"
+                },
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com",
+                    "homepage": "https://github.com/Nyholm"
+                },
+                {
+                    "name": "Márk Sági-Kazár",
+                    "email": "mark.sagikazar@gmail.com",
+                    "homepage": "https://github.com/sagikazarmark"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "email": "webmaster@tubo-world.de",
+                    "homepage": "https://github.com/Tobion"
+                }
+            ],
+            "description": "Guzzle is a PHP HTTP client library",
+            "keywords": [
+                "client",
+                "curl",
+                "framework",
+                "http",
+                "http client",
+                "psr-18",
+                "psr-7",
+                "rest",
+                "web service"
+            ],
+            "support": {
+                "issues": "https://github.com/guzzle/guzzle/issues",
+                "source": "https://github.com/guzzle/guzzle/tree/7.9.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/Nyholm",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-07-24T11:22:20+00:00"
+        },
+        {
+            "name": "guzzlehttp/promises",
+            "version": "2.0.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/promises.git",
+                "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455",
+                "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5 || ^8.0"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.8.2",
+                "phpunit/phpunit": "^8.5.39 || ^9.6.20"
+            },
+            "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "bin-links": true,
+                    "forward-command": false
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\Promise\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com",
+                    "homepage": "https://github.com/Nyholm"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "email": "webmaster@tubo-world.de",
+                    "homepage": "https://github.com/Tobion"
+                }
+            ],
+            "description": "Guzzle promises library",
+            "keywords": [
+                "promise"
+            ],
+            "support": {
+                "issues": "https://github.com/guzzle/promises/issues",
+                "source": "https://github.com/guzzle/promises/tree/2.0.4"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/Nyholm",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-10-17T10:06:22+00:00"
         },
         {
             "name": "guzzlehttp/psr7",
@@ -324,16 +539,16 @@
         },
         {
             "name": "jean85/pretty-package-versions",
-            "version": "2.1.0",
+            "version": "2.1.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Jean85/pretty-package-versions.git",
-                "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10"
+                "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/3c4e5f62ba8d7de1734312e4fff32f67a8daaf10",
-                "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10",
+                "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a",
+                "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a",
                 "shasum": ""
             },
             "require": {
@@ -343,8 +558,9 @@
             "require-dev": {
                 "friendsofphp/php-cs-fixer": "^3.2",
                 "jean85/composer-provided-replaced-stub-package": "^1.0",
-                "phpstan/phpstan": "^1.4",
+                "phpstan/phpstan": "^2.0",
                 "phpunit/phpunit": "^7.5|^8.5|^9.6",
+                "rector/rector": "^2.0",
                 "vimeo/psalm": "^4.3 || ^5.0"
             },
             "type": "library",
@@ -377,22 +593,22 @@
             ],
             "support": {
                 "issues": "https://github.com/Jean85/pretty-package-versions/issues",
-                "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.0"
+                "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1"
             },
-            "time": "2024-11-18T16:19:46+00:00"
+            "time": "2025-03-19T14:43:43+00:00"
         },
         {
             "name": "nesbot/carbon",
-            "version": "3.8.4",
+            "version": "3.8.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/CarbonPHP/carbon.git",
-                "reference": "129700ed449b1f02d70272d2ac802357c8c30c58"
+                "reference": "ff2f20cf83bd4d503720632ce8a426dc747bf7fd"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/129700ed449b1f02d70272d2ac802357c8c30c58",
-                "reference": "129700ed449b1f02d70272d2ac802357c8c30c58",
+                "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/ff2f20cf83bd4d503720632ce8a426dc747bf7fd",
+                "reference": "ff2f20cf83bd4d503720632ce8a426dc747bf7fd",
                 "shasum": ""
             },
             "require": {
@@ -468,8 +684,8 @@
             ],
             "support": {
                 "docs": "https://carbon.nesbot.com/docs",
-                "issues": "https://github.com/briannesbitt/Carbon/issues",
-                "source": "https://github.com/briannesbitt/Carbon"
+                "issues": "https://github.com/CarbonPHP/carbon/issues",
+                "source": "https://github.com/CarbonPHP/carbon"
             },
             "funding": [
                 {
@@ -485,7 +701,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-12-27T09:25:35+00:00"
+            "time": "2025-02-20T17:33:38+00:00"
         },
         {
             "name": "psr/clock",
@@ -535,6 +751,58 @@
             },
             "time": "2022-11-25T14:36:26+00:00"
         },
+        {
+            "name": "psr/http-client",
+            "version": "1.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-client.git",
+                "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+                "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.0 || ^8.0",
+                "psr/http-message": "^1.0 || ^2.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Client\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for HTTP clients",
+            "homepage": "https://github.com/php-fig/http-client",
+            "keywords": [
+                "http",
+                "http-client",
+                "psr",
+                "psr-18"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/http-client"
+            },
+            "time": "2023-09-23T14:17:50+00:00"
+        },
         {
             "name": "psr/http-factory",
             "version": "1.1.0",
@@ -643,6 +911,62 @@
             },
             "time": "2023-04-04T09:54:51+00:00"
         },
+        {
+            "name": "psr/http-server-handler",
+            "version": "1.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-server-handler.git",
+                "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4",
+                "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.0",
+                "psr/http-message": "^1.0 || ^2.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Server\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for HTTP server-side request handler",
+            "keywords": [
+                "handler",
+                "http",
+                "http-interop",
+                "psr",
+                "psr-15",
+                "psr-7",
+                "request",
+                "response",
+                "server"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2"
+            },
+            "time": "2023-04-10T20:06:20+00:00"
+        },
         {
             "name": "psr/log",
             "version": "3.0.2",
@@ -1024,16 +1348,16 @@
         },
         {
             "name": "symfony/mime",
-            "version": "v7.2.1",
+            "version": "v7.2.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/mime.git",
-                "reference": "7f9617fcf15cb61be30f8b252695ed5e2bfac283"
+                "reference": "87ca22046b78c3feaff04b337f33b38510fd686b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/mime/zipball/7f9617fcf15cb61be30f8b252695ed5e2bfac283",
-                "reference": "7f9617fcf15cb61be30f8b252695ed5e2bfac283",
+                "url": "https://api.github.com/repos/symfony/mime/zipball/87ca22046b78c3feaff04b337f33b38510fd686b",
+                "reference": "87ca22046b78c3feaff04b337f33b38510fd686b",
                 "shasum": ""
             },
             "require": {
@@ -1088,7 +1412,7 @@
                 "mime-type"
             ],
             "support": {
-                "source": "https://github.com/symfony/mime/tree/v7.2.1"
+                "source": "https://github.com/symfony/mime/tree/v7.2.4"
             },
             "funding": [
                 {
@@ -1104,7 +1428,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-12-07T08:50:44+00:00"
+            "time": "2025-02-19T08:51:20+00:00"
         },
         {
             "name": "symfony/options-resolver",
@@ -1496,82 +1820,6 @@
             ],
             "time": "2024-09-09T11:45:10+00:00"
         },
-        {
-            "name": "symfony/polyfill-php81",
-            "version": "v1.31.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/polyfill-php81.git",
-                "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
-                "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.2"
-            },
-            "type": "library",
-            "extra": {
-                "thanks": {
-                    "url": "https://github.com/symfony/polyfill",
-                    "name": "symfony/polyfill"
-                }
-            },
-            "autoload": {
-                "files": [
-                    "bootstrap.php"
-                ],
-                "psr-4": {
-                    "Symfony\\Polyfill\\Php81\\": ""
-                },
-                "classmap": [
-                    "Resources/stubs"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Nicolas Grekas",
-                    "email": "p@tchwork.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
-            "homepage": "https://symfony.com",
-            "keywords": [
-                "compatibility",
-                "polyfill",
-                "portable",
-                "shim"
-            ],
-            "support": {
-                "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2024-09-09T11:45:10+00:00"
-        },
         {
             "name": "symfony/polyfill-php83",
             "version": "v1.31.0",
@@ -1650,16 +1898,16 @@
         },
         {
             "name": "symfony/translation",
-            "version": "v7.2.2",
+            "version": "v7.2.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "e2674a30132b7cc4d74540d6c2573aa363f05923"
+                "reference": "283856e6981286cc0d800b53bd5703e8e363f05a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/e2674a30132b7cc4d74540d6c2573aa363f05923",
-                "reference": "e2674a30132b7cc4d74540d6c2573aa363f05923",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/283856e6981286cc0d800b53bd5703e8e363f05a",
+                "reference": "283856e6981286cc0d800b53bd5703e8e363f05a",
                 "shasum": ""
             },
             "require": {
@@ -1725,7 +1973,7 @@
             "description": "Provides tools to internationalize your application",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/translation/tree/v7.2.2"
+                "source": "https://github.com/symfony/translation/tree/v7.2.4"
             },
             "funding": [
                 {
@@ -1741,7 +1989,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-12-07T08:18:10+00:00"
+            "time": "2025-02-13T10:27:23+00:00"
         },
         {
             "name": "symfony/translation-contracts",
@@ -1823,20 +2071,20 @@
         },
         {
             "name": "twig/html-extra",
-            "version": "v3.18.0",
+            "version": "v3.20.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/twigphp/html-extra.git",
-                "reference": "c63b28e192c1b7c15bb60f81d2e48b140846239a"
+                "reference": "f7d54d4de1b64182af745cfb66777f699b599734"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/twigphp/html-extra/zipball/c63b28e192c1b7c15bb60f81d2e48b140846239a",
-                "reference": "c63b28e192c1b7c15bb60f81d2e48b140846239a",
+                "url": "https://api.github.com/repos/twigphp/html-extra/zipball/f7d54d4de1b64182af745cfb66777f699b599734",
+                "reference": "f7d54d4de1b64182af745cfb66777f699b599734",
                 "shasum": ""
             },
             "require": {
-                "php": ">=8.0.2",
+                "php": ">=8.1.0",
                 "symfony/deprecation-contracts": "^2.5|^3",
                 "symfony/mime": "^5.4|^6.4|^7.0",
                 "twig/twig": "^3.13|^4.0"
@@ -1875,7 +2123,7 @@
                 "twig"
             ],
             "support": {
-                "source": "https://github.com/twigphp/html-extra/tree/v3.18.0"
+                "source": "https://github.com/twigphp/html-extra/tree/v3.20.0"
             },
             "funding": [
                 {
@@ -1887,28 +2135,27 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-12-29T10:29:59+00:00"
+            "time": "2025-01-31T20:45:36+00:00"
         },
         {
             "name": "twig/twig",
-            "version": "v3.18.0",
+            "version": "v3.20.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/twigphp/Twig.git",
-                "reference": "acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50"
+                "reference": "3468920399451a384bef53cf7996965f7cd40183"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/twigphp/Twig/zipball/acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50",
-                "reference": "acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50",
+                "url": "https://api.github.com/repos/twigphp/Twig/zipball/3468920399451a384bef53cf7996965f7cd40183",
+                "reference": "3468920399451a384bef53cf7996965f7cd40183",
                 "shasum": ""
             },
             "require": {
-                "php": ">=8.0.2",
+                "php": ">=8.1.0",
                 "symfony/deprecation-contracts": "^2.5|^3",
                 "symfony/polyfill-ctype": "^1.8",
-                "symfony/polyfill-mbstring": "^1.3",
-                "symfony/polyfill-php81": "^1.29"
+                "symfony/polyfill-mbstring": "^1.3"
             },
             "require-dev": {
                 "phpstan/phpstan": "^2.0",
@@ -1955,7 +2202,7 @@
             ],
             "support": {
                 "issues": "https://github.com/twigphp/Twig/issues",
-                "source": "https://github.com/twigphp/Twig/tree/v3.18.0"
+                "source": "https://github.com/twigphp/Twig/tree/v3.20.0"
             },
             "funding": [
                 {
@@ -1967,22 +2214,22 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-12-29T10:51:50+00:00"
+            "time": "2025-02-13T08:34:43+00:00"
         }
     ],
     "packages-dev": [
         {
             "name": "phpstan/phpstan",
-            "version": "2.1.1",
+            "version": "2.1.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan.git",
-                "reference": "cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7"
+                "reference": "f9adff3b87c03b12cc7e46a30a524648e497758f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7",
-                "reference": "cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7",
+                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f9adff3b87c03b12cc7e46a30a524648e497758f",
+                "reference": "f9adff3b87c03b12cc7e46a30a524648e497758f",
                 "shasum": ""
             },
             "require": {
@@ -2027,7 +2274,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2025-01-05T16:43:48+00:00"
+            "time": "2025-03-09T09:30:48+00:00"
         }
     ],
     "aliases": [],
diff --git a/src/Auth/AuthContext.php b/src/Auth/AuthContext.php
index f6bd2a4..99abec0 100644
--- a/src/Auth/AuthContext.php
+++ b/src/Auth/AuthContext.php
@@ -28,7 +28,8 @@ class AuthContext {
 
     public function createClient(Credentials $credentials): FlashiiClient {
         return new FlashiiClient('EEPROM', $credentials, new FlashiiUrls(
-            $this->config->getString('api', FlashiiUrls::PROD_API_URL)
+            $this->config->getString('url', FlashiiUrls::PROD_URL),
+            $this->config->getString('api', FlashiiUrls::PROD_API_URL),
         ));
     }
 
diff --git a/src/Auth/AuthRoutes.php b/src/Auth/AuthRoutes.php
index 45404b7..af54c02 100644
--- a/src/Auth/AuthRoutes.php
+++ b/src/Auth/AuthRoutes.php
@@ -1,17 +1,19 @@
 <?php
 namespace EEPROM\Auth;
 
-use Index\Http\Routing\{HttpMiddleware,RouteHandler,RouteHandlerTrait};
+use Index\Http\{HttpResponseBuilder,HttpRequest};
+use Index\Http\Routing\Processors\Preprocessor;
+use Index\Http\Routing\{HttpMiddleware,RouteHandler,RouteHandlerCommon};
 
 class AuthRoutes implements RouteHandler {
-    use RouteHandlerTrait;
+    use RouteHandlerCommon;
 
     public function __construct(
         private AuthContext $authCtx
     ) {}
 
-    #[HttpMiddleware('/')]
-    public function getIndex($response, $request) {
+    #[Preprocessor('eeprom:attempt-auth')]
+    public function getIndex(HttpResponseBuilder $response, HttpRequest $request) {
         $auth = $request->getHeaderLine('Authorization');
         if(empty($auth)) {
             $cookie = (string)$request->getCookie('msz_auth');
@@ -21,7 +23,7 @@ class AuthRoutes implements RouteHandler {
 
         if(!empty($auth)) {
             $parts = explode(' ', $auth, 2);
-            $method = strval($parts[0] ?? '');
+            $method = $parts[0];
             $token = strval($parts[1] ?? '');
 
             $flashii = null;
diff --git a/src/DatabaseContext.php b/src/DatabaseContext.php
index 176ee3d..af1bc22 100644
--- a/src/DatabaseContext.php
+++ b/src/DatabaseContext.php
@@ -3,22 +3,23 @@ namespace EEPROM;
 
 use Index\Db\DbConnection;
 use Index\Db\Migration\{DbMigrationManager,DbMigrationRepo,FsDbMigrationRepo};
-use Index\Http\Routing\{HttpMiddleware,RouteHandler,RouteHandlerTrait};
+use Index\Http\Routing\{RouteHandler,RouteHandlerCommon};
+use Index\Http\Routing\Filters\PrefixFilter;
 
 class DatabaseContext implements RouteHandler {
-    use RouteHandlerTrait;
+    use RouteHandlerCommon;
 
     public function __construct(
-        public private(set) DbConnection $connection
+        public private(set) DbConnection $conn
     ) {}
 
     public function getQueryCount(): int {
-        $result = $this->connection->query('SHOW SESSION STATUS LIKE "Questions"');
+        $result = $this->conn->query('SHOW SESSION STATUS LIKE "Questions"');
         return $result->next() ? $result->getInteger(1) : 0;
     }
 
     public function createMigrationManager(): DbMigrationManager {
-        return new DbMigrationManager($this->connection, 'prm_' . DbMigrationManager::DEFAULT_TABLE);
+        return new DbMigrationManager($this->conn, 'prm_' . DbMigrationManager::DEFAULT_TABLE);
     }
 
     public function createMigrationRepo(): DbMigrationRepo {
@@ -29,7 +30,7 @@ class DatabaseContext implements RouteHandler {
         return sys_get_temp_dir() . '/eeprom-migrate-' . hash('sha256', PRM_ROOT) . '.lock';
     }
 
-    #[HttpMiddleware('/')]
+    #[PrefixFilter('/')]
     public function middleware() {
         if(is_file($this->getMigrateLockPath()))
             return 503;
diff --git a/src/EEPROMAccessControlHandler.php b/src/EEPROMAccessControlHandler.php
new file mode 100644
index 0000000..669b5b4
--- /dev/null
+++ b/src/EEPROMAccessControlHandler.php
@@ -0,0 +1,30 @@
+<?php
+namespace EEPROM;
+
+use Index\Http\HttpUri;
+use Index\Http\Routing\HandlerContext;
+use Index\Http\Routing\Routes\RouteInfo;
+use Index\Http\Routing\AccessControl\{AccessControl,SimpleAccessControlHandler};
+
+class EEPROMAccessControlHandler extends SimpleAccessControlHandler {
+    public function __construct(
+        private array $origins,
+    ) {}
+
+    #[\Override]
+    public function checkAccess(
+        HandlerContext $context,
+        AccessControl $accessControl,
+        HttpUri $origin,
+        ?RouteInfo $routeInfo = null,
+    ): string|bool {
+        if($accessControl->credentials) {
+            $host = '.' . $origin->host;
+            foreach($this->origins as $allowOrigin)
+                if(str_ends_with($host, '.' . $allowOrigin))
+                    return (string)$origin;
+        }
+
+        return true;
+    }
+}
diff --git a/src/EEPROMContext.php b/src/EEPROMContext.php
index 6fd662b..c6aeafd 100644
--- a/src/EEPROMContext.php
+++ b/src/EEPROMContext.php
@@ -1,14 +1,16 @@
 <?php
 namespace EEPROM;
 
-use EEPROM\Auth\AuthInfoWrapper;
+use Index\Dependencies;
 use Index\Config\Config;
 use Index\Db\DbConnection;
 use RPCii\HmacVerificationProvider;
 use RPCii\Server\HttpRpcServer;
 
 class EEPROMContext {
-    public private(set) DatabaseContext $database;
+    public private(set) Dependencies $deps;
+
+    public private(set) DatabaseContext $dbCtx;
     public private(set) SnowflakeGenerator $snowflake;
 
     public private(set) Auth\AuthContext $authCtx;
@@ -22,30 +24,26 @@ class EEPROMContext {
         private Config $config,
         DbConnection $dbConn
     ) {
-        $this->database = new DatabaseContext($dbConn);
-        $this->snowflake = new SnowflakeGenerator;
+        $this->deps = new Dependencies;
+        $this->deps->register($this);
+        $this->deps->register($this->deps);
 
-        $this->authCtx = new Auth\AuthContext($config->scopeTo('apii'));
-        $this->denylistCtx = new Denylist\DenylistContext($dbConn);
-        $this->poolsCtx = new Pools\PoolsContext($dbConn);
-        $this->storageCtx = new Storage\StorageContext($config->scopeTo('storage'), $dbConn, $this->snowflake);
-        $this->tasksCtx = new Tasks\TasksContext($config->scopeTo('domain'), $dbConn, $this->snowflake);
-        $this->uploadsCtx = new Uploads\UploadsContext(
-            $config->scopeTo('domain'), $dbConn, $this->snowflake, $this->poolsCtx, $this->storageCtx,
-        );
+        $this->deps->register($this->dbCtx = new DatabaseContext($dbConn));
+        $this->deps->register($this->dbCtx->conn);
+
+        $this->deps->register($this->snowflake = $this->deps->constructLazy(SnowflakeGenerator::class));
+
+        $this->deps->register($this->authCtx = $this->deps->constructLazy(Auth\AuthContext::class, $config->scopeTo('apii')));
+        $this->deps->register($this->denylistCtx = $this->deps->constructLazy(Denylist\DenylistContext::class));
+        $this->deps->register($this->poolsCtx = $this->deps->constructLazy(Pools\PoolsContext::class));
+        $this->deps->register($this->storageCtx = $this->deps->constructLazy(Storage\StorageContext::class, $config->scopeTo('storage')));
+        $this->deps->register($this->tasksCtx = $this->deps->constructLazy(Tasks\TasksContext::class, $config->scopeTo('domain')));
+        $this->deps->register($this->uploadsCtx = $this->deps->constructLazy(Uploads\UploadsContext::class, $config->scopeTo('domain')));
     }
 
     public function createRouting(bool $isApiDomain): RoutingContext {
         $routingCtx = new RoutingContext($this->config->scopeTo('cors'));
-        $routingCtx->register($this->database);
-
-        $routingCtx->register($uploadsViewsRoutes = new Uploads\UploadsViewRoutes(
-            $this->poolsCtx,
-            $this->uploadsCtx,
-            $this->storageCtx,
-            $this->denylistCtx,
-            $isApiDomain,
-        ));
+        $routingCtx->register($this->dbCtx);
 
         if($isApiDomain) {
             $rpcServer = new HttpRpcServer;
@@ -53,33 +51,16 @@ class EEPROMContext {
                 new HmacVerificationProvider(fn() => $this->config->getString('rpcii:secret'))
             ));
 
-            $rpcServer->register(new Pools\PoolsRpcHandler(
-                $this->poolsCtx,
-            ));
-            $rpcServer->register(new Tasks\TasksRpcHandler(
-                $this->tasksCtx,
-                $this->poolsCtx,
-                $this->uploadsCtx,
-                $this->storageCtx,
-                $this->denylistCtx,
-            ));
-            $rpcServer->register(new Uploads\UploadsRpcHandler(
-                $this->poolsCtx,
-                $this->uploadsCtx,
-                $this->storageCtx,
-            ));
+            $rpcServer->register($this->deps->constructLazy(Pools\PoolsRpcHandler::class));
+            $rpcServer->register($this->deps->constructLazy(Tasks\TasksRpcHandler::class));
+            $rpcServer->register($this->deps->constructLazy(Uploads\UploadsRpcHandler::class));
 
-            $routingCtx->register(new Auth\AuthRoutes($this->authCtx));
-            $routingCtx->register(new Tasks\TasksRoutes($this->tasksCtx));
-            $routingCtx->register(new Uploads\UploadsLegacyRoutes(
-                $this->authCtx,
-                $this->poolsCtx,
-                $this->uploadsCtx,
-                $this->storageCtx,
-                $this->denylistCtx,
-            ));
-            $routingCtx->register(new LandingRoutes($this->database));
-        }
+            $routingCtx->register($this->deps->constructLazy(Auth\AuthRoutes::class));
+            $routingCtx->register($this->deps->constructLazy(Tasks\TasksRoutes::class));
+            $routingCtx->register($this->deps->constructLazy(Uploads\UploadsLegacyRoutes::class));
+            $routingCtx->register($this->deps->constructLazy(LandingRoutes::class));
+        } else
+            $routingCtx->register($this->deps->constructLazy(Uploads\UploadsViewRoutes::class));
 
         return $routingCtx;
     }
diff --git a/src/EEPROMErrorHandler.php b/src/EEPROMErrorHandler.php
index 48bf353..f7b0c75 100644
--- a/src/EEPROMErrorHandler.php
+++ b/src/EEPROMErrorHandler.php
@@ -1,10 +1,23 @@
 <?php
 namespace EEPROM;
 
-use Index\Http\{HttpErrorHandler,HttpResponseBuilder,HttpRequest};
+use Index\Http\HttpResponse;
+use Index\Http\Routing\HandlerContext;
+use Index\Http\Routing\ErrorHandling\ErrorHandler;
+use Index\Http\Streams\Stream;
 
-class EEPROMErrorHandler implements HttpErrorHandler {
-    public function handle(HttpResponseBuilder $response, HttpRequest $request, int $code, string $message): void {
-        $response->setContent(sprintf('<!doctype html><title>%1$03d %2$s</title><strong>%1$03d %2$s</strong>', $code, $message));
+class EEPROMErrorHandler implements ErrorHandler {
+    public function handle(HandlerContext $context): void {
+        if(!$context->response->needsBody)
+            return;
+
+        $context->response->setTypeHTML();
+        $context->response->body = Stream::createStream(sprintf(
+            '<!doctype html><title>%1$03d %2$s</title><strong>%1$03d %2$s</strong>',
+            $context->response->statusCode,
+            $context->response->reasonPhrase === ''
+                ? HttpResponse::defaultReasonPhase($context->response->statusCode)
+                : $context->response->reasonPhrase
+            ));
     }
 }
diff --git a/src/LandingRoutes.php b/src/LandingRoutes.php
index 5c6f191..e9224b9 100644
--- a/src/LandingRoutes.php
+++ b/src/LandingRoutes.php
@@ -2,22 +2,24 @@
 namespace EEPROM;
 
 use stdClass;
-use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerTrait};
+use Index\Http\HttpResponseBuilder;
+use Index\Http\Routing\{RouteHandler,RouteHandlerCommon};
+use Index\Http\Routing\Routes\ExactRoute;
 
 class LandingRoutes implements RouteHandler {
-    use RouteHandlerTrait;
+    use RouteHandlerCommon;
 
     public function __construct(
         private DatabaseContext $dbCtx
     ) {}
 
-    #[HttpGet('/')]
-    public function getIndex($response) {
+    #[ExactRoute('GET', '/')]
+    public function getIndex(HttpResponseBuilder $response) {
         $response->accelRedirect('/index.html');
         $response->setContentType('text/html; charset=utf-8');
     }
 
-    #[HttpGet('/stats.json')]
+    #[ExactRoute('GET', '/stats.json')]
     public function getStats() {
         $stats = new stdClass;
         $stats->uploads = 0;
@@ -26,13 +28,13 @@ class LandingRoutes implements RouteHandler {
         $stats->types = 0;
         $stats->members = 0;
 
-        $result = $this->dbCtx->connection->query('SELECT COUNT(*), COUNT(DISTINCT user_id) FROM prm_uploads');
+        $result = $this->dbCtx->conn->query('SELECT COUNT(*), COUNT(DISTINCT user_id) FROM prm_uploads');
         if($result->next()) {
             $stats->uploads = $result->getInteger(0);
             $stats->members = $result->getInteger(1);
         }
 
-        $result = $this->dbCtx->connection->query('SELECT COUNT(*), SUM(file_size), COUNT(DISTINCT file_type) FROM prm_files');
+        $result = $this->dbCtx->conn->query('SELECT COUNT(*), SUM(file_size), COUNT(DISTINCT file_type) FROM prm_files');
         if($result->next()) {
             $stats->files = $result->getInteger(0);
             $stats->size = $result->getInteger(1);
@@ -42,8 +44,8 @@ class LandingRoutes implements RouteHandler {
         return $stats;
     }
 
-    #[HttpGet('/eeprom.js')]
-    public function getEepromJs($response) {
+    #[ExactRoute('GET', '/eeprom.js')]
+    public function getEepromJs(HttpResponseBuilder $response) {
         $response->accelRedirect('/scripts/eepromv1.js');
         $response->setContentType('application/javascript; charset=utf-8');
     }
diff --git a/src/Pools/PoolsContext.php b/src/Pools/PoolsContext.php
index c188ab5..3b1cd5a 100644
--- a/src/Pools/PoolsContext.php
+++ b/src/Pools/PoolsContext.php
@@ -2,7 +2,9 @@
 namespace EEPROM\Pools;
 
 use RuntimeException;
+use EEPROM\Storage\StorageRecord;
 use EEPROM\Uploads\UploadVariantInfo;
+use Index\XArray;
 use Index\Db\DbConnection;
 
 class PoolsContext {
@@ -34,7 +36,7 @@ class PoolsContext {
         );
     }
 
-    public function getPoolFromVariant(UploadVariantInfo $variantInfo): StorageRecord {
+    public function getPoolFromVariant(UploadVariantInfo $variantInfo): PoolInfo {
         return $this->pools->getPool($variantInfo->fileId, PoolInfoGetField::Id);
     }
 
@@ -69,7 +71,10 @@ class PoolsContext {
         if($removeStaleRule instanceof Rules\RemoveStaleRule && $removeStaleRule->inactiveForSeconds > 0)
             $body['idle_time'] = $removeStaleRule->inactiveForSeconds;
 
-        $ensureVariantRules = XArray::select($rules->getRules(Rules\EnsureVariantRule::TYPE), fn($ensureVariantRule) => $ensureVariantRule->variant);
+        $ensureVariantRules = XArray::select(
+            $rules->getRules(Rules\EnsureVariantRule::TYPE),
+            fn($ensureVariantRule) => $ensureVariantRule->variant
+        );
         if(!empty($ensureVariantRules))
             $body['variants'] = $ensureVariantRules;
 
diff --git a/src/Pools/PoolsData.php b/src/Pools/PoolsData.php
index 6b6f733..ad4afe4 100644
--- a/src/Pools/PoolsData.php
+++ b/src/Pools/PoolsData.php
@@ -49,7 +49,6 @@ class PoolsData {
             PoolInfoGetField::Id => 'pool_id = ?',
             PoolInfoGetField::Name => 'pool_name = ?',
             PoolInfoGetField::UploadId => 'pool_id = (SELECT pool_id FROM prm_uploads WHERE upload_id = ?)',
-            default => throw new InvalidArgumentException('$field is not an acceptable value'),
         };
 
         $stmt = $this->cache->get(<<<SQL
@@ -90,8 +89,10 @@ class PoolsData {
             SELECT rule_id, pool_id, rule_type, rule_params
             FROM prm_pools_rules
         SQL;
-        if($hasPoolInfo)
-            $query .= sprintf(' %s pool_id = ?', ++$args > 1 ? 'AND' : 'WHERE');
+        if($hasPoolInfo) {
+            ++$args;
+            $query .= ' WHERE pool_id = ?';
+        }
         if($hasTypes)
             $query .= sprintf(' %s rule_type IN (%s)', ++$args > 1 ? 'AND' : 'WHERE', DbTools::prepareListString($types));
 
diff --git a/src/RoutingContext.php b/src/RoutingContext.php
index 5b0048a..b6459fc 100644
--- a/src/RoutingContext.php
+++ b/src/RoutingContext.php
@@ -2,42 +2,19 @@
 namespace EEPROM;
 
 use Index\Config\Config;
-use Index\Http\HttpRequest;
-use Index\Http\Routing\{HttpRouter,Router,RouteHandler};
+use Index\Http\{HttpResponseBuilder,HttpRequest};
+use Index\Http\Routing\{Router,RouteHandler};
+use Index\Http\Routing\Filters\FilterInfo;
 
 class RoutingContext {
-    private HttpRouter $router;
+    public private(set) Router $router;
 
-    public function __construct(private Config $config) {
-        $this->router = new HttpRouter(
+    public function __construct(Config $config) {
+        $this->router = new Router(
             errorHandler: new EEPROMErrorHandler,
+            accessControlHandler: new EEPROMAccessControlHandler($config->getArray('origins')),
         );
-        $this->router->use('/', $this->middleware(...));
-    }
-
-    private function middleware($response, $request) {
-        $response->setPoweredBy('EEPROM');
-
-        if($request->hasHeader('Origin')) {
-            $origin = $request->getHeaderLine('Origin');
-            $response->setHeader('Access-Control-Allow-Origin', $origin);
-            $response->setHeader('Vary', 'Origin');
-            $host = parse_url($origin, PHP_URL_HOST);
-            if(is_string($host)) {
-                $host = '.' . $host;
-                $allowCookieOrigins = $this->config->getArray('origins');
-                foreach($allowCookieOrigins as $allowCookieOrigin)
-                    if(str_ends_with($host, '.' . $allowCookieOrigin)) {
-                        $response->setHeader('Access-Control-Allow-Credentials', 'true');
-                        break;
-                    }
-            }
-        } else
-            $response->setHeader('Access-Control-Allow-Origin', '*');
-    }
-
-    public function getRouter(): Router {
-        return $this->router;
+        $this->router->filter(FilterInfo::prefix('/', fn(HttpResponseBuilder $response) => $response->setPoweredBy('EEPROM')));
     }
 
     public function register(RouteHandler $handler): void {
diff --git a/src/Storage/StorageFiles.php b/src/Storage/StorageFiles.php
index 105dd0e..d24577a 100644
--- a/src/Storage/StorageFiles.php
+++ b/src/Storage/StorageFiles.php
@@ -79,18 +79,13 @@ class StorageFiles {
         $target = $this->getLocalPath($hash, true);
 
         if(is_file($target)) {
-            if($mode === StorageImportMode::Move || $mode === StorageImportMode::Upload)
+            if($mode === StorageImportMode::Move)
                 unlink($path);
         } else {
             if($mode === StorageImportMode::Move)
                 $success = rename($path, $target);
             elseif($mode === StorageImportMode::Copy)
                 $success = copy($path, $target);
-            elseif($mode === StorageImportMode::Upload)
-                $success = move_uploaded_file($path, $target);
-            else
-                $success = false;
-
             if(!$success)
                 throw new RuntimeException('unable to move or copy $path using given $mode');
         }
diff --git a/src/Storage/StorageImportMode.php b/src/Storage/StorageImportMode.php
index 0dcb3cf..c4a87ec 100644
--- a/src/Storage/StorageImportMode.php
+++ b/src/Storage/StorageImportMode.php
@@ -4,5 +4,4 @@ namespace EEPROM\Storage;
 enum StorageImportMode {
     case Copy;
     case Move;
-    case Upload;
 }
diff --git a/src/Storage/StorageRecords.php b/src/Storage/StorageRecords.php
index 44fe23b..7acdffc 100644
--- a/src/Storage/StorageRecords.php
+++ b/src/Storage/StorageRecords.php
@@ -37,12 +37,13 @@ class StorageRecords {
             SQL;
 
         $args = 0;
-        if($orphaned !== null)
+        if($orphaned !== null) {
+            ++$args;
             $query .= sprintf(
-                ' %s uf.file_id %s NULL',
-                ++$args > 1 ? 'OR' : 'WHERE',
+                ' WHERE uf.file_id %s NULL',
                 $orphaned ? 'IS' : 'IS NOT'
             );
+        }
         if($denied !== null)
             $query .= sprintf(
                 ' %s dl.deny_hash %s NULL',
@@ -63,7 +64,6 @@ class StorageRecords {
         $field = match($field) {
             StorageRecordGetFileField::Id => 'file_id',
             StorageRecordGetFileField::Hash => 'file_hash',
-            default => throw new InvalidArgumentException('$field is not an acceptable value'),
         };
 
         $stmt = $this->cache->get(<<<SQL
@@ -93,14 +93,14 @@ class StorageRecords {
         if($size < 0)
             throw new InvalidArgumentException('$size may not be negative');
 
-        $fileId = $this->snowflake->nextHash($hash);
+        $fileId = (string)$this->snowflake->nextHash($hash);
 
         $stmt = $this->cache->get(<<<SQL
             INSERT INTO prm_files (
                 file_id, file_hash, file_type, file_size
             ) VALUES (?, ?, ?, ?)
         SQL);
-        $stmt->nextParameter($fileId); // generate snowflake
+        $stmt->nextParameter($fileId);
         $stmt->nextParameter($hash);
         $stmt->nextParameter($type);
         $stmt->nextParameter($size);
@@ -120,7 +120,6 @@ class StorageRecords {
             $field = match($field) {
                 StorageRecordGetFileField::Id => 'file_id',
                 StorageRecordGetFileField::Hash => 'file_hash',
-                default => throw new InvalidArgumentException('$field is not an acceptable value'),
             };
 
         $stmt = $this->cache->get(<<<SQL
@@ -142,7 +141,6 @@ class StorageRecords {
             $field = match($field) {
                 StorageRecordGetFileField::Id => '?',
                 StorageRecordGetFileField::Hash => '(SELECT file_id FROM prm_files WHERE file_hash = ?)',
-                default => throw new InvalidArgumentException('$field is not an acceptable value'),
             };
 
         $stmt = $this->cache->get(<<<SQL
diff --git a/src/Tasks/TaskInfo.php b/src/Tasks/TaskInfo.php
index 702459a..3f2b912 100644
--- a/src/Tasks/TaskInfo.php
+++ b/src/Tasks/TaskInfo.php
@@ -28,7 +28,7 @@ class TaskInfo {
             secret: $result->getString(1),
             userId: $result->getString(2),
             poolId: $result->getString(3),
-            state: TaskState::tryFrom($result->getString(4)) ?? TaskState::Ready,
+            state: TaskState::tryFrom($result->getString(4)) ?? TaskState::Pending,
             remoteAddress: $result->getString(5),
             name: $result->getString(6),
             size: $result->getInteger(7),
diff --git a/src/Tasks/TasksData.php b/src/Tasks/TasksData.php
index faa69f6..8aa00b4 100644
--- a/src/Tasks/TasksData.php
+++ b/src/Tasks/TasksData.php
@@ -3,6 +3,7 @@ namespace EEPROM\Tasks;
 
 use InvalidArgumentException;
 use RuntimeException;
+use Stringable;
 use EEPROM\SnowflakeGenerator;
 use EEPROM\Pools\PoolInfo;
 use Index\XString;
@@ -29,8 +30,10 @@ class TasksData {
                 task_name, task_size, task_type, task_hash, UNIX_TIMESTAMP(task_created)
             FROM prm_tasks
         SQL;
-        if($hasState)
-            $query .= sprintf(' %s task_state = ?', ++$args > 1 ? 'AND' : 'WHERE');
+        if($hasState) {
+            ++$args;
+            $query .= ' WHERE task_state = ?';
+        }
 
         $stmt = $this->cache->get($query);
         if($hasState)
@@ -77,7 +80,7 @@ class TasksData {
         string $type,
         string $hash
     ): TaskInfo {
-        $taskId = $this->snowflake->nextRandom();
+        $taskId = (string)$this->snowflake->nextRandom();
 
         $stmt = $this->cache->get(<<<SQL
             INSERT INTO prm_tasks (
diff --git a/src/Tasks/TasksRoutes.php b/src/Tasks/TasksRoutes.php
index 6e28184..4f24af7 100644
--- a/src/Tasks/TasksRoutes.php
+++ b/src/Tasks/TasksRoutes.php
@@ -3,39 +3,36 @@ namespace EEPROM\Tasks;
 
 use RuntimeException;
 use Index\{ByteFormat,XNumber};
-use Index\Http\StringHttpContent;
-use Index\Http\Routing\{HttpPut,RouteHandler,RouteHandlerTrait};
+use Index\Http\{HttpResponseBuilder,HttpRequest};
+use Index\Http\Routing\{HttpPut,RouteHandler,RouteHandlerCommon};
+use Index\Http\Routing\Routes\PatternRoute;
 
 class TasksRoutes implements RouteHandler {
-    use RouteHandlerTrait;
+    use RouteHandlerCommon;
 
     public function __construct(
         private TasksContext $tasksCtx,
     ) {}
 
-    #[HttpPut('/uploads/([A-Za-z0-9]+)(?:-([a-z0-9]+))?(?:\.([A-Za-z0-9\-_]+))?')]
-    public function putUpload($response, $request, string $taskId, string $variant = '', string $extension = '') {
+    #[PatternRoute('PUT', '/uploads/([A-Za-z0-9]+)(?:-([a-z0-9]+))?(?:\.([A-Za-z0-9\-_]+))?')]
+    public function putUpload(
+        HttpResponseBuilder $response,
+        HttpRequest $request,
+        string $taskId,
+        string $variant = '',
+        string $extension = '',
+    ) {
         if($request->getHeaderLine('Content-Type') !== 'application/octet-stream') {
-            $response->setStatusCode(400);
+            $response->statusCode = 400;
             return [
                 'error' => 'bad_type',
                 'english' => 'Content-Type must be application/octet-stream.',
             ];
         }
 
-        $content = $request->getContent();
-        if($content instanceof StringHttpContent) {
-            $content = (string)$content;
-        } else {
-            $response->setStatusCode(400);
-            return [
-                'error' => 'bad_content',
-                'english' => 'Request content could not be parsed as expected.',
-            ];
-        }
-
+        $content = (string)$request->getBody();
         if(strlen($taskId) < 5) {
-            $response->setStatusCode(404);
+            $response->statusCode = 404;
             return [
                 'error' => 'not_found',
                 'english' => 'Requested upload task does not exist.',
@@ -43,8 +40,8 @@ class TasksRoutes implements RouteHandler {
         }
 
         if($variant !== '') {
-            $response->setStatusCode(405);
-            $response->setHeader('Allow', 'HEAD, GET');
+            $response->statusCode = 405;
+            $response->setAllow(['HEAD', 'GET']);
             return [
                 'error' => 'method_not_allowed',
                 'english' => 'PUT requests are not supported for upload variants.',
@@ -53,7 +50,7 @@ class TasksRoutes implements RouteHandler {
 
         $length = (int)$request->getHeaderLine('Content-Length');
         if($length < 1) {
-            $response->setStatusCode(411);
+            $response->statusCode = 411;
             return [
                 'error' => 'length_required',
                 'english' => 'A Content-Length header with the size of the request body is required.',
@@ -61,7 +58,7 @@ class TasksRoutes implements RouteHandler {
         }
 
         if(strlen($content) !== $length) {
-            $response->setStatusCode(400);
+            $response->statusCode = 400;
             return [
                 'error' => 'length_mismatch',
                 'english' => 'Length specified in Content-Length differs from actual request body length.',
@@ -69,7 +66,7 @@ class TasksRoutes implements RouteHandler {
         }
 
         if($length > TasksContext::CHUNK_SIZE) {
-            $response->setStatusCode(413);
+            $response->statusCode = 413;
             return [
                 'error' => 'content_too_large',
                 'english' => sprintf(
@@ -84,7 +81,7 @@ class TasksRoutes implements RouteHandler {
 
         if($request->hasHeader('X-Content-Index')) {
             if($request->hasParam('index')) {
-                $response->setStatusCode(400);
+                $response->statusCode = 400;
                 return [
                     'error' => 'bad_param',
                     'english' => 'the index query parameter may not be used at the same time as the X-Content-Index header.',
@@ -93,19 +90,11 @@ class TasksRoutes implements RouteHandler {
 
             $index = (int)filter_var($request->getHeaderLine('X-Content-Index'), FILTER_SANITIZE_NUMBER_INT);
         } elseif($request->hasParam('index')) {
-            if($request->hasHeader('X-Content-Index')) {
-                $response->setStatusCode(400);
-                return [
-                    'error' => 'bad_param',
-                    'english' => 'the X-Content-Index header may not be used at the same time as the index query parameter.',
-                ];
-            }
-
-            $index = (int)$request->getParam('index', FILTER_SANITIZE_NUMBER_INT);
+            $index = (int)$request->getFilteredParam('index', FILTER_SANITIZE_NUMBER_INT);
         } else $index = 0;
 
         if($index < 0) {
-            $response->setStatusCode(400);
+            $response->statusCode = 400;
             return [
                 'error' => 'bad_index',
                 'english' => 'index parameter must be greater than or equal to 0.',
@@ -113,19 +102,19 @@ class TasksRoutes implements RouteHandler {
         }
 
         $taskSecret = substr($taskId, -16);
-        $taskId = XNumber::fromBase62(substr($taskId, 0, -16));
+        $taskId = (string)XNumber::fromBase62(substr($taskId, 0, -16));
 
         try {
             $taskInfo = $this->tasksCtx->tasks->getTask($taskId);
         } catch(RuntimeException $ex) {
-            $response->setStatusCode(404);
+            $response->statusCode = 404;
             return [
                 'error' => 'not_found',
                 'english' => 'Requested upload task does not exist.',
             ];
         }
         if(!$taskInfo->verifySecret($taskSecret)) {
-            $response->setStatusCode(404);
+            $response->statusCode = 404;
             return [
                 'error' => 'not_found',
                 'english' => 'Requested upload task does not exist.',
@@ -133,7 +122,7 @@ class TasksRoutes implements RouteHandler {
         }
 
         if($taskInfo->state !== TaskState::Pending) {
-            $response->setStatusCode(409);
+            $response->statusCode = 409;
             return [
                 'error' => 'not_pending',
                 'english' => 'Requested upload task is no longer accepting data.',
@@ -142,7 +131,7 @@ class TasksRoutes implements RouteHandler {
 
         $offset = TasksContext::CHUNK_SIZE * $index;
         if($offset >= $taskInfo->size) {
-            $response->setStatusCode(400);
+            $response->statusCode = 400;
             return [
                 'error' => 'offset_too_large',
                 'english' => 'Provided offset is greater than file size.',
@@ -152,7 +141,7 @@ class TasksRoutes implements RouteHandler {
         $path = $this->tasksCtx->getTaskWorkingPath($taskInfo);
         if(!is_file($path)) {
             $this->tasksCtx->tasks->setTaskError($taskInfo, sprintf('working file despawned during PUT request: %s', $path));
-            $response->setStatusCode(500);
+            $response->statusCode = 500;
             return [
                 'error' => 'server_error',
                 'english' => 'Working file no longer exist on the server, the upload process cannot continue.',
@@ -162,52 +151,50 @@ class TasksRoutes implements RouteHandler {
         $handle = fopen($path, 'rb+');
         if($handle === false) {
             $this->tasksCtx->tasks->setTaskError($taskInfo, sprintf('failed to open during PUT request: %s', $path));
-            $response->setStatusCode(500);
+            $response->statusCode = 500;
             return [
                 'error' => 'server_error',
                 'english' => 'Was not able to open working file, the upload process cannot continue.',
             ];
         }
 
-        try {
-            if(fseek($handle, $offset, SEEK_SET) < 0) {
-                $this->tasksCtx->tasks->setTaskError($taskInfo, sprintf('was unable to seek during PUT request: %s', $path));
-                $response->setStatusCode(500);
-                return [
-                    'error' => 'server_error',
-                    'english' => 'Was not able to seek to provided offset, the upload process cannot continue.',
-                ];
-            }
-
-            if(fwrite($handle, $content, $length) === false) {
-                $this->tasksCtx->tasks->setTaskError($taskInfo, sprintf('was unable to write during PUT request: %s', $path));
-                $response->setStatusCode(500);
-                return [
-                    'error' => 'server_error',
-                    'english' => 'Was not able to seek to write to file, the upload process cannot continue.',
-                ];
-            }
-        } finally {
-            if(!fflush($handle)) {
-                $this->tasksCtx->tasks->setTaskError($taskInfo, sprintf('failed to flush file contents during PUT request: %s', $path));
-                $response->setStatusCode(500);
-                return [
-                    'error' => 'server_error',
-                    'english' => 'Was not able to flush file properly, the upload process cannot continue.',
-                ];
-            }
-
-            if(!fclose($handle)) {
-                $this->tasksCtx->tasks->setTaskError($taskInfo, sprintf('failed to close file contents during PUT request: %s', $path));
-                $response->setStatusCode(500);
-                return [
-                    'error' => 'server_error',
-                    'english' => 'Was not able to close file properly, the upload process cannot continue.',
-                ];
-            }
+        if(fseek($handle, $offset, SEEK_SET) < 0) {
+            $this->tasksCtx->tasks->setTaskError($taskInfo, sprintf('was unable to seek during PUT request: %s', $path));
+            $response->statusCode = 500;
+            return [
+                'error' => 'server_error',
+                'english' => 'Was not able to seek to provided offset, the upload process cannot continue.',
+            ];
         }
 
-        $response->setStatusCode(202);
+        if(fwrite($handle, $content, $length) === false) {
+            $this->tasksCtx->tasks->setTaskError($taskInfo, sprintf('was unable to write during PUT request: %s', $path));
+            $response->statusCode = 500;
+            return [
+                'error' => 'server_error',
+                'english' => 'Was not able to seek to write to file, the upload process cannot continue.',
+            ];
+        }
+
+        if(!fflush($handle)) {
+            $this->tasksCtx->tasks->setTaskError($taskInfo, sprintf('failed to flush file contents during PUT request: %s', $path));
+            $response->statusCode = 500;
+            return [
+                'error' => 'server_error',
+                'english' => 'Was not able to flush file properly, the upload process cannot continue.',
+            ];
+        }
+
+        if(!fclose($handle)) {
+            $this->tasksCtx->tasks->setTaskError($taskInfo, sprintf('failed to close file contents during PUT request: %s', $path));
+            $response->statusCode = 500;
+            return [
+                'error' => 'server_error',
+                'english' => 'Was not able to close file properly, the upload process cannot continue.',
+            ];
+        }
+
+        $response->statusCode = 202;
         return '';
     }
 }
diff --git a/src/Tasks/TasksRpcHandler.php b/src/Tasks/TasksRpcHandler.php
index 4d6fb72..83aaf93 100644
--- a/src/Tasks/TasksRpcHandler.php
+++ b/src/Tasks/TasksRpcHandler.php
@@ -65,11 +65,8 @@ final class TasksRpcHandler implements RpcHandler {
             if($signature === null)
                 return 'srpp';
 
-            if(!str_contains($signature, '=')) {
+            if(!str_contains($signature, '='))
                 $signature = UriBase64::decode($signature);
-                if(!is_string($signature))
-                    $signature = '';
-            }
 
             $signature = array_column(XArray::select(
                 explode(';', $signature),
@@ -94,7 +91,7 @@ final class TasksRpcHandler implements RpcHandler {
             if($sAlgo !== 'S256')
                 return 'sanv';
 
-            if(abs($cTime - $sTime) >= 30)
+            if(abs($cTime - (int)$sTime) >= 30)
                 return 'stnv';
 
             if(strlen($sSalt) < 20)
@@ -182,15 +179,13 @@ final class TasksRpcHandler implements RpcHandler {
                 return 'wfcf';
             }
 
-            try {
-                if(!ftruncate($data, $fileSize)) {
-                    $this->tasksCtx->tasks->setTaskError($taskInfo, sprintf('failed to reserve %d bytes for task working file: %s', $fileSize, $path));
-                    return 'wfrf';
-                }
-            } finally {
-                if(!fclose($data))
-                    return 'wfff';
+            if(!ftruncate($data, $fileSize)) {
+                $this->tasksCtx->tasks->setTaskError($taskInfo, sprintf('failed to reserve %d bytes for task working file: %s', $fileSize, $path));
+                return 'wfrf';
             }
+
+            if(!fclose($data))
+                return 'wfff';
         } catch(RuntimeException $ex) {
             return 'utcf';
         }
@@ -210,7 +205,7 @@ final class TasksRpcHandler implements RpcHandler {
     #[RpcQuery('eeprom:tasks:get')]
     public function actionTasksGet(string $taskId): string|array {
         $taskSecret = substr($taskId, -16);
-        $taskId = XNumber::fromBase62(substr($taskId, 0, -16));
+        $taskId = (string)XNumber::fromBase62(substr($taskId, 0, -16));
 
         try {
             $taskInfo = $this->tasksCtx->tasks->getTask($taskId);
@@ -256,7 +251,7 @@ final class TasksRpcHandler implements RpcHandler {
     #[RpcAction('eeprom:tasks:finish')]
     public function actionsTasksFinish(string $taskId) {
         $taskSecret = substr($taskId, -16);
-        $taskId = XNumber::fromBase62(substr($taskId, 0, -16));
+        $taskId = (string)XNumber::fromBase62(substr($taskId, 0, -16));
 
         try {
             $taskInfo = $this->tasksCtx->tasks->getTask($taskId);
diff --git a/src/Uploads/UploadsContext.php b/src/Uploads/UploadsContext.php
index facd161..ea4596a 100644
--- a/src/Uploads/UploadsContext.php
+++ b/src/Uploads/UploadsContext.php
@@ -77,7 +77,7 @@ class UploadsContext {
             'hash' => $fileInfo->hashHexString,
             'created' => $uploadInfo->createdAt->toIso8601ZuluString(),
             'accessed' => $uploadInfo->accessedAt->toIso8601ZuluString(),
-            'expires' => $uploadInfo->accessedAt->add(2, 'weeks')->toIso8601ZuluString(),
+            'expires' => $uploadInfo->accessedAt->add('weeks', 2)->toIso8601ZuluString(),
 
             // These can never be reached, and in situation where they technically could it's because of an outdated local record
             'deleted' => null,
diff --git a/src/Uploads/UploadsData.php b/src/Uploads/UploadsData.php
index 16036a4..162c8c4 100644
--- a/src/Uploads/UploadsData.php
+++ b/src/Uploads/UploadsData.php
@@ -13,7 +13,7 @@ class UploadsData {
     private DbStatementCache $cache;
 
     public function __construct(
-        private DbConnection $dbConn,
+        DbConnection $dbConn,
         private SnowflakeGenerator $snowflake
     ) {
         $this->cache = new DbStatementCache($dbConn);
@@ -48,8 +48,10 @@ class UploadsData {
             SQL;
 
         $args = 0;
-        if($hasPoolInfo)
-            $query .= sprintf(' %s u.pool_id = ?', ++$args > 1 ? 'AND' : 'WHERE');
+        if($hasPoolInfo) {
+            ++$args;
+            $query .= ' WHERE u.pool_id = ?';
+        }
         if($hasUserId)
             $query .= sprintf(' %s u.user_id = ?', ++$args > 1 ? 'AND' : 'WHERE');
         if($hasVariantExists)
diff --git a/src/Uploads/UploadsLegacyRoutes.php b/src/Uploads/UploadsLegacyRoutes.php
index 52c56a9..2e41722 100644
--- a/src/Uploads/UploadsLegacyRoutes.php
+++ b/src/Uploads/UploadsLegacyRoutes.php
@@ -5,12 +5,19 @@ use RuntimeException;
 use EEPROM\Auth\AuthContext;
 use EEPROM\Denylist\{DenylistContext,DenylistReason};
 use EEPROM\Pools\PoolsContext;
+use EEPROM\Pools\Rules\{ConstrainSizeRule,EnsureVariantRule,EnsureVariantRuleThumb};
 use EEPROM\Storage\{StorageContext,StorageImportMode,StorageRecordGetFileField};
-use Index\{ByteFormat,XNumber};
-use Index\Http\Routing\{HttpDelete,HttpOptions,HttpPost,RouteHandler,RouteHandlerTrait};
+use Index\{ByteFormat,XArray,XNumber};
+use Index\Http\{HttpResponseBuilder,HttpRequest};
+use Index\Http\Content\MultipartFormContent;
+use Index\Http\Content\Multipart\FileMultipartFormData;
+use Index\Http\Routing\{HttpDelete,HttpOptions,HttpPost,RouteHandler,RouteHandlerCommon};
+use Index\Http\Routing\AccessControl\AccessControl;
+use Index\Http\Routing\Processors\Before;
+use Index\Http\Routing\Routes\{ExactRoute,PatternRoute};
 
 class UploadsLegacyRoutes implements RouteHandler {
-    use RouteHandlerTrait;
+    use RouteHandlerCommon;
 
     public function __construct(
         private AuthContext $authCtx,
@@ -20,21 +27,111 @@ class UploadsLegacyRoutes implements RouteHandler {
         private DenylistContext $denylistCtx
     ) {}
 
-    #[HttpOptions('/uploads')]
-    public function optionsUpload($response, $request): int {
-        $response->setHeader('Access-Control-Allow-Headers', 'Authorization');
-        $response->setHeader('Access-Control-Allow-Methods', 'POST');
+    #[AccessControl]
+    #[PatternRoute('GET', '/uploads/([A-Za-z0-9]+|[A-Za-z0-9\-_]{32})(?:-([a-z0-9]+))?(?:\.([A-Za-z0-9\-_]+))?')]
+    public function getUpload(
+        HttpResponseBuilder $response,
+        HttpRequest $request,
+        string $uploadId,
+        string $variant = '',
+        string $extension = ''
+    ) {
+        if(strlen($uploadId) < 5)
+            return 404;
 
-        return 204;
+        $variantQuery = false;
+        if($variant === '' && $request->hasParam('v')) {
+            $variantQuery = true;
+            $variant = (string)$request->getParam('v');
+        }
+
+        $isV1Json = ($variantQuery || $variant === '') && $extension === 'json';
+        if($isV1Json)
+            $variant = '';
+
+        if(strlen($uploadId) === 32) {
+            $uploadId = $this->uploadsCtx->uploads->resolveLegacyId($uploadId) ?? $uploadId;
+            $uploadSecret = null;
+        } else {
+            $uploadSecret = substr($uploadId, -4);
+            $uploadId = XNumber::fromBase62(substr($uploadId, 0, -4));
+        }
+
+        try {
+            $uploadInfo = $this->uploadsCtx->uploads->getUpload(uploadId: $uploadId, secret: $uploadSecret);
+        } catch(RuntimeException $ex) {
+            return 404;
+        }
+
+        try {
+            $variantInfo = $this->uploadsCtx->uploads->getUploadVariant($uploadInfo, $variant);
+        } catch(RuntimeException $ex) {
+            if($variant === '')
+                return 404;
+
+            try {
+                $poolInfo = $this->poolsCtx->pools->getPool($uploadInfo->poolId);
+                $originalInfo = $this->storageCtx->records->getFile(
+                    $this->uploadsCtx->uploads->getUploadVariant($uploadInfo, '')->fileId
+                );
+
+                $rule = XArray::first(
+                    $this->poolsCtx->getPoolRules($poolInfo)->getRules(EnsureVariantRule::TYPE),
+                    fn($rule) => $rule->variant === $variant
+                );
+                if($rule instanceof EnsureVariantRule && $rule->onAccess) {
+                    if($rule->params instanceof EnsureVariantRuleThumb) {
+                        $storageInfo = $this->storageCtx->createThumbnailFromRule($originalInfo, $rule->params);
+                        $variantInfo = $this->uploadsCtx->uploads->createUploadVariant($uploadInfo, $rule->variant, $storageInfo);
+                    }
+                }
+            } catch(RuntimeException $ex) {
+                return 404;
+            }
+        }
+
+        if(!isset($variantInfo))
+            return 404;
+
+        if(!isset($storageInfo))
+            try {
+                $storageInfo = $this->storageCtx->records->getFile($variantInfo->fileId);
+            } catch(RuntimeException $ex) {
+                return 404;
+            }
+
+        $this->uploadsCtx->uploads->updateUpload(
+            $uploadInfo,
+            accessedAt: true,
+        );
+
+        $denyInfo = $this->denylistCtx->getDenylistEntry($storageInfo);
+        if($denyInfo !== null)
+            return $denyInfo->isCopyrightTakedown ? 451 : 410;
+
+        if($isV1Json)
+            return $this->uploadsCtx->convertToClientJsonV1($uploadInfo, $variantInfo, $storageInfo);
+
+        $fileName = $uploadInfo->name;
+        if($variantInfo->variant !== '') // FAIRLY SIGNIFICANT TODO: obviously this should support more file exts than .jpg...
+            $fileName = sprintf('%s-%s.jpg', pathinfo($fileName, PATHINFO_FILENAME), $variantInfo->variant);
+
+        $contentType = $variantInfo->type ?? $storageInfo->type;
+        if($contentType === 'application/octet-stream' || str_starts_with($contentType, 'text/'))
+            $contentType = 'text/plain';
+
+        $response->accelRedirect($this->storageCtx->files->getRemotePath($storageInfo));
+        $response->setContentType($contentType);
+        $response->setFileName(addslashes($fileName));
     }
 
-    #[HttpPost('/uploads')]
-    public function postUpload($response, $request) {
-        if(!$request->isFormContent())
-            return 400;
-
+    #[AccessControl(credentials: true, allowHeaders: ['Authorization'])]
+    #[Before('input:multipart')]
+    #[Before('eeprom:attempt-auth')]
+    #[ExactRoute('POST', '/uploads')]
+    public function postUpload(HttpResponseBuilder $response, HttpRequest $request, MultipartFormContent $content) {
         if(!$this->authCtx->info->authed) {
-            $response->setStatusCode(401);
+            $response->statusCode = 401;
             return [
                 'error' => 'auth',
                 'english' => "You must be logged in to upload files. If you are and you're getting this error anyway, try refreshing the page!",
@@ -42,29 +139,25 @@ class UploadsLegacyRoutes implements RouteHandler {
         }
 
         if($this->authCtx->info->restricted) {
-            $response->setStatusCode(403);
+            $response->statusCode = 403;
             return [
                 'error' => 'restricted',
                 'english' => 'You are currently banned and are not allowed to upload or manage files.',
             ];
         }
 
-        $content = $request->getContent();
-
-        try {
-            $file = $content->getUploadedFile('file');
-        } catch(RuntimeException $ex) {
-            $response->setStatusCode(400);
+        $file = $content->getParamData('file');
+        if(!($file instanceof FileMultipartFormData)) {
+            $response->statusCode = 400;
             return [
                 'error' => 'file',
                 'english' => 'File upload is missing from request body.',
             ];
         }
 
-        $localFile = $file->getLocalFileName();
-        $fileSize = filesize($localFile);
+        $fileSize = $file->getSize();
         if($fileSize < 1) {
-            $response->setStatusCode(400);
+            $response->statusCode = 400;
             return [
                 'error' => 'empty',
                 'english' => 'The file you attempted to upload is empty.',
@@ -72,16 +165,16 @@ class UploadsLegacyRoutes implements RouteHandler {
         }
 
         try {
-            $poolInfo = $this->poolsCtx->pools->getPool((string)$content->getParam('src', FILTER_VALIDATE_INT));
+            $poolInfo = $this->poolsCtx->pools->getPool((string)$content->getFilteredParam('src', FILTER_VALIDATE_INT));
             if($poolInfo->protected) {
-                $response->setStatusCode(404);
+                $response->statusCode = 404;
                 return [
                     'error' => 'pool_incompatible',
                     'english' => 'The target upload pool is not compatible with this endpoint.',
                 ];
             }
         } catch(RuntimeException $ex) {
-            $response->setStatusCode(404);
+            $response->statusCode = 404;
             return [
                 'error' => 'pool',
                 'english' => 'The target upload pool does not exist.',
@@ -92,8 +185,8 @@ class UploadsLegacyRoutes implements RouteHandler {
 
         // If there's no constrain_size rule on this pool its likely not meant to be used through this API!
         $maxSizeRule = $rules->getRule('constrain_size');
-        if($maxSizeRule === null) {
-            $response->setStatusCode(500);
+        if(!($maxSizeRule instanceof ConstrainSizeRule)) {
+            $response->statusCode = 500;
             return [
                 'error' => 'size_rule_missing',
                 'english' => 'A pool size constraint rule is required any this pool does not specify one.',
@@ -104,10 +197,10 @@ class UploadsLegacyRoutes implements RouteHandler {
         if($maxSizeRule->allowLegacyMultiplier)
             $maxFileSize *= $this->authCtx->info->legacyMultiplier;
 
-        if($file->getSize() !== $fileSize || $fileSize > $maxFileSize) {
-            $response->setStatusCode(413);
+        if($fileSize > $maxFileSize) {
+            $response->statusCode = 413;
             $response->setHeader('Access-Control-Expose-Headers', 'X-EEPROM-Max-Size');
-            $response->setHeader('X-EEPROM-Max-Size', $maxFileSize);
+            $response->setHeader('X-EEPROM-Max-Size', (string)$maxFileSize);
             return [
                 'error' => 'size',
                 'english' => sprintf(
@@ -120,11 +213,13 @@ class UploadsLegacyRoutes implements RouteHandler {
             ];
         }
 
-        $hash = hash_file('sha256', $localFile, true);
+        $tmpFile = tempnam(sys_get_temp_dir(), 'eeprom-upload-');
+        $file->moveTo($tmpFile);
+        $hash = hash_file('sha256', $tmpFile, true);
 
         $denyInfo = $this->denylistCtx->getDenylistEntry($hash);
         if($denyInfo !== null) {
-            $response->setStatusCode(451);
+            $response->statusCode = 451;
             return [
                 'error' => 'takedown',
                 'english' => sprintf(
@@ -139,17 +234,17 @@ class UploadsLegacyRoutes implements RouteHandler {
             ];
         }
 
-        $fileName = $file->getSuggestedFileName();
-        $mediaType = (string)$file->getSuggestedMediaType();
-        if($mediaType === '/' || $mediaType === 'application/octet-stream')
-            $mediaType = mime_content_type($localFile);
+        $fileName = $file->getClientFilename();
+        $mediaType = $file->getClientMediaType();
+        if($mediaType === null || $mediaType === 'application/octet-stream')
+            $mediaType = mime_content_type($tmpFile);
 
         try {
             $storageInfo = $this->storageCtx->records->getFile($hash, StorageRecordGetFileField::Hash);
         } catch(RuntimeException $ex) {
             $storageInfo = $this->storageCtx->importFile(
-                $file->getLocalFileName(),
-                StorageImportMode::Upload,
+                $tmpFile,
+                StorageImportMode::Move,
                 $mediaType
             );
         }
@@ -172,14 +267,14 @@ class UploadsLegacyRoutes implements RouteHandler {
                 $poolInfo,
                 $this->authCtx->info->userId,
                 $fileName,
-                $request->getRemoteAddress()
+                $request->remoteAddress
             );
             $variantInfo = $this->uploadsCtx->uploads->createUploadVariant(
                 $uploadInfo, '', $storageInfo, $mediaType
             );
         }
 
-        $response->setStatusCode(201);
+        $response->statusCode = 201;
         $response->setHeader('Content-Type', 'application/json; charset=utf-8');
 
         return $this->uploadsCtx->convertToClientJsonV1($uploadInfo, $variantInfo, $storageInfo, [
@@ -187,10 +282,12 @@ class UploadsLegacyRoutes implements RouteHandler {
         ]);
     }
 
-    #[HttpDelete('/uploads/([A-Za-z0-9]+|[A-Za-z0-9\-_]{32})(?:-([a-z0-9]+))?(?:\.([A-Za-z0-9\-_]+))?')]
-    public function deleteUpload($response, $request, string $uploadId) {
+    #[AccessControl(credentials: true, allowHeaders: ['Authorization'])]
+    #[Before('eeprom:attempt-auth')]
+    #[PatternRoute('DELETE', '/uploads/([A-Za-z0-9]+|[A-Za-z0-9\-_]{32})(?:-([a-z0-9]+))?(?:\.([A-Za-z0-9\-_]+))?')]
+    public function deleteUpload(HttpResponseBuilder $response, HttpRequest $request, string $uploadId) {
         if(!$this->authCtx->info->authed) {
-            $response->setStatusCode(401);
+            $response->statusCode = 401;
             return [
                 'error' => 'auth',
                 'english' => "You must be logged in to delete uploaded files. If you are and you're getting this error anyway, try refreshing the page!",
@@ -198,7 +295,7 @@ class UploadsLegacyRoutes implements RouteHandler {
         }
 
         if(strlen($uploadId) < 5) {
-            $response->setStatusCode(404);
+            $response->statusCode = 404;
             return [
                 'error' => 'none',
                 'english' => 'That file does not exist.',
@@ -216,7 +313,7 @@ class UploadsLegacyRoutes implements RouteHandler {
         try {
             $uploadInfo = $this->uploadsCtx->uploads->getUpload(uploadId: $uploadId, secret: $uploadSecret);
         } catch(RuntimeException $ex) {
-            $response->setStatusCode(404);
+            $response->statusCode = 404;
             return [
                 'error' => 'none',
                 'english' => 'That file does not exist.',
@@ -224,7 +321,7 @@ class UploadsLegacyRoutes implements RouteHandler {
         }
 
         if($this->authCtx->info->restricted) {
-            $response->setStatusCode(403);
+            $response->statusCode = 403;
             return [
                 'error' => 'restricted',
                 'english' => 'You are currently banned and are not allowed to upload or manage files.',
@@ -232,7 +329,7 @@ class UploadsLegacyRoutes implements RouteHandler {
         }
 
         if($this->authCtx->info->userId !== $uploadInfo->userId) {
-            $response->setStatusCode(403);
+            $response->statusCode = 403;
             return [
                 'error' => 'owner',
                 'english' => "You aren't allowed to delete files uploaded by others.",
diff --git a/src/Uploads/UploadsRpcHandler.php b/src/Uploads/UploadsRpcHandler.php
index 1389520..0465878 100644
--- a/src/Uploads/UploadsRpcHandler.php
+++ b/src/Uploads/UploadsRpcHandler.php
@@ -71,7 +71,7 @@ final class UploadsRpcHandler implements RpcHandler {
             return [
                 'result' => $this->uploadsCtx->extractUpload(
                     $this->uploadsCtx->uploads->getUpload(
-                        uploadId: XNumber::fromBase62(substr($uploadId, 0, -4)),
+                        uploadId: (string)XNumber::fromBase62(substr($uploadId, 0, -4)),
                         secret: substr($uploadId, -4),
                         userId: $userId
                     ),
@@ -93,7 +93,7 @@ final class UploadsRpcHandler implements RpcHandler {
 
         try {
             $uploadInfo = $this->uploadsCtx->uploads->getUpload(
-                uploadId: XNumber::fromBase62(substr($uploadId, 0, -4)),
+                uploadId: (string)XNumber::fromBase62(substr($uploadId, 0, -4)),
                 secret: substr($uploadId, -4),
             );
         } catch(RuntimeException $ex) {
@@ -120,7 +120,7 @@ final class UploadsRpcHandler implements RpcHandler {
 
         try {
             $uploadInfo = $this->uploadsCtx->uploads->getUpload(
-                uploadId: XNumber::fromBase62(substr($uploadId, 0, -4)),
+                uploadId: (string)XNumber::fromBase62(substr($uploadId, 0, -4)),
                 secret: substr($uploadId, -4),
             );
         } catch(RuntimeException $ex) {
@@ -149,7 +149,7 @@ final class UploadsRpcHandler implements RpcHandler {
 
         try {
             $uploadInfo = $this->uploadsCtx->uploads->getUpload(
-                uploadId: XNumber::fromBase62(substr($uploadId, 0, -4)),
+                uploadId: (string)XNumber::fromBase62(substr($uploadId, 0, -4)),
                 secret: substr($uploadId, -4),
             );
         } catch(RuntimeException $ex) {
diff --git a/src/Uploads/UploadsViewRoutes.php b/src/Uploads/UploadsViewRoutes.php
index 7150168..967ccdc 100644
--- a/src/Uploads/UploadsViewRoutes.php
+++ b/src/Uploads/UploadsViewRoutes.php
@@ -7,39 +7,30 @@ use EEPROM\Pools\PoolsContext;
 use EEPROM\Pools\Rules\{EnsureVariantRule,EnsureVariantRuleThumb};
 use EEPROM\Storage\StorageContext;
 use Index\{XArray,XNumber};
-use Index\Http\Routing\{HandlerAttribute,HttpGet,HttpOptions,Router,RouteHandler};
+use Index\Http\{HttpResponseBuilder,HttpRequest};
+use Index\Http\Routing\{Router,RouteHandler,RouteHandlerCommon};
+use Index\Http\Routing\AccessControl\AccessControl;
+use Index\Http\Routing\Routes\PatternRoute;
 
 class UploadsViewRoutes implements RouteHandler {
+    use RouteHandlerCommon;
+
     public function __construct(
         private PoolsContext $poolsCtx,
         private UploadsContext $uploadsCtx,
         private StorageContext $storageCtx,
-        private DenylistContext $denylistCtx,
-        private bool $isApiDomain
+        private DenylistContext $denylistCtx
     ) {}
 
-    public function registerRoutes(Router $router): void {
-        if($this->isApiDomain)
-            $router = $router->scopeTo('/uploads');
-
-        HandlerAttribute::register($router, $this);
-    }
-
-    #[HttpOptions('/([A-Za-z0-9]+|[A-Za-z0-9\-_]{32})(?:-([a-z0-9]+))?(?:\.([A-Za-z0-9\-_]+))?')]
-    public function optionsUpload($response, $request, string $uploadId, string $variant = '', string $extension = ''): int {
-        if($this->isApiDomain && $variant === '') {
-            $response->setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type, Content-Length, X-Content-Index');
-            $response->setHeader('Access-Control-Allow-Methods', 'HEAD, GET, PUT, DELETE');
-            $response->setHeader('Access-Control-Max-Age', '300');
-        } else {
-            $response->setHeader('Access-Control-Allow-Methods', 'HEAD, GET');
-        }
-
-        return 204;
-    }
-
-    #[HttpGet('/([A-Za-z0-9]+|[A-Za-z0-9\-_]{32})(?:-([a-z0-9]+))?(?:\.([A-Za-z0-9\-_]+))?')]
-    public function getUpload($response, $request, string $uploadId, string $variant = '', string $extension = '') {
+    #[AccessControl]
+    #[PatternRoute('GET', '/([A-Za-z0-9]+|[A-Za-z0-9\-_]{32})(?:-([a-z0-9]+))?(?:\.([A-Za-z0-9\-_]+))?')]
+    public function getUpload(
+        HttpResponseBuilder $response,
+        HttpRequest $request,
+        string $uploadId,
+        string $variant = '',
+        string $extension = ''
+    ) {
         if(strlen($uploadId) < 5)
             return 404;
 
@@ -49,10 +40,6 @@ class UploadsViewRoutes implements RouteHandler {
             $variant = (string)$request->getParam('v');
         }
 
-        $isV1Json = $this->isApiDomain && ($variantQuery || $variant === '') && $extension === 'json';
-        if($isV1Json)
-            $variant = '';
-
         if(strlen($uploadId) === 32) {
             $uploadId = $this->uploadsCtx->uploads->resolveLegacyId($uploadId) ?? $uploadId;
             $uploadSecret = null;
@@ -113,9 +100,6 @@ class UploadsViewRoutes implements RouteHandler {
         if($denyInfo !== null)
             return $denyInfo->isCopyrightTakedown ? 451 : 410;
 
-        if($isV1Json)
-            return $this->uploadsCtx->convertToClientJsonV1($uploadInfo, $variantInfo, $storageInfo);
-
         $fileName = $uploadInfo->name;
         if($variantInfo->variant !== '') // FAIRLY SIGNIFICANT TODO: obviously this should support more file exts than .jpg...
             $fileName = sprintf('%s-%s.jpg', pathinfo($fileName, PATHINFO_FILENAME), $variantInfo->variant);
diff --git a/tools/migrate b/tools/migrate
index b899dc7..4166d9c 100755
--- a/tools/migrate
+++ b/tools/migrate
@@ -2,20 +2,20 @@
 <?php
 require_once __DIR__ . '/../eeprom.php';
 
-$lockPath = $eeprom->database->getMigrateLockPath();
+$lockPath = $eeprom->dbCtx->getMigrateLockPath();
 if(is_file($lockPath))
     die('A migration script is already running.' . PHP_EOL);
 
 touch($lockPath);
 try {
     echo 'Creating migration manager...' . PHP_EOL;
-    $manager = $eeprom->database->createMigrationManager();
+    $manager = $eeprom->dbCtx->createMigrationManager();
 
     echo 'Preparing to run migrations...' . PHP_EOL;
     $manager->init();
 
     echo 'Creating migration repository...' . PHP_EOL;
-    $repo = $eeprom->database->createMigrationRepo();
+    $repo = $eeprom->dbCtx->createMigrationRepo();
 
     echo 'Running migrations...' . PHP_EOL;
     $completed = $manager->processMigrations($repo);
diff --git a/tools/migrate-override-toggle b/tools/migrate-override-toggle
index 877d51e..0fafc00 100755
--- a/tools/migrate-override-toggle
+++ b/tools/migrate-override-toggle
@@ -2,7 +2,7 @@
 <?php
 require_once __DIR__ . '/../eeprom.php';
 
-$lockPath = $eeprom->database->getMigrateLockPath();
+$lockPath = $eeprom->dbCtx->getMigrateLockPath();
 if(is_file($lockPath)) {
     printf('Removing migration lock...%s', PHP_EOL);
     unlink($lockPath);
diff --git a/tools/new-migration b/tools/new-migration
index 5ed2eb2..919ae9e 100755
--- a/tools/new-migration
+++ b/tools/new-migration
@@ -4,14 +4,14 @@ use Index\Db\Migration\FsDbMigrationRepo;
 
 require_once __DIR__ . '/../eeprom.php';
 
-$repo = $eeprom->database->createMigrationRepo();
+$repo = $eeprom->dbCtx->createMigrationRepo();
 if(!($repo instanceof FsDbMigrationRepo)) {
     echo 'Migration repository type does not support creation of templates.' . PHP_EOL;
     return;
 }
 
 $baseName = implode(' ', array_slice($argv, 1));
-$manager = $eeprom->database->createMigrationManager();
+$manager = $eeprom->dbCtx->createMigrationManager();
 
 try {
     $names = $manager->createNames($baseName);