feat(api): Add OpenAPI visualizer
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 53s
Build Docker Images / lint (push) Failing after 10s
Build Docker Images / check (push) Failing after 10s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 5s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 5s

This commit is contained in:
Jesse Wierzbinski 2025-03-24 15:25:40 +01:00
parent 65e2e19ff1
commit c674a1309c
No known key found for this signature in database
32 changed files with 772 additions and 732 deletions

12
app.ts
View file

@ -3,10 +3,10 @@ import { handleZodError } from "@/api";
import { applyToHono } from "@/bull-board.ts"; import { applyToHono } from "@/bull-board.ts";
import { configureLoggers } from "@/loggers"; import { configureLoggers } from "@/loggers";
import { sentry } from "@/sentry"; import { sentry } from "@/sentry";
import { swaggerUI } from "@hono/swagger-ui";
import { OpenAPIHono } from "@hono/zod-openapi"; import { OpenAPIHono } from "@hono/zod-openapi";
/* import { prometheus } from "@hono/prometheus"; */ /* import { prometheus } from "@hono/prometheus"; */
import { getLogger } from "@logtape/logtape"; import { getLogger } from "@logtape/logtape";
import { apiReference } from "@scalar/hono-api-reference";
import { inspect } from "bun"; import { inspect } from "bun";
import chalk from "chalk"; import chalk from "chalk";
import { cors } from "hono/cors"; import { cors } from "hono/cors";
@ -148,7 +148,15 @@ export const appFactory = async (): Promise<OpenAPIHono<HonoEnv>> => {
contact: pkg.author, contact: pkg.author,
}, },
}); });
app.get("/docs", swaggerUI({ url: "/openapi.json" })); app.get(
"/docs",
apiReference({
theme: "deepSpace",
hideClientButton: true,
pageTitle: "Versia Server API",
url: "/openapi.json",
}),
);
applyToHono(app); applyToHono(app);
app.options("*", (context) => { app.options("*", (context) => {

View file

@ -13,12 +13,12 @@
"@clerc/plugin-version": "^0.44.0", "@clerc/plugin-version": "^0.44.0",
"@hackmd/markdown-it-task-lists": "^2.1.4", "@hackmd/markdown-it-task-lists": "^2.1.4",
"@hono/prometheus": "^1.0.1", "@hono/prometheus": "^1.0.1",
"@hono/swagger-ui": "^0.5.1",
"@hono/zod-openapi": "0.19.2", "@hono/zod-openapi": "0.19.2",
"@hono/zod-validator": "^0.4.3", "@hono/zod-validator": "^0.4.3",
"@inquirer/confirm": "^5.1.8", "@inquirer/confirm": "^5.1.8",
"@logtape/file": "^0.9.0", "@logtape/file": "^0.9.0",
"@logtape/logtape": "^0.9.0", "@logtape/logtape": "^0.9.0",
"@scalar/hono-api-reference": "^0.7.2",
"@sentry/bun": "^9.8.0", "@sentry/bun": "^9.8.0",
"@versia/client": "workspace:*", "@versia/client": "workspace:*",
"@versia/federation": "^0.2.1", "@versia/federation": "^0.2.1",
@ -271,8 +271,6 @@
"@hono/prometheus": ["@hono/prometheus@1.0.1", "", { "peerDependencies": { "hono": ">=3.*", "prom-client": "^15.0.0" } }, "sha512-PjMbjAppCgbvRP2aLxqJc1XJLxfmg4dLsS5R5ITt7qCf9Ab/xSRul/LHNVvYK2/ECi3BOPprSnlSizksBJmXBQ=="], "@hono/prometheus": ["@hono/prometheus@1.0.1", "", { "peerDependencies": { "hono": ">=3.*", "prom-client": "^15.0.0" } }, "sha512-PjMbjAppCgbvRP2aLxqJc1XJLxfmg4dLsS5R5ITt7qCf9Ab/xSRul/LHNVvYK2/ECi3BOPprSnlSizksBJmXBQ=="],
"@hono/swagger-ui": ["@hono/swagger-ui@0.5.1", "", { "peerDependencies": { "hono": "*" } }, "sha512-XpUCfszLJ9b1rtFdzqOSHfdg9pfBiC2J5piEjuSanYpDDTIwpMz0ciiv5N3WWUaQpz9fEgH8lttQqL41vIFuDA=="],
"@hono/zod-openapi": ["@hono/zod-openapi@0.19.2", "", { "dependencies": { "@asteasolutions/zod-to-openapi": "^7.1.0", "@hono/zod-validator": "^0.4.1" }, "peerDependencies": { "hono": ">=4.3.6", "zod": "3.*" } }, "sha512-lkFa6wdQVgY7d7/m++Ixr3hvKCF5Y+zjTIPM37fex5ylCfX53A/W28gZRDuFZx3aR+noKob7lHfwdk9dURLzxw=="], "@hono/zod-openapi": ["@hono/zod-openapi@0.19.2", "", { "dependencies": { "@asteasolutions/zod-to-openapi": "^7.1.0", "@hono/zod-validator": "^0.4.1" }, "peerDependencies": { "hono": ">=4.3.6", "zod": "3.*" } }, "sha512-lkFa6wdQVgY7d7/m++Ixr3hvKCF5Y+zjTIPM37fex5ylCfX53A/W28gZRDuFZx3aR+noKob7lHfwdk9dURLzxw=="],
"@hono/zod-validator": ["@hono/zod-validator@0.4.3", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-xIgMYXDyJ4Hj6ekm9T9Y27s080Nl9NXHcJkOvkXPhubOLj8hZkOL8pDnnXfvCf5xEE8Q4oMFenQUZZREUY2gqQ=="], "@hono/zod-validator": ["@hono/zod-validator@0.4.3", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-xIgMYXDyJ4Hj6ekm9T9Y27s080Nl9NXHcJkOvkXPhubOLj8hZkOL8pDnnXfvCf5xEE8Q4oMFenQUZZREUY2gqQ=="],
@ -465,6 +463,14 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.36.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aRXd7tRZkWLqGbChgcMMDEHjOKudo1kChb1Jt1IfR8cY/KIpgNviLeJy5FUb9IpSuQj8dU2fAYNMPW/hLKOSTw=="], "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.36.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aRXd7tRZkWLqGbChgcMMDEHjOKudo1kChb1Jt1IfR8cY/KIpgNviLeJy5FUb9IpSuQj8dU2fAYNMPW/hLKOSTw=="],
"@scalar/core": ["@scalar/core@0.2.2", "", { "dependencies": { "@scalar/types": "0.1.2" } }, "sha512-jT6vfz37yQnqVjj8kXYEmV2cZvODW1A0PXjxZ9DzKqjm9tIssNwP4vvcdD1FSuiMcj+rgxAxOjIYMI+ybI/9RQ=="],
"@scalar/hono-api-reference": ["@scalar/hono-api-reference@0.7.2", "", { "dependencies": { "@scalar/core": "0.2.2" }, "peerDependencies": { "hono": "^4.0.0" } }, "sha512-CnxRjGfAWPGkV0D5TEwogvn7JSx/f9+ag6vQ6g25GigSDyj/UkxYbZqwe/QOV/+2EWruY3ypOvPuNMf7nEQhdQ=="],
"@scalar/openapi-types": ["@scalar/openapi-types@0.1.9", "", {}, "sha512-HQQudOSQBU7ewzfnBW9LhDmBE2XOJgSfwrh5PlUB7zJup/kaRkBGNgV2wMjNz9Af/uztiU/xNrO179FysmUT+g=="],
"@scalar/types": ["@scalar/types@0.1.2", "", { "dependencies": { "@scalar/openapi-types": "0.1.9", "@unhead/schema": "^1.11.11", "zod": "^3.23.8" } }, "sha512-5kCLQRwAYWt1ds110EaUb9yonc3KoQYNyo4YUCigJLOnoNugbqkEX0zRudGevItiuk+xg4uOYd30r3C+6xAasA=="],
"@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="],
"@sentry/bun": ["@sentry/bun@9.8.0", "", { "dependencies": { "@sentry/core": "9.8.0", "@sentry/node": "9.8.0", "@sentry/opentelemetry": "9.8.0" } }, "sha512-3JSaxyEomfZYLp1mwvwugj308QeZ4fYRzjAOQcWZqHFKbr6pMFKTvZKfEKIoAEXsNfka33+VpPdltlMgGe9auw=="], "@sentry/bun": ["@sentry/bun@9.8.0", "", { "dependencies": { "@sentry/core": "9.8.0", "@sentry/node": "9.8.0", "@sentry/opentelemetry": "9.8.0" } }, "sha512-3JSaxyEomfZYLp1mwvwugj308QeZ4fYRzjAOQcWZqHFKbr6pMFKTvZKfEKIoAEXsNfka33+VpPdltlMgGe9auw=="],
@ -543,6 +549,8 @@
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
"@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="],
"@versia/client": ["@versia/client@workspace:packages/client"], "@versia/client": ["@versia/client@workspace:packages/client"],
"@versia/federation": ["@versia/federation@0.2.1", "", { "dependencies": { "magic-regexp": "^0.8.0", "mime-types": "^2.1.35", "zod": "^3.24.1", "zod-validation-error": "^3.4.0" } }, "sha512-FTo3VGNJBGmCi0ZEQMzqFZBbcfbX81kmg0UgY4cKamr1dJWgEf72IAZnEDgrBffFjYtreLGdEjFkkcq3JfS8oQ=="], "@versia/federation": ["@versia/federation@0.2.1", "", { "dependencies": { "magic-regexp": "^0.8.0", "mime-types": "^2.1.35", "zod": "^3.24.1", "zod-validation-error": "^3.4.0" } }, "sha512-FTo3VGNJBGmCi0ZEQMzqFZBbcfbX81kmg0UgY4cKamr1dJWgEf72IAZnEDgrBffFjYtreLGdEjFkkcq3JfS8oQ=="],
@ -1339,6 +1347,8 @@
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="], "yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="],
"zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="],
"zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
"zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], "zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],

View file

@ -85,12 +85,12 @@
"@clerc/plugin-version": "^0.44.0", "@clerc/plugin-version": "^0.44.0",
"@hackmd/markdown-it-task-lists": "^2.1.4", "@hackmd/markdown-it-task-lists": "^2.1.4",
"@hono/prometheus": "^1.0.1", "@hono/prometheus": "^1.0.1",
"@hono/swagger-ui": "^0.5.1",
"@hono/zod-openapi": "0.19.2", "@hono/zod-openapi": "0.19.2",
"@hono/zod-validator": "^0.4.3", "@hono/zod-validator": "^0.4.3",
"@inquirer/confirm": "^5.1.8", "@inquirer/confirm": "^5.1.8",
"@logtape/file": "^0.9.0", "@logtape/file": "^0.9.0",
"@logtape/logtape": "^0.9.0", "@logtape/logtape": "^0.9.0",
"@scalar/hono-api-reference": "^0.7.2",
"@sentry/bun": "^9.8.0", "@sentry/bun": "^9.8.0",
"@versia/client": "workspace:*", "@versia/client": "workspace:*",
"@versia/federation": "^0.2.1", "@versia/federation": "^0.2.1",

View file

@ -49,7 +49,7 @@ export const AccountWarning = z
example: "2025-01-04T14:11:00Z", example: "2025-01-04T14:11:00Z",
}), }),
}) })
.openapi({ .openapi("AccountWarning", {
description: "Moderation warning against a particular account.", description: "Moderation warning against a particular account.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/AccountWarning", url: "https://docs.joinmastodon.org/entities/AccountWarning",

View file

@ -5,7 +5,8 @@ import { iso631, zBoolean } from "./common.ts";
import { CustomEmoji } from "./emoji.ts"; import { CustomEmoji } from "./emoji.ts";
import { Role } from "./versia.ts"; import { Role } from "./versia.ts";
export const Field = z.object({ export const Field = z
.object({
name: z name: z
.string() .string()
.trim() .trim()
@ -42,7 +43,8 @@ export const Field = z.object({
url: "https://docs.joinmastodon.org/entities/Account/#verified_at", url: "https://docs.joinmastodon.org/entities/Account/#verified_at",
}, },
}), }),
}); })
.openapi("AccountField");
export const Source = z export const Source = z
.object({ .object({
@ -107,7 +109,7 @@ export const Source = z
description: "Metadata about the account.", description: "Metadata about the account.",
}), }),
}) })
.openapi({ .openapi("AccountSource", {
description: description:
"An extra attribute that contains source values to be used with API methods that verify credentials and update credentials.", "An extra attribute that contains source values to be used with API methods that verify credentials and update credentials.",
externalDocs: { externalDocs: {
@ -116,7 +118,8 @@ export const Source = z
}); });
// Because Account has some recursive references, we need to define it like this // Because Account has some recursive references, we need to define it like this
const BaseAccount = z.object({ const BaseAccount = z
.object({
id: z id: z
.string() .string()
.uuid() .uuid()
@ -151,7 +154,8 @@ const BaseAccount = z.object({
"Username is disallowed", "Username is disallowed",
) )
.openapi({ .openapi({
description: "The username of the account, not including domain.", description:
"The username of the account, not including domain.",
example: "lexi", example: "lexi",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#username", url: "https://docs.joinmastodon.org/entities/Account/#username",
@ -206,7 +210,9 @@ const BaseAccount = z.object({
.trim() .trim()
.refine( .refine(
(s) => (s) =>
!config.validation.filters.bio.some((filter) => filter.test(s)), !config.validation.filters.bio.some((filter) =>
filter.test(s),
),
"Bio contains blocked words", "Bio contains blocked words",
) )
.openapi({ .openapi({
@ -265,7 +271,8 @@ const BaseAccount = z.object({
}, },
}), }),
locked: zBoolean.openapi({ locked: zBoolean.openapi({
description: "Whether the account manually approves follow requests.", description:
"Whether the account manually approves follow requests.",
example: false, example: false,
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#locked", url: "https://docs.joinmastodon.org/entities/Account/#locked",
@ -413,7 +420,8 @@ const BaseAccount = z.object({
description: "When a timed mute will expire, if applicable.", description: "When a timed mute will expire, if applicable.",
example: "2025-03-01T14:00:00.000Z", example: "2025-03-01T14:00:00.000Z",
}), }),
}); })
.openapi("BaseAccount");
export const Account = BaseAccount.extend({ export const Account = BaseAccount.extend({
moved: BaseAccount.nullable() moved: BaseAccount.nullable()
@ -426,4 +434,4 @@ export const Account = BaseAccount.extend({
url: "https://docs.joinmastodon.org/entities/Account/#moved", url: "https://docs.joinmastodon.org/entities/Account/#moved",
}, },
}), }),
}); }).openapi("Account");

View file

@ -13,7 +13,7 @@ export const Appeal = z
example: "pending", example: "pending",
}), }),
}) })
.openapi({ .openapi("Appeal", {
description: "Appeal against a moderation action.", description: "Appeal against a moderation action.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Appeal", url: "https://docs.joinmastodon.org/entities/Appeal",

View file

@ -1,6 +1,7 @@
import { z } from "@hono/zod-openapi"; import { z } from "@hono/zod-openapi";
export const Application = z.object({ export const Application = z
.object({
name: z name: z
.string() .string()
.trim() .trim()
@ -59,7 +60,8 @@ export const Application = z.object({
url: "https://docs.joinmastodon.org/entities/Application/#redirect_uri", url: "https://docs.joinmastodon.org/entities/Application/#redirect_uri",
}, },
}), }),
}); })
.openapi("Application");
export const CredentialApplication = Application.extend({ export const CredentialApplication = Application.extend({
client_id: z.string().openapi({ client_id: z.string().openapi({
@ -81,4 +83,4 @@ export const CredentialApplication = Application.extend({
url: "https://docs.joinmastodon.org/entities/CredentialApplication/#client_secret_expires_at", url: "https://docs.joinmastodon.org/entities/CredentialApplication/#client_secret_expires_at",
}, },
}), }),
}); }).openapi("CredentialApplication");

View file

@ -67,7 +67,7 @@ export const Attachment = z
example: "UFBWY:8_0Jxv4mx]t8t64.%M-:IUWGWAt6M}", example: "UFBWY:8_0Jxv4mx]t8t64.%M-:IUWGWAt6M}",
}), }),
}) })
.openapi({ .openapi("Attachment", {
description: description:
"Represents a file or media attachment that can be added to a status.", "Represents a file or media attachment that can be added to a status.",
externalDocs: { externalDocs: {

View file

@ -1,7 +1,8 @@
import { z } from "@hono/zod-openapi"; import { z } from "@hono/zod-openapi";
import { Account } from "./account.ts"; import { Account } from "./account.ts";
export const PreviewCardAuthor = z.object({ export const PreviewCardAuthor = z
.object({
name: z.string().openapi({ name: z.string().openapi({
description: "The original resource authors name.", description: "The original resource authors name.",
example: "The Doubleclicks", example: "The Doubleclicks",
@ -25,7 +26,8 @@ export const PreviewCardAuthor = z.object({
url: "https://docs.joinmastodon.org/entities/PreviewCardAuthor/#account", url: "https://docs.joinmastodon.org/entities/PreviewCardAuthor/#account",
}, },
}), }),
}); })
.openapi("PreviewCardAuthor");
export const PreviewCard = z export const PreviewCard = z
.object({ .object({
@ -150,7 +152,7 @@ export const PreviewCard = z
}, },
}), }),
}) })
.openapi({ .openapi("PreviewCard", {
description: description:
"Represents a rich preview card that is generated using OpenGraph tags from a URL.", "Represents a rich preview card that is generated using OpenGraph tags from a URL.",
externalDocs: { externalDocs: {

View file

@ -3,7 +3,9 @@ import ISO6391 from "iso-639-1";
export const Id = z.string().uuid(); export const Id = z.string().uuid();
export const iso631 = z.enum(ISO6391.getAllCodes() as [string, ...string[]]); export const iso631 = z
.enum(ISO6391.getAllCodes() as [string, ...string[]])
.openapi("ISO631");
export const zBoolean = z export const zBoolean = z
.string() .string()

View file

@ -16,7 +16,7 @@ export const Context = z
}, },
}), }),
}) })
.openapi({ .openapi("Context", {
description: description:
"Represents the tree around a given status. Used for reconstructing threads of statuses.", "Represents the tree around a given status. Used for reconstructing threads of statuses.",
externalDocs: { externalDocs: {

View file

@ -87,7 +87,7 @@ export const CustomEmoji = z
}, },
}), }),
}) })
.openapi({ .openapi("CustomEmoji", {
description: "Represents a custom emoji.", description: "Represents a custom emoji.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/CustomEmoji", url: "https://docs.joinmastodon.org/entities/CustomEmoji",

View file

@ -22,7 +22,7 @@ export const ExtendedDescription = z
}, },
}), }),
}) })
.openapi({ .openapi("ExtendedDescription", {
description: description:
"Represents an extended description for the instance, to be shown on its about page.", "Represents an extended description for the instance, to be shown on its about page.",
externalDocs: { externalDocs: {

View file

@ -17,7 +17,7 @@ export const FamiliarFollowers = z
}, },
}), }),
}) })
.openapi({ .openapi("FamiliarFollowers", {
description: description:
"Represents a subset of your follows who also follow some other user.", "Represents a subset of your follows who also follow some other user.",
externalDocs: { externalDocs: {

View file

@ -18,7 +18,7 @@ export const FilterStatus = z
}, },
}), }),
}) })
.openapi({ .openapi("FilterStatus", {
description: description:
"Represents a status ID that, if matched, should cause the filter action to be taken.", "Represents a status ID that, if matched, should cause the filter action to be taken.",
externalDocs: { externalDocs: {
@ -51,7 +51,7 @@ export const FilterKeyword = z
}, },
}), }),
}) })
.openapi({ .openapi("FilterKeyword", {
description: description:
"Represents a keyword that, if matched, should cause the filter action to be taken.", "Represents a keyword that, if matched, should cause the filter action to be taken.",
externalDocs: { externalDocs: {
@ -133,7 +133,7 @@ export const Filter = z
}, },
}), }),
}) })
.openapi({ .openapi("Filter", {
description: description:
"Represents a user-defined filter for determining which statuses should not be shown to the user.", "Represents a user-defined filter for determining which statuses should not be shown to the user.",
externalDocs: { externalDocs: {
@ -171,7 +171,7 @@ export const FilterResult = z
}, },
}), }),
}) })
.openapi({ .openapi("FilterResult", {
description: description:
"Represents a filter whose keywords matched a given status.", "Represents a filter whose keywords matched a given status.",
externalDocs: { externalDocs: {

View file

@ -132,7 +132,7 @@ export const InstanceV1 = z
/* Versia Server API extension */ /* Versia Server API extension */
sso: SSOConfig, sso: SSOConfig,
}) })
.openapi({ .openapi("InstanceV1", {
description: description:
"Represents the software instance of Versia Server running on this domain.", "Represents the software instance of Versia Server running on this domain.",
externalDocs: { externalDocs: {

View file

@ -18,7 +18,7 @@ const InstanceIcon = z
example: "36x36", example: "36x36",
}), }),
}) })
.openapi({ .openapi("InstanceIcon", {
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/InstanceIcon", url: "https://docs.joinmastodon.org/entities/InstanceIcon",
}, },
@ -375,7 +375,7 @@ export const Instance = z
/* Versia Server API extension */ /* Versia Server API extension */
sso: SSOConfig, sso: SSOConfig,
}) })
.openapi({ .openapi("Instance", {
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Instance", url: "https://docs.joinmastodon.org/entities/Instance",
}, },

View file

@ -17,7 +17,7 @@ export const Marker = z
example: "2025-01-12T13:11:00Z", example: "2025-01-12T13:11:00Z",
}), }),
}) })
.openapi({ .openapi("Marker", {
description: description:
"Represents the last read position within a user's timelines.", "Represents the last read position within a user's timelines.",
externalDocs: { externalDocs: {

View file

@ -62,7 +62,7 @@ export const Notification = z
"Moderation warning that caused the notification. Attached when type of the notification is moderation_warning.", "Moderation warning that caused the notification. Attached when type of the notification is moderation_warning.",
}), }),
}) })
.openapi({ .openapi("Notification", {
description: description:
"Represents a notification of an event relevant to the user.", "Represents a notification of an event relevant to the user.",
externalDocs: { externalDocs: {

View file

@ -31,7 +31,7 @@ export const PollOption = z
}, },
}), }),
}) })
.openapi({ .openapi("PollOption", {
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#Option", url: "https://docs.joinmastodon.org/entities/Poll/#Option",
}, },
@ -130,7 +130,7 @@ export const Poll = z
}, },
}), }),
}) })
.openapi({ .openapi("Poll", {
description: "Represents a poll attached to a status.", description: "Represents a poll attached to a status.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll", url: "https://docs.joinmastodon.org/entities/Poll",

View file

@ -42,7 +42,7 @@ export const Preferences = z
}, },
}), }),
}) })
.openapi({ .openapi("Preferences", {
description: "Represents a user's preferences.", description: "Represents a user's preferences.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Preferences", url: "https://docs.joinmastodon.org/entities/Preferences",

View file

@ -21,7 +21,7 @@ export const PrivacyPolicy = z
}, },
}), }),
}) })
.openapi({ .openapi("PrivacyPolicy", {
description: "Represents the privacy policy of the instance.", description: "Represents the privacy policy of the instance.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/PrivacyPolicy", url: "https://docs.joinmastodon.org/entities/PrivacyPolicy",

View file

@ -80,7 +80,7 @@ export const WebPushSubscription = z
description: "The streaming servers VAPID key.", description: "The streaming servers VAPID key.",
}), }),
}) })
.openapi({}); .openapi("WebPushSubscription");
export const WebPushSubscriptionInput = z export const WebPushSubscriptionInput = z
.object({ .object({

View file

@ -65,7 +65,7 @@ export const Relationship = z
example: "they also like Kerbal Space Program", example: "they also like Kerbal Space Program",
}), }),
}) })
.openapi({ .openapi("Relationship", {
description: description:
"Represents the relationship between accounts, such as following / blocking / muting / etc.", "Represents the relationship between accounts, such as following / blocking / muting / etc.",
externalDocs: { externalDocs: {

View file

@ -50,7 +50,7 @@ export const Report = z
description: "The account that was reported.", description: "The account that was reported.",
}), }),
}) })
.openapi({ .openapi("Report", {
description: description:
"Reports filed against users and/or statuses, to be taken action on by moderators.", "Reports filed against users and/or statuses, to be taken action on by moderators.",
externalDocs: { externalDocs: {

View file

@ -15,7 +15,7 @@ export const Rule = z
example: "Please, we beg you.", example: "Please, we beg you.",
}), }),
}) })
.openapi({ .openapi("Rule", {
description: "Represents a rule that server users should follow.", description: "Represents a rule that server users should follow.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Rule", url: "https://docs.joinmastodon.org/entities/Rule",

View file

@ -15,7 +15,7 @@ export const Search = z
description: "Hashtags which match the given query", description: "Hashtags which match the given query",
}), }),
}) })
.openapi({ .openapi("Search", {
description: "Represents the results of a search.", description: "Represents the results of a search.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Search", url: "https://docs.joinmastodon.org/entities/Search",

View file

@ -42,7 +42,7 @@ export const Mention = z
}, },
}), }),
}) })
.openapi({ .openapi("Mention", {
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Mention", url: "https://docs.joinmastodon.org/entities/Status/#Mention",
}, },
@ -75,14 +75,15 @@ export const StatusSource = z
example: "", example: "",
}), }),
}) })
.openapi({ .openapi("StatusSource", {
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/StatusSource", url: "https://docs.joinmastodon.org/entities/StatusSource",
}, },
}); });
// Because Status has some recursive references, we need to define it like this // Because Status has some recursive references, we need to define it like this
const BaseStatus = z.object({ const BaseStatus = z
.object({
id: Id.openapi({ id: Id.openapi({
description: "ID of the status in the database.", description: "ID of the status in the database.",
example: "2de861d3-a3dd-42ee-ba38-2c7d3f4af588", example: "2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
@ -164,7 +165,8 @@ const BaseStatus = z.object({
}, },
}), }),
emojis: z.array(CustomEmoji).openapi({ emojis: z.array(CustomEmoji).openapi({
description: "Custom emoji to be used when rendering status content.", description:
"Custom emoji to be used when rendering status content.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#emojis", url: "https://docs.joinmastodon.org/entities/Status/#emojis",
}, },
@ -241,7 +243,9 @@ const BaseStatus = z.object({
url: "https://docs.joinmastodon.org/entities/Status/#spoiler_text", url: "https://docs.joinmastodon.org/entities/Status/#spoiler_text",
}, },
}), }),
visibility: z.enum(["public", "unlisted", "private", "direct"]).openapi({ visibility: z
.enum(["public", "unlisted", "private", "direct"])
.openapi({
description: "Visibility of this status.", description: "Visibility of this status.",
example: "public", example: "public",
externalDocs: { externalDocs: {
@ -267,7 +271,8 @@ const BaseStatus = z.object({
}, },
}), }),
card: PreviewCard.nullable().openapi({ card: PreviewCard.nullable().openapi({
description: "Preview card for links included within status content.", description:
"Preview card for links included within status content.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#card", url: "https://docs.joinmastodon.org/entities/Status/#card",
}, },
@ -350,7 +355,8 @@ const BaseStatus = z.object({
url: "https://docs.joinmastodon.org/entities/Status/#filtered", url: "https://docs.joinmastodon.org/entities/Status/#filtered",
}, },
}), }),
}); })
.openapi("BaseStatus");
export const Status = BaseStatus.extend({ export const Status = BaseStatus.extend({
reblog: BaseStatus.nullable().openapi({ reblog: BaseStatus.nullable().openapi({
@ -360,9 +366,10 @@ export const Status = BaseStatus.extend({
}, },
}), }),
quote: BaseStatus.nullable(), quote: BaseStatus.nullable(),
}); }).openapi("Status");
export const ScheduledStatus = z.object({ export const ScheduledStatus = z
.object({
id: Id.openapi({ id: Id.openapi({
description: "ID of the scheduled status in the database.", description: "ID of the scheduled status in the database.",
example: "2de861d3-a3dd-42ee-ba38-2c7d3f4af588", example: "2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
@ -406,4 +413,5 @@ export const ScheduledStatus = z.object({
example: "1234567890", example: "1234567890",
}), }),
}), }),
}); })
.openapi("ScheduledStatus");

View file

@ -24,7 +24,7 @@ export const Tag = z
}, },
}), }),
}) })
.openapi({ .openapi("Tag", {
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Tag", url: "https://docs.joinmastodon.org/entities/Status/#Tag",
}, },

View file

@ -20,7 +20,7 @@ export const Token = z
example: 1573979017, example: 1573979017,
}), }),
}) })
.openapi({ .openapi("Token", {
description: description:
"Represents an OAuth token used for authenticating with the API and performing actions.", "Represents an OAuth token used for authenticating with the API and performing actions.",
externalDocs: { externalDocs: {

View file

@ -11,6 +11,6 @@ export const TermsOfService = z
example: "<p><h1>ToS</h1><p>None, have fun.</p></p>", example: "<p><h1>ToS</h1><p>None, have fun.</p></p>",
}), }),
}) })
.openapi({ .openapi("TermsOfService", {
description: "Represents the ToS of the instance.", description: "Represents the ToS of the instance.",
}); });

View file

@ -45,7 +45,7 @@ export const Role = z
example: "https://example.com/role-icon.png", example: "https://example.com/role-icon.png",
}), }),
}) })
.openapi({ .openapi("Role", {
description: description:
"Information about a role in the system, as well as its permissions.", "Information about a role in the system, as well as its permissions.",
}); });
@ -72,7 +72,7 @@ export const NoteReaction = z
example: true, example: true,
}), }),
}) })
.openapi({ .openapi("NoteReaction", {
description: "Information about a reaction to a note.", description: "Information about a reaction to a note.",
}); });
@ -134,6 +134,6 @@ export const Challenge = z
example: "1234567890", example: "1234567890",
}), }),
}) })
.openapi({ .openapi("Challenge", {
description: "A cryptographic challenge to solve. Used for Captchas.", description: "A cryptographic challenge to solve. Used for Captchas.",
}); });