docs(federation): 📝 Update SDK documentation

This commit is contained in:
Jesse Wierzbinski 2025-04-08 21:54:55 +02:00
parent f79b0bc999
commit 45e5460975
No known key found for this signature in database
67 changed files with 332 additions and 65 deletions

View file

@ -0,0 +1,16 @@
import { z } from "zod";
import { url, u64 } from "./common.ts";
export const CollectionSchema = z.strictObject({
author: url.nullable(),
first: url,
last: url,
total: u64,
next: url.nullable(),
previous: url.nullable(),
items: z.array(z.any()),
});
export const URICollectionSchema = CollectionSchema.extend({
items: z.array(url),
});

View file

@ -0,0 +1,17 @@
import { z } from "zod";
export const f64 = z
.number()
.nonnegative()
.max(2 ** 64 - 1);
export const u64 = z
.number()
.int()
.nonnegative()
.max(2 ** 64 - 1);
export const url = z
.string()
.url()
.transform((z) => new URL(z));

View file

@ -0,0 +1,117 @@
import { types } from "mime-types";
import { z } from "zod";
import { f64, u64 } from "./common.ts";
const hashSizes = {
sha256: 64,
sha512: 128,
"sha3-256": 64,
"sha3-512": 128,
"blake2b-256": 64,
"blake2b-512": 128,
"blake3-256": 64,
"blake3-512": 128,
md5: 32,
sha1: 40,
sha224: 56,
sha384: 96,
"sha3-224": 56,
"sha3-384": 96,
"blake2s-256": 64,
"blake2s-512": 128,
"blake3-224": 56,
"blake3-384": 96,
};
const allMimeTypes = Object.values(types) as [string, ...string[]];
const textMimeTypes = Object.values(types).filter((v) =>
v.startsWith("text/"),
) as [string, ...string[]];
const nonTextMimeTypes = Object.values(types).filter(
(v) => !v.startsWith("text/"),
) as [string, ...string[]];
const imageMimeTypes = Object.values(types).filter((v) =>
v.startsWith("image/"),
) as [string, ...string[]];
const videoMimeTypes = Object.values(types).filter((v) =>
v.startsWith("video/"),
) as [string, ...string[]];
const audioMimeTypes = Object.values(types).filter((v) =>
v.startsWith("audio/"),
) as [string, ...string[]];
export const ContentFormatSchema = z.record(
z.enum(allMimeTypes),
z.strictObject({
content: z.string().or(z.string().url()),
remote: z.boolean(),
description: z.string().nullish(),
size: u64.nullish(),
hash: z
.strictObject(
Object.fromEntries(
Object.entries(hashSizes).map(([k, v]) => [
k,
z.string().length(v).nullish(),
]),
),
)
.nullish(),
thumbhash: z.string().nullish(),
width: u64.nullish(),
height: u64.nullish(),
duration: f64.nullish(),
fps: u64.nullish(),
}),
);
export const TextContentFormatSchema = z.record(
z.enum(textMimeTypes),
ContentFormatSchema.valueSchema
.pick({
content: true,
remote: true,
})
.extend({
content: z.string(),
remote: z.literal(false),
}),
);
export const NonTextContentFormatSchema = z.record(
z.enum(nonTextMimeTypes),
ContentFormatSchema.valueSchema
.pick({
content: true,
remote: true,
description: true,
size: true,
hash: true,
thumbhash: true,
width: true,
height: true,
})
.extend({
content: z.string().url(),
remote: z.literal(true),
}),
);
export const ImageContentFormatSchema = z.record(
z.enum(imageMimeTypes),
NonTextContentFormatSchema.valueSchema,
);
export const VideoContentFormatSchema = z.record(
z.enum(videoMimeTypes),
NonTextContentFormatSchema.valueSchema.extend({
duration: ContentFormatSchema.valueSchema.shape.duration,
fps: ContentFormatSchema.valueSchema.shape.fps,
}),
);
export const AudioContentFormatSchema = z.record(
z.enum(audioMimeTypes),
NonTextContentFormatSchema.valueSchema.extend({
duration: ContentFormatSchema.valueSchema.shape.duration,
}),
);

View file

@ -0,0 +1,11 @@
import { z } from "zod";
import { url } from "./common.ts";
import { EntitySchema } from "./entity.ts";
export const DeleteSchema = EntitySchema.extend({
uri: z.null().optional(),
type: z.literal("Delete"),
author: url.nullable(),
deleted_type: z.string(),
deleted: url,
});

View file

@ -0,0 +1,23 @@
import { z } from "zod";
import { isISOString } from "../regex.ts";
import { url } from "./common.ts";
import { CustomEmojiExtensionSchema } from "./extensions/emojis.ts";
export const ExtensionPropertySchema = z
.object({
"pub.versia:custom_emojis":
CustomEmojiExtensionSchema.optional().nullable(),
})
.catchall(z.any());
export const EntitySchema = z.strictObject({
// biome-ignore lint/style/useNamingConvention:
$schema: z.string().url().nullish(),
id: z.string().max(512),
created_at: z
.string()
.refine((v) => isISOString(v), "must be a valid ISO8601 datetime"),
uri: url,
type: z.string(),
extensions: ExtensionPropertySchema.nullish(),
});

View file

@ -0,0 +1,25 @@
/**
* Custom emojis extension.
* @module federation/schemas/extensions/custom_emojis
* @see module:federation/schemas/base
* @see https://versia.pub/extensions/custom-emojis
*/
import { z } from "zod";
import { emojiRegex } from "../../regex.ts";
import { ImageContentFormatSchema } from "../contentformat.ts";
export const CustomEmojiExtensionSchema = z.strictObject({
emojis: z.array(
z.strictObject({
name: z
.string()
.min(1)
.max(256)
.regex(
emojiRegex,
"Emoji name must be alphanumeric, underscores, or dashes, and surrounded by identifiers",
),
url: ImageContentFormatSchema,
}),
),
});

View file

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

View file

@ -0,0 +1,15 @@
import { z } from "zod";
import { url } from "../common.ts";
import { EntitySchema } from "../entity.ts";
export const LikeSchema = EntitySchema.extend({
type: z.literal("pub.versia:likes/Like"),
author: url,
liked: url,
});
export const DislikeSchema = EntitySchema.extend({
type: z.literal("pub.versia:likes/Dislike"),
author: url,
disliked: url,
});

View file

@ -0,0 +1,15 @@
import { z } from "zod";
import { url } from "../common.ts";
import { EntitySchema } from "../entity.ts";
export const MigrationSchema = EntitySchema.extend({
type: z.literal("pub.versia:migration/Migration"),
uri: z.null().optional(),
author: url,
destination: url,
});
export const MigrationExtensionSchema = z.strictObject({
previous: url,
new: url.nullish(),
});

View file

@ -0,0 +1,22 @@
import { z } from "zod";
import { isISOString } from "../../regex.ts";
import { url, u64 } from "../common.ts";
import { TextContentFormatSchema } from "../contentformat.ts";
import { EntitySchema } from "../entity.ts";
export const VoteSchema = EntitySchema.extend({
type: z.literal("pub.versia:polls/Vote"),
author: url,
poll: url,
option: u64,
});
export const PollExtensionSchema = z.strictObject({
options: z.array(TextContentFormatSchema),
votes: z.array(u64),
multiple_choice: z.boolean(),
expires_at: z
.string()
.refine((v) => isISOString(v), "must be a valid ISO8601 datetime")
.nullish(),
});

View file

@ -0,0 +1,10 @@
import { z } from "zod";
import { url } from "../common.ts";
import { EntitySchema } from "../entity.ts";
export const ReactionSchema = EntitySchema.extend({
type: z.literal("pub.versia:reactions/Reaction"),
author: url,
object: url,
content: z.string().min(1).max(256),
});

View file

@ -0,0 +1,15 @@
import { z } from "zod";
import { url } from "../common.ts";
import { EntitySchema } from "../entity.ts";
export const ReportSchema = EntitySchema.extend({
type: z.literal("pub.versia:reports/Report"),
uri: z.null().optional(),
author: url.nullish(),
reported: z.array(url),
tags: z.array(z.string()),
comment: z
.string()
.max(2 ** 16)
.nullish(),
});

View file

@ -0,0 +1,9 @@
import { z } from "zod";
import { url } from "../common.ts";
import { EntitySchema } from "../entity.ts";
export const ShareSchema = EntitySchema.extend({
type: z.literal("pub.versia:share/Share"),
author: url,
shared: url,
});

View file

@ -0,0 +1,46 @@
/**
* Vanity extension schema.
* @module federation/schemas/extensions/vanity
* @see module:federation/schemas/base
* @see https://versia.pub/extensions/vanity
*/
import { z } from "zod";
import { ianaTimezoneRegex, isISOString } from "../../regex.ts";
import { url } from "../common.ts";
import {
AudioContentFormatSchema,
ImageContentFormatSchema,
} from "../contentformat.ts";
export const VanityExtensionSchema = z.strictObject({
avatar_overlays: z.array(ImageContentFormatSchema).nullish(),
avatar_mask: ImageContentFormatSchema.nullish(),
background: ImageContentFormatSchema.nullish(),
audio: AudioContentFormatSchema.nullish(),
pronouns: z.record(
z.string(),
z.array(
z.union([
z.strictObject({
subject: z.string(),
object: z.string(),
dependent_possessive: z.string(),
independent_possessive: z.string(),
reflexive: z.string(),
}),
z.string(),
]),
),
),
birthday: z
.string()
.refine((v) => isISOString(v), "must be a valid ISO8601 datetime")
.nullish(),
location: z.string().nullish(),
aliases: z.array(url).nullish(),
timezone: z
.string()
.regex(ianaTimezoneRegex, "must be a valid IANA timezone")
.nullish(),
});

View file

@ -0,0 +1,31 @@
import { z } from "zod";
import { url } from "./common.ts";
import { EntitySchema } from "./entity.ts";
export const FollowSchema = EntitySchema.extend({
type: z.literal("Follow"),
uri: z.null().optional(),
author: url,
followee: url,
});
export const FollowAcceptSchema = EntitySchema.extend({
type: z.literal("FollowAccept"),
uri: z.null().optional(),
author: url,
follower: url,
});
export const FollowRejectSchema = EntitySchema.extend({
type: z.literal("FollowReject"),
uri: z.null().optional(),
author: url,
follower: url,
});
export const UnfollowSchema = EntitySchema.extend({
type: z.literal("Unfollow"),
uri: z.null().optional(),
author: url,
followee: url,
});

View file

@ -0,0 +1,27 @@
// biome-ignore lint/performance/noBarrelFile: <explanation>
export { UserSchema } from "./user.ts";
export { NoteSchema } from "./note.ts";
export { EntitySchema } from "./entity.ts";
export { DeleteSchema } from "./delete.ts";
export { InstanceMetadataSchema } from "./instance.ts";
export {
ContentFormatSchema,
ImageContentFormatSchema,
AudioContentFormatSchema,
NonTextContentFormatSchema,
TextContentFormatSchema,
VideoContentFormatSchema,
} from "./contentformat.ts";
export {
FollowSchema,
FollowAcceptSchema,
FollowRejectSchema,
UnfollowSchema,
} from "./follow.ts";
export { CollectionSchema, URICollectionSchema } from "./collection.ts";
export { LikeSchema, DislikeSchema } from "./extensions/likes.ts";
export { VoteSchema } from "./extensions/polls.ts";
export { ReactionSchema } from "./extensions/reactions.ts";
export { ReportSchema } from "./extensions/reports.ts";
export { ShareSchema } from "./extensions/share.ts";
export { WebFingerSchema } from "./webfinger.ts";

View file

@ -0,0 +1,41 @@
import { z } from "zod";
import { extensionRegex, semverRegex } from "../regex.ts";
import { url } from "./common.ts";
import { ImageContentFormatSchema } from "./contentformat.ts";
import { EntitySchema } from "./entity.ts";
export const InstanceMetadataSchema = EntitySchema.extend({
type: z.literal("InstanceMetadata"),
id: z.null().optional(),
uri: z.null().optional(),
name: z.string().min(1),
software: z.strictObject({
name: z.string().min(1),
version: z.string().min(1),
}),
compatibility: z.strictObject({
versions: z.array(
z.string().regex(semverRegex, "must be a valid SemVer version"),
),
extensions: z.array(
z
.string()
.min(1)
.regex(
extensionRegex,
"must be in the format 'namespaced_url:extension_name', e.g. 'pub.versia:reactions'",
),
),
}),
description: z.string().nullish(),
host: z.string(),
shared_inbox: url.nullish(),
public_key: z.strictObject({
key: z.string().min(1),
algorithm: z.literal("ed25519"),
}),
moderators: url.nullish(),
admins: url.nullish(),
logo: ImageContentFormatSchema.nullish(),
banner: ImageContentFormatSchema.nullish(),
});

View file

@ -0,0 +1,67 @@
import { z } from "zod";
import { url } from "./common.ts";
import {
NonTextContentFormatSchema,
TextContentFormatSchema,
} from "./contentformat.ts";
import { EntitySchema } from "./entity.ts";
import { PollExtensionSchema } from "./extensions/polls.ts";
export const NoteSchema = EntitySchema.extend({
type: z.literal("Note"),
attachments: z.array(NonTextContentFormatSchema).nullish(),
author: url,
category: z
.enum([
"microblog",
"forum",
"blog",
"image",
"video",
"audio",
"messaging",
])
.nullish(),
content: TextContentFormatSchema.nullish(),
collections: z
.strictObject({
replies: url,
quotes: url,
"pub.versia:reactions/Reactions": url.nullish(),
"pub.versia:share/Shares": url.nullish(),
"pub.versia:likes/Likes": url.nullish(),
"pub.versia:likes/Dislikes": url.nullish(),
})
.catchall(url),
device: z
.strictObject({
name: z.string(),
version: z.string().nullish(),
url: url.nullish(),
})
.nullish(),
group: url.or(z.enum(["public", "followers"])).nullish(),
is_sensitive: z.boolean().nullish(),
mentions: z.array(url).nullish(),
previews: z
.array(
z.strictObject({
link: url,
title: z.string(),
description: z.string().nullish(),
image: url.nullish(),
icon: url.nullish(),
}),
)
.nullish(),
quotes: url.nullish(),
replies_to: url.nullish(),
subject: z.string().nullish(),
extensions: EntitySchema.shape.extensions
.unwrap()
.unwrap()
.extend({
"pub.versia:polls": PollExtensionSchema.nullish(),
})
.nullish(),
});

View file

@ -0,0 +1,60 @@
import { z } from "zod";
import { url } from "./common.ts";
import {
ImageContentFormatSchema,
TextContentFormatSchema,
} from "./contentformat.ts";
import { EntitySchema } from "./entity.ts";
import { MigrationExtensionSchema } from "./extensions/migration.ts";
import { VanityExtensionSchema } from "./extensions/vanity.ts";
export const PublicKeyDataSchema = z.strictObject({
key: z.string().min(1),
actor: url,
algorithm: z.literal("ed25519"),
});
export const UserSchema = EntitySchema.extend({
type: z.literal("User"),
avatar: ImageContentFormatSchema.nullish(),
bio: TextContentFormatSchema.nullish(),
display_name: z.string().nullish(),
fields: z
.array(
z.strictObject({
key: TextContentFormatSchema,
value: TextContentFormatSchema,
}),
)
.nullish(),
username: z
.string()
.min(1)
.regex(
/^[a-zA-Z0-9_-]+$/,
"must be alphanumeric, and may contain _ or -",
),
header: ImageContentFormatSchema.nullish(),
public_key: PublicKeyDataSchema,
manually_approves_followers: z.boolean().nullish(),
indexable: z.boolean().nullish(),
inbox: url,
collections: z
.object({
featured: url,
followers: url,
following: url,
outbox: url,
"pub.versia:likes/Likes": url.nullish(),
"pub.versia:likes/Dislikes": url.nullish(),
})
.catchall(url),
extensions: EntitySchema.shape.extensions
.unwrap()
.unwrap()
.extend({
"pub.versia:vanity": VanityExtensionSchema.nullish(),
"pub.versia:migration": MigrationExtensionSchema.nullish(),
})
.nullish(),
});

View file

@ -0,0 +1,19 @@
import { z } from "zod";
import { url } from "./common.ts";
export const WebFingerSchema = z.object({
subject: url,
aliases: z.array(url).optional(),
properties: z.record(url, z.string().or(z.null())).optional(),
links: z
.array(
z.object({
rel: z.string(),
type: z.string().optional(),
href: url.optional(),
titles: z.record(z.string(), z.string()).optional(),
properties: z.record(url, z.string().or(z.null())).optional(),
}),
)
.optional(),
});