Compare commits

..

3 commits

Author SHA1 Message Date
Jesse Wierzbinski ae5c3bb281
chore(federation): 🔖 [publish] Release 0.2.0
Some checks failed
Test Build / build (push) Failing after 5s
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 5s
Lint & Format / tests (push) Failing after 5s
Mirror to Codeberg / Mirror (push) Failing after 1s
Build & Publish Package / publish (push) Failing after 5s
Test Publish / build (client) (push) Failing after 5s
Test Publish / build (federation) (push) Failing after 5s
Run Tests / build (push) Failing after 4s
2025-02-13 18:04:28 +01:00
Jesse Wierzbinski 5114df4454
feat(federation): 👽 Update to Versia 0.5 2025-02-13 18:03:04 +01:00
Jesse Wierzbinski afec384a51
feat(federation): 👽 Update cryptography code to Versia 0.5 2025-02-13 17:39:16 +01:00
12 changed files with 278 additions and 84 deletions

View file

@ -7,7 +7,7 @@ describe("SignatureValidator", () => {
let publicKey: CryptoKey;
let body: string;
let signature: string;
let nonce: string;
let timestamp: Date;
beforeAll(async () => {
const keys = await crypto.subtle.generateKey("Ed25519", true, [
@ -25,8 +25,8 @@ describe("SignatureValidator", () => {
"https://bob.org/users/6a18f2c3-120e-4949-bda4-2aa4c8264d51",
).sign("GET", new URL("https://example.com"), body);
signature = headers.get("X-Signature") ?? "";
nonce = headers.get("X-Nonce") ?? "";
signature = headers.get("Versia-Signature") ?? "";
timestamp = new Date(Number(headers.get("Versia-Signed-At")) * 1000);
});
test("fromStringKey", async () => {
@ -46,8 +46,8 @@ describe("SignatureValidator", () => {
const request = new Request("https://example.com", {
method: "GET",
headers: {
"X-Signature": signature,
"X-Nonce": nonce,
"Versia-Signature": signature,
"Versia-Signed-At": String(timestamp.getTime() / 1000),
},
body: body,
});
@ -59,8 +59,8 @@ describe("SignatureValidator", () => {
const request = new Request("https://example.com", {
method: "GET",
headers: {
"X-Signature": "invalid",
"X-Nonce": nonce,
"Versia-Signature": "invalid",
"Versia-Signed-At": String(timestamp.getTime() / 1000),
},
body: body,
});
@ -70,16 +70,16 @@ describe("SignatureValidator", () => {
expect(isValid).toBe(false);
});
test("should throw with missing nonce", async () => {
test("should throw with missing timestamp", async () => {
const request = new Request("https://example.com", {
method: "GET",
headers: {
"X-Signature": signature,
"Versia-Signature": signature,
},
body: body,
});
expect(() => validator.validate(request)).toThrow(
"Headers are missing in request: X-Nonce",
"Headers are missing in request: Versia-Signed-At",
);
});
@ -87,8 +87,8 @@ describe("SignatureValidator", () => {
const request = new Request("https://example.com", {
method: "GET",
headers: {
"X-Signature": signature,
"X-Nonce": nonce,
"Versia-Signature": signature,
"Versia-Signed-At": String(timestamp.getTime() / 1000),
},
body: "different",
});
@ -101,8 +101,8 @@ describe("SignatureValidator", () => {
const request = new Request("https://example.com", {
method: "GET",
headers: {
"X-Signature": "thisIsNotbase64OhNo$^ù",
"X-Nonce": nonce,
"Versia-Signature": "thisIsNotbase64OhNo$^ù",
"Versia-Signed-At": String(timestamp.getTime() / 1000),
},
body: body,
});
@ -151,11 +151,11 @@ describe("SignatureConstructor", () => {
test("should correctly sign ", async () => {
const url = new URL("https://example.com");
headers = (await ctor.sign("GET", url, body)).headers;
expect(headers.get("X-Signature")).toBeDefined();
expect(headers.get("X-Nonce")).toBeDefined();
expect(headers.get("Versia-Signature")).toBeDefined();
expect(headers.get("Versia-Signed-At")).toBeDefined();
expect(headers.get("X-Nonce")?.length).toBeGreaterThan(10);
expect(headers.get("X-Signature")?.length).toBeGreaterThan(10);
expect(headers.get("Versia-Signed-At")?.length).toBeGreaterThan(10);
expect(headers.get("Versia-Signature")?.length).toBeGreaterThan(10);
});
test("should correctly sign a Request", async () => {
@ -167,8 +167,8 @@ describe("SignatureConstructor", () => {
const { request: newRequest } = await ctor.sign(request);
headers = newRequest.headers;
expect(headers.get("X-Signature")).toBeDefined();
expect(headers.get("X-Nonce")).toBeDefined();
expect(headers.get("Versia-Signature")).toBeDefined();
expect(headers.get("Versia-Signed-At")).toBeDefined();
expect(await newRequest.text()).toBe(body);
});

View file

@ -16,9 +16,6 @@ const base64ToArrayBuffer = (base64: string) =>
const arrayBufferToBase64 = (arrayBuffer: ArrayBuffer) =>
btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
const uint8ArrayToBase64 = (uint8Array: Uint8Array) =>
btoa(String.fromCharCode(...uint8Array));
const checkEvironmentSupport = () => {
// Check if WebCrypto is supported
if (!globalThis.crypto?.subtle) {
@ -78,7 +75,7 @@ export class SignatureValidator {
/**
* Validates the signature of a request.
* @param signature The signature string.
* @param nonce Signature nonce.
* @param timestamp Signature timestamp.
* @param method The HTTP verb.
* @param url The URL object.
* @param body The request body.
@ -86,15 +83,15 @@ export class SignatureValidator {
* @throws TypeError if any required parameters are missing or empty.
* @example
* const signature = "k4QNt5Grl40KK8orIdiaq118Z+P5pa6vIeArq55wsvfL7wNy4cE3f2fhsGcpZql+PStm+x2ZjZIhudrAC/32Cg==";
* const nonce = "bJzyhTNK2RXUCetKIpm0Fw==";
* const date = new Date(1549312452000)
* const method = "GET";
* const url = new URL("https://example.com/users/ff54ee40-2ce9-4d2e-86ac-3cd06a1e1480");
* const body = "{ ... }";
* const isValid = await validator.validate(signature, nonce, method, url, body);
* const isValid = await validator.validate(signature, date, method, url, body);
*/
async validate(
signature: string,
nonce: string,
timestamp: Date,
method: HttpVerb,
url: URL,
body: string,
@ -102,25 +99,28 @@ export class SignatureValidator {
async validate(
requestOrSignature: Request | string,
nonce?: string,
timestamp?: Date,
method?: HttpVerb,
url?: URL,
body?: string,
): Promise<boolean> {
if (requestOrSignature instanceof Request) {
const signature = requestOrSignature.headers.get("X-Signature");
const nonce = requestOrSignature.headers.get("X-Nonce");
const signature =
requestOrSignature.headers.get("Versia-Signature");
const timestampHeader =
requestOrSignature.headers.get("Versia-Signed-At");
const timestamp = new Date(Number(timestampHeader) * 1000);
const url = new URL(requestOrSignature.url);
const body = await requestOrSignature.text();
const method = requestOrSignature.method as HttpVerb;
const missingHeaders = [
!signature && "X-Signature",
!nonce && "X-Nonce",
!signature && "Versia-Signature",
!timestampHeader && "Versia-Signed-At",
].filter(Boolean);
// Check if all headers are present
if (!(signature && nonce && method && url && body)) {
if (!(signature && timestampHeader && method && url && body)) {
// Say which headers are missing
throw new TypeError(
`Headers are missing in request: ${missingHeaders.join(
@ -129,12 +129,12 @@ export class SignatureValidator {
);
}
return this.validate(signature, nonce, method, url, body);
return this.validate(signature, timestamp, method, url, body);
}
if (!(nonce && method && url && body)) {
if (!(timestamp && method && url && body)) {
throw new TypeError(
"Missing or empty required parameters: nonce, method, url or body",
"Missing or empty required parameters: timestamp, method, url or body",
);
}
@ -152,7 +152,7 @@ export class SignatureValidator {
new TextEncoder().encode(body),
);
const expectedSignedString = `${method.toLowerCase()} ${encodeURIComponent(url.pathname)} ${nonce} ${arrayBufferToBase64(digest)}`;
const expectedSignedString = `${method.toLowerCase()} ${encodeURIComponent(url.pathname)} ${timestamp.getTime() / 1000} ${arrayBufferToBase64(digest)}`;
// Check if signed string is valid
const isValid = await crypto.subtle.verify(
@ -232,7 +232,7 @@ export class SignatureConstructor {
* @param url The URL object.
* @param body The request body.
* @param headers The request headers.
* @param nonce The signature nonce (optional).
* @param timestamp The signature timestamp (optional).
* @returns A Promise that resolves to the signed headers, and the signed string.
* @throws TypeError if any required parameters are missing or empty.
* @example
@ -246,7 +246,7 @@ export class SignatureConstructor {
url: URL,
body?: string,
headers?: Headers,
nonce?: string,
timestamp?: Date,
): Promise<{
headers: Headers;
signedString: string;
@ -257,7 +257,7 @@ export class SignatureConstructor {
url?: URL,
body?: string,
headers: Headers = new Headers(),
nonce?: string,
timestamp?: Date,
): Promise<
| {
headers: Headers;
@ -270,19 +270,23 @@ export class SignatureConstructor {
> {
if (requestOrMethod instanceof Request) {
const request = requestOrMethod.clone();
const signedAt = requestOrMethod.headers.get("Versia-Signed-At");
const { headers, signedString } = await this.sign(
requestOrMethod.method as HttpVerb,
new URL(requestOrMethod.url),
await requestOrMethod.text(),
requestOrMethod.headers,
requestOrMethod.headers.get("X-Nonce") ?? undefined,
signedAt ? new Date(Number(signedAt) * 1000) : undefined,
);
request.headers.set("X-Nonce", headers.get("X-Nonce") ?? "");
request.headers.set(
"X-Signature",
headers.get("X-Signature") ?? "",
"Versia-Signed-At",
headers.get("Versia-Signed-At") ?? "",
);
request.headers.set(
"Versia-Signature",
headers.get("Versia-Signature") ?? "",
);
return { request, signedString };
@ -294,9 +298,7 @@ export class SignatureConstructor {
);
}
const finalNonce =
nonce ||
uint8ArrayToBase64(crypto.getRandomValues(new Uint8Array(16)));
const finalTimestamp = timestamp || new Date();
const digest = await crypto.subtle.digest(
"SHA-256",
@ -305,7 +307,7 @@ export class SignatureConstructor {
const signedString = `${requestOrMethod.toLowerCase()} ${encodeURIComponent(
url.pathname,
)} ${finalNonce} ${arrayBufferToBase64(digest)}`;
)} ${finalTimestamp.getTime() / 1000} ${arrayBufferToBase64(digest)}`;
const signature = await crypto.subtle.sign(
"Ed25519",
@ -315,9 +317,12 @@ export class SignatureConstructor {
const signatureBase64 = arrayBufferToBase64(signature);
headers.set("X-Nonce", finalNonce);
headers.set("X-Signature", signatureBase64);
headers.set("X-Signed-By", this.authorUri.toString());
headers.set(
"Versia-Signed-At",
String(finalTimestamp.getTime() / 1000),
);
headers.set("Versia-Signature", signatureBase64);
headers.set("Versia-Signed-By", this.authorUri.toString());
return {
headers,

View file

@ -4,7 +4,10 @@ import type {
Follow,
FollowAccept,
FollowReject,
Group,
GroupExtensionSubscribe,
GroupExtensionSubscribeAccept,
GroupExtensionSubscribeReject,
GroupExtensionUnsubscribe,
InstanceMetadata,
LikeExtension,
Note,
@ -28,7 +31,18 @@ type ParserCallbacks<T> = {
"pub.versia:likes/Dislike": (dislike: DislikeExtension) => MaybePromise<T>;
delete: (undo: Delete) => MaybePromise<T>;
instanceMetadata: (instanceMetadata: InstanceMetadata) => MaybePromise<T>;
group: (group: Group) => MaybePromise<T>;
"pub.versia:groups/Subscribe": (
groupSubscribe: GroupExtensionSubscribe,
) => MaybePromise<T>;
"pub.versia:groups/SubscribeAccept": (
groupSubscribeAccept: GroupExtensionSubscribeAccept,
) => MaybePromise<T>;
"pub.versia:groups/SubscribeReject": (
groupSubscribeReject: GroupExtensionSubscribeReject,
) => MaybePromise<T>;
"pub.versia:groups/Unsubscribe": (
groupUnsubscribe: GroupExtensionUnsubscribe,
) => MaybePromise<T>;
"pub.versia:reactions/Reaction": (
reaction: ReactionExtension,
) => MaybePromise<T>;
@ -182,11 +196,52 @@ export class RequestParserHandler {
break;
}
case "Group": {
const group = await this.validator.Group(this.body);
case "pub.versia:groups/Subscribe": {
const groupSubscribe = await this.validator.GroupSubscribe(
this.body,
);
if (callbacks.group) {
return await callbacks.group(group);
if (callbacks["pub.versia:groups/Subscribe"]) {
return await callbacks["pub.versia:groups/Subscribe"](
groupSubscribe,
);
}
break;
}
case "pub.versia:groups/SubscribeAccept": {
const groupSubscribeAccept =
await this.validator.GroupSubscribeAccept(this.body);
if (callbacks["pub.versia:groups/SubscribeAccept"]) {
return await callbacks["pub.versia:groups/SubscribeAccept"](
groupSubscribeAccept,
);
}
break;
}
case "pub.versia:groups/SubscribeReject": {
const groupSubscribeReject =
await this.validator.GroupSubscribeReject(this.body);
if (callbacks["pub.versia:groups/SubscribeReject"]) {
return await callbacks["pub.versia:groups/SubscribeReject"](
groupSubscribeReject,
);
}
break;
}
case "pub.versia:groups/Unsubscribe": {
const groupUnsubscribe = await this.validator.GroupUnsubscribe(
this.body,
);
if (callbacks["pub.versia:groups/Unsubscribe"]) {
return await callbacks["pub.versia:groups/Unsubscribe"](
groupUnsubscribe,
);
}
break;

View file

@ -1,7 +1,7 @@
{
"$schema": "https://jsr.io/schema/config-file.v1.json",
"name": "@versia/federation",
"version": "0.1.4",
"version": "0.2.0",
"exports": {
".": "./index.ts",
"./types": "./types.ts",

View file

@ -1,7 +1,7 @@
{
"name": "@versia/federation",
"displayName": "Versia Federation",
"version": "0.1.4",
"version": "0.2.0",
"author": {
"email": "jesse.wierzbinski@lysand.org",
"name": "Jesse Wierzbinski (CPlusPatch)",

View file

@ -11,7 +11,7 @@ export {
FollowAcceptSchema as FollowAccept,
FollowRejectSchema as FollowReject,
FollowSchema as Follow,
GroupSchema as Group,
URICollectionSchema as URICollection,
InstanceMetadataSchema as InstanceMetadata,
NoteSchema as Note,
UnfollowSchema as Unfollow,
@ -20,6 +20,13 @@ export {
export { ContentFormatSchema as ContentFormat } from "./schemas/content_format.ts";
export { ExtensionPropertySchema as EntityExtensionProperty } from "./schemas/extensions.ts";
export { CustomEmojiExtensionSchema as CustomEmojiExtension } from "./schemas/extensions/custom_emojis.ts";
export {
GroupSchema as GroupExtension,
GroupSubscribeSchema as GroupExtensionSubscribe,
GroupSubscribeAcceptSchema as GroupExtensionSubscribeAccept,
GroupSubscribeRejectSchema as GroupExtensionSubscribeReject,
GroupUnsubscribeSchema as GroupExtensionUnsubscribe,
} from "./schemas/extensions/groups.ts";
export {
DislikeSchema as DislikeExtension,
LikeSchema as LikeExtension,

View file

@ -10,6 +10,8 @@ import { extensionRegex, isISOString, semverRegex } from "./regex.ts";
export const EntitySchema = z
.object({
// biome-ignore lint/style/useNamingConvention:
$schema: z.string().url().optional().nullable(),
id: z.string().max(512),
created_at: z
.string()
@ -37,6 +39,17 @@ export const NoteSchema = EntitySchema.extend({
.optional()
.nullable(),
content: TextOnlyContentFormatSchema.optional().nullable(),
collections: z.object({
replies: z.string().url(),
quotes: z.string().url(),
"pub.versia:reactions/Reactions": z
.string()
.url()
.optional()
.nullable(),
"pub.versia:likes/Likes": z.string().url().optional().nullable(),
"pub.versia:likes/Dislikes": z.string().url().optional().nullable(),
}),
device: z
.object({
name: z.string(),
@ -72,13 +85,6 @@ export const NoteSchema = EntitySchema.extend({
replies_to: z.string().url().optional().nullable(),
subject: z.string().optional().nullable(),
extensions: ExtensionPropertySchema.extend({
"pub.versia:reactions": z
.object({
reactions: z.string().url(),
})
.strict()
.optional()
.nullable(),
"pub.versia:polls": z
.object({
options: z.array(TextOnlyContentFormatSchema),
@ -119,6 +125,10 @@ export const CollectionSchema = z.object({
items: z.array(z.any()),
});
export const URICollectionSchema = CollectionSchema.extend({
items: z.array(z.string().url()),
});
export const PublicKeyDataSchema = z
.object({
key: z.string().min(1),
@ -147,8 +157,8 @@ export const UserSchema = EntitySchema.extend({
.string()
.min(1)
.regex(
/^[a-z0-9_-]+$/,
"must be lowercase, alphanumeric, and may contain _ or -",
/^[a-zA-Z0-9_-]+$/,
"must be alphanumeric, and may contain _ or -",
),
header: ImageOnlyContentFormatSchema.optional().nullable(),
public_key: PublicKeyDataSchema,
@ -208,14 +218,6 @@ export const UnfollowSchema = EntitySchema.extend({
followee: z.string().url(),
});
export const GroupSchema = EntitySchema.extend({
type: z.literal("Group"),
name: TextOnlyContentFormatSchema.optional().nullable(),
description: TextOnlyContentFormatSchema.optional().nullable(),
members: z.string().url(),
notes: z.string().url().optional().nullable(),
});
export const InstanceMetadataSchema = EntitySchema.extend({
type: z.literal("InstanceMetadata"),
id: z.null().optional(),

View file

@ -0,0 +1,40 @@
import { z } from "zod";
import { EntitySchema } from "../base.ts";
import { TextOnlyContentFormatSchema } from "../content_format.ts";
export const GroupSchema = EntitySchema.extend({
type: z.literal("pub.versia:groups/Group"),
name: TextOnlyContentFormatSchema.optional().nullable(),
description: TextOnlyContentFormatSchema.optional().nullable(),
open: z.boolean().optional().nullable(),
members: z.string().url(),
notes: z.string().url().optional().nullable(),
});
export const GroupSubscribeSchema = EntitySchema.extend({
type: z.literal("pub.versia:groups/Subscribe"),
uri: z.null().optional(),
subscriber: z.string().url(),
group: z.string().url(),
});
export const GroupUnsubscribeSchema = EntitySchema.extend({
type: z.literal("pub.versia:groups/Unsubscribe"),
uri: z.null().optional(),
subscriber: z.string().url(),
group: z.string().url(),
});
export const GroupSubscribeAcceptSchema = EntitySchema.extend({
type: z.literal("pub.versia:groups/SubscribeAccept"),
uri: z.null().optional(),
subscriber: z.string().url(),
group: z.string().url(),
});
export const GroupSubscribeRejectSchema = EntitySchema.extend({
type: z.literal("pub.versia:groups/SubscribeReject"),
uri: z.null().optional(),
subscriber: z.string().url(),
group: z.string().url(),
});

View file

@ -10,7 +10,7 @@ import {
AudioOnlyContentFormatSchema,
ImageOnlyContentFormatSchema,
} from "../content_format.ts";
import { isISOString } from "../regex.ts";
import { ianaTimezoneRegex, isISOString } from "../regex.ts";
/**
* @description Vanity extension entity
@ -103,5 +103,10 @@ export const VanityExtensionSchema = z
.nullable(),
location: z.string().optional().nullable(),
aliases: z.array(z.string().url()).optional().nullable(),
timezone: z
.string()
.regex(ianaTimezoneRegex, "must be a valid IANA timezone")
.optional()
.nullable(),
})
.strict();

View file

@ -66,3 +66,5 @@ export const isISOString = (val: string | Date) => {
const d = new Date(val);
return !Number.isNaN(d.valueOf());
};
export const ianaTimezoneRegex = /^(?:[A-Za-z]+(?:\/[A-Za-z_]+)+|UTC)$/;

View file

@ -11,15 +11,22 @@ import type {
FollowAcceptSchema,
FollowRejectSchema,
FollowSchema,
GroupSchema,
InstanceMetadataSchema,
NoteSchema,
URICollectionSchema,
UnfollowSchema,
UserSchema,
} from "./schemas/base.ts";
import type { ContentFormatSchema } from "./schemas/content_format.ts";
import type { ExtensionPropertySchema } from "./schemas/extensions.ts";
import type { CustomEmojiExtensionSchema } from "./schemas/extensions/custom_emojis.ts";
import type {
GroupSchema,
GroupSubscribeAcceptSchema,
GroupSubscribeRejectSchema,
GroupSubscribeSchema,
GroupUnsubscribeSchema,
} from "./schemas/extensions/groups.ts";
import type { DislikeSchema, LikeSchema } from "./schemas/extensions/likes.ts";
import type { VoteSchema } from "./schemas/extensions/polls.ts";
import type { ReactionSchema } from "./schemas/extensions/reactions.ts";
@ -33,6 +40,7 @@ type InferType<T extends AnyZod> = z.infer<T>;
export type Note = InferType<typeof NoteSchema>;
export type Collection = InferType<typeof CollectionSchema>;
export type URICollection = InferType<typeof URICollectionSchema>;
export type EntityExtensionProperty = InferType<typeof ExtensionPropertySchema>;
export type VanityExtension = InferType<typeof VanityExtensionSchema>;
export type User = InferType<typeof UserSchema>;
@ -43,9 +51,19 @@ export type ContentFormat = InferType<typeof ContentFormatSchema>;
export type CustomEmojiExtension = InferType<typeof CustomEmojiExtensionSchema>;
export type Entity = InferType<typeof EntitySchema>;
export type Delete = InferType<typeof DeleteSchema>;
export type Group = InferType<typeof GroupSchema>;
export type InstanceMetadata = InferType<typeof InstanceMetadataSchema>;
export type Unfollow = InferType<typeof UnfollowSchema>;
export type GroupExtension = InferType<typeof GroupSchema>;
export type GroupExtensionSubscribe = InferType<typeof GroupSubscribeSchema>;
export type GroupExtensionSubscribeAccept = InferType<
typeof GroupSubscribeAcceptSchema
>;
export type GroupExtensionSubscribeReject = InferType<
typeof GroupSubscribeRejectSchema
>;
export type GroupExtensionUnsubscribe = InferType<
typeof GroupUnsubscribeSchema
>;
export type LikeExtension = InferType<typeof LikeSchema>;
export type DislikeExtension = InferType<typeof DislikeSchema>;
export type PollVoteExtension = InferType<typeof VoteSchema>;

View file

@ -7,15 +7,22 @@ import {
FollowAcceptSchema,
FollowRejectSchema,
FollowSchema,
GroupSchema,
InstanceMetadataSchema,
NoteSchema,
URICollectionSchema,
UnfollowSchema,
UserSchema,
} from "./schemas/base.ts";
import { ContentFormatSchema } from "./schemas/content_format.ts";
import { ExtensionPropertySchema } from "./schemas/extensions.ts";
import { CustomEmojiExtensionSchema } from "./schemas/extensions/custom_emojis.ts";
import {
GroupSchema,
GroupSubscribeAcceptSchema,
GroupSubscribeRejectSchema,
GroupSubscribeSchema,
GroupUnsubscribeSchema,
} from "./schemas/extensions/groups.ts";
import { DislikeSchema, LikeSchema } from "./schemas/extensions/likes.ts";
import { VoteSchema } from "./schemas/extensions/polls.ts";
import { ReactionSchema } from "./schemas/extensions/reactions.ts";
@ -32,18 +39,22 @@ import type {
Follow,
FollowAccept,
FollowReject,
Group,
GroupExtension,
GroupExtensionSubscribe,
GroupExtensionSubscribeAccept,
GroupExtensionSubscribeReject,
GroupExtensionUnsubscribe,
InstanceMetadata,
LikeExtension,
Note,
PollVoteExtension,
ReactionExtension,
ShareExtension,
URICollection,
Unfollow,
User,
VanityExtension,
} from "./types.ts";
// biome-ignore lint/suspicious/noExplicitAny: Used only as a base type
type AnyZod = z.ZodType<any, any, any>;
@ -110,6 +121,15 @@ export class EntityValidator {
return this.validate(CollectionSchema, data);
}
/**
* Validates a URICollection entity.
* @param data - The data to validate
* @returns A promise that resolves to the validated data.
*/
public URICollection(data: unknown): Promise<URICollection> {
return this.validate(URICollectionSchema, data);
}
/**
* Validates a VanityExtension entity.
* @param data - The data to validate
@ -207,10 +227,50 @@ export class EntityValidator {
* @param data - The data to validate
* @returns A promise that resolves to the validated data.
*/
public Group(data: unknown): Promise<Group> {
public Group(data: unknown): Promise<GroupExtension> {
return this.validate(GroupSchema, data);
}
/**
* Validates a GroupSubscribe entity.
* @param data - The data to validate
* @returns A promise that resolves to the validated data.
*/
public GroupSubscribe(data: unknown): Promise<GroupExtensionSubscribe> {
return this.validate(GroupSubscribeSchema, data);
}
/**
* Validates a GroupSubscribeAccept entity.
* @param data - The data to validate
* @returns A promise that resolves to the validated data.
*/
public GroupSubscribeAccept(
data: unknown,
): Promise<GroupExtensionSubscribeAccept> {
return this.validate(GroupSubscribeAcceptSchema, data);
}
/**
* Validates a GroupSubscribeReject entity.
* @param data - The data to validate
* @returns A promise that resolves to the validated data.
*/
public GroupSubscribeReject(
data: unknown,
): Promise<GroupExtensionSubscribeReject> {
return this.validate(GroupSubscribeRejectSchema, data);
}
/**
* Validates a GroupUnsubscribe entity.
* @param data - The data to validate
* @returns A promise that resolves to the validated data.
*/
public GroupUnsubscribe(data: unknown): Promise<GroupExtensionUnsubscribe> {
return this.validate(GroupUnsubscribeSchema, data);
}
/**
* Validates an InstanceMetadata entity.
* @param data - The data to validate