diff --git a/api/api/v1/accounts/:id/followers.ts b/api/api/v1/accounts/:id/followers.ts index 548f8770..9eafe3cc 100644 --- a/api/api/v1/accounts/:id/followers.ts +++ b/api/api/v1/accounts/:id/followers.ts @@ -69,7 +69,7 @@ export default apiRoute((app) => sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${otherUser.id} AND "Relationships"."ownerId" = ${Users.id} AND "Relationships"."following" = true)`, ), limit, - context.req.url, + new URL(context.req.url), ); return context.json( diff --git a/api/api/v1/accounts/:id/following.ts b/api/api/v1/accounts/:id/following.ts index 272f8b28..bcdcf4ae 100644 --- a/api/api/v1/accounts/:id/following.ts +++ b/api/api/v1/accounts/:id/following.ts @@ -71,7 +71,7 @@ export default apiRoute((app) => sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${otherUser.id} AND "Relationships"."following" = true)`, ), context.req.valid("query").limit, - context.req.url, + new URL(context.req.url), ); return context.json( diff --git a/api/api/v1/accounts/:id/roles/index.test.ts b/api/api/v1/accounts/:id/roles/index.test.ts index 2ad4379d..574caafc 100644 --- a/api/api/v1/accounts/:id/roles/index.test.ts +++ b/api/api/v1/accounts/:id/roles/index.test.ts @@ -14,7 +14,7 @@ beforeAll(async () => { priority: 2, description: "test", visible: true, - icon: "test", + icon: "https://test.com", }); expect(role).toBeDefined(); diff --git a/api/api/v1/accounts/:id/statuses.ts b/api/api/v1/accounts/:id/statuses.ts index 5f5f918a..230c01a0 100644 --- a/api/api/v1/accounts/:id/statuses.ts +++ b/api/api/v1/accounts/:id/statuses.ts @@ -115,7 +115,7 @@ export default apiRoute((app) => exclude_replies ? isNull(Notes.replyId) : undefined, ), limit, - context.req.url, + new URL(context.req.url), user?.id, ); diff --git a/api/api/v1/accounts/update_credentials/index.test.ts b/api/api/v1/accounts/update_credentials/index.test.ts index 78edd2e9..079565bb 100644 --- a/api/api/v1/accounts/update_credentials/index.test.ts +++ b/api/api/v1/accounts/update_credentials/index.test.ts @@ -60,11 +60,11 @@ describe("/api/v1/accounts/update_credentials", () => { const object = (await response.json()) as APIAccount; // Proxy url is base_url/media/proxy/ expect(object.note).toBe( - `

\n`, ); diff --git a/api/api/v1/blocks/index.ts b/api/api/v1/blocks/index.ts index 90da37d8..fe5c6666 100644 --- a/api/api/v1/blocks/index.ts +++ b/api/api/v1/blocks/index.ts @@ -55,7 +55,7 @@ export default apiRoute((app) => sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${user.id} AND "Relationships"."blocking" = true)`, ), limit, - context.req.url, + new URL(context.req.url), ); return context.json( diff --git a/api/api/v1/favourites/index.ts b/api/api/v1/favourites/index.ts index 45dcada6..4d667acf 100644 --- a/api/api/v1/favourites/index.ts +++ b/api/api/v1/favourites/index.ts @@ -53,7 +53,7 @@ export default apiRoute((app) => sql`EXISTS (SELECT 1 FROM "Likes" WHERE "Likes"."likedId" = ${Notes.id} AND "Likes"."likerId" = ${user.id})`, ), limit, - context.req.url, + new URL(context.req.url), user?.id, ); diff --git a/api/api/v1/follow_requests/index.ts b/api/api/v1/follow_requests/index.ts index f83c01fe..ffb638bb 100644 --- a/api/api/v1/follow_requests/index.ts +++ b/api/api/v1/follow_requests/index.ts @@ -54,7 +54,7 @@ export default apiRoute((app) => sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${user.id} AND "Relationships"."ownerId" = ${Users.id} AND "Relationships"."requested" = true)`, ), limit, - context.req.url, + new URL(context.req.url), ); return context.json( diff --git a/api/api/v1/instance/index.ts b/api/api/v1/instance/index.ts index 4f0e864e..208dd442 100644 --- a/api/api/v1/instance/index.ts +++ b/api/api/v1/instance/index.ts @@ -98,8 +98,12 @@ export default apiRoute((app) => status_count: statusCount, user_count: userCount, }, - thumbnail: proxyUrl(config.instance.logo), - banner: proxyUrl(config.instance.banner), + thumbnail: config.instance.logo + ? proxyUrl(config.instance.logo).toString() + : null, + banner: config.instance.banner + ? proxyUrl(config.instance.banner).toString() + : null, title: config.instance.name, uri: config.http.base_url, urls: { @@ -113,7 +117,9 @@ export default apiRoute((app) => providers: oidcConfig?.providers?.map((p) => ({ name: p.name, - icon: proxyUrl(p.icon) || undefined, + icon: p.icon + ? proxyUrl(new URL(p.icon)).toString() + : undefined, id: p.id, })) ?? [], }, diff --git a/api/api/v1/mutes/index.ts b/api/api/v1/mutes/index.ts index 3c6fd1ae..f7c2c50e 100644 --- a/api/api/v1/mutes/index.ts +++ b/api/api/v1/mutes/index.ts @@ -53,7 +53,7 @@ export default apiRoute((app) => sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${user.id} AND "Relationships"."muting" = true)`, ), limit, - context.req.url, + new URL(context.req.url), ); return context.json( diff --git a/api/api/v1/notifications/index.ts b/api/api/v1/notifications/index.ts index b0053d05..cbf2b16f 100644 --- a/api/api/v1/notifications/index.ts +++ b/api/api/v1/notifications/index.ts @@ -148,7 +148,7 @@ export default apiRoute((app) => )`, ), limit, - context.req.url, + new URL(context.req.url), user.id, ); diff --git a/api/api/v1/roles/:id/index.test.ts b/api/api/v1/roles/:id/index.test.ts index 2bcb6bb2..5bff9a9e 100644 --- a/api/api/v1/roles/:id/index.test.ts +++ b/api/api/v1/roles/:id/index.test.ts @@ -15,7 +15,7 @@ beforeAll(async () => { priority: 2, description: "test", visible: true, - icon: "test", + icon: "https://test.com", }); expect(role).toBeDefined(); diff --git a/api/api/v1/roles/index.test.ts b/api/api/v1/roles/index.test.ts index 8eb5be54..faecb91b 100644 --- a/api/api/v1/roles/index.test.ts +++ b/api/api/v1/roles/index.test.ts @@ -15,7 +15,7 @@ beforeAll(async () => { priority: 10, description: "test", visible: true, - icon: "test", + icon: "https://test.com", }); expect(role).toBeDefined(); diff --git a/api/api/v1/statuses/:id/favourited_by.ts b/api/api/v1/statuses/:id/favourited_by.ts index 5287b248..8d0d6269 100644 --- a/api/api/v1/statuses/:id/favourited_by.ts +++ b/api/api/v1/statuses/:id/favourited_by.ts @@ -60,7 +60,7 @@ export default apiRoute((app) => sql`EXISTS (SELECT 1 FROM "Likes" WHERE "Likes"."likedId" = ${note.id} AND "Likes"."likerId" = ${Users.id})`, ), limit, - context.req.url, + new URL(context.req.url), ); return context.json( diff --git a/api/api/v1/statuses/:id/reblogged_by.ts b/api/api/v1/statuses/:id/reblogged_by.ts index 72a06c5b..72392182 100644 --- a/api/api/v1/statuses/:id/reblogged_by.ts +++ b/api/api/v1/statuses/:id/reblogged_by.ts @@ -60,7 +60,7 @@ export default apiRoute((app) => sql`EXISTS (SELECT 1 FROM "Notes" WHERE "Notes"."reblogId" = ${note.id} AND "Notes"."authorId" = ${Users.id})`, ), limit, - context.req.url, + new URL(context.req.url), ); return context.json( diff --git a/api/api/v1/statuses/index.test.ts b/api/api/v1/statuses/index.test.ts index e5c002d1..ff12b272 100644 --- a/api/api/v1/statuses/index.test.ts +++ b/api/api/v1/statuses/index.test.ts @@ -429,11 +429,11 @@ describe("/api/v1/statuses", () => { const object = (await response.json()) as ApiStatus; // Proxy url is base_url/media/proxy/ expect(object.content).toBe( - `

`, ); diff --git a/api/api/v1/timelines/home.test.ts b/api/api/v1/timelines/home.test.ts index 63ccc25d..c0a81097 100644 --- a/api/api/v1/timelines/home.test.ts +++ b/api/api/v1/timelines/home.test.ts @@ -70,7 +70,7 @@ describe("/api/v1/timelines/home", () => { ); expect(response.headers.get("link")).toBe( - `<${config.http.base_url}/api/v1/timelines/home?limit=20&max_id=${timeline[19].id}>; rel="next"`, + `<${config.http.base_url}api/v1/timelines/home?limit=20&max_id=${timeline[19].id}>; rel="next"`, ); }); @@ -112,7 +112,7 @@ describe("/api/v1/timelines/home", () => { ); expect(response.headers.get("link")).toInclude( - `${config.http.base_url}/api/v1/timelines/home?limit=20&min_id=${timeline[20].id}>; rel="prev"`, + `${config.http.base_url}api/v1/timelines/home?limit=20&min_id=${timeline[20].id}>; rel="prev"`, ); }); diff --git a/api/api/v1/timelines/home.ts b/api/api/v1/timelines/home.ts index ebbc27b6..7a98ce38 100644 --- a/api/api/v1/timelines/home.ts +++ b/api/api/v1/timelines/home.ts @@ -70,7 +70,7 @@ export default apiRoute((app) => sql`NOT EXISTS (SELECT 1 FROM "Filters" WHERE "Filters"."userId" = ${user.id} AND "Filters"."filter_action" = 'hide' AND EXISTS (SELECT 1 FROM "FilterKeywords" WHERE "FilterKeywords"."filterId" = "Filters"."id" AND "Notes"."content" LIKE '%' || "FilterKeywords"."keyword" || '%') AND "Filters"."context" @> ARRAY['home'])`, ), limit, - context.req.url, + new URL(context.req.url), user.id, ); diff --git a/api/api/v1/timelines/public.test.ts b/api/api/v1/timelines/public.test.ts index 006621a4..1f7ec651 100644 --- a/api/api/v1/timelines/public.test.ts +++ b/api/api/v1/timelines/public.test.ts @@ -111,7 +111,7 @@ describe("/api/v1/timelines/public", () => { ); expect(response.headers.get("link")).toBe( - `<${config.http.base_url}/api/v1/timelines/public?limit=20&max_id=${timeline[19].id}>; rel="next"`, + `<${config.http.base_url}api/v1/timelines/public?limit=20&max_id=${timeline[19].id}>; rel="next"`, ); }); @@ -153,7 +153,7 @@ describe("/api/v1/timelines/public", () => { ); expect(response.headers.get("link")).toInclude( - `${config.http.base_url}/api/v1/timelines/public?limit=20&min_id=${timeline[20].id}>; rel="prev"`, + `${config.http.base_url}api/v1/timelines/public?limit=20&min_id=${timeline[20].id}>; rel="prev"`, ); }); diff --git a/api/api/v1/timelines/public.ts b/api/api/v1/timelines/public.ts index 55c13bce..9b239dfc 100644 --- a/api/api/v1/timelines/public.ts +++ b/api/api/v1/timelines/public.ts @@ -93,7 +93,7 @@ export default apiRoute((app) => : eq(Notes.visibility, "public"), ), limit, - context.req.url, + new URL(context.req.url), user?.id, ); diff --git a/api/api/v2/instance/index.ts b/api/api/v2/instance/index.ts index 629a64c0..ce999dd6 100644 --- a/api/api/v2/instance/index.ts +++ b/api/api/v2/instance/index.ts @@ -147,10 +147,14 @@ export default apiRoute((app) => }, }, thumbnail: { - url: proxyUrl(config.instance.logo), + url: config.instance.logo + ? proxyUrl(config.instance.logo) + : null, }, banner: { - url: proxyUrl(config.instance.banner), + url: config.instance.banner + ? proxyUrl(config.instance.banner) + : null, }, languages: ["en"], configuration: { @@ -227,7 +231,7 @@ export default apiRoute((app) => providers: oidcConfig?.providers?.map((p) => ({ name: p.name, - icon: proxyUrl(p.icon) ?? "", + icon: p.icon ? proxyUrl(new URL(p.icon)) : "", id: p.id, })) ?? [], }, diff --git a/api/users/:uuid/index.ts b/api/users/:uuid/index.ts index 01b3f06c..ff416cb5 100644 --- a/api/users/:uuid/index.ts +++ b/api/users/:uuid/index.ts @@ -72,7 +72,11 @@ export default apiRoute((app) => const userJson = user.toVersia(); - const { headers } = await user.sign(userJson, context.req.url, "GET"); + const { headers } = await user.sign( + userJson, + new URL(context.req.url), + "GET", + ); return context.json(userJson, 200, headers.toJSON()); }), diff --git a/api/users/:uuid/outbox/index.ts b/api/users/:uuid/outbox/index.ts index 8884ed00..1eeb5cc6 100644 --- a/api/users/:uuid/outbox/index.ts +++ b/api/users/:uuid/outbox/index.ts @@ -107,7 +107,7 @@ export default apiRoute((app) => config.http.base_url, ).toString(), total: totalNotes, - author: author.getUri(), + author: author.getUri().toString(), next: notes.length === NOTES_PER_PAGE ? new URL( @@ -125,7 +125,11 @@ export default apiRoute((app) => items: notes.map((note) => note.toVersia()), }; - const { headers } = await author.sign(json, context.req.url, "GET"); + const { headers } = await author.sign( + json, + new URL(context.req.url), + "GET", + ); return context.json(json, 200, headers.toJSON()); }), diff --git a/api/well-known/webfinger/index.ts b/api/well-known/webfinger/index.ts index 5d6f6f28..d7089c91 100644 --- a/api/well-known/webfinger/index.ts +++ b/api/well-known/webfinger/index.ts @@ -99,7 +99,7 @@ export default apiRoute((app) => user.data.username, new URL(config.http.base_url).host, "application/activity+json", - config.federation.bridge.url, + config.federation.bridge.url?.toString(), ); } catch (e) { const error = e as ResponseError; diff --git a/classes/database/emoji.ts b/classes/database/emoji.ts index 14abd4f4..08d103d7 100644 --- a/classes/database/emoji.ts +++ b/classes/database/emoji.ts @@ -188,8 +188,8 @@ export class Emoji extends BaseInterface { return { id: this.id, shortcode: this.data.shortcode, - static_url: proxyUrl(this.media.getUrl()) ?? "", // TODO: Add static version - url: proxyUrl(this.media.getUrl()) ?? "", + static_url: proxyUrl(this.media.getUrl()).toString(), + url: proxyUrl(this.media.getUrl()).toString(), visible_in_picker: this.data.visibleInPicker, category: this.data.category ?? undefined, global: this.data.ownerId === null, diff --git a/classes/database/instance.ts b/classes/database/instance.ts index d03e6c28..4d3dcfaf 100644 --- a/classes/database/instance.ts +++ b/classes/database/instance.ts @@ -135,7 +135,7 @@ export class Instance extends BaseInterface { return this.data.id; } - public static async fetchMetadata(url: string): Promise<{ + public static async fetchMetadata(url: URL): Promise<{ metadata: InstanceMetadata; protocol: "versia" | "activitypub"; }> { @@ -184,7 +184,7 @@ export class Instance extends BaseInterface { } private static async fetchActivityPubMetadata( - url: string, + url: URL, ): Promise { const origin = new URL(url).origin; const wellKnownUrl = new URL("/.well-known/nodeinfo", origin); @@ -311,18 +311,18 @@ export class Instance extends BaseInterface { public static resolveFromHost(host: string): Promise { if (host.startsWith("http")) { - const url = new URL(host).host; + const url = new URL(host); return Instance.resolve(url); } const url = new URL(`https://${host}`); - return Instance.resolve(url.origin); + return Instance.resolve(url); } - public static async resolve(url: string): Promise { - const host = new URL(url).host; + public static async resolve(url: URL): Promise { + const host = url.host; const existingInstance = await Instance.fromSql( eq(Instances.baseUrl, host), @@ -352,7 +352,7 @@ export class Instance extends BaseInterface { const logger = getLogger(["federation", "resolvers"]); const output = await Instance.fetchMetadata( - `https://${this.data.baseUrl}`, + new URL(`https://${this.data.baseUrl}`), ); if (!output) { diff --git a/classes/database/like.ts b/classes/database/like.ts index 5759b2ac..a6d5d948 100644 --- a/classes/database/like.ts +++ b/classes/database/like.ts @@ -167,15 +167,17 @@ export class Like extends BaseInterface { id: this.data.id, author: User.getUri( this.data.liker.id, - this.data.liker.uri, - config.http.base_url, - ), + this.data.liker.uri ? new URL(this.data.liker.uri) : null, + ).toString(), type: "pub.versia:likes/Like", created_at: new Date(this.data.createdAt).toISOString(), - liked: Note.getUri( - this.data.liked.id, - this.data.liked.uri, - ) as string, + liked: + Note.getUri( + this.data.liked.id, + this.data.liked.uri + ? new URL(this.data.liked.uri) + : undefined, + )?.toString() ?? "", uri: this.getUri().toString(), }; } @@ -187,9 +189,12 @@ export class Like extends BaseInterface { created_at: new Date().toISOString(), author: User.getUri( unliker?.id ?? this.data.liker.id, - unliker?.data.uri ?? this.data.liker.uri, - config.http.base_url, - ), + unliker?.data.uri + ? new URL(unliker.data.uri) + : this.data.liker.uri + ? new URL(this.data.liker.uri) + : null, + ).toString(), deleted_type: "pub.versia:likes/Like", deleted: this.getUri().toString(), }; diff --git a/classes/database/media.ts b/classes/database/media.ts index 0fe0477e..b5b44881 100644 --- a/classes/database/media.ts +++ b/classes/database/media.ts @@ -209,7 +209,7 @@ export class Media extends BaseInterface { const url = Media.getUrl(path); - let thumbnailUrl = ""; + let thumbnailUrl: URL | null = null; if (options?.thumbnail) { const { path } = await Media.upload(options.thumbnail); @@ -220,11 +220,16 @@ export class Media extends BaseInterface { const content = await Media.fileToContentFormat(file, url, { description: options?.description, }); - const thumbnailContent = options?.thumbnail - ? await Media.fileToContentFormat(options.thumbnail, thumbnailUrl, { - description: options?.description, - }) - : undefined; + const thumbnailContent = + thumbnailUrl && options?.thumbnail + ? await Media.fileToContentFormat( + options.thumbnail, + thumbnailUrl, + { + description: options?.description, + }, + ) + : undefined; const newAttachment = await Media.insert({ content, @@ -246,6 +251,12 @@ export class Media extends BaseInterface { return newAttachment; } + /** + * Creates and adds a new media attachment from a URL + * @param uri + * @param options + * @returns + */ public static async fromUrl( uri: URL, options?: { @@ -377,20 +388,21 @@ export class Media extends BaseInterface { return this.data.id; } - public static getUrl(name: string): string { + public static getUrl(name: string): URL { if (config.media.backend === MediaBackendType.Local) { - return new URL(`/media/${name}`, config.http.base_url).toString(); + return new URL(`/media/${name}`, config.http.base_url); } if (config.media.backend === MediaBackendType.S3) { - return new URL(`/${name}`, config.s3?.public_url).toString(); + return new URL(`/${name}`, config.s3?.public_url); } - return ""; + + throw new Error("Unknown media backend"); } - public getUrl(): string { + public getUrl(): URL { const type = this.getPreferredMimeType(); - return this.data.content[type]?.content; + return new URL(this.data.content[type]?.content ?? ""); } /** @@ -461,7 +473,7 @@ export class Media extends BaseInterface { */ public static async fileToContentFormat( file: File, - uri: string, + uri: URL, options?: Partial<{ description: string; }>, @@ -475,7 +487,7 @@ export class Media extends BaseInterface { // Thumbhash should be added in a worker after the file is uploaded return { [file.type]: { - content: uri, + content: uri.toString(), remote: true, hash: { sha256: hash, @@ -528,9 +540,11 @@ export class Media extends BaseInterface { return { id: this.data.id, type: this.getMastodonType(), - url: proxyUrl(data.content) ?? "", + url: proxyUrl(new URL(data.content)).toString(), remote_url: null, - preview_url: proxyUrl(thumbnailData?.content), + preview_url: thumbnailData?.content + ? proxyUrl(new URL(thumbnailData.content)).toString() + : null, text_url: null, meta: this.toApiMeta(), description: data.description || null, diff --git a/classes/database/note.ts b/classes/database/note.ts index e8ce756f..23eb1cf1 100644 --- a/classes/database/note.ts +++ b/classes/database/note.ts @@ -646,17 +646,17 @@ export class Note extends BaseInterface { * @param uri - The URI of the note to resolve * @returns The resolved note */ - public static async resolve(uri: string): Promise { + public static async resolve(uri: URL): Promise { // Check if note not already in database - const foundNote = await Note.fromSql(eq(Notes.uri, uri)); + const foundNote = await Note.fromSql(eq(Notes.uri, uri.toString())); if (foundNote) { return foundNote; } // Check if URI is of a local note - if (uri.startsWith(config.http.base_url)) { - const uuid = uri.match(idValidator); + if (uri.origin === config.http.base_url.origin) { + const uuid = uri.pathname.match(idValidator); if (!uuid?.[0]) { throw new Error( @@ -675,7 +675,7 @@ export class Note extends BaseInterface { * @param uri - The URI of the note to save * @returns The saved note, or null if the note could not be fetched */ - public static async fetchFromRemote(uri: string): Promise { + public static async fetchFromRemote(uri: URL): Promise { const instance = await Instance.resolve(uri); if (!instance) { @@ -691,7 +691,7 @@ export class Note extends BaseInterface { const note = await new EntityValidator().Note(data); - const author = await User.resolve(note.author); + const author = await User.resolve(new URL(note.author)); if (!author) { throw new Error("Invalid object author"); @@ -773,15 +773,15 @@ export class Note extends BaseInterface { uri: note.uri, mentions: await Promise.all( (note.mentions ?? []) - .map((mention) => User.resolve(mention)) + .map((mention) => User.resolve(new URL(mention))) .filter((mention) => mention !== null) as Promise[], ), mediaAttachments: attachments, replyId: note.replies_to - ? (await Note.resolve(note.replies_to))?.data.id + ? (await Note.resolve(new URL(note.replies_to)))?.data.id : undefined, quoteId: note.quotes - ? (await Note.resolve(note.quotes))?.data.id + ? (await Note.resolve(new URL(note.quotes)))?.data.id : undefined, }; @@ -908,7 +908,10 @@ export class Note extends BaseInterface { mention.username, mention.instance?.baseUrl, ), - url: User.getUri(mention.id, mention.uri, config.http.base_url), + url: User.getUri( + mention.id, + mention.uri ? new URL(mention.uri) : null, + ).toString(), username: mention.username, })), language: null, @@ -927,9 +930,9 @@ export class Note extends BaseInterface { sensitive: data.sensitive, spoiler_text: data.spoilerText, tags: [], - uri: data.uri || this.getUri(), + uri: data.uri || this.getUri().toString(), visibility: data.visibility as ApiStatus["visibility"], - url: data.uri || this.getMastoUri(), + url: data.uri || this.getMastoUri().toString(), bookmarked: false, quote: data.quotingId ? ((await Note.fromId(data.quotingId, userFetching?.id).then( @@ -944,14 +947,11 @@ export class Note extends BaseInterface { }; } - public getUri(): string { - return this.data.uri || localObjectUri(this.id); + public getUri(): URL { + return new URL(this.data.uri || localObjectUri(this.id)); } - public static getUri( - id: string | null, - uri?: string | null, - ): string | null { + public static getUri(id: string | null, uri?: URL | null): URL | null { if (!id) { return null; } @@ -962,11 +962,11 @@ export class Note extends BaseInterface { * Get the frontend URI of this note * @returns The frontend URI of this note */ - public getMastoUri(): string { + public getMastoUri(): URL { return new URL( `/@${this.author.data.username}/${this.id}`, config.http.base_url, - ).toString(); + ); } public deleteToVersia(): VersiaDelete { @@ -975,9 +975,9 @@ export class Note extends BaseInterface { return { type: "Delete", id, - author: this.author.getUri(), + author: this.author.getUri().toString(), deleted_type: "Note", - deleted: this.getUri(), + deleted: this.getUri().toString(), created_at: new Date().toISOString(), }; } @@ -992,8 +992,8 @@ export class Note extends BaseInterface { type: "Note", created_at: new Date(status.createdAt).toISOString(), id: status.id, - author: this.author.getUri(), - uri: this.getUri(), + author: this.author.getUri().toString(), + uri: this.getUri().toString(), content: { "text/html": { content: status.content, @@ -1009,12 +1009,19 @@ export class Note extends BaseInterface { ), is_sensitive: status.sensitive, mentions: status.mentions.map((mention) => - User.getUri(mention.id, mention.uri, config.http.base_url), + User.getUri( + mention.id, + mention.uri ? new URL(mention.uri) : null, + ).toString(), ), - quotes: - Note.getUri(status.quotingId, status.quote?.uri) ?? undefined, - replies_to: - Note.getUri(status.replyId, status.reply?.uri) ?? undefined, + quotes: Note.getUri( + status.quotingId, + status.quote?.uri ? new URL(status.quote.uri) : null, + )?.toString(), + replies_to: Note.getUri( + status.replyId, + status.reply?.uri ? new URL(status.reply.uri) : null, + )?.toString(), subject: status.spoilerText, // TODO: Refactor as part of groups group: status.visibility === "public" ? "public" : "followers", diff --git a/classes/database/reaction.ts b/classes/database/reaction.ts index e7e0a23a..8aa2b86f 100644 --- a/classes/database/reaction.ts +++ b/classes/database/reaction.ts @@ -179,14 +179,13 @@ export class Reaction extends BaseInterface { }; } - public getUri(baseUrl: string): string { - return ( - this.data.uri || - new URL( - `/objects/${this.data.noteId}/reactions/${this.id}`, - baseUrl, - ).toString() - ); + public getUri(baseUrl: URL): URL { + return this.data.uri + ? new URL(this.data.uri) + : new URL( + `/objects/${this.data.noteId}/reactions/${this.id}`, + baseUrl, + ); } public isLocal(): boolean { @@ -203,19 +202,21 @@ export class Reaction extends BaseInterface { } return { - uri: this.getUri(config.http.base_url), + uri: this.getUri(config.http.base_url).toString(), type: "pub.versia:reactions/Reaction", author: User.getUri( this.data.authorId, - this.data.author.uri, - config.http.base_url, - ), + this.data.author.uri ? new URL(this.data.author.uri) : null, + ).toString(), created_at: new Date(this.data.createdAt).toISOString(), id: this.id, - object: Note.getUri( - this.data.note.id, - this.data.note.uri, - ) as string, + object: + Note.getUri( + this.data.note.id, + this.data.note.uri + ? new URL(this.data.note.uri) + : undefined, + )?.toString() ?? "", content: this.hasCustomEmoji() ? `:${this.data.emoji?.shortcode}:` : this.data.emojiText || "", diff --git a/classes/database/role.ts b/classes/database/role.ts index 3ab7bc51..0f439a6d 100644 --- a/classes/database/role.ts +++ b/classes/database/role.ts @@ -227,7 +227,9 @@ export class Role extends BaseInterface { priority: this.data.priority, description: this.data.description ?? undefined, visible: this.data.visible, - icon: proxyUrl(this.data.icon) ?? undefined, + icon: this.data.icon + ? proxyUrl(new URL(this.data.icon)).toString() + : undefined, }; } } diff --git a/classes/database/timeline.ts b/classes/database/timeline.ts index fca146e2..74c1adcd 100644 --- a/classes/database/timeline.ts +++ b/classes/database/timeline.ts @@ -17,7 +17,7 @@ export class Timeline { public static getNoteTimeline( sql: SQL | undefined, limit: number, - url: string, + url: URL, userId?: string, ): Promise<{ link: string; objects: Note[] }> { return new Timeline(TimelineType.Note).fetchTimeline( @@ -31,7 +31,7 @@ export class Timeline { public static getUserTimeline( sql: SQL | undefined, limit: number, - url: string, + url: URL, ): Promise<{ link: string; objects: User[] }> { return new Timeline(TimelineType.User).fetchTimeline( sql, @@ -43,7 +43,7 @@ export class Timeline { public static getNotificationTimeline( sql: SQL | undefined, limit: number, - url: string, + url: URL, userId?: string, ): Promise<{ link: string; objects: Notification[] }> { return new Timeline( @@ -85,14 +85,11 @@ export class Timeline { private async fetchLinkHeader( objects: Type[], - url: string, + url: URL, limit: number, ): Promise { const linkHeader: string[] = []; - const urlWithoutQuery = new URL( - new URL(url).pathname, - config.http.base_url, - ).toString(); + const urlWithoutQuery = new URL(url.pathname, config.http.base_url); if (objects.length > 0) { switch (this.type) { @@ -130,7 +127,7 @@ export class Timeline { private static async fetchNoteLinkHeader( notes: Note[], - urlWithoutQuery: string, + urlWithoutQuery: URL, limit: number, ): Promise { const linkHeader: string[] = []; @@ -158,7 +155,7 @@ export class Timeline { private static async fetchUserLinkHeader( users: User[], - urlWithoutQuery: string, + urlWithoutQuery: URL, limit: number, ): Promise { const linkHeader: string[] = []; @@ -186,7 +183,7 @@ export class Timeline { private static async fetchNotificationLinkHeader( notifications: Notification[], - urlWithoutQuery: string, + urlWithoutQuery: URL, limit: number, ): Promise { const linkHeader: string[] = []; @@ -220,7 +217,7 @@ export class Timeline { private async fetchTimeline( sql: SQL | undefined, limit: number, - url: string, + url: URL, userId?: string, ): Promise<{ link: string; objects: Type[] }> { const objects = await this.fetchObjects(sql, limit, userId); diff --git a/classes/database/user.ts b/classes/database/user.ts index 261e3a7e..ec4d2557 100644 --- a/classes/database/user.ts +++ b/classes/database/user.ts @@ -228,19 +228,14 @@ export class User extends BaseInterface { return !this.isLocal(); } - public getUri(): string { - return ( - this.data.uri || - new URL(`/users/${this.data.id}`, config.http.base_url).toString() - ); + public getUri(): URL { + return this.data.uri + ? new URL(this.data.uri) + : new URL(`/users/${this.data.id}`, config.http.base_url); } - public static getUri( - id: string, - uri: string | null, - baseUrl: string, - ): string { - return uri || new URL(`/users/${id}`, baseUrl).toString(); + public static getUri(id: string, uri: URL | null): URL { + return uri ? uri : new URL(`/users/${id}`, config.http.base_url); } public hasPermission(permission: RolePermissions): boolean { @@ -290,8 +285,8 @@ export class User extends BaseInterface { entity: { type: "Follow", id: crypto.randomUUID(), - author: this.getUri(), - followee: otherUser.getUri(), + author: this.getUri().toString(), + followee: otherUser.getUri().toString(), created_at: new Date().toISOString(), }, recipientId: otherUser.id, @@ -329,9 +324,9 @@ export class User extends BaseInterface { return { type: "Unfollow", id, - author: this.getUri(), + author: this.getUri().toString(), created_at: new Date().toISOString(), - followee: followee.getUri(), + followee: followee.getUri().toString(), }; } @@ -347,9 +342,9 @@ export class User extends BaseInterface { const entity: VersiaFollowAccept = { type: "FollowAccept", id: crypto.randomUUID(), - author: this.getUri(), + author: this.getUri().toString(), created_at: new Date().toISOString(), - follower: follower.getUri(), + follower: follower.getUri().toString(), }; await deliveryQueue.add(DeliveryJobType.FederateEntity, { @@ -371,9 +366,9 @@ export class User extends BaseInterface { const entity: VersiaFollowReject = { type: "FollowReject", id: crypto.randomUUID(), - author: this.getUri(), + author: this.getUri().toString(), created_at: new Date().toISOString(), - follower: follower.getUri(), + follower: follower.getUri().toString(), }; await deliveryQueue.add(DeliveryJobType.FederateEntity, { @@ -390,19 +385,21 @@ export class User extends BaseInterface { * @param hostname * @returns URI, or null if not found */ - public static webFinger( + public static async webFinger( manager: FederationRequester, username: string, hostname: string, - ): Promise { + ): Promise { try { - return manager.webFinger(username, hostname); + return new URL(await manager.webFinger(username, hostname)); } catch { try { - return manager.webFinger( - username, - hostname, - "application/activity+json", + return new URL( + await manager.webFinger( + username, + hostname, + "application/activity+json", + ), ); } catch { return Promise.resolve(null); @@ -511,7 +508,9 @@ export class User extends BaseInterface { id: issuer.id, name: issuer.name, url: issuer.url, - icon: proxyUrl(issuer.icon) || undefined, + icon: issuer.icon + ? proxyUrl(new URL(issuer.icon)).toString() + : undefined, server_id: account.serverId, }; }) @@ -662,11 +661,7 @@ export class User extends BaseInterface { return this; } - public static async fetchFromRemote(uri: string): Promise { - if (!URL.canParse(uri)) { - throw new Error(`Invalid URI: ${uri}`); - } - + public static async fetchFromRemote(uri: URL): Promise { const instance = await Instance.resolve(uri); if (!instance) { @@ -684,19 +679,19 @@ export class User extends BaseInterface { const bridgeUri = new URL( `/apbridge/versia/query?${new URLSearchParams({ - user_url: uri, + user_url: uri.toString(), })}`, config.federation.bridge.url, ); - return await User.saveFromVersia(bridgeUri.toString(), instance); + return await User.saveFromVersia(bridgeUri, instance); } throw new Error(`Unsupported protocol: ${instance.data.protocol}`); } private static async saveFromVersia( - uri: string, + uri: URL, instance: Instance, ): Promise { const requester = await User.getFederationRequester(); @@ -859,19 +854,19 @@ export class User extends BaseInterface { return user; } - public static async resolve(uri: string): Promise { + public static async resolve(uri: URL): Promise { getLogger(["federation", "resolvers"]) .debug`Resolving user ${chalk.gray(uri)}`; // Check if user not already in database - const foundUser = await User.fromSql(eq(Users.uri, uri)); + const foundUser = await User.fromSql(eq(Users.uri, uri.toString())); if (foundUser) { return foundUser; } // Check if URI is of a local user - if (uri.startsWith(config.http.base_url)) { - const uuid = uri.match(idValidator); + if (uri.origin === config.http.base_url.origin) { + const uuid = uri.href.match(idValidator); if (!uuid?.[0]) { throw new Error( @@ -893,11 +888,13 @@ export class User extends BaseInterface { * @param config The config to use * @returns The raw URL for the user's avatar */ - public getAvatarUrl(config: Config): string { + public getAvatarUrl(config: Config): URL { if (!this.avatar) { return ( config.defaults.avatar || - `https://api.dicebear.com/8.x/${config.defaults.placeholder_style}/svg?seed=${this.data.username}` + new URL( + `https://api.dicebear.com/8.x/${config.defaults.placeholder_style}/svg?seed=${this.data.username}`, + ) ); } return this.avatar?.getUrl(); @@ -988,9 +985,9 @@ export class User extends BaseInterface { * @param config The config to use * @returns The raw URL for the user's header */ - public getHeaderUrl(config: Config): string { + public getHeaderUrl(config: Config): URL | null { if (!this.header) { - return config.defaults.header || ""; + return config.defaults.header ?? null; } return this.header.getUrl(); } @@ -1052,7 +1049,7 @@ export class User extends BaseInterface { */ public async sign( entity: KnownEntity | Collection, - signatureUrl: string | URL, + signatureUrl: URL, signatureMethod: HttpVerb = "POST", ): Promise<{ headers: Headers; @@ -1065,7 +1062,7 @@ export class User extends BaseInterface { const output = await signatureConstructor.sign( signatureMethod, - new URL(signatureUrl), + signatureUrl, JSON.stringify(entity), ); @@ -1155,7 +1152,7 @@ export class User extends BaseInterface { ); } - const { headers } = await this.sign(entity, inbox); + const { headers } = await this.sign(entity, new URL(inbox)); try { await new FederationRequester().post(inbox, entity, { @@ -1185,12 +1182,14 @@ export class User extends BaseInterface { username: user.username, display_name: user.displayName, note: user.note, - uri: this.getUri(), + uri: this.getUri().toString(), url: user.uri || new URL(`/@${user.username}`, config.http.base_url).toString(), - avatar: proxyUrl(this.getAvatarUrl(config)) ?? "", - header: proxyUrl(this.getHeaderUrl(config)) ?? "", + avatar: proxyUrl(this.getAvatarUrl(config)).toString(), + header: this.getHeaderUrl(config) + ? proxyUrl(this.getHeaderUrl(config) as URL).toString() + : "", locked: user.isLocked, created_at: new Date(user.createdAt).toISOString(), followers_count: user.followerCount, @@ -1204,8 +1203,10 @@ export class User extends BaseInterface { bot: user.isBot, source: isOwnAccount ? user.source : undefined, // TODO: Add static avatar and header - avatar_static: proxyUrl(this.getAvatarUrl(config)) ?? "", - header_static: proxyUrl(this.getHeaderUrl(config)) ?? "", + avatar_static: proxyUrl(this.getAvatarUrl(config)).toString(), + header_static: this.getHeaderUrl(config) + ? proxyUrl(this.getHeaderUrl(config) as URL).toString() + : "", acct: this.getAcct(), // TODO: Add these fields limited: false, @@ -1258,7 +1259,7 @@ export class User extends BaseInterface { return { id: user.id, type: "User", - uri: this.getUri(), + uri: this.getUri().toString(), bio: { "text/html": { content: user.note, @@ -1308,11 +1309,12 @@ export class User extends BaseInterface { this.getAvatarUrl(config), this.data.source.avatar?.content_type, ) ?? undefined, - header: - urlToContentFormat( - this.getHeaderUrl(config), - this.data.source.header?.content_type, - ) ?? undefined, + header: this.getHeaderUrl(config) + ? (urlToContentFormat( + this.getHeaderUrl(config) as URL, + this.data.source.header?.content_type, + ) ?? undefined) + : undefined, display_name: user.displayName, fields: user.fields, public_key: { @@ -1335,7 +1337,7 @@ export class User extends BaseInterface { public toMention(): ApiMention { return { - url: this.getUri(), + url: this.getUri().toString(), username: this.data.username, acct: this.getAcct(), id: this.id, diff --git a/classes/inbox/processor.test.ts b/classes/inbox/processor.test.ts index 9ca79afe..4e0076bb 100644 --- a/classes/inbox/processor.test.ts +++ b/classes/inbox/processor.test.ts @@ -76,7 +76,7 @@ mock.module("~/packages/config-manager/index.ts", () => ({ describe("InboxProcessor", () => { let mockRequest: { - url: string; + url: URL; method: string; body: string; }; @@ -95,7 +95,7 @@ describe("InboxProcessor", () => { // Setup basic mock context mockRequest = { - url: "https://test.com", + url: new URL("https://test.com"), method: "POST", body: "test-body", }; @@ -196,7 +196,10 @@ describe("InboxProcessor", () => { describe("processNote", () => { test("successfully processes valid note", async () => { - const mockNote = { author: "test-author" }; + const mockNote = { + author: "https://example.com", + uri: "https://note.example.com", + }; const mockAuthor = { id: "test-id" }; const mockInstance = { id: "test-id" }; @@ -209,7 +212,9 @@ describe("InboxProcessor", () => { // biome-ignore lint/complexity/useLiteralKeys: Private method const result = await processor["processNote"](); - expect(User.resolve).toHaveBeenCalledWith("test-author"); + expect(User.resolve).toHaveBeenCalledWith( + new URL("https://example.com"), + ); expect(Note.fromVersia).toHaveBeenCalledWith( mockNote, mockAuthor, @@ -220,7 +225,13 @@ describe("InboxProcessor", () => { test("returns 404 when author not found", async () => { User.resolve = jest.fn().mockResolvedValue(null); + const mockNote = { + author: "https://example.com", + uri: "https://note.example.com", + }; + // biome-ignore lint/complexity/useLiteralKeys: Private variable + processor["body"] = mockNote as VersiaNote; // biome-ignore lint/complexity/useLiteralKeys: Private method const result = await processor["processNote"](); @@ -233,8 +244,8 @@ describe("InboxProcessor", () => { describe("processFollowRequest", () => { test("successfully processes follow request for unlocked account", async () => { const mockFollow = { - author: "test-author", - followee: "test-followee", + author: "https://example.com", + followee: "https://followee.note.com", }; const mockAuthor = { id: "author-id" }; const mockFollowee = { @@ -273,7 +284,13 @@ describe("InboxProcessor", () => { test("returns 404 when author not found", async () => { User.resolve = jest.fn().mockResolvedValue(null); + const mockFollow = { + author: "https://example.com", + followee: "https://followee.note.com", + }; + // biome-ignore lint/complexity/useLiteralKeys: Private variable + processor["body"] = mockFollow as unknown as Entity; // biome-ignore lint/complexity/useLiteralKeys: Private method const result = await processor["processFollowRequest"](); @@ -287,7 +304,7 @@ describe("InboxProcessor", () => { test("successfully deletes a note", async () => { const mockDelete = { deleted_type: "Note", - deleted: "test-uri", + deleted: "https://example.com", }; const mockNote = { delete: jest.fn(), @@ -307,7 +324,7 @@ describe("InboxProcessor", () => { test("returns 404 when note not found", async () => { const mockDelete = { deleted_type: "Note", - deleted: "test-uri", + deleted: "https://example.com", }; Note.fromSql = jest.fn().mockResolvedValue(null); @@ -331,9 +348,9 @@ describe("InboxProcessor", () => { describe("processLikeRequest", () => { test("successfully processes like request", async () => { const mockLike = { - author: "test-author", - liked: "test-note", - uri: "test-uri", + author: "https://example.com", + liked: "https://example.note.com", + uri: "https://example.com", }; const mockAuthor = { like: jest.fn(), @@ -348,13 +365,23 @@ describe("InboxProcessor", () => { // biome-ignore lint/complexity/useLiteralKeys: Private method const result = await processor["processLikeRequest"](); - expect(mockAuthor.like).toHaveBeenCalledWith(mockNote, "test-uri"); + expect(mockAuthor.like).toHaveBeenCalledWith( + mockNote, + "https://example.com", + ); expect(result).toBeNull(); }); test("returns 404 when author not found", async () => { User.resolve = jest.fn().mockResolvedValue(null); + const mockLike = { + author: "https://example.com", + liked: "https://example.note.com", + uri: "https://example.com", + }; + // biome-ignore lint/complexity/useLiteralKeys: Private variable + processor["body"] = mockLike as unknown as Entity; // biome-ignore lint/complexity/useLiteralKeys: Private method const result = await processor["processLikeRequest"](); @@ -367,7 +394,7 @@ describe("InboxProcessor", () => { describe("processUserRequest", () => { test("successfully processes user update", async () => { const mockUser = { - uri: "test-uri", + uri: "https://example.com", }; const mockUpdatedUser = { id: "user-id" }; @@ -378,13 +405,20 @@ describe("InboxProcessor", () => { // biome-ignore lint/complexity/useLiteralKeys: Private method const result = await processor["processUserRequest"](); - expect(User.fetchFromRemote).toHaveBeenCalledWith("test-uri"); + expect(User.fetchFromRemote).toHaveBeenCalledWith( + new URL("https://example.com"), + ); expect(result).toBeNull(); }); test("returns 500 when update fails", async () => { + const mockUser = { + uri: "https://example.com", + }; User.fetchFromRemote = jest.fn().mockResolvedValue(null); + // biome-ignore lint/complexity/useLiteralKeys: Private variable + processor["body"] = mockUser as unknown as Entity; // biome-ignore lint/complexity/useLiteralKeys: Private method const result = await processor["processUserRequest"](); diff --git a/classes/inbox/processor.ts b/classes/inbox/processor.ts index 5717c2ed..6f4a4ca0 100644 --- a/classes/inbox/processor.ts +++ b/classes/inbox/processor.ts @@ -40,7 +40,7 @@ function isDefederated(hostname: string): boolean { return ( config.federation.blocked.find( - (blocked) => pattern.match(blocked) !== null, + (blocked) => pattern.match(blocked.toString()) !== null, ) !== undefined ); } @@ -70,7 +70,7 @@ export class InboxProcessor { */ public constructor( private request: { - url: string; + url: URL; method: string; body: string; }, @@ -269,8 +269,8 @@ export class InboxProcessor { */ private async processNote(): Promise { const note = this.body as VersiaNote; - const author = await User.resolve(note.author); - const instance = await Instance.resolve(note.uri); + const author = await User.resolve(new URL(note.author)); + const instance = await Instance.resolve(new URL(note.uri)); if (!instance) { return Response.json( @@ -298,8 +298,8 @@ export class InboxProcessor { */ private async processFollowRequest(): Promise { const follow = this.body as unknown as VersiaFollow; - const author = await User.resolve(follow.author); - const followee = await User.resolve(follow.followee); + const author = await User.resolve(new URL(follow.author)); + const followee = await User.resolve(new URL(follow.followee)); if (!author) { return Response.json( @@ -352,8 +352,8 @@ export class InboxProcessor { */ private async processFollowAccept(): Promise { const followAccept = this.body as unknown as VersiaFollowAccept; - const author = await User.resolve(followAccept.author); - const follower = await User.resolve(followAccept.follower); + const author = await User.resolve(new URL(followAccept.author)); + const follower = await User.resolve(new URL(followAccept.follower)); if (!author) { return Response.json( @@ -393,8 +393,8 @@ export class InboxProcessor { */ private async processFollowReject(): Promise { const followReject = this.body as unknown as VersiaFollowReject; - const author = await User.resolve(followReject.author); - const follower = await User.resolve(followReject.follower); + const author = await User.resolve(new URL(followReject.author)); + const follower = await User.resolve(new URL(followReject.follower)); if (!author) { return Response.json( @@ -438,7 +438,7 @@ export class InboxProcessor { const toDelete = delete_.deleted; const author = delete_.author - ? await User.resolve(delete_.author) + ? await User.resolve(new URL(delete_.author)) : null; switch (delete_.deleted_type) { @@ -461,7 +461,7 @@ export class InboxProcessor { return null; } case "User": { - const userToDelete = await User.resolve(toDelete); + const userToDelete = await User.resolve(new URL(toDelete)); if (!userToDelete) { return Response.json( @@ -516,8 +516,8 @@ export class InboxProcessor { */ private async processLikeRequest(): Promise { const like = this.body as unknown as VersiaLikeExtension; - const author = await User.resolve(like.author); - const likedNote = await Note.resolve(like.liked); + const author = await User.resolve(new URL(like.author)); + const likedNote = await Note.resolve(new URL(like.liked)); if (!author) { return Response.json( @@ -546,7 +546,7 @@ export class InboxProcessor { private async processUserRequest(): Promise { const user = this.body as unknown as VersiaUser; // FIXME: Instead of refetching the remote user, we should read the incoming json and update from that - const updatedAccount = await User.fetchFromRemote(user.uri); + const updatedAccount = await User.fetchFromRemote(new URL(user.uri)); if (!updatedAccount) { return Response.json( diff --git a/classes/workers/fetch.ts b/classes/workers/fetch.ts index 0fab31d2..f29cb366 100644 --- a/classes/workers/fetch.ts +++ b/classes/workers/fetch.ts @@ -41,7 +41,7 @@ export const getFetchWorker = (): Worker => return; } - await Instance.resolve(uri); + await Instance.resolve(new URL(uri)); await job.log( `✔ Finished fetching instance metadata from [${uri}]`, diff --git a/classes/workers/inbox.ts b/classes/workers/inbox.ts index 5c6addb4..37521ad7 100644 --- a/classes/workers/inbox.ts +++ b/classes/workers/inbox.ts @@ -22,7 +22,10 @@ export const getInboxWorker = (): Worker => if (headers.authorization) { const processor = new InboxProcessor( - request, + { + ...request, + url: new URL(request.url), + }, data, null, { @@ -67,7 +70,7 @@ export const getInboxWorker = (): Worker => "x-signed-by": string; }; - const sender = await User.resolve(signedBy); + const sender = await User.resolve(new URL(signedBy)); if (!(sender || signedBy.startsWith("instance "))) { await job.log( @@ -106,7 +109,10 @@ export const getInboxWorker = (): Worker => } const processor = new InboxProcessor( - request, + { + ...request, + url: new URL(request.url), + }, data, { instance: remoteInstance, diff --git a/classes/workers/push.ts b/classes/workers/push.ts index 663d224d..82c10cd6 100644 --- a/classes/workers/push.ts +++ b/classes/workers/push.ts @@ -113,7 +113,7 @@ export const getPushWorker = (): Worker => vapidDetails: { subject: config.notifications.push.vapid.subject || - config.http.base_url, + config.http.base_url.origin, privateKey: config.notifications.push.vapid.private, publicKey: config.notifications.push.vapid.public, }, diff --git a/cli/classes.ts b/cli/classes.ts index 1fc7cbc4..16211ed9 100644 --- a/cli/classes.ts +++ b/cli/classes.ts @@ -101,7 +101,7 @@ export abstract class UserFinderCommand< // Check instance exists, if not, create it await Instance.resolve( - `https://${parseUserAddress(identifier).domain}`, + new URL(`https://${parseUserAddress(identifier).domain}`), ); } diff --git a/cli/commands/federation/user/fetch.ts b/cli/commands/federation/user/fetch.ts index 9cd70e68..aa369998 100644 --- a/cli/commands/federation/user/fetch.ts +++ b/cli/commands/federation/user/fetch.ts @@ -43,7 +43,7 @@ export default class FederationUserFetch extends BaseCommand< } // Check instance exists, if not, create it - await Instance.resolve(`https://${host}`); + await Instance.resolve(new URL(`https://${host}`)); const manager = await User.getFederationRequester(); diff --git a/cli/commands/federation/user/finger.ts b/cli/commands/federation/user/finger.ts index 1f969aba..d6e90398 100644 --- a/cli/commands/federation/user/finger.ts +++ b/cli/commands/federation/user/finger.ts @@ -44,7 +44,7 @@ export default class FederationUserFinger extends BaseCommand< } // Check instance exists, if not, create it - await Instance.resolve(`https://${host}`); + await Instance.resolve(new URL(`https://${host}`)); const manager = await User.getFederationRequester(); diff --git a/packages/config-manager/config.type.ts b/packages/config-manager/config.type.ts index 91974f6b..0818f50c 100644 --- a/packages/config-manager/config.type.ts +++ b/packages/config-manager/config.type.ts @@ -23,7 +23,8 @@ const zUrl = z .trim() .min(1) .refine((arg) => URL.canParse(arg), "Invalid url") - .transform((arg) => arg.replace(/\/$/, "")); + .transform((arg) => arg.replace(/\/$/, "")) + .transform((arg) => new URL(arg)); export const configValidator = z .object({ @@ -124,7 +125,7 @@ export const configValidator = z .strict(), http: z .object({ - base_url: z.string().min(1).default("http://versia.social"), + base_url: zUrl.default("http://versia.social"), bind: z.string().min(1).default("0.0.0.0"), bind_port: z .number() diff --git a/plugins/openid/routes/sso/:id/index.ts b/plugins/openid/routes/sso/:id/index.ts index fc25cdf9..d187bdab 100644 --- a/plugins/openid/routes/sso/:id/index.ts +++ b/plugins/openid/routes/sso/:id/index.ts @@ -86,7 +86,9 @@ export default (plugin: PluginType): void => { { id: issuer.id, name: issuer.name, - icon: proxyUrl(issuer.icon) ?? undefined, + icon: issuer.icon + ? proxyUrl(new URL(issuer.icon)) + : undefined, }, 200, ); diff --git a/plugins/openid/routes/sso/index.ts b/plugins/openid/routes/sso/index.ts index 16ccfad6..67b7b004 100644 --- a/plugins/openid/routes/sso/index.ts +++ b/plugins/openid/routes/sso/index.ts @@ -114,7 +114,9 @@ export default (plugin: PluginType): void => { ); } - const authServer = await oauthDiscoveryRequest(issuer.url); + const authServer = await oauthDiscoveryRequest( + new URL(issuer.url), + ); const codeVerifier = generateRandomCodeVerifier(); @@ -130,7 +132,7 @@ export default (plugin: PluginType): void => { crypto.getRandomValues(new Uint8Array(32)), ).toString("base64"), name: "Versia", - redirectUri, + redirectUri: redirectUri.toString(), scopes: "openid profile email", secret: "", }); diff --git a/plugins/openid/utils.ts b/plugins/openid/utils.ts index b7f36c84..62c3255a 100644 --- a/plugins/openid/utils.ts +++ b/plugins/openid/utils.ts @@ -20,17 +20,15 @@ import { } from "oauth4webapi"; export const oauthDiscoveryRequest = ( - issuerUrl: string | URL, + issuerUrl: URL, ): Promise => { - const issuerUrlurl = new URL(issuerUrl); - - return discoveryRequest(issuerUrlurl, { + return discoveryRequest(issuerUrl, { algorithm: "oidc", - }).then((res) => processDiscoveryResponse(issuerUrlurl, res)); + }).then((res) => processDiscoveryResponse(issuerUrl, res)); }; -export const oauthRedirectUri = (baseUrl: string, issuer: string): string => - new URL(`/oauth/sso/${issuer}/callback`, baseUrl).toString(); +export const oauthRedirectUri = (baseUrl: URL, issuer: string): URL => + new URL(`/oauth/sso/${issuer}/callback`, baseUrl); const getFlow = ( flowId: string, @@ -73,7 +71,7 @@ const getOIDCResponse = ( authServer: AuthorizationServer, clientId: string, clientSecret: string, - redirectUri: string, + redirectUri: URL, codeVerifier: string, parameters: URLSearchParams, ): Promise => { @@ -84,7 +82,7 @@ const getOIDCResponse = ( }, ClientSecretPost(clientSecret), parameters, - redirectUri, + redirectUri.toString(), codeVerifier, ); }; @@ -177,7 +175,7 @@ export const automaticOidcFlow = async ( authServer, issuer.client_id, issuer.client_secret, - redirectUrl.toString(), + redirectUrl, flow.codeVerifier, parameters, ); diff --git a/utils/bull-board.ts b/utils/bull-board.ts index 8ba1d845..306c176a 100644 --- a/utils/bull-board.ts +++ b/utils/bull-board.ts @@ -32,7 +32,7 @@ export const applyToHono = (app: OpenAPIHono): void => { }, boardLogo: { path: - config.instance.logo ?? + config.instance.logo?.toString() ?? "https://cdn.versia.pub/branding/icon.svg", height: 40, }, diff --git a/utils/constants.ts b/utils/constants.ts index 1a4698da..1431d36b 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -1,4 +1,4 @@ import { config } from "~/packages/config-manager/index.ts"; -export const localObjectUri = (id: string): string => - new URL(`/objects/${id}`, config.http.base_url).toString(); +export const localObjectUri = (id: string): URL => + new URL(`/objects/${id}`, config.http.base_url); diff --git a/utils/content_types.ts b/utils/content_types.ts index b67b67a0..4769e42e 100644 --- a/utils/content_types.ts +++ b/utils/content_types.ts @@ -30,28 +30,25 @@ export const getBestContentType = ( }; export const urlToContentFormat = ( - url: string, + url: URL, contentType?: string, ): ContentFormat | null => { - if (!url) { - return null; - } - if (url.startsWith("https://api.dicebear.com/")) { + if (url.href.startsWith("https://api.dicebear.com/")) { return { "image/svg+xml": { - content: url, + content: url.toString(), remote: true, }, }; } const mimeType = contentType || - lookup(url.replace(new URL(url).search, "")) || + lookup(url.toString().replace(url.search, "")) || "application/octet-stream"; return { [mimeType]: { - content: url, + content: url.toString(), remote: true, }, }; diff --git a/utils/response.ts b/utils/response.ts index a1d95577..e2beb84f 100644 --- a/utils/response.ts +++ b/utils/response.ts @@ -9,12 +9,9 @@ export type Json = | Json[] | { [key: string]: Json }; -export const proxyUrl = (url: string | null = null): string | null => { - const urlAsBase64Url = Buffer.from(url || "").toString("base64url"); - return url - ? new URL( - `/media/proxy/${urlAsBase64Url}`, - config.http.base_url, - ).toString() - : url; +export const proxyUrl = (url: URL): URL => { + const urlAsBase64Url = Buffer.from(url.toString() || "").toString( + "base64url", + ); + return new URL(`/media/proxy/${urlAsBase64Url}`, config.http.base_url); }; diff --git a/utils/sanitization.ts b/utils/sanitization.ts index cd0b65fc..ee579a94 100644 --- a/utils/sanitization.ts +++ b/utils/sanitization.ts @@ -136,7 +136,11 @@ export const sanitizeHtml = async ( element(element): void { element.setAttribute( "src", - proxyUrl(element.getAttribute("src") ?? "") ?? "", + element.getAttribute("src") + ? proxyUrl( + new URL(element.getAttribute("src") as string), + ).toString() + : "", ); }, })