diff --git a/src/util/util/Url.ts b/src/util/util/Url.ts
new file mode 100644
index 0000000..808bdea
--- /dev/null
+++ b/src/util/util/Url.ts
@@ -0,0 +1,41 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2025 Spacebar and Spacebar Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+export function normalizeUrl(input: string): string {
+ try {
+ const u = new URL(input);
+ // Remove fragment
+ u.hash = "";
+ // Normalize pathname - remove trailing slash except for root "/"
+ if (u.pathname !== "/" && u.pathname.endsWith("/")) {
+ u.pathname = u.pathname.slice(0, -1);
+ }
+ // Normalize query params: sort by key
+ if (u.search) {
+ const params = Array.from(u.searchParams.entries());
+ params.sort(([a], [b]) => a.localeCompare(b));
+ u.search = params.length ? "?" + params.map(([k, v]) => `${k}=${v}`).join("&") : "";
+ } else {
+ // Ensure no empty search string
+ u.search = "";
+ }
+ return u.toString();
+ } catch (e) {
+ return input;
+ }
+}
diff --git a/src/util/util/extensions/Url.test.ts b/src/util/util/extensions/Url.test.ts
deleted file mode 100644
index 37afd0a..0000000
--- a/src/util/util/extensions/Url.test.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import moduleAlias from "module-alias";
-moduleAlias();
-import './Url';
-import { describe, it } from 'node:test';
-import assert from 'node:assert/strict';
-
-describe("URL extensions", () => {
-
- it("normalize", async () => {
- const tests: [string, string][] = [
- ["http://example.com", "http://example.com/"],
- ["http://example.com/", "http://example.com/"],
- ["http://example.com/path/", "http://example.com/path"],
- ["http://example.com/path//", "http://example.com/path/"],
- ["http://example.com/path?b=2&a=1", "http://example.com/path?a=1&b=2"],
- ["http://example.com/path?b=2&a=1&", "http://example.com/path?a=1&b=2"],
- ["http://example.com/path?", "http://example.com/path"],
- ["http://example.com/path#fragment", "http://example.com/path"],
- ["http://example.com/path/?b=2&a=1#fragment", "http://example.com/path?a=1&b=2"],
- ["ftp://example.com/resource/", "ftp://example.com/resource"],
- ["https://example.com/resource?z=3&y=2&x=1", "https://example.com/resource?x=1&y=2&z=3"],
- ["https://example.com/resource?z=3&y=2&x=1#", "https://example.com/resource?x=1&y=2&z=3"],
- ["https://example.com/resource?z=3&y=2&x=1#section", "https://example.com/resource?x=1&y=2&z=3"],
- ["https://example.com/resource/?z=3&y=2&x=1#section", "https://example.com/resource?x=1&y=2&z=3"],
- ["https://example.com/resource//?z=3&y=2&x=1#section", "https://example.com/resource/?x=1&y=2&z=3"],
- ["https://example.com/", "https://example.com/"],
- ["https://example.com", "https://example.com/"],
- ];
- for (const [input, expected] of tests) {
- assert.doesNotThrow(() => new URL(input), `URL("${input}") should not throw`);
- const url = new URL(input);
- const normalized = url.normalize();
- assert.strictEqual(normalized, expected, `normalize("${input}") = "${normalized}", expected "${expected}"`);
- }
- });
-
-});
\ No newline at end of file
diff --git a/src/util/util/extensions/Url.ts b/src/util/util/extensions/Url.ts
deleted file mode 100644
index c0f07b3..0000000
--- a/src/util/util/extensions/Url.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2025 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-declare module "url" {
- interface URL {
- normalize(): string;
- }
-}
-
-/**
- * Normalize a URL by:
- * - Removing trailing slashes (except root path)
- * - Sorting query params alphabetically
- * - Removing empty query strings
- * - Removing fragments
- */
-export function normalizeUrl(input: string): string {
- try {
- const u = new URL(input);
- // Remove fragment
- u.hash = "";
- // Normalize pathname - remove trailing slash except for root "/"
- if (u.pathname !== "/" && u.pathname.endsWith("/")) {
- u.pathname = u.pathname.slice(0, -1);
- }
- // Normalize query params: sort by key
- if (u.search) {
- const params = Array.from(u.searchParams.entries());
- params.sort(([a], [b]) => a.localeCompare(b));
- u.search = params.length ? "?" + params.map(([k, v]) => `${k}=${v}`).join("&") : "";
- } else {
- // Ensure no empty search string
- u.search = "";
- }
- return u.toString();
- } catch (e) {
- return input;
- }
-}
-
-// register extensions
-if (!URL.prototype.normalize)
- URL.prototype.normalize = function () {
- return normalizeUrl(this.toString());
- };
diff --git a/src/util/util/extensions/index.ts b/src/util/util/extensions/index.ts
index afd6c0b..b7dcf4d 100644
--- a/src/util/util/extensions/index.ts
+++ b/src/util/util/extensions/index.ts
@@ -1,3 +1,2 @@
export * from "./Array";
-export * from "./Url";
export * from "./String";
diff --git a/src/util/util/index.ts b/src/util/util/index.ts
index 11c4899..920c330 100644
--- a/src/util/util/index.ts
+++ b/src/util/util/index.ts
@@ -48,4 +48,5 @@
export * from "./NameValidation";
export * from "../../schemas/HelperTypes";
export * from "./extensions";
-export * from "./Random";
\ No newline at end of file
+export * from "./Random";
+export * from "./Url";