refactor(api): ♻️ Use URL literal instead of strings

This commit is contained in:
Jesse Wierzbinski 2025-02-01 16:32:18 +01:00
parent 99fac323c8
commit 76d1ccc859
No known key found for this signature in database
50 changed files with 343 additions and 256 deletions

View file

@ -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(

View file

@ -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(

View file

@ -14,7 +14,7 @@ beforeAll(async () => {
priority: 2,
description: "test",
visible: true,
icon: "test",
icon: "https://test.com",
});
expect(role).toBeDefined();

View file

@ -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,
);

View file

@ -60,11 +60,11 @@ describe("/api/v1/accounts/update_credentials", () => {
const object = (await response.json()) as APIAccount;
// Proxy url is base_url/media/proxy/<base64url encoded url>
expect(object.note).toBe(
`<p><img src="${config.http.base_url}/media/proxy/${Buffer.from(
`<p><img src="${config.http.base_url}media/proxy/${Buffer.from(
"https://example.com/image.jpg",
).toString("base64url")}"> <video src="${
config.http.base_url
}/media/proxy/${Buffer.from(
}media/proxy/${Buffer.from(
"https://example.com/video.mp4",
).toString("base64url")}"> Test!</p>\n`,
);

View file

@ -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(

View file

@ -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,
);

View file

@ -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(

View file

@ -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,
})) ?? [],
},

View file

@ -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(

View file

@ -148,7 +148,7 @@ export default apiRoute((app) =>
)`,
),
limit,
context.req.url,
new URL(context.req.url),
user.id,
);

View file

@ -15,7 +15,7 @@ beforeAll(async () => {
priority: 2,
description: "test",
visible: true,
icon: "test",
icon: "https://test.com",
});
expect(role).toBeDefined();

View file

@ -15,7 +15,7 @@ beforeAll(async () => {
priority: 10,
description: "test",
visible: true,
icon: "test",
icon: "https://test.com",
});
expect(role).toBeDefined();

View file

@ -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(

View file

@ -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(

View file

@ -429,11 +429,11 @@ describe("/api/v1/statuses", () => {
const object = (await response.json()) as ApiStatus;
// Proxy url is base_url/media/proxy/<base64url encoded url>
expect(object.content).toBe(
`<p><img src="${config.http.base_url}/media/proxy/${Buffer.from(
`<p><img src="${config.http.base_url}media/proxy/${Buffer.from(
"https://example.com/image.jpg",
).toString("base64url")}"> <video src="${
config.http.base_url
}/media/proxy/${Buffer.from(
}media/proxy/${Buffer.from(
"https://example.com/video.mp4",
).toString("base64url")}"> Test!</p>`,
);

View file

@ -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"`,
);
});

View file

@ -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,
);

View file

@ -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"`,
);
});

View file

@ -93,7 +93,7 @@ export default apiRoute((app) =>
: eq(Notes.visibility, "public"),
),
limit,
context.req.url,
new URL(context.req.url),
user?.id,
);

View file

@ -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,
})) ?? [],
},

View file

@ -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());
}),

View file

@ -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());
}),

View file

@ -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;

View file

@ -188,8 +188,8 @@ export class Emoji extends BaseInterface<typeof Emojis, EmojiType> {
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,

View file

@ -135,7 +135,7 @@ export class Instance extends BaseInterface<typeof Instances> {
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<typeof Instances> {
}
private static async fetchActivityPubMetadata(
url: string,
url: URL,
): Promise<InstanceMetadata | null> {
const origin = new URL(url).origin;
const wellKnownUrl = new URL("/.well-known/nodeinfo", origin);
@ -311,18 +311,18 @@ export class Instance extends BaseInterface<typeof Instances> {
public static resolveFromHost(host: string): Promise<Instance> {
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<Instance> {
const host = new URL(url).host;
public static async resolve(url: URL): Promise<Instance> {
const host = url.host;
const existingInstance = await Instance.fromSql(
eq(Instances.baseUrl, host),
@ -352,7 +352,7 @@ export class Instance extends BaseInterface<typeof Instances> {
const logger = getLogger(["federation", "resolvers"]);
const output = await Instance.fetchMetadata(
`https://${this.data.baseUrl}`,
new URL(`https://${this.data.baseUrl}`),
);
if (!output) {

View file

@ -167,15 +167,17 @@ export class Like extends BaseInterface<typeof Likes, LikeType> {
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(
liked:
Note.getUri(
this.data.liked.id,
this.data.liked.uri,
) as string,
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<typeof Likes, LikeType> {
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(),
};

View file

@ -209,7 +209,7 @@ export class Media extends BaseInterface<typeof Medias> {
const url = Media.getUrl(path);
let thumbnailUrl = "";
let thumbnailUrl: URL | null = null;
if (options?.thumbnail) {
const { path } = await Media.upload(options.thumbnail);
@ -220,10 +220,15 @@ export class Media extends BaseInterface<typeof Medias> {
const content = await Media.fileToContentFormat(file, url, {
description: options?.description,
});
const thumbnailContent = options?.thumbnail
? await Media.fileToContentFormat(options.thumbnail, thumbnailUrl, {
const thumbnailContent =
thumbnailUrl && options?.thumbnail
? await Media.fileToContentFormat(
options.thumbnail,
thumbnailUrl,
{
description: options?.description,
})
},
)
: undefined;
const newAttachment = await Media.insert({
@ -246,6 +251,12 @@ export class Media extends BaseInterface<typeof Medias> {
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<typeof Medias> {
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 "";
return new URL(`/${name}`, config.s3?.public_url);
}
public getUrl(): string {
throw new Error("Unknown media backend");
}
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<typeof Medias> {
*/
public static async fileToContentFormat(
file: File,
uri: string,
uri: URL,
options?: Partial<{
description: string;
}>,
@ -475,7 +487,7 @@ export class Media extends BaseInterface<typeof Medias> {
// 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<typeof Medias> {
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,

View file

@ -646,17 +646,17 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
* @param uri - The URI of the note to resolve
* @returns The resolved note
*/
public static async resolve(uri: string): Promise<Note | null> {
public static async resolve(uri: URL): Promise<Note | null> {
// 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<typeof Notes, NoteTypeWithRelations> {
* @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<Note | null> {
public static async fetchFromRemote(uri: URL): Promise<Note | null> {
const instance = await Instance.resolve(uri);
if (!instance) {
@ -691,7 +691,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
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<typeof Notes, NoteTypeWithRelations> {
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<User>[],
),
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<typeof Notes, NoteTypeWithRelations> {
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<typeof Notes, NoteTypeWithRelations> {
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<typeof Notes, NoteTypeWithRelations> {
};
}
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<typeof Notes, NoteTypeWithRelations> {
* 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<typeof Notes, NoteTypeWithRelations> {
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<typeof Notes, NoteTypeWithRelations> {
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<typeof Notes, NoteTypeWithRelations> {
),
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",

View file

@ -179,13 +179,12 @@ export class Reaction extends BaseInterface<typeof Reactions, ReactionType> {
};
}
public getUri(baseUrl: string): string {
return (
this.data.uri ||
new URL(
public getUri(baseUrl: URL): URL {
return this.data.uri
? new URL(this.data.uri)
: new URL(
`/objects/${this.data.noteId}/reactions/${this.id}`,
baseUrl,
).toString()
);
}
@ -203,19 +202,21 @@ export class Reaction extends BaseInterface<typeof Reactions, ReactionType> {
}
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(
object:
Note.getUri(
this.data.note.id,
this.data.note.uri,
) as string,
this.data.note.uri
? new URL(this.data.note.uri)
: undefined,
)?.toString() ?? "",
content: this.hasCustomEmoji()
? `:${this.data.emoji?.shortcode}:`
: this.data.emojiText || "",

View file

@ -227,7 +227,9 @@ export class Role extends BaseInterface<typeof Roles> {
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,
};
}
}

View file

@ -17,7 +17,7 @@ export class Timeline<Type extends Note | User | Notification> {
public static getNoteTimeline(
sql: SQL<unknown> | undefined,
limit: number,
url: string,
url: URL,
userId?: string,
): Promise<{ link: string; objects: Note[] }> {
return new Timeline<Note>(TimelineType.Note).fetchTimeline(
@ -31,7 +31,7 @@ export class Timeline<Type extends Note | User | Notification> {
public static getUserTimeline(
sql: SQL<unknown> | undefined,
limit: number,
url: string,
url: URL,
): Promise<{ link: string; objects: User[] }> {
return new Timeline<User>(TimelineType.User).fetchTimeline(
sql,
@ -43,7 +43,7 @@ export class Timeline<Type extends Note | User | Notification> {
public static getNotificationTimeline(
sql: SQL<unknown> | undefined,
limit: number,
url: string,
url: URL,
userId?: string,
): Promise<{ link: string; objects: Notification[] }> {
return new Timeline<Notification>(
@ -85,14 +85,11 @@ export class Timeline<Type extends Note | User | Notification> {
private async fetchLinkHeader(
objects: Type[],
url: string,
url: URL,
limit: number,
): Promise<string> {
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<Type extends Note | User | Notification> {
private static async fetchNoteLinkHeader(
notes: Note[],
urlWithoutQuery: string,
urlWithoutQuery: URL,
limit: number,
): Promise<string[]> {
const linkHeader: string[] = [];
@ -158,7 +155,7 @@ export class Timeline<Type extends Note | User | Notification> {
private static async fetchUserLinkHeader(
users: User[],
urlWithoutQuery: string,
urlWithoutQuery: URL,
limit: number,
): Promise<string[]> {
const linkHeader: string[] = [];
@ -186,7 +183,7 @@ export class Timeline<Type extends Note | User | Notification> {
private static async fetchNotificationLinkHeader(
notifications: Notification[],
urlWithoutQuery: string,
urlWithoutQuery: URL,
limit: number,
): Promise<string[]> {
const linkHeader: string[] = [];
@ -220,7 +217,7 @@ export class Timeline<Type extends Note | User | Notification> {
private async fetchTimeline(
sql: SQL<unknown> | undefined,
limit: number,
url: string,
url: URL,
userId?: string,
): Promise<{ link: string; objects: Type[] }> {
const objects = await this.fetchObjects(sql, limit, userId);

View file

@ -228,19 +228,14 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
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<typeof Users, UserWithRelations> {
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<typeof Users, UserWithRelations> {
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<typeof Users, UserWithRelations> {
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<typeof Users, UserWithRelations> {
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<typeof Users, UserWithRelations> {
* @param hostname
* @returns URI, or null if not found
*/
public static webFinger(
public static async webFinger(
manager: FederationRequester,
username: string,
hostname: string,
): Promise<string | null> {
): Promise<URL | null> {
try {
return manager.webFinger(username, hostname);
return new URL(await manager.webFinger(username, hostname));
} catch {
try {
return manager.webFinger(
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<typeof Users, UserWithRelations> {
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<typeof Users, UserWithRelations> {
return this;
}
public static async fetchFromRemote(uri: string): Promise<User | null> {
if (!URL.canParse(uri)) {
throw new Error(`Invalid URI: ${uri}`);
}
public static async fetchFromRemote(uri: URL): Promise<User | null> {
const instance = await Instance.resolve(uri);
if (!instance) {
@ -684,19 +679,19 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
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<User> {
const requester = await User.getFederationRequester();
@ -859,19 +854,19 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
return user;
}
public static async resolve(uri: string): Promise<User | null> {
public static async resolve(uri: URL): Promise<User | null> {
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<typeof Users, UserWithRelations> {
* @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<typeof Users, UserWithRelations> {
* @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<typeof Users, UserWithRelations> {
*/
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<typeof Users, UserWithRelations> {
const output = await signatureConstructor.sign(
signatureMethod,
new URL(signatureUrl),
signatureUrl,
JSON.stringify(entity),
);
@ -1155,7 +1152,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
);
}
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<typeof Users, UserWithRelations> {
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<typeof Users, UserWithRelations> {
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<typeof Users, UserWithRelations> {
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<typeof Users, UserWithRelations> {
this.getAvatarUrl(config),
this.data.source.avatar?.content_type,
) ?? undefined,
header:
urlToContentFormat(
this.getHeaderUrl(config),
header: this.getHeaderUrl(config)
? (urlToContentFormat(
this.getHeaderUrl(config) as URL,
this.data.source.header?.content_type,
) ?? undefined,
) ?? undefined)
: undefined,
display_name: user.displayName,
fields: user.fields,
public_key: {
@ -1335,7 +1337,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
public toMention(): ApiMention {
return {
url: this.getUri(),
url: this.getUri().toString(),
username: this.data.username,
acct: this.getAcct(),
id: this.id,

View file

@ -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"]();

View file

@ -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<Response | null> {
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<Response | null> {
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<Response | null> {
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<Response | null> {
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<Response | null> {
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<Response | null> {
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(

View file

@ -41,7 +41,7 @@ export const getFetchWorker = (): Worker<FetchJobData, void, FetchJobType> =>
return;
}
await Instance.resolve(uri);
await Instance.resolve(new URL(uri));
await job.log(
`✔ Finished fetching instance metadata from [${uri}]`,

View file

@ -22,7 +22,10 @@ export const getInboxWorker = (): Worker<InboxJobData, void, InboxJobType> =>
if (headers.authorization) {
const processor = new InboxProcessor(
request,
{
...request,
url: new URL(request.url),
},
data,
null,
{
@ -67,7 +70,7 @@ export const getInboxWorker = (): Worker<InboxJobData, void, InboxJobType> =>
"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<InboxJobData, void, InboxJobType> =>
}
const processor = new InboxProcessor(
request,
{
...request,
url: new URL(request.url),
},
data,
{
instance: remoteInstance,

View file

@ -113,7 +113,7 @@ export const getPushWorker = (): Worker<PushJobData, void, PushJobType> =>
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,
},

View file

@ -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}`),
);
}

View file

@ -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();

View file

@ -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();

View file

@ -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()

View file

@ -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,
);

View file

@ -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: "",
});

View file

@ -20,17 +20,15 @@ import {
} from "oauth4webapi";
export const oauthDiscoveryRequest = (
issuerUrl: string | URL,
issuerUrl: URL,
): Promise<AuthorizationServer> => {
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<Response> => {
@ -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,
);

View file

@ -32,7 +32,7 @@ export const applyToHono = (app: OpenAPIHono<HonoEnv>): void => {
},
boardLogo: {
path:
config.instance.logo ??
config.instance.logo?.toString() ??
"https://cdn.versia.pub/branding/icon.svg",
height: 40,
},

View file

@ -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);

View file

@ -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,
},
};

View file

@ -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);
};

View file

@ -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()
: "",
);
},
})