From 24d4150da4c3c2bad130194dad29e9a99897ac9a Mon Sep 17 00:00:00 2001
From: Jesse Wierzbinski
Date: Mon, 7 Jul 2025 03:42:35 +0200
Subject: [PATCH] refactor: :arrow_up: Upgrade to Zod v4 and hono-openapi 0.5.0
---
benchmarks/timeline.ts | 2 +-
bun.lock | 44 ++--
package.json | 18 +-
packages/api/app.ts | 33 ++-
packages/api/middlewares/rate-limit.ts | 2 +-
packages/api/package.json | 2 +-
packages/api/plugin-loader.ts | 2 +-
packages/api/plugins/openid/index.ts | 2 +-
.../api/plugins/openid/routes/authorize.ts | 8 +-
packages/api/plugins/openid/routes/jwks.ts | 5 +-
.../plugins/openid/routes/oauth/callback.ts | 15 +-
.../api/plugins/openid/routes/oauth/revoke.ts | 5 +-
.../api/plugins/openid/routes/oauth/sso.ts | 7 +-
.../api/plugins/openid/routes/oauth/token.ts | 7 +-
.../plugins/openid/routes/sso/:id/index.ts | 5 +-
.../api/plugins/openid/routes/sso/index.ts | 5 +-
packages/api/routes/api/auth/login/index.ts | 8 +-
.../api/routes/api/auth/redirect/index.ts | 7 +-
packages/api/routes/api/auth/reset/index.ts | 5 +-
.../api/routes/api/v1/accounts/[id]/block.ts | 3 +-
.../routes/api/v1/accounts/[id]/feed.atom.ts | 8 +-
.../routes/api/v1/accounts/[id]/feed.rss.ts | 8 +-
.../api/routes/api/v1/accounts/[id]/follow.ts | 11 +-
.../routes/api/v1/accounts/[id]/followers.ts | 15 +-
.../routes/api/v1/accounts/[id]/following.ts | 15 +-
.../api/routes/api/v1/accounts/[id]/index.ts | 3 +-
.../api/routes/api/v1/accounts/[id]/mute.ts | 9 +-
.../api/routes/api/v1/accounts/[id]/note.ts | 7 +-
.../api/routes/api/v1/accounts/[id]/pin.ts | 3 +-
.../routes/api/v1/accounts/[id]/refetch.ts | 3 +-
.../v1/accounts/[id]/remove_from_followers.ts | 3 +-
.../v1/accounts/[id]/roles/[role_id]/index.ts | 5 +-
.../api/v1/accounts/[id]/roles/index.ts | 5 +-
.../routes/api/v1/accounts/[id]/statuses.ts | 34 ++-
.../routes/api/v1/accounts/[id]/unblock.ts | 3 +-
.../routes/api/v1/accounts/[id]/unfollow.ts | 3 +-
.../api/routes/api/v1/accounts/[id]/unmute.ts | 3 +-
.../api/routes/api/v1/accounts/[id]/unpin.ts | 3 +-
.../v1/accounts/familiar_followers/index.ts | 13 +-
packages/api/routes/api/v1/accounts/index.ts | 25 +-
.../routes/api/v1/accounts/lookup/index.ts | 7 +-
.../api/v1/accounts/relationships/index.ts | 18 +-
.../routes/api/v1/accounts/search/index.ts | 27 +--
.../v1/accounts/update_credentials/index.ts | 63 ++---
.../v1/accounts/verify_credentials/index.ts | 3 +-
packages/api/routes/api/v1/apps/index.ts | 33 ++-
.../api/v1/apps/verify_credentials/index.ts | 3 +-
packages/api/routes/api/v1/blocks/index.ts | 25 +-
.../api/routes/api/v1/challenges/index.ts | 3 +-
.../api/routes/api/v1/custom_emojis/index.ts | 5 +-
.../api/routes/api/v1/emojis/[id]/index.ts | 26 +--
packages/api/routes/api/v1/emojis/index.ts | 29 +--
.../api/routes/api/v1/favourites/index.ts | 25 +-
.../follow_requests/[account_id]/authorize.ts | 5 +-
.../v1/follow_requests/[account_id]/reject.ts | 5 +-
.../routes/api/v1/follow_requests/index.ts | 25 +-
.../routes/api/v1/frontend/config/index.ts | 5 +-
.../api/v1/instance/extended_description.ts | 3 +-
packages/api/routes/api/v1/instance/index.ts | 5 +-
.../routes/api/v1/instance/privacy_policy.ts | 3 +-
packages/api/routes/api/v1/instance/rules.ts | 5 +-
.../api/v1/instance/terms_of_service.ts | 3 +-
packages/api/routes/api/v1/markers/index.ts | 21 +-
.../api/routes/api/v1/media/[id]/index.ts | 9 +-
packages/api/routes/api/v1/media/index.ts | 11 +-
packages/api/routes/api/v1/mutes/index.ts | 25 +-
.../api/v1/notifications/[id]/dismiss.test.ts | 2 +-
.../api/v1/notifications/[id]/dismiss.ts | 5 +-
.../api/v1/notifications/[id]/index.test.ts | 2 +-
.../routes/api/v1/notifications/[id]/index.ts | 5 +-
.../destroy_multiple/index.test.ts | 2 +-
.../notifications/destroy_multiple/index.ts | 7 +-
.../api/routes/api/v1/notifications/index.ts | 21 +-
packages/api/routes/api/v1/profile/avatar.ts | 3 +-
packages/api/routes/api/v1/profile/header.ts | 3 +-
.../api/v1/push/subscription/index.delete.ts | 5 +-
.../api/v1/push/subscription/index.get.ts | 3 +-
.../api/v1/push/subscription/index.post.ts | 3 +-
.../api/v1/push/subscription/index.put.ts | 3 +-
.../api/routes/api/v1/roles/[id]/index.ts | 13 +-
packages/api/routes/api/v1/roles/index.ts | 5 +-
.../routes/api/v1/statuses/[id]/context.ts | 3 +-
.../routes/api/v1/statuses/[id]/favourite.ts | 3 +-
.../api/v1/statuses/[id]/favourited_by.ts | 25 +-
.../api/routes/api/v1/statuses/[id]/index.ts | 23 +-
.../api/routes/api/v1/statuses/[id]/pin.ts | 3 +-
.../api/v1/statuses/[id]/reactions/[name].ts | 5 +-
.../api/v1/statuses/[id]/reactions/index.ts | 5 +-
.../api/routes/api/v1/statuses/[id]/reblog.ts | 5 +-
.../api/v1/statuses/[id]/reblogged_by.ts | 25 +-
.../api/routes/api/v1/statuses/[id]/source.ts | 3 +-
.../api/v1/statuses/[id]/unfavourite.ts | 3 +-
.../api/routes/api/v1/statuses/[id]/unpin.ts | 3 +-
.../routes/api/v1/statuses/[id]/unreblog.ts | 3 +-
.../api/routes/api/v1/statuses/index.test.ts | 4 +-
packages/api/routes/api/v1/statuses/index.ts | 45 ++--
packages/api/routes/api/v1/timelines/home.ts | 25 +-
.../api/routes/api/v1/timelines/public.ts | 21 +-
.../api/routes/api/v2/filters/[id]/index.ts | 9 +-
packages/api/routes/api/v2/filters/index.ts | 7 +-
packages/api/routes/api/v2/instance/index.ts | 3 +-
packages/api/routes/api/v2/media/index.ts | 11 +-
packages/api/routes/api/v2/search/index.ts | 37 ++-
packages/api/routes/inbox/index.ts | 6 +-
packages/api/routes/likes/[uuid]/index.ts | 5 +-
.../api/routes/media/[hash]/[name]/index.ts | 5 +-
packages/api/routes/media/proxy/[id].ts | 23 +-
packages/api/routes/notes/[uuid]/index.ts | 5 +-
packages/api/routes/notes/[uuid]/quotes.ts | 5 +-
packages/api/routes/notes/[uuid]/replies.ts | 5 +-
packages/api/routes/notes/[uuid]/shares.ts | 5 +-
packages/api/routes/oauth.test.ts | 2 +-
packages/api/routes/shares/[uuid]/index.ts | 5 +-
.../api/routes/users/[uuid]/inbox/index.ts | 8 +-
packages/api/routes/users/[uuid]/index.ts | 7 +-
.../api/routes/users/[uuid]/outbox/index.ts | 7 +-
.../api/routes/well-known/host-meta/index.ts | 5 +-
.../routes/well-known/nodeinfo/2.0/index.ts | 5 +-
.../api/routes/well-known/nodeinfo/index.ts | 5 +-
.../well-known/openid-configuration/index.ts | 5 +-
packages/api/routes/well-known/versia.ts | 3 +-
.../api/routes/well-known/webfinger/index.ts | 5 +-
packages/client/schemas/account-warning.ts | 20 +-
packages/client/schemas/account.ts | 215 ++++++++----------
packages/client/schemas/appeal.ts | 10 +-
packages/client/schemas/application.ts | 36 ++-
packages/client/schemas/attachment.ts | 22 +-
packages/client/schemas/card.ts | 105 ++++-----
packages/client/schemas/common.ts | 15 +-
packages/client/schemas/context.ts | 10 +-
packages/client/schemas/emoji.ts | 56 ++---
.../client/schemas/extended-description.ts | 27 +--
packages/client/schemas/familiar-followers.ts | 10 +-
packages/client/schemas/filters.ts | 48 ++--
packages/client/schemas/instance-v1.ts | 32 +--
packages/client/schemas/instance.ts | 154 ++++++-------
packages/client/schemas/marker.ts | 12 +-
packages/client/schemas/notification.ts | 25 +-
packages/client/schemas/poll.ts | 37 ++-
packages/client/schemas/preferences.ts | 16 +-
packages/client/schemas/privacy-policy.ts | 27 +--
packages/client/schemas/pushsubscription.ts | 121 +++++-----
packages/client/schemas/relationship.ts | 36 +--
packages/client/schemas/report.ts | 26 +--
packages/client/schemas/rule.ts | 12 +-
packages/client/schemas/search.ts | 12 +-
packages/client/schemas/status.ts | 189 +++++++--------
packages/client/schemas/tag.ts | 25 +-
packages/client/schemas/token.ts | 14 +-
packages/client/schemas/tos.ts | 10 +-
packages/client/schemas/versia.ts | 84 ++++---
packages/client/versia/client.ts | 2 +-
packages/config/index.ts | 107 ++++-----
packages/config/package.json | 3 +-
packages/config/to-json-schema.ts | 4 +-
packages/kit/api-error.ts | 4 +-
packages/kit/api.ts | 16 +-
packages/kit/db/application.ts | 2 +-
packages/kit/db/emoji.ts | 4 +-
packages/kit/db/media.ts | 18 +-
packages/kit/db/note.ts | 2 +-
packages/kit/db/notification.ts | 3 +-
packages/kit/db/pushsubscription.ts | 2 +-
packages/kit/db/relationship.ts | 2 +-
packages/kit/db/role.ts | 2 +-
packages/kit/db/token.ts | 2 +-
packages/kit/db/user.ts | 2 +-
packages/kit/example.ts | 2 +-
packages/kit/inbox-processor.ts | 6 +-
packages/kit/json-schema.ts | 4 +-
packages/kit/package.json | 3 +-
packages/kit/plugin.ts | 2 +-
packages/kit/schema.ts | 8 +-
packages/kit/tables/schema.ts | 2 +-
packages/sdk/entities/collection.ts | 2 +-
packages/sdk/entities/contentformat.ts | 2 +-
packages/sdk/entities/delete.ts | 2 +-
packages/sdk/entities/extensions/likes.ts | 2 +-
packages/sdk/entities/extensions/polls.ts | 2 +-
packages/sdk/entities/extensions/reactions.ts | 2 +-
packages/sdk/entities/extensions/reports.ts | 2 +-
packages/sdk/entities/extensions/share.ts | 2 +-
packages/sdk/entities/follow.ts | 2 +-
packages/sdk/entities/instancemetadata.ts | 2 +-
packages/sdk/entities/note.ts | 2 +-
packages/sdk/entities/user.ts | 2 +-
packages/sdk/schemas/collection.ts | 2 +-
packages/sdk/schemas/common.ts | 4 +-
packages/sdk/schemas/contentformat.ts | 34 +--
packages/sdk/schemas/delete.ts | 2 +-
packages/sdk/schemas/entity.ts | 4 +-
packages/sdk/schemas/extensions/emojis.ts | 2 +-
packages/sdk/schemas/extensions/groups.ts | 2 +-
packages/sdk/schemas/extensions/likes.ts | 2 +-
packages/sdk/schemas/extensions/migration.ts | 2 +-
packages/sdk/schemas/extensions/polls.ts | 2 +-
packages/sdk/schemas/extensions/reactions.ts | 2 +-
packages/sdk/schemas/extensions/reports.ts | 2 +-
packages/sdk/schemas/extensions/share.ts | 2 +-
packages/sdk/schemas/extensions/vanity.ts | 2 +-
packages/sdk/schemas/follow.ts | 2 +-
packages/sdk/schemas/instance.ts | 2 +-
packages/sdk/schemas/note.ts | 2 +-
packages/sdk/schemas/user.ts | 2 +-
packages/sdk/schemas/webfinger.ts | 2 +-
types/api.ts | 2 +-
utils/content_types.ts | 2 +-
utils/rss.ts | 16 +-
utils/server.ts | 2 +-
209 files changed, 1331 insertions(+), 1622 deletions(-)
diff --git a/benchmarks/timeline.ts b/benchmarks/timeline.ts
index 67a7cfb1..1cbf9b81 100644
--- a/benchmarks/timeline.ts
+++ b/benchmarks/timeline.ts
@@ -5,7 +5,7 @@ import {
getTestUsers,
} from "@versia-server/tests";
import { bench, run } from "mitata";
-import type { z } from "zod";
+import type { z } from "zod/v4";
const { users, tokens, deleteUsers } = await getTestUsers(5);
await getTestStatuses(40, users[0]);
diff --git a/bun.lock b/bun.lock
index 3e7f76bc..f4c396b7 100644
--- a/bun.lock
+++ b/bun.lock
@@ -12,7 +12,7 @@
"@clerc/plugin-not-found": "catalog:",
"@clerc/plugin-version": "catalog:",
"@hackmd/markdown-it-task-lists": "catalog:",
- "@hono/zod-validator": "catalog:",
+ "@hono/standard-validator": "catalog:",
"@inquirer/confirm": "catalog:",
"@scalar/hono-api-reference": "catalog:",
"@sentry/bun": "catalog:",
@@ -84,14 +84,13 @@
"vitepress-plugin-tabs": "catalog:",
"vitepress-sidebar": "catalog:",
"vue": "catalog:",
- "zod-to-json-schema": "catalog:",
},
},
"packages/api": {
"name": "@versia-server/api",
"version": "0.9.0-alpha.0",
"dependencies": {
- "@hono/zod-validator": "catalog:",
+ "@hono/standard-validator": "catalog:",
"@scalar/hono-api-reference": "catalog:",
"@versia-server/config": "workspace:*",
"@versia-server/kit": "workspace:*",
@@ -143,7 +142,6 @@
"mime-types": "catalog:",
"web-push": "catalog:",
"zod": "catalog:",
- "zod-to-json-schema": "catalog:",
"zod-validation-error": "catalog:",
},
},
@@ -152,7 +150,7 @@
"version": "0.0.0",
"dependencies": {
"@hackmd/markdown-it-task-lists": "catalog:",
- "@hono/zod-validator": "catalog:",
+ "@hono/standard-validator": "catalog:",
"@versia-server/config": "workspace:*",
"@versia-server/logging": "workspace:*",
"@versia/client": "workspace:*",
@@ -177,7 +175,6 @@
"sonic-channel": "catalog:",
"web-push": "catalog:",
"zod": "catalog:",
- "zod-to-json-schema": "catalog:",
"zod-validation-error": "catalog:",
},
},
@@ -242,7 +239,7 @@
"@clerc/plugin-not-found": "^0.44.0",
"@clerc/plugin-version": "^0.44.0",
"@hackmd/markdown-it-task-lists": "^2.1.4",
- "@hono/zod-validator": "^0.7.0",
+ "@hono/standard-validator": "^0.1.2",
"@inquirer/confirm": "^5.1.13",
"@logtape/file": "^1.0.0",
"@logtape/logtape": "^1.0.0",
@@ -267,7 +264,7 @@
"drizzle-orm": "^0.44.2",
"feed": "^5.1.0",
"hono": "^4.8.4",
- "hono-openapi": "^0.4.8",
+ "hono-openapi": "npm:@cpluspatch/hono-openapi@0.5.1",
"hono-rate-limiter": "^0.4.2",
"html-to-text": "^9.0.5",
"ioredis": "^5.6.1",
@@ -306,9 +303,8 @@
"xss": "^1.0.15",
"youch": "^4.1.0-beta.7",
"zod": "^3.25.74",
- "zod-openapi": "^4.2.4",
- "zod-to-json-schema": "^3.24.6",
- "zod-validation-error": "^3.5.2",
+ "zod-openapi": "^5.0.0",
+ "zod-validation-error": "^4.0.0-beta.1",
},
"packages": {
"@algolia/autocomplete-core": ["@algolia/autocomplete-core@1.17.7", "", { "dependencies": { "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", "@algolia/autocomplete-shared": "1.17.7" } }, "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q=="],
@@ -345,8 +341,6 @@
"@algolia/requester-node-http": ["@algolia/requester-node-http@5.30.0", "", { "dependencies": { "@algolia/client-common": "5.30.0" } }, "sha512-uSTUh9fxeHde1c7KhvZKUrivk90sdiDftC+rSKNFKKEU9TiIKAGA7B2oKC+AoMCqMymot1vW9SGbeESQPTZd0w=="],
- "@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@11.9.3", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" } }, "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ=="],
-
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
@@ -467,7 +461,7 @@
"@hackmd/markdown-it-task-lists": ["@hackmd/markdown-it-task-lists@2.1.4", "", {}, "sha512-njMloWVihC7a7N4zxczv547bgNxPVG3GBzh6Z6f2xnO8/92JaxTmQuMV7YvaKKkOyhh2RW4RT84uSgax8u4qfQ=="],
- "@hono/zod-validator": ["@hono/zod-validator@0.7.0", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0" } }, "sha512-qe2ZE6sHFE98dcUrbYMtS3bAV8hqcCOflykvZga2S7XhmNSZzT+dIz4OuMILsjLHkJw9JMn912/dB7dQOmuPvg=="],
+ "@hono/standard-validator": ["@hono/standard-validator@0.1.2", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-mVyv2fpx/o0MNAEhjXhvuVbW3BWTGnf8F4w8ZifztE+TWXjUAKr7KAOZfcDhVrurgVhKw7RbTnEog2beZM6QtQ=="],
"@iconify-json/simple-icons": ["@iconify-json/simple-icons@1.2.41", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-4tt29cKzNsxvt6rjAOVhEgpZV0L8jleTDTMdtvIJjF14Afp9aH8peuwGYyX35l6idfFwuzbvjSVfVyVjJtfmYA=="],
@@ -529,8 +523,6 @@
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
- "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="],
-
"@logtape/file": ["@logtape/file@1.0.2", "", { "peerDependencies": { "@logtape/logtape": "1.0.2" } }, "sha512-V5fiudPkjz0+R5+eVNceYwn65oZ/XrUXlRqbn0xFaHZ/XBPgVRTPf2fReFeyzcl3d3hcPBGk2K6smsJQBSJavw=="],
"@logtape/logtape": ["@logtape/logtape@1.0.2", "", {}, "sha512-6EWfs4KyTAVsiAnXXSFpzEmUYI2k7qLJogqPv3JqwFd8S8Zr2iUBPv3pbIC+70cW4P6Zpq1l1hnX/jDVZwvc+Q=="],
@@ -739,6 +731,12 @@
"@speed-highlight/core": ["@speed-highlight/core@1.2.7", "", {}, "sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g=="],
+ "@standard-community/standard-json": ["@standard-community/standard-json@0.3.0-rc.1", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "@types/json-schema": "^7.0.15", "@valibot/to-json-schema": "^1.3.0", "arktype": "^2.1.20", "effect": "^3.16.8", "valibot": "^1.1.0", "zod": "^3.25.67", "zod-to-json-schema": "^3.24.5" }, "optionalPeers": ["@valibot/to-json-schema", "arktype", "effect", "valibot", "zod", "zod-to-json-schema"] }, "sha512-WF0OkR3cbKwtUxis8HFDRzkwPVbmk4WFhrZa35gFslIOKKLKlkh/ejjIeW6nGVoCxVQOQg5AayuggJo8bhn0Cg=="],
+
+ "@standard-community/standard-openapi": ["@standard-community/standard-openapi@0.2.0-rc.0", "", { "dependencies": { "zod-openapi": "^4.2.4" }, "peerDependencies": { "@standard-community/standard-json": "^0.3.0-rc.1", "@standard-schema/spec": "^1.0.0", "arktype": "^2.1.20", "openapi-types": "^12.1.3", "valibot": "^1.1.0", "zod": "^3.25.67" }, "optionalPeers": ["arktype", "valibot", "zod"] }, "sha512-UFN2H9aB7rCbvY4z072IikQMQ6PYrCAushiBrjgpGmhHww2Q8NDa8wwY1vPc5tJTTufjGAtMUZnVe2rskdD8/w=="],
+
+ "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
+
"@ts-morph/common": ["@ts-morph/common@0.12.3", "", { "dependencies": { "fast-glob": "^3.2.7", "minimatch": "^3.0.4", "mkdirp": "^1.0.4", "path-browserify": "^1.0.1" } }, "sha512-4tUmeLyXJnJWvTFOKtcNJ1yh0a3SsTLi2MUoyj8iUNznFRN1ZquaNe7Oukqrnki2FzZkm0J9adCNLDZxUzvj+w=="],
"@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
@@ -921,8 +919,6 @@
"cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
- "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
-
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
"code-block-writer": ["code-block-writer@11.0.3", "", {}, "sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw=="],
@@ -1081,7 +1077,7 @@
"hono": ["hono@4.8.4", "", {}, "sha512-KOIBp1+iUs0HrKztM4EHiB2UtzZDTBihDtOF5K6+WaJjCPeaW4Q92R8j63jOhvJI5+tZSMuKD9REVEXXY9illg=="],
- "hono-openapi": ["hono-openapi@0.4.8", "", { "dependencies": { "json-schema-walker": "^2.0.0" }, "peerDependencies": { "@hono/arktype-validator": "^2.0.0", "@hono/effect-validator": "^1.2.0", "@hono/typebox-validator": "^0.2.0 || ^0.3.0", "@hono/valibot-validator": "^0.5.1", "@hono/zod-validator": "^0.4.1", "@sinclair/typebox": "^0.34.9", "@valibot/to-json-schema": "^1.0.0-beta.3", "arktype": "^2.0.0", "effect": "^3.11.3", "hono": "^4.6.13", "openapi-types": "^12.1.3", "valibot": "^1.0.0-beta.9", "zod": "^3.23.8", "zod-openapi": "^4.0.0" }, "optionalPeers": ["@hono/arktype-validator", "@hono/effect-validator", "@hono/typebox-validator", "@hono/valibot-validator", "@hono/zod-validator", "@sinclair/typebox", "@valibot/to-json-schema", "arktype", "effect", "hono", "valibot", "zod", "zod-openapi"] }, "sha512-LYr5xdtD49M7hEAduV1PftOMzuT8ZNvkyWfh1DThkLsIr4RkvDb12UxgIiFbwrJB6FLtFXLoOZL9x4IeDk2+VA=="],
+ "hono-openapi": ["@cpluspatch/hono-openapi@0.5.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.1.2", "@sinclair/typebox": "^0.34.9", "@standard-community/standard-json": "^0.3.0-rc.1", "@standard-community/standard-openapi": "^0.2.0-rc.0", "arktype": "^2.0.0", "effect": "^3.16.12", "hono": "^4.8.3", "openapi-types": "^12.1.3", "valibot": "^1.0.0-beta.9", "zod": "^3.23.8" }, "optionalPeers": ["@hono/standard-validator", "@sinclair/typebox", "arktype", "effect", "hono", "valibot", "zod"] }, "sha512-lecsN4jEzIwDb1HfArk5BuaR1O1AG2i6Dmtkc+K9BCs0LWRMZ0iWqPvo5LOwTTPttR4oG+mg50vepYQ5imV5Pg=="],
"hono-rate-limiter": ["hono-rate-limiter@0.4.2", "", { "peerDependencies": { "hono": "^4.1.1" } }, "sha512-AAtFqgADyrmbDijcRTT/HJfwqfvhalya2Zo+MgfdrMPas3zSMD8SU03cv+ZsYwRU1swv7zgVt0shwN059yzhjw=="],
@@ -1145,8 +1141,6 @@
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
- "json-schema-walker": ["json-schema-walker@2.0.0", "", { "dependencies": { "@apidevtools/json-schema-ref-parser": "^11.1.0", "clone": "^2.1.2" } }, "sha512-nXN2cMky0Iw7Af28w061hmxaPDaML5/bQD9nwm1lOoIKEGjHcRGxqWe4MfrkYThYAPjSUhmsp4bJNoLAyVn9Xw=="],
-
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"juice": ["juice@8.1.0", "", { "dependencies": { "cheerio": "1.0.0-rc.10", "commander": "^6.1.0", "mensch": "^0.3.4", "slick": "^1.12.2", "web-resource-inliner": "^6.0.1" }, "bin": { "juice": "bin/juice" } }, "sha512-FLzurJrx5Iv1e7CfBSZH68dC04EEvXvvVvPYB7Vx1WAuhCp1ZPIMtqxc+WTWxVkpTIC2Ach/GAv0rQbtGf6YMA=="],
@@ -1571,16 +1565,14 @@
"zod": ["zod@3.25.74", "", {}, "sha512-J8poo92VuhKjNknViHRAIuuN6li/EwFbAC8OedzI8uxpEPGiXHGQu9wemIAioIpqgfB4SySaJhdk0mH5Y4ICBg=="],
- "zod-openapi": ["zod-openapi@4.2.4", "", { "peerDependencies": { "zod": "^3.21.4" } }, "sha512-tsrQpbpqFCXqVXUzi3TPwFhuMtLN3oNZobOtYnK6/5VkXsNdnIgyNr4r8no4wmYluaxzN3F7iS+8xCW8BmMQ8g=="],
+ "zod-openapi": ["zod-openapi@5.0.0", "", { "peerDependencies": { "zod": "^3.25.74" } }, "sha512-fNwuOsflpILVVsx+3e8ODA0AnI60xGtMVWcvzv733ggEj7fVvE4NQMoOlQGbqIleyOdFQuc5N6cpO1BevApQig=="],
"zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="],
- "zod-validation-error": ["zod-validation-error@3.5.2", "", { "peerDependencies": { "zod": "^3.25.0" } }, "sha512-mdi7YOLtram5dzJ5aDtm1AG9+mxRma1iaMrZdYIpFO7epdKBUwLHIxTF8CPDeCQ828zAXYtizrKlEJAtzgfgrw=="],
+ "zod-validation-error": ["zod-validation-error@4.0.0-beta.1", "", { "peerDependencies": { "zod": "^3.25.0" } }, "sha512-42DSXwZyDKeLHrug+luXt6RMaoYsgMXc68bCz9kOyk66k7XBG35cAxsu2Lg42uVrJ1kEem2RyHxMBAv25SeZzQ=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
- "@apidevtools/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
-
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
@@ -1619,6 +1611,8 @@
"@sentry/node/@opentelemetry/resources": ["@opentelemetry/resources@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA=="],
+ "@standard-community/standard-openapi/zod-openapi": ["zod-openapi@4.2.4", "", { "peerDependencies": { "zod": "^3.21.4" } }, "sha512-tsrQpbpqFCXqVXUzi3TPwFhuMtLN3oNZobOtYnK6/5VkXsNdnIgyNr4r8no4wmYluaxzN3F7iS+8xCW8BmMQ8g=="],
+
"@ts-morph/common/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
diff --git a/package.json b/package.json
index 27d92bc3..3177a42b 100644
--- a/package.json
+++ b/package.json
@@ -42,7 +42,6 @@
"vitepress-plugin-tabs": "^0.7.1",
"vitepress-sidebar": "^1.32.1",
"vue": "^3.5.17",
- "zod-to-json-schema": "^3.24.6",
"@bull-board/api": "^6.11.0",
"@bull-board/hono": "^6.11.0",
"@clerc/plugin-completions": "^0.44.0",
@@ -51,7 +50,7 @@
"@clerc/plugin-not-found": "^0.44.0",
"@clerc/plugin-version": "^0.44.0",
"@hackmd/markdown-it-task-lists": "^2.1.4",
- "@hono/zod-validator": "^0.7.0",
+ "@hono/standard-validator": "^0.1.2",
"@inquirer/confirm": "^5.1.13",
"@logtape/file": "^1.0.0",
"@logtape/logtape": "^1.0.0",
@@ -68,7 +67,7 @@
"drizzle-orm": "^0.44.2",
"feed": "^5.1.0",
"hono": "^4.8.4",
- "hono-openapi": "^0.4.8",
+ "hono-openapi": "npm:@cpluspatch/hono-openapi@0.5.1",
"hono-rate-limiter": "^0.4.2",
"html-to-text": "^9.0.5",
"ioredis": "^5.6.1",
@@ -99,8 +98,8 @@
"xss": "^1.0.15",
"youch": "^4.1.0-beta.7",
"zod": "^3.25.74",
- "zod-openapi": "^4.2.4",
- "zod-validation-error": "^3.5.2"
+ "zod-openapi": "^5.0.0",
+ "zod-validation-error": "^4.0.0-beta.1"
}
},
"maintainers": [
@@ -153,8 +152,7 @@
"vitepress": "catalog:",
"vitepress-plugin-tabs": "catalog:",
"vitepress-sidebar": "catalog:",
- "vue": "catalog:",
- "zod-to-json-schema": "catalog:"
+ "vue": "catalog:"
},
"dependencies": {
"@bull-board/api": "catalog:",
@@ -165,15 +163,15 @@
"@clerc/plugin-not-found": "catalog:",
"@clerc/plugin-version": "catalog:",
"@hackmd/markdown-it-task-lists": "catalog:",
- "@hono/zod-validator": "catalog:",
+ "@hono/standard-validator": "catalog:",
"@inquirer/confirm": "catalog:",
"@scalar/hono-api-reference": "catalog:",
"@sentry/bun": "catalog:",
+ "@versia-server/api": "workspace:*",
"@versia-server/config": "workspace:*",
"@versia-server/kit": "workspace:*",
- "@versia-server/tests": "workspace:*",
"@versia-server/logging": "workspace:*",
- "@versia-server/api": "workspace:*",
+ "@versia-server/tests": "workspace:*",
"@versia-server/worker": "workspace:*",
"@versia/client": "workspace:*",
"@versia/sdk": "workspace:*",
diff --git a/packages/api/app.ts b/packages/api/app.ts
index 58a3726f..7d422a43 100644
--- a/packages/api/app.ts
+++ b/packages/api/app.ts
@@ -10,7 +10,7 @@ import { cors } from "hono/cors";
import { createMiddleware } from "hono/factory";
import { prettyJSON } from "hono/pretty-json";
import { secureHeaders } from "hono/secure-headers";
-import { openAPISpecs } from "hono-openapi";
+import { generateSpecs } from "hono-openapi";
import { Youch } from "youch";
import { applyToHono } from "@/bull-board.ts";
import pkg from "../../package.json" with { type: "application/json" };
@@ -22,8 +22,6 @@ import { logger } from "./middlewares/logger.ts";
import { rateLimit } from "./middlewares/rate-limit.ts";
import { PluginLoader } from "./plugin-loader.ts";
import { routes } from "./routes.ts";
-// Extends Zod with OpenAPI schema generation
-import "zod-openapi/extend";
export const appFactory = async (): Promise> => {
const app = new Hono({
@@ -127,22 +125,23 @@ export const appFactory = async (): Promise> => {
(time2 - time1).toFixed(2),
)}ms`}`;
- app.get(
- "/openapi.json",
- openAPISpecs(app, {
- documentation: {
- info: {
- title: "Versia Server API",
- version: pkg.version,
- license: {
- name: "AGPL-3.0",
- url: "https://www.gnu.org/licenses/agpl-3.0.html",
- },
- contact: pkg.author,
+ const openApiSpecs = await generateSpecs(app, {
+ documentation: {
+ info: {
+ title: "Versia Server API",
+ version: pkg.version,
+ license: {
+ name: "AGPL-3.0",
+ url: "https://www.gnu.org/licenses/agpl-3.0.html",
},
+ contact: pkg.author,
},
- }),
- );
+ },
+ });
+
+ app.get("/openapi.json", (context) => {
+ return context.json(openApiSpecs, 200);
+ });
app.get(
"/docs",
diff --git a/packages/api/middlewares/rate-limit.ts b/packages/api/middlewares/rate-limit.ts
index b14565b8..b8cf1208 100644
--- a/packages/api/middlewares/rate-limit.ts
+++ b/packages/api/middlewares/rate-limit.ts
@@ -2,7 +2,7 @@ import type { ApiError } from "@versia-server/kit";
import { env } from "bun";
import type { MiddlewareHandler } from "hono";
import { rateLimiter } from "hono-rate-limiter";
-import type { z } from "zod";
+import type { z } from "zod/v4";
import type { HonoEnv } from "~/types/api";
// Not exported by hono-rate-limiter
diff --git a/packages/api/package.json b/packages/api/package.json
index 2ce799d3..667217f0 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -74,7 +74,7 @@
"ip-matching": "catalog:",
"qs": "catalog:",
"altcha-lib": "catalog:",
- "@hono/zod-validator": "catalog:",
+ "@hono/standard-validator": "catalog:",
"zod-validation-error": "catalog:",
"confbox": "catalog:",
"oauth4webapi": "catalog:"
diff --git a/packages/api/plugin-loader.ts b/packages/api/plugin-loader.ts
index c97e43fe..64e773f4 100644
--- a/packages/api/plugin-loader.ts
+++ b/packages/api/plugin-loader.ts
@@ -6,7 +6,7 @@ import { file, sleep } from "bun";
import chalk from "chalk";
import { parseJSON5, parseJSONC } from "confbox";
import type { Hono } from "hono";
-import type { ZodTypeAny } from "zod";
+import type { ZodTypeAny } from "zod/v4";
import { fromZodError, type ValidationError } from "zod-validation-error";
import type { HonoEnv } from "~/types/api";
diff --git a/packages/api/plugins/openid/index.ts b/packages/api/plugins/openid/index.ts
index a0d7383c..b7daed32 100644
--- a/packages/api/plugins/openid/index.ts
+++ b/packages/api/plugins/openid/index.ts
@@ -5,7 +5,7 @@ import { User } from "@versia-server/kit/db";
import { getCookie } from "hono/cookie";
import { jwtVerify } from "jose";
import { JOSEError, JWTExpired } from "jose/errors";
-import { z } from "zod";
+import { z } from "zod/v4";
import authorizeRoute from "./routes/authorize.ts";
import jwksRoute from "./routes/jwks.ts";
import ssoLoginCallbackRoute from "./routes/oauth/callback.ts";
diff --git a/packages/api/plugins/openid/routes/authorize.ts b/packages/api/plugins/openid/routes/authorize.ts
index 2db1df2f..bb18310b 100644
--- a/packages/api/plugins/openid/routes/authorize.ts
+++ b/packages/api/plugins/openid/routes/authorize.ts
@@ -2,11 +2,10 @@ import { RolePermission } from "@versia/client/schemas";
import { auth, handleZodError, jsonOrForm } from "@versia-server/kit/api";
import { Application, Token, User } from "@versia-server/kit/db";
import { randomUUIDv7 } from "bun";
-import { describeRoute } from "hono-openapi";
-import { validator } from "hono-openapi/zod";
+import { describeRoute, validator } from "hono-openapi";
import { type JWTPayload, jwtVerify, SignJWT } from "jose";
import { JOSEError } from "jose/errors";
-import { z } from "zod";
+import { z } from "zod/v4";
import { randomString } from "@/math";
import { errorRedirect, errors } from "../errors.ts";
import type { PluginType } from "../index.ts";
@@ -50,7 +49,6 @@ export default (plugin: PluginType): void =>
.object({
scope: z.string().optional(),
redirect_uri: z
- .string()
.url()
.optional()
.or(z.literal("urn:ietf:wg:oauth:2.0:oob")),
@@ -141,7 +139,7 @@ export default (plugin: PluginType): void =>
);
}
- if (!z.string().uuid().safeParse(sub).success) {
+ if (!z.uuid().safeParse(sub).success) {
return errorRedirect(
context,
errors.InvalidSub,
diff --git a/packages/api/plugins/openid/routes/jwks.ts b/packages/api/plugins/openid/routes/jwks.ts
index 061dfbac..31896563 100644
--- a/packages/api/plugins/openid/routes/jwks.ts
+++ b/packages/api/plugins/openid/routes/jwks.ts
@@ -1,8 +1,7 @@
import { auth } from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
import { exportJWK } from "jose";
-import { z } from "zod";
+import { z } from "zod/v4";
import type { PluginType } from "../index.ts";
export default (plugin: PluginType): void => {
diff --git a/packages/api/plugins/openid/routes/oauth/callback.ts b/packages/api/plugins/openid/routes/oauth/callback.ts
index 4f0a0965..60db45e7 100644
--- a/packages/api/plugins/openid/routes/oauth/callback.ts
+++ b/packages/api/plugins/openid/routes/oauth/callback.ts
@@ -1,6 +1,7 @@
import {
Account as AccountSchema,
RolePermission,
+ zBoolean,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { handleZodError } from "@versia-server/kit/api";
@@ -10,10 +11,9 @@ import { OpenIdAccounts, Users } from "@versia-server/kit/tables";
import { randomUUIDv7 } from "bun";
import { and, eq, isNull, type SQL } from "drizzle-orm";
import { setCookie } from "hono/cookie";
-import { describeRoute } from "hono-openapi";
-import { validator } from "hono-openapi/zod";
+import { describeRoute, validator } from "hono-openapi";
import { SignJWT } from "jose";
-import { z } from "zod";
+import { z } from "zod/v4";
import { randomString } from "@/math.ts";
import type { PluginType } from "../../index.ts";
import { automaticOidcFlow } from "../../utils.ts";
@@ -47,13 +47,8 @@ export default (plugin: PluginType): void => {
z.object({
client_id: z.string().optional(),
flow: z.string(),
- link: z
- .string()
- .transform((v) =>
- ["true", "1", "on"].includes(v.toLowerCase()),
- )
- .optional(),
- user_id: z.string().uuid().optional(),
+ link: zBoolean.optional(),
+ user_id: z.uuid().optional(),
}),
handleZodError,
),
diff --git a/packages/api/plugins/openid/routes/oauth/revoke.ts b/packages/api/plugins/openid/routes/oauth/revoke.ts
index 252c88f5..958cd2b5 100644
--- a/packages/api/plugins/openid/routes/oauth/revoke.ts
+++ b/packages/api/plugins/openid/routes/oauth/revoke.ts
@@ -2,9 +2,8 @@ import { handleZodError, jsonOrForm } from "@versia-server/kit/api";
import { db, Token } from "@versia-server/kit/db";
import { Tokens } from "@versia-server/kit/tables";
import { and, eq } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
import type { PluginType } from "../../index.ts";
export default (plugin: PluginType): void => {
diff --git a/packages/api/plugins/openid/routes/oauth/sso.ts b/packages/api/plugins/openid/routes/oauth/sso.ts
index 57c4c2d1..73f7ea35 100644
--- a/packages/api/plugins/openid/routes/oauth/sso.ts
+++ b/packages/api/plugins/openid/routes/oauth/sso.ts
@@ -2,15 +2,14 @@ import { handleZodError } from "@versia-server/kit/api";
import { Application, db } from "@versia-server/kit/db";
import { OpenIdLoginFlows } from "@versia-server/kit/tables";
import { randomUUIDv7 } from "bun";
-import { describeRoute } from "hono-openapi";
-import { validator } from "hono-openapi/zod";
+import { describeRoute, validator } from "hono-openapi";
import {
calculatePKCECodeChallenge,
discoveryRequest,
generateRandomCodeVerifier,
processDiscoveryResponse,
} from "oauth4webapi";
-import { z } from "zod";
+import { z } from "zod/v4";
import type { PluginType } from "../../index.ts";
import { oauthRedirectUri } from "../../utils.ts";
@@ -34,7 +33,7 @@ export default (plugin: PluginType): void => {
z.object({
issuer: z.string(),
client_id: z.string().optional(),
- redirect_uri: z.string().url().optional(),
+ redirect_uri: z.url().optional(),
scope: z.string().optional(),
response_type: z.enum(["code"]).optional(),
}),
diff --git a/packages/api/plugins/openid/routes/oauth/token.ts b/packages/api/plugins/openid/routes/oauth/token.ts
index 21634dc5..9fe2ebf6 100644
--- a/packages/api/plugins/openid/routes/oauth/token.ts
+++ b/packages/api/plugins/openid/routes/oauth/token.ts
@@ -2,9 +2,8 @@ import { handleZodError, jsonOrForm } from "@versia-server/kit/api";
import { Application, Token } from "@versia-server/kit/db";
import { Tokens } from "@versia-server/kit/tables";
import { and, eq } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
import type { PluginType } from "../../index.ts";
export default (plugin: PluginType): void => {
@@ -80,7 +79,7 @@ export default (plugin: PluginType): void => {
client_secret: z.string().optional(),
username: z.string().trim().optional(),
password: z.string().trim().optional(),
- redirect_uri: z.string().url().optional(),
+ redirect_uri: z.url().optional(),
refresh_token: z.string().optional(),
scope: z.string().optional(),
assertion: z.string().optional(),
diff --git a/packages/api/plugins/openid/routes/sso/:id/index.ts b/packages/api/plugins/openid/routes/sso/:id/index.ts
index f4b844d0..7669882a 100644
--- a/packages/api/plugins/openid/routes/sso/:id/index.ts
+++ b/packages/api/plugins/openid/routes/sso/:id/index.ts
@@ -4,9 +4,8 @@ import { auth, handleZodError } from "@versia-server/kit/api";
import { db } from "@versia-server/kit/db";
import { OpenIdAccounts } from "@versia-server/kit/tables";
import { and, eq, type SQL } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
import type { PluginType } from "../../../index.ts";
export default (plugin: PluginType): void => {
diff --git a/packages/api/plugins/openid/routes/sso/index.ts b/packages/api/plugins/openid/routes/sso/index.ts
index f678eb53..b2746db7 100644
--- a/packages/api/plugins/openid/routes/sso/index.ts
+++ b/packages/api/plugins/openid/routes/sso/index.ts
@@ -4,13 +4,12 @@ import { auth, handleZodError } from "@versia-server/kit/api";
import { Application, db } from "@versia-server/kit/db";
import { OpenIdLoginFlows } from "@versia-server/kit/tables";
import { randomUUIDv7 } from "bun";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
import {
calculatePKCECodeChallenge,
generateRandomCodeVerifier,
} from "oauth4webapi";
-import { z } from "zod";
+import { z } from "zod/v4";
import type { PluginType } from "../../index.ts";
import { oauthDiscoveryRequest, oauthRedirectUri } from "../../utils.ts";
diff --git a/packages/api/routes/api/auth/login/index.ts b/packages/api/routes/api/auth/login/index.ts
index 9940d08b..dd32644e 100644
--- a/packages/api/routes/api/auth/login/index.ts
+++ b/packages/api/routes/api/auth/login/index.ts
@@ -7,10 +7,9 @@ import { password as bunPassword } from "bun";
import { eq, or } from "drizzle-orm";
import type { Context } from "hono";
import { setCookie } from "hono/cookie";
-import { describeRoute } from "hono-openapi";
-import { validator } from "hono-openapi/zod";
+import { describeRoute, validator } from "hono-openapi";
import { SignJWT } from "jose";
-import { z } from "zod";
+import { z } from "zod/v4";
const returnError = (
context: Context,
@@ -59,7 +58,7 @@ export default apiRoute((app) =>
"query",
z.object({
scope: z.string().optional(),
- redirect_uri: z.string().url().optional(),
+ redirect_uri: z.url().optional(),
response_type: z.enum([
"code",
"token",
@@ -90,7 +89,6 @@ export default apiRoute((app) =>
"form",
z.object({
identifier: z
- .string()
.email()
.toLowerCase()
.or(z.string().toLowerCase()),
diff --git a/packages/api/routes/api/auth/redirect/index.ts b/packages/api/routes/api/auth/redirect/index.ts
index 42fe1210..5e43431d 100644
--- a/packages/api/routes/api/auth/redirect/index.ts
+++ b/packages/api/routes/api/auth/redirect/index.ts
@@ -3,9 +3,8 @@ import { apiRoute, handleZodError } from "@versia-server/kit/api";
import { db } from "@versia-server/kit/db";
import { Applications, Tokens } from "@versia-server/kit/tables";
import { and, eq } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, validator } from "hono-openapi";
+import { z } from "zod/v4";
/**
* OAuth Code flow
@@ -28,7 +27,7 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
- redirect_uri: z.string().url(),
+ redirect_uri: z.url(),
client_id: z.string(),
code: z.string(),
}),
diff --git a/packages/api/routes/api/auth/reset/index.ts b/packages/api/routes/api/auth/reset/index.ts
index da200b57..f45a12ab 100644
--- a/packages/api/routes/api/auth/reset/index.ts
+++ b/packages/api/routes/api/auth/reset/index.ts
@@ -5,9 +5,8 @@ import { Users } from "@versia-server/kit/tables";
import { password as bunPassword } from "bun";
import { eq } from "drizzle-orm";
import type { Context } from "hono";
-import { describeRoute } from "hono-openapi";
-import { validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, validator } from "hono-openapi";
+import { z } from "zod/v4";
const returnError = (
context: Context,
diff --git a/packages/api/routes/api/v1/accounts/[id]/block.ts b/packages/api/routes/api/v1/accounts/[id]/block.ts
index 27af0a61..d9e98a05 100644
--- a/packages/api/routes/api/v1/accounts/[id]/block.ts
+++ b/packages/api/routes/api/v1/accounts/[id]/block.ts
@@ -5,8 +5,7 @@ import {
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.post(
diff --git a/packages/api/routes/api/v1/accounts/[id]/feed.atom.ts b/packages/api/routes/api/v1/accounts/[id]/feed.atom.ts
index e814026e..53bcb81d 100644
--- a/packages/api/routes/api/v1/accounts/[id]/feed.atom.ts
+++ b/packages/api/routes/api/v1/accounts/[id]/feed.atom.ts
@@ -6,9 +6,8 @@ import {
handleZodError,
withUserParam,
} from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
import { getFeed } from "@/rss";
export default apiRoute((app) =>
@@ -39,12 +38,13 @@ export default apiRoute((app) =>
RolePermission.ViewNotes,
RolePermission.ViewAccounts,
],
+
scopes: ["read:statuses"],
}),
validator(
"query",
z.object({
- page: z.coerce.number().default(0).openapi({
+ page: z.coerce.number().default(0).meta({
description: "Page number to fetch. Defaults to 0.",
example: 2,
}),
diff --git a/packages/api/routes/api/v1/accounts/[id]/feed.rss.ts b/packages/api/routes/api/v1/accounts/[id]/feed.rss.ts
index 9baf5e66..296389e5 100644
--- a/packages/api/routes/api/v1/accounts/[id]/feed.rss.ts
+++ b/packages/api/routes/api/v1/accounts/[id]/feed.rss.ts
@@ -6,9 +6,8 @@ import {
handleZodError,
withUserParam,
} from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
import { getFeed } from "@/rss";
export default apiRoute((app) =>
@@ -38,12 +37,13 @@ export default apiRoute((app) =>
RolePermission.ViewNotes,
RolePermission.ViewAccounts,
],
+
scopes: ["read:statuses"],
}),
validator(
"query",
z.object({
- page: z.coerce.number().default(0).openapi({
+ page: z.coerce.number().default(0).meta({
description: "Page number to fetch. Defaults to 0.",
example: 2,
}),
diff --git a/packages/api/routes/api/v1/accounts/[id]/follow.ts b/packages/api/routes/api/v1/accounts/[id]/follow.ts
index 34792bcc..086cd894 100644
--- a/packages/api/routes/api/v1/accounts/[id]/follow.ts
+++ b/packages/api/routes/api/v1/accounts/[id]/follow.ts
@@ -11,9 +11,8 @@ import {
withUserParam,
} from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.post(
@@ -62,12 +61,12 @@ export default apiRoute((app) =>
validator(
"json",
z.object({
- reblogs: z.boolean().default(true).openapi({
+ reblogs: z.boolean().default(true).meta({
description:
"Receive this account’s reblogs in home timeline?",
example: true,
}),
- notify: z.boolean().default(false).openapi({
+ notify: z.boolean().default(false).meta({
description:
"Receive notifications when this account posts a status?",
example: false,
@@ -75,7 +74,7 @@ export default apiRoute((app) =>
languages: z
.array(iso631)
.default([])
- .openapi({
+ .meta({
description:
"Array of String (ISO 639-1 language two-letter code). Filter received statuses for these languages. If not provided, you will receive this account’s posts in all languages.",
example: ["en", "fr"],
diff --git a/packages/api/routes/api/v1/accounts/[id]/followers.ts b/packages/api/routes/api/v1/accounts/[id]/followers.ts
index 6b3d4dfd..dc132a05 100644
--- a/packages/api/routes/api/v1/accounts/[id]/followers.ts
+++ b/packages/api/routes/api/v1/accounts/[id]/followers.ts
@@ -12,9 +12,8 @@ import {
import { Timeline } from "@versia-server/kit/db";
import { Users } from "@versia-server/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
@@ -39,7 +38,7 @@ export default apiRoute((app) =>
link: z
.string()
.optional()
- .openapi({
+ .meta({
description:
"Links to the next and previous pages",
example:
@@ -66,22 +65,22 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
- max_id: AccountSchema.shape.id.optional().openapi({
+ max_id: AccountSchema.shape.id.optional().meta({
description:
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
}),
- since_id: AccountSchema.shape.id.optional().openapi({
+ since_id: AccountSchema.shape.id.optional().meta({
description:
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
example: undefined,
}),
- min_id: AccountSchema.shape.id.optional().openapi({
+ min_id: AccountSchema.shape.id.optional().meta({
description:
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
example: undefined,
}),
- limit: z.number().int().min(1).max(40).default(20).openapi({
+ limit: z.number().int().min(1).max(40).default(20).meta({
description: "Maximum number of results to return.",
}),
}),
diff --git a/packages/api/routes/api/v1/accounts/[id]/following.ts b/packages/api/routes/api/v1/accounts/[id]/following.ts
index a105aace..6fa5d021 100644
--- a/packages/api/routes/api/v1/accounts/[id]/following.ts
+++ b/packages/api/routes/api/v1/accounts/[id]/following.ts
@@ -12,9 +12,8 @@ import {
import { Timeline } from "@versia-server/kit/db";
import { Users } from "@versia-server/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
@@ -40,7 +39,7 @@ export default apiRoute((app) =>
link: z
.string()
.optional()
- .openapi({
+ .meta({
description:
"Links to the next and previous pages",
example:
@@ -67,22 +66,22 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
- max_id: AccountSchema.shape.id.optional().openapi({
+ max_id: AccountSchema.shape.id.optional().meta({
description:
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
}),
- since_id: AccountSchema.shape.id.optional().openapi({
+ since_id: AccountSchema.shape.id.optional().meta({
description:
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
example: undefined,
}),
- min_id: AccountSchema.shape.id.optional().openapi({
+ min_id: AccountSchema.shape.id.optional().meta({
description:
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
example: undefined,
}),
- limit: z.number().int().min(1).max(40).default(20).openapi({
+ limit: z.number().int().min(1).max(40).default(20).meta({
description: "Maximum number of results to return.",
}),
}),
diff --git a/packages/api/routes/api/v1/accounts/[id]/index.ts b/packages/api/routes/api/v1/accounts/[id]/index.ts
index f9462e72..771ebd4c 100644
--- a/packages/api/routes/api/v1/accounts/[id]/index.ts
+++ b/packages/api/routes/api/v1/accounts/[id]/index.ts
@@ -4,8 +4,7 @@ import {
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/api/v1/accounts/[id]/mute.ts b/packages/api/routes/api/v1/accounts/[id]/mute.ts
index 44455d74..90993ab3 100644
--- a/packages/api/routes/api/v1/accounts/[id]/mute.ts
+++ b/packages/api/routes/api/v1/accounts/[id]/mute.ts
@@ -14,9 +14,8 @@ import {
RelationshipJobType,
relationshipQueue,
} from "@versia-server/kit/queues/relationships";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.post(
@@ -56,7 +55,7 @@ export default apiRoute((app) =>
validator(
"json",
z.object({
- notifications: z.boolean().default(true).openapi({
+ notifications: z.boolean().default(true).meta({
description: "Mute notifications in addition to statuses?",
}),
duration: z
@@ -65,7 +64,7 @@ export default apiRoute((app) =>
.min(0)
.max(60 * 60 * 24 * 365 * 5)
.default(0)
- .openapi({
+ .meta({
description:
"How long the mute should last, in seconds. 0 means indefinite.",
}),
diff --git a/packages/api/routes/api/v1/accounts/[id]/note.ts b/packages/api/routes/api/v1/accounts/[id]/note.ts
index c635c9ce..5c835eb3 100644
--- a/packages/api/routes/api/v1/accounts/[id]/note.ts
+++ b/packages/api/routes/api/v1/accounts/[id]/note.ts
@@ -10,9 +10,8 @@ import {
withUserParam,
} from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.post(
@@ -50,7 +49,7 @@ export default apiRoute((app) =>
validator(
"json",
z.object({
- comment: RelationshipSchema.shape.note.optional().openapi({
+ comment: RelationshipSchema.shape.note.optional().meta({
description:
"The comment to be set on that user. Provide an empty string or leave out this parameter to clear the currently set note.",
}),
diff --git a/packages/api/routes/api/v1/accounts/[id]/pin.ts b/packages/api/routes/api/v1/accounts/[id]/pin.ts
index 81dba955..48870a2c 100644
--- a/packages/api/routes/api/v1/accounts/[id]/pin.ts
+++ b/packages/api/routes/api/v1/accounts/[id]/pin.ts
@@ -4,8 +4,7 @@ import {
} from "@versia/client/schemas";
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.post(
diff --git a/packages/api/routes/api/v1/accounts/[id]/refetch.ts b/packages/api/routes/api/v1/accounts/[id]/refetch.ts
index 5ad4ea73..c817269d 100644
--- a/packages/api/routes/api/v1/accounts/[id]/refetch.ts
+++ b/packages/api/routes/api/v1/accounts/[id]/refetch.ts
@@ -5,8 +5,7 @@ import {
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
import { User } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.post(
diff --git a/packages/api/routes/api/v1/accounts/[id]/remove_from_followers.ts b/packages/api/routes/api/v1/accounts/[id]/remove_from_followers.ts
index 3905c845..205836d4 100644
--- a/packages/api/routes/api/v1/accounts/[id]/remove_from_followers.ts
+++ b/packages/api/routes/api/v1/accounts/[id]/remove_from_followers.ts
@@ -5,8 +5,7 @@ import {
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.post(
diff --git a/packages/api/routes/api/v1/accounts/[id]/roles/[role_id]/index.ts b/packages/api/routes/api/v1/accounts/[id]/roles/[role_id]/index.ts
index 24c6f10c..18d7a98b 100644
--- a/packages/api/routes/api/v1/accounts/[id]/roles/[role_id]/index.ts
+++ b/packages/api/routes/api/v1/accounts/[id]/roles/[role_id]/index.ts
@@ -11,9 +11,8 @@ import {
withUserParam,
} from "@versia-server/kit/api";
import { Role } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) => {
app.post(
diff --git a/packages/api/routes/api/v1/accounts/[id]/roles/index.ts b/packages/api/routes/api/v1/accounts/[id]/roles/index.ts
index e120a0d8..eaf28716 100644
--- a/packages/api/routes/api/v1/accounts/[id]/roles/index.ts
+++ b/packages/api/routes/api/v1/accounts/[id]/roles/index.ts
@@ -1,9 +1,8 @@
import { Role as RoleSchema } from "@versia/client/schemas";
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
import { Role } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) => {
app.get(
diff --git a/packages/api/routes/api/v1/accounts/[id]/statuses.ts b/packages/api/routes/api/v1/accounts/[id]/statuses.ts
index 00dfc24b..25f97ffd 100644
--- a/packages/api/routes/api/v1/accounts/[id]/statuses.ts
+++ b/packages/api/routes/api/v1/accounts/[id]/statuses.ts
@@ -13,9 +13,8 @@ import {
import { Timeline } from "@versia-server/kit/db";
import { Notes } from "@versia-server/kit/tables";
import { and, eq, gt, gte, inArray, isNull, lt, or, sql } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
@@ -47,50 +46,45 @@ export default apiRoute((app) =>
RolePermission.ViewNotes,
RolePermission.ViewAccounts,
],
+
scopes: ["read:statuses"],
}),
validator(
"query",
z.object({
- max_id: StatusSchema.shape.id.optional().openapi({
+ max_id: StatusSchema.shape.id.optional().meta({
description:
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
}),
- since_id: StatusSchema.shape.id.optional().openapi({
+ since_id: StatusSchema.shape.id.optional().meta({
description:
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
example: undefined,
}),
- min_id: StatusSchema.shape.id.optional().openapi({
+ min_id: StatusSchema.shape.id.optional().meta({
description:
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
example: undefined,
}),
- limit: z.coerce
- .number()
- .int()
- .min(1)
- .max(40)
- .default(20)
- .openapi({
- description: "Maximum number of results to return.",
- }),
- only_media: zBoolean.default(false).openapi({
+ limit: z.coerce.number().int().min(1).max(40).default(20).meta({
+ description: "Maximum number of results to return.",
+ }),
+ only_media: zBoolean.default(false).meta({
description: "Filter out statuses without attachments.",
}),
- exclude_replies: zBoolean.default(false).openapi({
+ exclude_replies: zBoolean.default(false).meta({
description:
"Filter out statuses in reply to a different account.",
}),
- exclude_reblogs: zBoolean.default(false).openapi({
+ exclude_reblogs: zBoolean.default(false).meta({
description: "Filter out boosts from the response.",
}),
- pinned: zBoolean.default(false).openapi({
+ pinned: zBoolean.default(false).meta({
description:
"Filter for pinned statuses only. Pinned statuses do not receive special priority in the order of the returned results.",
}),
- tagged: z.string().optional().openapi({
+ tagged: z.string().optional().meta({
description:
"Filter for statuses using a specific hashtag.",
}),
diff --git a/packages/api/routes/api/v1/accounts/[id]/unblock.ts b/packages/api/routes/api/v1/accounts/[id]/unblock.ts
index b05fdd98..9e2b0ef3 100644
--- a/packages/api/routes/api/v1/accounts/[id]/unblock.ts
+++ b/packages/api/routes/api/v1/accounts/[id]/unblock.ts
@@ -5,8 +5,7 @@ import {
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.post(
diff --git a/packages/api/routes/api/v1/accounts/[id]/unfollow.ts b/packages/api/routes/api/v1/accounts/[id]/unfollow.ts
index df547d91..493dbe3b 100644
--- a/packages/api/routes/api/v1/accounts/[id]/unfollow.ts
+++ b/packages/api/routes/api/v1/accounts/[id]/unfollow.ts
@@ -5,8 +5,7 @@ import {
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.post(
diff --git a/packages/api/routes/api/v1/accounts/[id]/unmute.ts b/packages/api/routes/api/v1/accounts/[id]/unmute.ts
index c6b09666..03c0fc3c 100644
--- a/packages/api/routes/api/v1/accounts/[id]/unmute.ts
+++ b/packages/api/routes/api/v1/accounts/[id]/unmute.ts
@@ -5,8 +5,7 @@ import {
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.post(
diff --git a/packages/api/routes/api/v1/accounts/[id]/unpin.ts b/packages/api/routes/api/v1/accounts/[id]/unpin.ts
index e704f3af..c7676b57 100644
--- a/packages/api/routes/api/v1/accounts/[id]/unpin.ts
+++ b/packages/api/routes/api/v1/accounts/[id]/unpin.ts
@@ -5,8 +5,7 @@ import {
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.post(
diff --git a/packages/api/routes/api/v1/accounts/familiar_followers/index.ts b/packages/api/routes/api/v1/accounts/familiar_followers/index.ts
index ee896513..7d002ae6 100644
--- a/packages/api/routes/api/v1/accounts/familiar_followers/index.ts
+++ b/packages/api/routes/api/v1/accounts/familiar_followers/index.ts
@@ -13,9 +13,8 @@ import {
import { db, User } from "@versia-server/kit/db";
import type { Users } from "@versia-server/kit/tables";
import { type InferSelectModel, sql } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
export default apiRoute((app) =>
@@ -56,8 +55,8 @@ export default apiRoute((app) =>
.array(AccountSchema.shape.id)
.min(1)
.max(10)
- .or(AccountSchema.shape.id.transform((v) => [v]))
- .openapi({
+ .or(AccountSchema.shape.id)
+ .meta({
description:
"Find familiar followers for the provided account IDs.",
example: [
@@ -70,11 +69,11 @@ export default apiRoute((app) =>
),
async (context) => {
const { user } = context.get("auth");
- const { id: ids } = context.req.valid("query");
+ const { id } = context.req.valid("query");
// Find followers of the accounts in "ids", that you also follow
const finalUsers = await Promise.all(
- ids.map(async (id) => ({
+ (Array.isArray(id) ? id : [id]).map(async (id) => ({
id,
accounts: await User.fromIds(
(
diff --git a/packages/api/routes/api/v1/accounts/index.ts b/packages/api/routes/api/v1/accounts/index.ts
index 66f0e5ef..d16d8793 100644
--- a/packages/api/routes/api/v1/accounts/index.ts
+++ b/packages/api/routes/api/v1/accounts/index.ts
@@ -12,38 +12,37 @@ import { User } from "@versia-server/kit/db";
import { searchManager } from "@versia-server/kit/search";
import { Users } from "@versia-server/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
import ISO6391 from "iso-639-1";
-import { z } from "zod";
+import { z } from "zod/v4";
import { tempmailDomains } from "@/tempmail";
import { rateLimit } from "../../../../middlewares/rate-limit.ts";
const schema = z.object({
- username: z.string().openapi({
+ username: z.string().meta({
description: "The desired username for the account",
example: "alice",
}),
- email: z.string().toLowerCase().openapi({
+ email: z.string().toLowerCase().meta({
description:
"The email address to be used for login. Transformed to lowercase.",
example: "alice@gmail.com",
}),
- password: z.string().openapi({
+ password: z.string().meta({
description: "The password to be used for login",
example: "hunter2",
}),
- agreement: zBoolean.openapi({
+ agreement: zBoolean.meta({
description:
"Whether the user agrees to the local rules, terms, and policies. These should be presented to the user in order to allow them to consent before setting this parameter to TRUE.",
example: true,
}),
- locale: z.string().openapi({
+ locale: z.string().meta({
description:
"The language of the confirmation email that will be sent. ISO 639-1 code.",
example: "en",
}),
- reason: z.string().optional().openapi({
+ reason: z.string().optional().meta({
description:
"If registrations require manual approval, this text will be reviewed by moderators.",
}),
@@ -86,8 +85,8 @@ export default apiRoute((app) => {
.array(AccountSchema.shape.id)
.min(1)
.max(40)
- .or(AccountSchema.shape.id.transform((v) => [v]))
- .openapi({
+ .or(AccountSchema.shape.id)
+ .meta({
description: "The IDs of the Accounts in the database.",
example: [
"f137ce6f-ff5e-4998-b20f-0361ba9be007",
@@ -98,10 +97,10 @@ export default apiRoute((app) => {
handleZodError,
),
async (context) => {
- const { id: ids } = context.req.valid("query");
+ const { id } = context.req.valid("query");
// Find accounts by IDs
- const accounts = await User.fromIds(ids);
+ const accounts = await User.fromIds(Array.isArray(id) ? id : [id]);
return context.json(
accounts.map((account) => account.toApi()),
diff --git a/packages/api/routes/api/v1/accounts/lookup/index.ts b/packages/api/routes/api/v1/accounts/lookup/index.ts
index fb24d756..3cdb5d39 100644
--- a/packages/api/routes/api/v1/accounts/lookup/index.ts
+++ b/packages/api/routes/api/v1/accounts/lookup/index.ts
@@ -9,9 +9,8 @@ import { Instance, User } from "@versia-server/kit/db";
import { parseUserAddress } from "@versia-server/kit/parsers";
import { Users } from "@versia-server/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
export default apiRoute((app) =>
@@ -43,7 +42,7 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
- acct: AccountSchema.shape.acct.openapi({
+ acct: AccountSchema.shape.acct.meta({
description: "The username or Webfinger address to lookup.",
example: "lexi@beta.versia.social",
}),
diff --git a/packages/api/routes/api/v1/accounts/relationships/index.ts b/packages/api/routes/api/v1/accounts/relationships/index.ts
index c7bbc394..7abb3c9b 100644
--- a/packages/api/routes/api/v1/accounts/relationships/index.ts
+++ b/packages/api/routes/api/v1/accounts/relationships/index.ts
@@ -12,9 +12,8 @@ import {
qsQuery,
} from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
export default apiRoute((app) =>
@@ -55,8 +54,8 @@ export default apiRoute((app) =>
.array(AccountSchema.shape.id)
.min(1)
.max(10)
- .or(AccountSchema.shape.id.transform((v) => [v]))
- .openapi({
+ .or(AccountSchema.shape.id)
+ .meta({
description:
"Check relationships for the provided account IDs.",
example: [
@@ -64,7 +63,7 @@ export default apiRoute((app) =>
"8424c654-5d03-4a1b-bec8-4e87db811b5d",
],
}),
- with_suspended: zBoolean.default(false).openapi({
+ with_suspended: zBoolean.default(false).meta({
description:
"Whether relationships should be returned for suspended users",
example: false,
@@ -76,17 +75,16 @@ export default apiRoute((app) =>
const { user } = context.get("auth");
// TODO: Implement with_suspended
- const { id: ids } = context.req.valid("query");
+ const { id } = context.req.valid("query");
const relationships = await Relationship.fromOwnerAndSubjects(
user,
- ids,
+ Array.isArray(id) ? id : [id],
);
relationships.sort(
(a, b) =>
- ids.indexOf(a.data.subjectId) -
- ids.indexOf(b.data.subjectId),
+ id.indexOf(a.data.subjectId) - id.indexOf(b.data.subjectId),
);
return context.json(
diff --git a/packages/api/routes/api/v1/accounts/search/index.ts b/packages/api/routes/api/v1/accounts/search/index.ts
index caab1408..fb97d929 100644
--- a/packages/api/routes/api/v1/accounts/search/index.ts
+++ b/packages/api/routes/api/v1/accounts/search/index.ts
@@ -9,10 +9,9 @@ import { User } from "@versia-server/kit/db";
import { parseUserAddress } from "@versia-server/kit/parsers";
import { Users } from "@versia-server/kit/tables";
import { eq, ilike, not, or, sql } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
import stringComparison from "string-comparison";
-import { z } from "zod";
+import { z } from "zod/v4";
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
export default apiRoute((app) =>
@@ -48,30 +47,24 @@ export default apiRoute((app) =>
z.object({
q: AccountSchema.shape.username
.or(AccountSchema.shape.acct)
- .openapi({
+ .meta({
description: "Search query for accounts.",
example: "username",
}),
- limit: z.coerce
- .number()
- .int()
- .min(1)
- .max(80)
- .default(40)
- .openapi({
- description: "Maximum number of results.",
- example: 40,
- }),
- offset: z.coerce.number().int().default(0).openapi({
+ limit: z.coerce.number().int().min(1).max(80).default(40).meta({
+ description: "Maximum number of results.",
+ example: 40,
+ }),
+ offset: z.coerce.number().int().default(0).meta({
description: "Skip the first n results.",
example: 0,
}),
- resolve: zBoolean.default(false).openapi({
+ resolve: zBoolean.default(false).meta({
description:
"Attempt WebFinger lookup. Use this when q is an exact address.",
example: false,
}),
- following: zBoolean.default(false).openapi({
+ following: zBoolean.default(false).meta({
description: "Limit the search to users you are following.",
example: false,
}),
diff --git a/packages/api/routes/api/v1/accounts/update_credentials/index.ts b/packages/api/routes/api/v1/accounts/update_credentials/index.ts
index ba4c8354..47ede0b2 100644
--- a/packages/api/routes/api/v1/accounts/update_credentials/index.ts
+++ b/packages/api/routes/api/v1/accounts/update_credentials/index.ts
@@ -16,9 +16,8 @@ import { Emoji, Media, User } from "@versia-server/kit/db";
import { versiaTextToHtml } from "@versia-server/kit/parsers";
import { Users } from "@versia-server/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
import { mergeAndDeduplicate } from "@/lib";
import { sanitizedHtmlStrip } from "@/sanitization";
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
@@ -58,7 +57,7 @@ export default apiRoute((app) =>
z
.object({
display_name: AccountSchema.shape.display_name
- .openapi({
+ .meta({
description:
"The display name to use for the profile.",
example: "Lexi",
@@ -75,7 +74,7 @@ export default apiRoute((app) =>
"Display name contains blocked words",
),
username: AccountSchema.shape.username
- .openapi({
+ .meta({
description: "The username to use for the profile.",
example: "lexi",
})
@@ -95,7 +94,7 @@ export default apiRoute((app) =>
"Username is disallowed",
),
note: AccountSchema.shape.note
- .openapi({
+ .meta({
description:
"The account bio. Markdown is supported.",
})
@@ -108,72 +107,60 @@ export default apiRoute((app) =>
"Bio contains blocked words",
),
avatar: z
- .string()
.url()
- .transform((a) => new URL(a))
- .openapi({
+ .meta({
description: "Avatar image URL",
})
.or(
z
- .instanceof(File)
- .refine(
- (v) =>
- v.size <=
- config.validation.accounts
- .max_avatar_bytes,
- `Avatar must be less than ${config.validation.accounts.max_avatar_bytes} bytes`,
+ .file()
+ .max(
+ config.validation.accounts.max_avatar_bytes,
)
- .openapi({
+ .meta({
description:
"Avatar image encoded using multipart/form-data",
}),
),
header: z
- .string()
.url()
- .transform((v) => new URL(v))
- .openapi({
+ .meta({
description: "Header image URL",
})
.or(
z
- .instanceof(File)
- .refine(
- (v) =>
- v.size <=
- config.validation.accounts
- .max_header_bytes,
- `Header must be less than ${config.validation.accounts.max_header_bytes} bytes`,
+ .file()
+ .max(
+ config.validation.accounts.max_header_bytes,
)
- .openapi({
+ .meta({
description:
"Header image encoded using multipart/form-data",
}),
),
- locked: AccountSchema.shape.locked.openapi({
+ locked: AccountSchema.shape.locked.meta({
description:
"Whether manual approval of follow requests is required.",
}),
- bot: AccountSchema.shape.bot.openapi({
+ bot: AccountSchema.shape.bot.meta({
description: "Whether the account has a bot flag.",
}),
discoverable: AccountSchema.shape.discoverable
.unwrap()
- .openapi({
+ .meta({
description:
"Whether the account should be shown in the profile directory.",
}),
- hide_collections: zBoolean.openapi({
+ hide_collections: zBoolean.meta({
description:
"Whether to hide followers and followed accounts.",
}),
- indexable: zBoolean.openapi({
+ indexable: zBoolean.meta({
description:
"Whether public posts should be searchable to anyone.",
}),
// TODO: Implement :(
- attribution_domains: z.array(z.string()).openapi({
+ attribution_domains: z.array(z.string()).meta({
description:
"Domains of websites allowed to credit the account.",
example: ["cnn.com", "myblog.com"],
@@ -287,9 +274,9 @@ export default apiRoute((app) =>
user.avatar = await Media.fromFile(avatar);
}
} else if (user.avatar) {
- await user.avatar.updateFromUrl(avatar);
+ await user.avatar.updateFromUrl(new URL(avatar));
} else {
- user.avatar = await Media.fromUrl(avatar);
+ user.avatar = await Media.fromUrl(new URL(avatar));
}
}
@@ -301,9 +288,9 @@ export default apiRoute((app) =>
user.header = await Media.fromFile(header);
}
} else if (user.header) {
- await user.header.updateFromUrl(header);
+ await user.header.updateFromUrl(new URL(header));
} else {
- user.header = await Media.fromUrl(header);
+ user.header = await Media.fromUrl(new URL(header));
}
}
diff --git a/packages/api/routes/api/v1/accounts/verify_credentials/index.ts b/packages/api/routes/api/v1/accounts/verify_credentials/index.ts
index e53788e8..7da9b0fa 100644
--- a/packages/api/routes/api/v1/accounts/verify_credentials/index.ts
+++ b/packages/api/routes/api/v1/accounts/verify_credentials/index.ts
@@ -1,8 +1,7 @@
import { Account } from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth } from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/api/v1/apps/index.ts b/packages/api/routes/api/v1/apps/index.ts
index e9994fea..0f2dbd2b 100644
--- a/packages/api/routes/api/v1/apps/index.ts
+++ b/packages/api/routes/api/v1/apps/index.ts
@@ -6,9 +6,8 @@ import { ApiError } from "@versia-server/kit";
import { apiRoute, handleZodError, jsonOrForm } from "@versia-server/kit/api";
import { Application } from "@versia-server/kit/db";
import { randomUUIDv7 } from "bun";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
import { randomString } from "@/math";
import { rateLimit } from "../../../../middlewares/rate-limit.ts";
@@ -43,22 +42,20 @@ export default apiRoute((app) =>
z.object({
client_name: ApplicationSchema.shape.name,
redirect_uris: ApplicationSchema.shape.redirect_uris.or(
- ApplicationSchema.shape.redirect_uri.transform((u) =>
- u.split("\n"),
- ),
+ ApplicationSchema.shape.redirect_uri,
),
- scopes: z
- .string()
- .default("read")
- .transform((s) => s.split(" "))
- .openapi({
- description: "Space separated list of scopes.",
- }),
+ scopes: z.string().default("read").meta({
+ description: "Space separated list of scopes.",
+ type: "string",
+ }),
// Allow empty websites because Traewelling decides to give an empty
// value instead of not providing anything at all
website: ApplicationSchema.shape.website
.optional()
- .or(z.literal("").transform(() => undefined)),
+ .or(z.literal(""))
+ .meta({
+ type: "string",
+ }),
}),
handleZodError,
),
@@ -69,9 +66,11 @@ export default apiRoute((app) =>
const app = await Application.insert({
id: randomUUIDv7(),
name: client_name,
- redirectUri: redirect_uris.join("\n"),
- scopes: scopes.join(" "),
- website,
+ redirectUri: Array.isArray(redirect_uris)
+ ? redirect_uris.join("\n")
+ : redirect_uris,
+ scopes,
+ website: website || undefined,
clientId: randomString(32, "base64url"),
secret: randomString(64, "base64url"),
});
diff --git a/packages/api/routes/api/v1/apps/verify_credentials/index.ts b/packages/api/routes/api/v1/apps/verify_credentials/index.ts
index 2ca405dd..9f42f8dd 100644
--- a/packages/api/routes/api/v1/apps/verify_credentials/index.ts
+++ b/packages/api/routes/api/v1/apps/verify_credentials/index.ts
@@ -5,8 +5,7 @@ import {
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth } from "@versia-server/kit/api";
import { Application } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/api/v1/blocks/index.ts b/packages/api/routes/api/v1/blocks/index.ts
index 5772bfc4..efe26d62 100644
--- a/packages/api/routes/api/v1/blocks/index.ts
+++ b/packages/api/routes/api/v1/blocks/index.ts
@@ -7,9 +7,8 @@ import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Timeline } from "@versia-server/kit/db";
import { Users } from "@versia-server/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
@@ -33,7 +32,7 @@ export default apiRoute((app) =>
link: z
.string()
.optional()
- .openapi({
+ .meta({
description:
"Links to the next and previous pages",
example:
@@ -56,30 +55,24 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
- max_id: AccountSchema.shape.id.optional().openapi({
+ max_id: AccountSchema.shape.id.optional().meta({
description:
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
}),
- since_id: AccountSchema.shape.id.optional().openapi({
+ since_id: AccountSchema.shape.id.optional().meta({
description:
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
example: undefined,
}),
- min_id: AccountSchema.shape.id.optional().openapi({
+ min_id: AccountSchema.shape.id.optional().meta({
description:
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
example: undefined,
}),
- limit: z.coerce
- .number()
- .int()
- .min(1)
- .max(80)
- .default(40)
- .openapi({
- description: "Maximum number of results to return.",
- }),
+ limit: z.coerce.number().int().min(1).max(80).default(40).meta({
+ description: "Maximum number of results to return.",
+ }),
}),
handleZodError,
),
diff --git a/packages/api/routes/api/v1/challenges/index.ts b/packages/api/routes/api/v1/challenges/index.ts
index 22c663b6..b80be92c 100644
--- a/packages/api/routes/api/v1/challenges/index.ts
+++ b/packages/api/routes/api/v1/challenges/index.ts
@@ -2,8 +2,7 @@ import { Challenge } from "@versia/client/schemas";
import { config } from "@versia-server/config";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth } from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
import { generateChallenge } from "@/challenges";
export default apiRoute((app) =>
diff --git a/packages/api/routes/api/v1/custom_emojis/index.ts b/packages/api/routes/api/v1/custom_emojis/index.ts
index de25ed14..e8b8b91c 100644
--- a/packages/api/routes/api/v1/custom_emojis/index.ts
+++ b/packages/api/routes/api/v1/custom_emojis/index.ts
@@ -7,9 +7,8 @@ import { apiRoute, auth } from "@versia-server/kit/api";
import { Emoji } from "@versia-server/kit/db";
import { Emojis } from "@versia-server/kit/tables";
import { and, eq, isNull, or } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/api/v1/emojis/[id]/index.ts b/packages/api/routes/api/v1/emojis/[id]/index.ts
index 19e01834..2a563b0b 100644
--- a/packages/api/routes/api/v1/emojis/[id]/index.ts
+++ b/packages/api/routes/api/v1/emojis/[id]/index.ts
@@ -11,9 +11,8 @@ import {
jsonOrForm,
withEmojiParam,
} from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
import { mimeLookup } from "@/content_types";
export default apiRoute((app) => {
@@ -123,25 +122,18 @@ export default apiRoute((app) => {
"Shortcode contains blocked words",
),
element: z
- .string()
.url()
- .transform((a) => new URL(a))
- .openapi({
+ .meta({
description: "Emoji image URL",
})
.or(
z
- .instanceof(File)
- .openapi({
+ .file()
+ .max(config.validation.emojis.max_bytes)
+ .meta({
description:
"Emoji image encoded using multipart/form-data",
- })
- .refine(
- (v) =>
- v.size <=
- config.validation.emojis.max_bytes,
- `Emoji must be less than ${config.validation.emojis.max_bytes} bytes`,
- ),
+ }),
),
category: CustomEmojiSchema.shape.category.optional(),
alt: CustomEmojiSchema.shape.description
@@ -195,7 +187,7 @@ export default apiRoute((app) => {
const contentType =
element instanceof File
? element.type
- : await mimeLookup(element);
+ : await mimeLookup(new URL(element));
if (!contentType.startsWith("image/")) {
throw new ApiError(
@@ -208,7 +200,7 @@ export default apiRoute((app) => {
if (element instanceof File) {
await emoji.media.updateFromFile(element);
} else {
- await emoji.media.updateFromUrl(element);
+ await emoji.media.updateFromUrl(new URL(element));
}
}
diff --git a/packages/api/routes/api/v1/emojis/index.ts b/packages/api/routes/api/v1/emojis/index.ts
index ab589b23..2fa8e4d6 100644
--- a/packages/api/routes/api/v1/emojis/index.ts
+++ b/packages/api/routes/api/v1/emojis/index.ts
@@ -14,9 +14,8 @@ import { Emoji, Media } from "@versia-server/kit/db";
import { Emojis } from "@versia-server/kit/tables";
import { randomUUIDv7 } from "bun";
import { and, eq, isNull, or } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
import { mimeLookup } from "@/content_types";
export default apiRoute((app) =>
@@ -60,25 +59,15 @@ export default apiRoute((app) =>
"Shortcode contains blocked words",
),
element: z
- .string()
.url()
- .transform((a) => new URL(a))
- .openapi({
+ .meta({
description: "Emoji image URL",
})
.or(
- z
- .instanceof(File)
- .openapi({
- description:
- "Emoji image encoded using multipart/form-data",
- })
- .refine(
- (v) =>
- v.size <=
- config.validation.emojis.max_bytes,
- `Emoji must be less than ${config.validation.emojis.max_bytes} bytes`,
- ),
+ z.file().max(config.validation.emojis.max_bytes).meta({
+ description:
+ "Emoji image encoded using multipart/form-data",
+ }),
),
category: CustomEmojiSchema.shape.category.optional(),
alt: CustomEmojiSchema.shape.description
@@ -123,7 +112,7 @@ export default apiRoute((app) =>
const contentType =
element instanceof File
? element.type
- : await mimeLookup(element);
+ : await mimeLookup(new URL(element));
if (!contentType.startsWith("image/")) {
throw new ApiError(
@@ -138,7 +127,7 @@ export default apiRoute((app) =>
? await Media.fromFile(element, {
description: alt ?? undefined,
})
- : await Media.fromUrl(element, {
+ : await Media.fromUrl(new URL(element), {
description: alt ?? undefined,
});
diff --git a/packages/api/routes/api/v1/favourites/index.ts b/packages/api/routes/api/v1/favourites/index.ts
index 8676f219..38cc3173 100644
--- a/packages/api/routes/api/v1/favourites/index.ts
+++ b/packages/api/routes/api/v1/favourites/index.ts
@@ -4,9 +4,8 @@ import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Timeline } from "@versia-server/kit/db";
import { Notes } from "@versia-server/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
@@ -30,7 +29,7 @@ export default apiRoute((app) =>
link: z
.string()
.optional()
- .openapi({
+ .meta({
description:
"Links to the next and previous pages",
example:
@@ -52,30 +51,24 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
- max_id: StatusSchema.shape.id.optional().openapi({
+ max_id: StatusSchema.shape.id.optional().meta({
description:
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
}),
- since_id: StatusSchema.shape.id.optional().openapi({
+ since_id: StatusSchema.shape.id.optional().meta({
description:
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
example: undefined,
}),
- min_id: StatusSchema.shape.id.optional().openapi({
+ min_id: StatusSchema.shape.id.optional().meta({
description:
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
example: undefined,
}),
- limit: z.coerce
- .number()
- .int()
- .min(1)
- .max(80)
- .default(40)
- .openapi({
- description: "Maximum number of results to return.",
- }),
+ limit: z.coerce.number().int().min(1).max(80).default(40).meta({
+ description: "Maximum number of results to return.",
+ }),
}),
handleZodError,
),
diff --git a/packages/api/routes/api/v1/follow_requests/[account_id]/authorize.ts b/packages/api/routes/api/v1/follow_requests/[account_id]/authorize.ts
index 112f1a78..c7177704 100644
--- a/packages/api/routes/api/v1/follow_requests/[account_id]/authorize.ts
+++ b/packages/api/routes/api/v1/follow_requests/[account_id]/authorize.ts
@@ -6,9 +6,8 @@ import {
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Relationship, User } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.post(
diff --git a/packages/api/routes/api/v1/follow_requests/[account_id]/reject.ts b/packages/api/routes/api/v1/follow_requests/[account_id]/reject.ts
index b7495fb6..c7c797bf 100644
--- a/packages/api/routes/api/v1/follow_requests/[account_id]/reject.ts
+++ b/packages/api/routes/api/v1/follow_requests/[account_id]/reject.ts
@@ -6,9 +6,8 @@ import {
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Relationship, User } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.post(
diff --git a/packages/api/routes/api/v1/follow_requests/index.ts b/packages/api/routes/api/v1/follow_requests/index.ts
index 551c1391..2e25f3af 100644
--- a/packages/api/routes/api/v1/follow_requests/index.ts
+++ b/packages/api/routes/api/v1/follow_requests/index.ts
@@ -7,9 +7,8 @@ import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Timeline } from "@versia-server/kit/db";
import { Users } from "@versia-server/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
@@ -35,7 +34,7 @@ export default apiRoute((app) =>
link: z
.string()
.optional()
- .openapi({
+ .meta({
description:
"Links to the next and previous pages",
example:
@@ -57,30 +56,24 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
- max_id: AccountSchema.shape.id.optional().openapi({
+ max_id: AccountSchema.shape.id.optional().meta({
description:
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
}),
- since_id: AccountSchema.shape.id.optional().openapi({
+ since_id: AccountSchema.shape.id.optional().meta({
description:
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
example: undefined,
}),
- min_id: AccountSchema.shape.id.optional().openapi({
+ min_id: AccountSchema.shape.id.optional().meta({
description:
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
example: undefined,
}),
- limit: z.coerce
- .number()
- .int()
- .min(1)
- .max(80)
- .default(40)
- .openapi({
- description: "Maximum number of results to return.",
- }),
+ limit: z.coerce.number().int().min(1).max(80).default(40).meta({
+ description: "Maximum number of results to return.",
+ }),
}),
handleZodError,
),
diff --git a/packages/api/routes/api/v1/frontend/config/index.ts b/packages/api/routes/api/v1/frontend/config/index.ts
index 2050cebc..bcbb016e 100644
--- a/packages/api/routes/api/v1/frontend/config/index.ts
+++ b/packages/api/routes/api/v1/frontend/config/index.ts
@@ -1,8 +1,7 @@
import { config } from "@versia-server/config";
import { apiRoute } from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/api/v1/instance/extended_description.ts b/packages/api/routes/api/v1/instance/extended_description.ts
index e92f892c..2a2fd9b5 100644
--- a/packages/api/routes/api/v1/instance/extended_description.ts
+++ b/packages/api/routes/api/v1/instance/extended_description.ts
@@ -2,8 +2,7 @@ import { ExtendedDescription as ExtendedDescriptionSchema } from "@versia/client
import { config } from "@versia-server/config";
import { apiRoute } from "@versia-server/kit/api";
import { markdownToHtml } from "@versia-server/kit/markdown";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/api/v1/instance/index.ts b/packages/api/routes/api/v1/instance/index.ts
index 73577875..7abf34c0 100644
--- a/packages/api/routes/api/v1/instance/index.ts
+++ b/packages/api/routes/api/v1/instance/index.ts
@@ -5,9 +5,8 @@ import { Instance, Note, User } from "@versia-server/kit/db";
import { markdownToHtml } from "@versia-server/kit/markdown";
import { Users } from "@versia-server/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
-import type { z } from "zod";
+import { describeRoute, resolver } from "hono-openapi";
+import type { z } from "zod/v4";
import manifest from "../../../../../../package.json" with { type: "json" };
export default apiRoute((app) =>
diff --git a/packages/api/routes/api/v1/instance/privacy_policy.ts b/packages/api/routes/api/v1/instance/privacy_policy.ts
index b6f75187..4f3c1925 100644
--- a/packages/api/routes/api/v1/instance/privacy_policy.ts
+++ b/packages/api/routes/api/v1/instance/privacy_policy.ts
@@ -2,8 +2,7 @@ import { PrivacyPolicy as PrivacyPolicySchema } from "@versia/client/schemas";
import { config } from "@versia-server/config";
import { apiRoute } from "@versia-server/kit/api";
import { markdownToHtml } from "@versia-server/kit/markdown";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/api/v1/instance/rules.ts b/packages/api/routes/api/v1/instance/rules.ts
index ffabe8c2..085746a5 100644
--- a/packages/api/routes/api/v1/instance/rules.ts
+++ b/packages/api/routes/api/v1/instance/rules.ts
@@ -1,9 +1,8 @@
import { Rule as RuleSchema } from "@versia/client/schemas";
import { config } from "@versia-server/config";
import { apiRoute } from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/api/v1/instance/terms_of_service.ts b/packages/api/routes/api/v1/instance/terms_of_service.ts
index 99908634..71927530 100644
--- a/packages/api/routes/api/v1/instance/terms_of_service.ts
+++ b/packages/api/routes/api/v1/instance/terms_of_service.ts
@@ -2,8 +2,7 @@ import { TermsOfService as TermsOfServiceSchema } from "@versia/client/schemas";
import { config } from "@versia-server/config";
import { apiRoute } from "@versia-server/kit/api";
import { markdownToHtml } from "@versia-server/kit/markdown";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/api/v1/markers/index.ts b/packages/api/routes/api/v1/markers/index.ts
index f5103290..155c0450 100644
--- a/packages/api/routes/api/v1/markers/index.ts
+++ b/packages/api/routes/api/v1/markers/index.ts
@@ -10,9 +10,8 @@ import { db } from "@versia-server/kit/db";
import { Markers } from "@versia-server/kit/tables";
import { randomUUIDv7 } from "bun";
import { and, eq, type SQL } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
const MarkerResponseSchema = z.object({
notifications: MarkerSchema.optional(),
@@ -52,9 +51,9 @@ export default apiRoute((app) => {
"timeline[]": z
.array(z.enum(["home", "notifications"]))
.max(2)
- .or(z.enum(["home", "notifications"]).transform((t) => [t]))
+ .or(z.enum(["home", "notifications"]))
.optional()
- .openapi({
+ .meta({
description:
"Specify the timeline(s) for which markers should be fetched. Possible values: home, notifications. If not provided, an empty object will be returned.",
}),
@@ -62,13 +61,17 @@ export default apiRoute((app) => {
handleZodError,
),
async (context) => {
- const { "timeline[]": timeline } = context.req.valid("query");
+ const { "timeline[]": queryTimeline } = context.req.valid("query");
const { user } = context.get("auth");
- if (!timeline) {
+ if (!queryTimeline) {
return context.json({}, 200);
}
+ const timeline = Array.isArray(queryTimeline)
+ ? queryTimeline
+ : [queryTimeline];
+
const markers: z.infer = {
home: undefined,
notifications: undefined,
@@ -160,13 +163,13 @@ export default apiRoute((app) => {
"query",
z
.object({
- "home[last_read_id]": StatusSchema.shape.id.openapi({
+ "home[last_read_id]": StatusSchema.shape.id.meta({
description:
"ID of the last status read in the home timeline.",
example: "c62aa212-8198-4ce5-a388-2cc8344a84ef",
}),
"notifications[last_read_id]":
- NotificationSchema.shape.id.openapi({
+ NotificationSchema.shape.id.meta({
description: "ID of the last notification read.",
}),
})
diff --git a/packages/api/routes/api/v1/media/[id]/index.ts b/packages/api/routes/api/v1/media/[id]/index.ts
index 7a26c5ec..8c5a9978 100644
--- a/packages/api/routes/api/v1/media/[id]/index.ts
+++ b/packages/api/routes/api/v1/media/[id]/index.ts
@@ -6,9 +6,8 @@ import { config } from "@versia-server/config";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Media } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) => {
app.get(
@@ -106,7 +105,7 @@ export default apiRoute((app) => {
"form",
z
.object({
- thumbnail: z.instanceof(File).openapi({
+ thumbnail: z.file().meta({
description:
"The custom thumbnail of the media to be attached, encoded using multipart form data.",
}),
@@ -114,7 +113,7 @@ export default apiRoute((app) => {
.unwrap()
.max(config.validation.media.max_description_characters)
.optional(),
- focus: z.string().openapi({
+ focus: z.string().meta({
description:
"Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0. Used for media cropping on clients.",
externalDocs: {
diff --git a/packages/api/routes/api/v1/media/index.ts b/packages/api/routes/api/v1/media/index.ts
index 2dcfded2..55f5cb58 100644
--- a/packages/api/routes/api/v1/media/index.ts
+++ b/packages/api/routes/api/v1/media/index.ts
@@ -6,9 +6,8 @@ import { config } from "@versia-server/config";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Media } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.post(
@@ -60,11 +59,11 @@ export default apiRoute((app) =>
validator(
"form",
z.object({
- file: z.instanceof(File).openapi({
+ file: z.file().meta({
description:
"The file to be attached, encoded using multipart form data. The file must have a MIME type.",
}),
- thumbnail: z.instanceof(File).optional().openapi({
+ thumbnail: z.file().optional().meta({
description:
"The custom thumbnail of the media to be attached, encoded using multipart form data.",
}),
@@ -75,7 +74,7 @@ export default apiRoute((app) =>
focus: z
.string()
.optional()
- .openapi({
+ .meta({
description:
"Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0. Used for media cropping on clients.",
externalDocs: {
diff --git a/packages/api/routes/api/v1/mutes/index.ts b/packages/api/routes/api/v1/mutes/index.ts
index 595e9c87..df5e46d4 100644
--- a/packages/api/routes/api/v1/mutes/index.ts
+++ b/packages/api/routes/api/v1/mutes/index.ts
@@ -7,9 +7,8 @@ import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Timeline } from "@versia-server/kit/db";
import { Users } from "@versia-server/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
@@ -33,7 +32,7 @@ export default apiRoute((app) =>
link: z
.string()
.optional()
- .openapi({
+ .meta({
description:
"Links to the next and previous pages",
example:
@@ -56,30 +55,24 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
- max_id: AccountSchema.shape.id.optional().openapi({
+ max_id: AccountSchema.shape.id.optional().meta({
description:
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
}),
- since_id: AccountSchema.shape.id.optional().openapi({
+ since_id: AccountSchema.shape.id.optional().meta({
description:
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
example: undefined,
}),
- min_id: AccountSchema.shape.id.optional().openapi({
+ min_id: AccountSchema.shape.id.optional().meta({
description:
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
example: undefined,
}),
- limit: z.coerce
- .number()
- .int()
- .min(1)
- .max(80)
- .default(40)
- .openapi({
- description: "Maximum number of results to return.",
- }),
+ limit: z.coerce.number().int().min(1).max(80).default(40).meta({
+ description: "Maximum number of results to return.",
+ }),
}),
handleZodError,
),
diff --git a/packages/api/routes/api/v1/notifications/[id]/dismiss.test.ts b/packages/api/routes/api/v1/notifications/[id]/dismiss.test.ts
index 114f00e8..6d12ecd6 100644
--- a/packages/api/routes/api/v1/notifications/[id]/dismiss.test.ts
+++ b/packages/api/routes/api/v1/notifications/[id]/dismiss.test.ts
@@ -1,7 +1,7 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Notification } from "@versia/client/schemas";
import { generateClient, getTestUsers } from "@versia-server/tests";
-import type { z } from "zod";
+import type { z } from "zod/v4";
const { users, deleteUsers } = await getTestUsers(2);
let notifications: z.infer[] = [];
diff --git a/packages/api/routes/api/v1/notifications/[id]/dismiss.ts b/packages/api/routes/api/v1/notifications/[id]/dismiss.ts
index 1da2d616..acf837ac 100644
--- a/packages/api/routes/api/v1/notifications/[id]/dismiss.ts
+++ b/packages/api/routes/api/v1/notifications/[id]/dismiss.ts
@@ -5,9 +5,8 @@ import {
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Notification } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.post(
diff --git a/packages/api/routes/api/v1/notifications/[id]/index.test.ts b/packages/api/routes/api/v1/notifications/[id]/index.test.ts
index e15cd9c0..9428b51f 100644
--- a/packages/api/routes/api/v1/notifications/[id]/index.test.ts
+++ b/packages/api/routes/api/v1/notifications/[id]/index.test.ts
@@ -1,7 +1,7 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import type { Notification } from "@versia/client/schemas";
import { generateClient, getTestUsers } from "@versia-server/tests";
-import type { z } from "zod";
+import type { z } from "zod/v4";
const { users, deleteUsers } = await getTestUsers(2);
let notifications: z.infer[] = [];
diff --git a/packages/api/routes/api/v1/notifications/[id]/index.ts b/packages/api/routes/api/v1/notifications/[id]/index.ts
index d1454b4a..d48b1206 100644
--- a/packages/api/routes/api/v1/notifications/[id]/index.ts
+++ b/packages/api/routes/api/v1/notifications/[id]/index.ts
@@ -5,9 +5,8 @@ import {
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Notification } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/api/v1/notifications/destroy_multiple/index.test.ts b/packages/api/routes/api/v1/notifications/destroy_multiple/index.test.ts
index 6a08e547..558ad1b5 100644
--- a/packages/api/routes/api/v1/notifications/destroy_multiple/index.test.ts
+++ b/packages/api/routes/api/v1/notifications/destroy_multiple/index.test.ts
@@ -5,7 +5,7 @@ import {
getTestStatuses,
getTestUsers,
} from "@versia-server/tests";
-import type { z } from "zod";
+import type { z } from "zod/v4";
const { users, deleteUsers } = await getTestUsers(2);
const statuses = await getTestStatuses(5, users[0]);
diff --git a/packages/api/routes/api/v1/notifications/destroy_multiple/index.ts b/packages/api/routes/api/v1/notifications/destroy_multiple/index.ts
index 6f9995d4..23dd2dba 100644
--- a/packages/api/routes/api/v1/notifications/destroy_multiple/index.ts
+++ b/packages/api/routes/api/v1/notifications/destroy_multiple/index.ts
@@ -6,9 +6,8 @@ import {
handleZodError,
qsQuery,
} from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.delete(
@@ -32,7 +31,7 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
- ids: z.array(z.string().uuid()),
+ ids: z.array(z.uuid()),
}),
handleZodError,
),
diff --git a/packages/api/routes/api/v1/notifications/index.ts b/packages/api/routes/api/v1/notifications/index.ts
index cb1664de..bd4ea34a 100644
--- a/packages/api/routes/api/v1/notifications/index.ts
+++ b/packages/api/routes/api/v1/notifications/index.ts
@@ -9,9 +9,8 @@ import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Timeline } from "@versia-server/kit/db";
import { Notifications } from "@versia-server/kit/tables";
import { and, eq, gt, gte, inArray, lt, not, sql } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
@@ -47,17 +46,17 @@ export default apiRoute((app) =>
"query",
z
.object({
- max_id: NotificationSchema.shape.id.optional().openapi({
+ max_id: NotificationSchema.shape.id.optional().meta({
description:
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
}),
- since_id: NotificationSchema.shape.id.optional().openapi({
+ since_id: NotificationSchema.shape.id.optional().meta({
description:
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
example: undefined,
}),
- min_id: NotificationSchema.shape.id.optional().openapi({
+ min_id: NotificationSchema.shape.id.optional().meta({
description:
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
example: undefined,
@@ -68,27 +67,27 @@ export default apiRoute((app) =>
.min(1)
.max(80)
.default(40)
- .openapi({
+ .meta({
description: "Maximum number of results to return.",
}),
types: z
.array(NotificationSchema.shape.type)
.optional()
- .openapi({
+ .meta({
description: "Types to include in the result.",
}),
exclude_types: z
.array(NotificationSchema.shape.type)
.optional()
- .openapi({
+ .meta({
description: "Types to exclude from the results.",
}),
- account_id: AccountSchema.shape.id.optional().openapi({
+ account_id: AccountSchema.shape.id.optional().meta({
description:
"Return only notifications received from the specified account.",
}),
// TODO: Implement
- include_filtered: zBoolean.default(false).openapi({
+ include_filtered: zBoolean.default(false).meta({
description:
"Whether to include notifications filtered by the user's NotificationPolicy.",
}),
diff --git a/packages/api/routes/api/v1/profile/avatar.ts b/packages/api/routes/api/v1/profile/avatar.ts
index 2607485d..2f9cbb2e 100644
--- a/packages/api/routes/api/v1/profile/avatar.ts
+++ b/packages/api/routes/api/v1/profile/avatar.ts
@@ -1,8 +1,7 @@
import { Account, RolePermission } from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth } from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.delete(
diff --git a/packages/api/routes/api/v1/profile/header.ts b/packages/api/routes/api/v1/profile/header.ts
index 9be52065..630f89cd 100644
--- a/packages/api/routes/api/v1/profile/header.ts
+++ b/packages/api/routes/api/v1/profile/header.ts
@@ -1,8 +1,7 @@
import { Account, RolePermission } from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth } from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.delete(
diff --git a/packages/api/routes/api/v1/push/subscription/index.delete.ts b/packages/api/routes/api/v1/push/subscription/index.delete.ts
index 58744ebf..7661aa74 100644
--- a/packages/api/routes/api/v1/push/subscription/index.delete.ts
+++ b/packages/api/routes/api/v1/push/subscription/index.delete.ts
@@ -2,9 +2,8 @@ import { RolePermission } from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth } from "@versia-server/kit/api";
import { PushSubscription } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.delete(
diff --git a/packages/api/routes/api/v1/push/subscription/index.get.ts b/packages/api/routes/api/v1/push/subscription/index.get.ts
index 0ab18633..8b4e3b7f 100644
--- a/packages/api/routes/api/v1/push/subscription/index.get.ts
+++ b/packages/api/routes/api/v1/push/subscription/index.get.ts
@@ -5,8 +5,7 @@ import {
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth } from "@versia-server/kit/api";
import { PushSubscription } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/api/v1/push/subscription/index.post.ts b/packages/api/routes/api/v1/push/subscription/index.post.ts
index 3b41af90..cdd7fbac 100644
--- a/packages/api/routes/api/v1/push/subscription/index.post.ts
+++ b/packages/api/routes/api/v1/push/subscription/index.post.ts
@@ -12,8 +12,7 @@ import {
} from "@versia-server/kit/api";
import { PushSubscription } from "@versia-server/kit/db";
import { randomUUIDv7 } from "bun";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
export default apiRoute((app) =>
app.post(
diff --git a/packages/api/routes/api/v1/push/subscription/index.put.ts b/packages/api/routes/api/v1/push/subscription/index.put.ts
index e2735065..54d5a5a0 100644
--- a/packages/api/routes/api/v1/push/subscription/index.put.ts
+++ b/packages/api/routes/api/v1/push/subscription/index.put.ts
@@ -11,8 +11,7 @@ import {
jsonOrForm,
} from "@versia-server/kit/api";
import { PushSubscription } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
export default apiRoute((app) =>
app.put(
diff --git a/packages/api/routes/api/v1/roles/[id]/index.ts b/packages/api/routes/api/v1/roles/[id]/index.ts
index 92eb119e..c642d82e 100644
--- a/packages/api/routes/api/v1/roles/[id]/index.ts
+++ b/packages/api/routes/api/v1/roles/[id]/index.ts
@@ -2,9 +2,8 @@ import { RolePermission, Role as RoleSchema } from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Role } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) => {
app.get(
@@ -28,7 +27,7 @@ export default apiRoute((app) => {
auth({
auth: true,
}),
- validator("param", z.object({ id: z.string().uuid() }), handleZodError),
+ validator("param", z.object({ id: z.uuid() }), handleZodError),
async (context) => {
const { id } = context.req.valid("param");
@@ -62,7 +61,7 @@ export default apiRoute((app) => {
validator(
"param",
z.object({
- id: z.string().uuid(),
+ id: z.uuid(),
}),
handleZodError,
),
@@ -118,7 +117,7 @@ export default apiRoute((app) => {
}
await role.update({
- permissions: permissions as unknown as RolePermission[],
+ permissions,
priority,
description,
icon,
@@ -150,7 +149,7 @@ export default apiRoute((app) => {
validator(
"param",
z.object({
- id: z.string().uuid(),
+ id: z.uuid(),
}),
handleZodError,
),
diff --git a/packages/api/routes/api/v1/roles/index.ts b/packages/api/routes/api/v1/roles/index.ts
index a93cadf5..4c71eb4f 100644
--- a/packages/api/routes/api/v1/roles/index.ts
+++ b/packages/api/routes/api/v1/roles/index.ts
@@ -3,9 +3,8 @@ import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Role } from "@versia-server/kit/db";
import { randomUUIDv7 } from "bun";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) => {
app.get(
diff --git a/packages/api/routes/api/v1/statuses/[id]/context.ts b/packages/api/routes/api/v1/statuses/[id]/context.ts
index cccac02f..ec6b3fe1 100644
--- a/packages/api/routes/api/v1/statuses/[id]/context.ts
+++ b/packages/api/routes/api/v1/statuses/[id]/context.ts
@@ -4,8 +4,7 @@ import {
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withNoteParam } from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/api/v1/statuses/[id]/favourite.ts b/packages/api/routes/api/v1/statuses/[id]/favourite.ts
index 535a9e30..120e7c49 100644
--- a/packages/api/routes/api/v1/statuses/[id]/favourite.ts
+++ b/packages/api/routes/api/v1/statuses/[id]/favourite.ts
@@ -1,8 +1,7 @@
import { RolePermission, Status as StatusSchema } from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withNoteParam } from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.post(
diff --git a/packages/api/routes/api/v1/statuses/[id]/favourited_by.ts b/packages/api/routes/api/v1/statuses/[id]/favourited_by.ts
index beee6ff4..70c501b0 100644
--- a/packages/api/routes/api/v1/statuses/[id]/favourited_by.ts
+++ b/packages/api/routes/api/v1/statuses/[id]/favourited_by.ts
@@ -12,9 +12,8 @@ import {
import { Timeline } from "@versia-server/kit/db";
import { Users } from "@versia-server/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
@@ -38,7 +37,7 @@ export default apiRoute((app) =>
link: z
.string()
.optional()
- .openapi({
+ .meta({
description:
"Links to the next and previous pages",
example:
@@ -65,30 +64,24 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
- max_id: AccountSchema.shape.id.optional().openapi({
+ max_id: AccountSchema.shape.id.optional().meta({
description:
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
}),
- since_id: AccountSchema.shape.id.optional().openapi({
+ since_id: AccountSchema.shape.id.optional().meta({
description:
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
example: undefined,
}),
- min_id: AccountSchema.shape.id.optional().openapi({
+ min_id: AccountSchema.shape.id.optional().meta({
description:
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
example: undefined,
}),
- limit: z.coerce
- .number()
- .int()
- .min(1)
- .max(80)
- .default(40)
- .openapi({
- description: "Maximum number of results to return.",
- }),
+ limit: z.coerce.number().int().min(1).max(80).default(40).meta({
+ description: "Maximum number of results to return.",
+ }),
}),
handleZodError,
),
diff --git a/packages/api/routes/api/v1/statuses/[id]/index.ts b/packages/api/routes/api/v1/statuses/[id]/index.ts
index 37f3278b..03bafe88 100644
--- a/packages/api/routes/api/v1/statuses/[id]/index.ts
+++ b/packages/api/routes/api/v1/statuses/[id]/index.ts
@@ -21,9 +21,8 @@ import {
parseMentionsFromText,
versiaTextToHtml,
} from "@versia-server/kit/parsers";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
import { sanitizedHtmlStrip } from "@/sanitization";
const schema = z
@@ -38,7 +37,7 @@ const schema = z
"Status contains blocked words",
)
.optional()
- .openapi({
+ .meta({
description:
"The text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided.",
}),
@@ -46,7 +45,7 @@ const schema = z
content_type: z
.enum(["text/plain", "text/html", "text/markdown"])
.default("text/plain")
- .openapi({
+ .meta({
description: "Content-Type of the status text.",
example: "text/markdown",
}),
@@ -54,15 +53,15 @@ const schema = z
.array(AttachmentSchema.shape.id)
.max(config.validation.notes.max_attachments)
.default([])
- .openapi({
+ .meta({
description:
"Include Attachment IDs to be attached as media. If provided, status becomes optional, and poll cannot be used.",
}),
- spoiler_text: StatusSourceSchema.shape.spoiler_text.optional().openapi({
+ spoiler_text: StatusSourceSchema.shape.spoiler_text.optional().meta({
description:
"Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field.",
}),
- sensitive: zBoolean.default(false).openapi({
+ sensitive: zBoolean.default(false).meta({
description: "Mark status and attached media as sensitive?",
}),
language: StatusSchema.shape.language.optional(),
@@ -74,7 +73,7 @@ const schema = z
)
.max(config.validation.polls.max_options)
.optional()
- .openapi({
+ .meta({
description:
"Possible answers to the poll. If provided, media_ids cannot be used, and poll[expires_in] must be provided.",
}),
@@ -84,14 +83,14 @@ const schema = z
.min(config.validation.polls.min_duration_seconds)
.max(config.validation.polls.max_duration_seconds)
.optional()
- .openapi({
+ .meta({
description:
"Duration that the poll should be open, in seconds. If provided, media_ids cannot be used, and poll[options] must be provided.",
}),
- "poll[multiple]": zBoolean.optional().openapi({
+ "poll[multiple]": zBoolean.optional().meta({
description: "Allow multiple choices?",
}),
- "poll[hide_totals]": zBoolean.optional().openapi({
+ "poll[hide_totals]": zBoolean.optional().meta({
description: "Hide vote counts until the poll ends?",
}),
})
diff --git a/packages/api/routes/api/v1/statuses/[id]/pin.ts b/packages/api/routes/api/v1/statuses/[id]/pin.ts
index 345b3496..cc9f832b 100644
--- a/packages/api/routes/api/v1/statuses/[id]/pin.ts
+++ b/packages/api/routes/api/v1/statuses/[id]/pin.ts
@@ -3,8 +3,7 @@ import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withNoteParam } from "@versia-server/kit/api";
import { db } from "@versia-server/kit/db";
import { and, eq, type SQL } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.post(
diff --git a/packages/api/routes/api/v1/statuses/[id]/reactions/[name].ts b/packages/api/routes/api/v1/statuses/[id]/reactions/[name].ts
index 6ced1821..5130502a 100644
--- a/packages/api/routes/api/v1/statuses/[id]/reactions/[name].ts
+++ b/packages/api/routes/api/v1/statuses/[id]/reactions/[name].ts
@@ -9,12 +9,11 @@ import {
import { Emoji } from "@versia-server/kit/db";
import { Emojis } from "@versia-server/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
import emojis from "unicode-emoji-json/data-ordered-emoji.json" with {
type: "json",
};
-import { z } from "zod";
+import { z } from "zod/v4";
export default apiRoute((app) => {
app.put(
diff --git a/packages/api/routes/api/v1/statuses/[id]/reactions/index.ts b/packages/api/routes/api/v1/statuses/[id]/reactions/index.ts
index fe3b8060..8436dd5b 100644
--- a/packages/api/routes/api/v1/statuses/[id]/reactions/index.ts
+++ b/packages/api/routes/api/v1/statuses/[id]/reactions/index.ts
@@ -4,9 +4,8 @@ import {
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withNoteParam } from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/api/v1/statuses/[id]/reblog.ts b/packages/api/routes/api/v1/statuses/[id]/reblog.ts
index 66e8a8c8..5325ce46 100644
--- a/packages/api/routes/api/v1/statuses/[id]/reblog.ts
+++ b/packages/api/routes/api/v1/statuses/[id]/reblog.ts
@@ -6,9 +6,8 @@ import {
jsonOrForm,
withNoteParam,
} from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.post(
diff --git a/packages/api/routes/api/v1/statuses/[id]/reblogged_by.ts b/packages/api/routes/api/v1/statuses/[id]/reblogged_by.ts
index 4f9cbcbd..12ad42c3 100644
--- a/packages/api/routes/api/v1/statuses/[id]/reblogged_by.ts
+++ b/packages/api/routes/api/v1/statuses/[id]/reblogged_by.ts
@@ -12,9 +12,8 @@ import {
import { Timeline } from "@versia-server/kit/db";
import { Users } from "@versia-server/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
@@ -38,7 +37,7 @@ export default apiRoute((app) =>
link: z
.string()
.optional()
- .openapi({
+ .meta({
description:
"Links to the next and previous pages",
example:
@@ -65,30 +64,24 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
- max_id: AccountSchema.shape.id.optional().openapi({
+ max_id: AccountSchema.shape.id.optional().meta({
description:
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
}),
- since_id: AccountSchema.shape.id.optional().openapi({
+ since_id: AccountSchema.shape.id.optional().meta({
description:
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
example: undefined,
}),
- min_id: AccountSchema.shape.id.optional().openapi({
+ min_id: AccountSchema.shape.id.optional().meta({
description:
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
example: undefined,
}),
- limit: z.coerce
- .number()
- .int()
- .min(1)
- .max(80)
- .default(40)
- .openapi({
- description: "Maximum number of results to return.",
- }),
+ limit: z.coerce.number().int().min(1).max(80).default(40).meta({
+ description: "Maximum number of results to return.",
+ }),
}),
handleZodError,
),
diff --git a/packages/api/routes/api/v1/statuses/[id]/source.ts b/packages/api/routes/api/v1/statuses/[id]/source.ts
index 8e19789d..8f82b24c 100644
--- a/packages/api/routes/api/v1/statuses/[id]/source.ts
+++ b/packages/api/routes/api/v1/statuses/[id]/source.ts
@@ -4,8 +4,7 @@ import {
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withNoteParam } from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/api/v1/statuses/[id]/unfavourite.ts b/packages/api/routes/api/v1/statuses/[id]/unfavourite.ts
index 5c016461..49837b2f 100644
--- a/packages/api/routes/api/v1/statuses/[id]/unfavourite.ts
+++ b/packages/api/routes/api/v1/statuses/[id]/unfavourite.ts
@@ -1,8 +1,7 @@
import { RolePermission, Status as StatusSchema } from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withNoteParam } from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.post(
diff --git a/packages/api/routes/api/v1/statuses/[id]/unpin.ts b/packages/api/routes/api/v1/statuses/[id]/unpin.ts
index af56409a..3e219710 100644
--- a/packages/api/routes/api/v1/statuses/[id]/unpin.ts
+++ b/packages/api/routes/api/v1/statuses/[id]/unpin.ts
@@ -1,8 +1,7 @@
import { RolePermission, Status as StatusSchema } from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withNoteParam } from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.post(
diff --git a/packages/api/routes/api/v1/statuses/[id]/unreblog.ts b/packages/api/routes/api/v1/statuses/[id]/unreblog.ts
index b19ed7af..89bcc1f6 100644
--- a/packages/api/routes/api/v1/statuses/[id]/unreblog.ts
+++ b/packages/api/routes/api/v1/statuses/[id]/unreblog.ts
@@ -2,8 +2,7 @@ import { RolePermission, Status as StatusSchema } from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withNoteParam } from "@versia-server/kit/api";
import { Note } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
export default apiRoute((app) =>
app.post(
diff --git a/packages/api/routes/api/v1/statuses/index.test.ts b/packages/api/routes/api/v1/statuses/index.test.ts
index 25572129..52f2c633 100644
--- a/packages/api/routes/api/v1/statuses/index.test.ts
+++ b/packages/api/routes/api/v1/statuses/index.test.ts
@@ -6,7 +6,7 @@ import { Emojis } from "@versia-server/kit/tables";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { randomUUIDv7 } from "bun";
import { eq } from "drizzle-orm";
-import type { z } from "zod";
+import type { z } from "zod/v4";
const { users, deleteUsers } = await getTestUsers(5);
let media: Media;
@@ -87,7 +87,7 @@ describe("/api/v1/statuses", () => {
expect(raw.status).toBe(422);
expect(data).toMatchObject({
error: expect.stringContaining(
- "must be at least 5 minutes in the future",
+ "Must be at least 5 minutes in the future",
),
});
});
diff --git a/packages/api/routes/api/v1/statuses/index.ts b/packages/api/routes/api/v1/statuses/index.ts
index cb822be1..7b2a52ca 100644
--- a/packages/api/routes/api/v1/statuses/index.ts
+++ b/packages/api/routes/api/v1/statuses/index.ts
@@ -21,9 +21,8 @@ import {
versiaTextToHtml,
} from "@versia-server/kit/parsers";
import { randomUUIDv7 } from "bun";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
import { sanitizedHtmlStrip } from "@/sanitization";
const schema = z
@@ -38,7 +37,7 @@ const schema = z
"Status contains blocked words",
)
.optional()
- .openapi({
+ .meta({
description:
"The text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided.",
}),
@@ -46,7 +45,7 @@ const schema = z
content_type: z
.enum(["text/plain", "text/html", "text/markdown"])
.default("text/plain")
- .openapi({
+ .meta({
description: "Content-Type of the status text.",
example: "text/markdown",
}),
@@ -54,15 +53,15 @@ const schema = z
.array(AttachmentSchema.shape.id)
.max(config.validation.notes.max_attachments)
.default([])
- .openapi({
+ .meta({
description:
"Include Attachment IDs to be attached as media. If provided, status becomes optional, and poll cannot be used.",
}),
- spoiler_text: StatusSourceSchema.shape.spoiler_text.optional().openapi({
+ spoiler_text: StatusSourceSchema.shape.spoiler_text.optional().meta({
description:
"Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field.",
}),
- sensitive: zBoolean.default(false).openapi({
+ sensitive: zBoolean.default(false).meta({
description: "Mark status and attached media as sensitive?",
}),
language: StatusSchema.shape.language.optional(),
@@ -74,7 +73,7 @@ const schema = z
)
.max(config.validation.polls.max_options)
.optional()
- .openapi({
+ .meta({
description:
"Possible answers to the poll. If provided, media_ids cannot be used, and poll[expires_in] must be provided.",
}),
@@ -84,39 +83,43 @@ const schema = z
.min(config.validation.polls.min_duration_seconds)
.max(config.validation.polls.max_duration_seconds)
.optional()
- .openapi({
+ .meta({
description:
"Duration that the poll should be open, in seconds. If provided, media_ids cannot be used, and poll[options] must be provided.",
}),
- "poll[multiple]": zBoolean.optional().openapi({
+ "poll[multiple]": zBoolean.optional().meta({
description: "Allow multiple choices?",
}),
- "poll[hide_totals]": zBoolean.optional().openapi({
+ "poll[hide_totals]": zBoolean.optional().meta({
description: "Hide vote counts until the poll ends?",
}),
- in_reply_to_id: StatusSchema.shape.id.optional().nullable().openapi({
+ in_reply_to_id: StatusSchema.shape.id.optional().nullable().meta({
description:
"ID of the status being replied to, if status is a reply.",
}),
/* Versia Server API Extension */
- quote_id: StatusSchema.shape.id.optional().nullable().openapi({
+ quote_id: StatusSchema.shape.id.optional().nullable().meta({
description: "ID of the status being quoted, if status is a quote.",
}),
visibility: StatusSchema.shape.visibility.default("public"),
- scheduled_at: z.coerce
- .date()
- .min(
- new Date(Date.now() + 5 * 60 * 1000),
- "must be at least 5 minutes in the future.",
+ scheduled_at: z.iso
+ .datetime()
+ .refine(
+ (date) =>
+ new Date(date).getTime() >=
+ new Date(Date.now() + 5 * 60 * 1000).getTime(),
+ {
+ message: "must be at least 5 minutes in the future.",
+ },
)
.optional()
.nullable()
- .openapi({
+ .meta({
description:
"Datetime at which to schedule a status. Providing this parameter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future.",
}),
/* Versia Server API Extension */
- local_only: zBoolean.default(false).openapi({
+ local_only: zBoolean.default(false).meta({
description: "If true, this status will not be federated.",
}),
})
diff --git a/packages/api/routes/api/v1/timelines/home.ts b/packages/api/routes/api/v1/timelines/home.ts
index b580c534..a56bb7ca 100644
--- a/packages/api/routes/api/v1/timelines/home.ts
+++ b/packages/api/routes/api/v1/timelines/home.ts
@@ -4,9 +4,8 @@ import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Timeline } from "@versia-server/kit/db";
import { Notes } from "@versia-server/kit/tables";
import { and, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
@@ -31,7 +30,7 @@ export default apiRoute((app) =>
link: z
.string()
.optional()
- .openapi({
+ .meta({
description:
"Links to the next and previous pages",
example:
@@ -57,30 +56,24 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
- max_id: StatusSchema.shape.id.optional().openapi({
+ max_id: StatusSchema.shape.id.optional().meta({
description:
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
}),
- since_id: StatusSchema.shape.id.optional().openapi({
+ since_id: StatusSchema.shape.id.optional().meta({
description:
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
example: undefined,
}),
- min_id: StatusSchema.shape.id.optional().openapi({
+ min_id: StatusSchema.shape.id.optional().meta({
description:
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
example: undefined,
}),
- limit: z.coerce
- .number()
- .int()
- .min(1)
- .max(40)
- .default(20)
- .openapi({
- description: "Maximum number of results to return.",
- }),
+ limit: z.coerce.number().int().min(1).max(40).default(20).meta({
+ description: "Maximum number of results to return.",
+ }),
}),
handleZodError,
),
diff --git a/packages/api/routes/api/v1/timelines/public.ts b/packages/api/routes/api/v1/timelines/public.ts
index 013028f1..8224fa0e 100644
--- a/packages/api/routes/api/v1/timelines/public.ts
+++ b/packages/api/routes/api/v1/timelines/public.ts
@@ -8,9 +8,8 @@ import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Timeline } from "@versia-server/kit/db";
import { Notes } from "@versia-server/kit/tables";
import { and, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
@@ -34,7 +33,7 @@ export default apiRoute((app) =>
link: z
.string()
.optional()
- .openapi({
+ .meta({
description:
"Links to the next and previous pages",
example:
@@ -60,28 +59,28 @@ export default apiRoute((app) =>
"query",
z
.object({
- max_id: StatusSchema.shape.id.optional().openapi({
+ max_id: StatusSchema.shape.id.optional().meta({
description:
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
}),
- since_id: StatusSchema.shape.id.optional().openapi({
+ since_id: StatusSchema.shape.id.optional().meta({
description:
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
example: undefined,
}),
- min_id: StatusSchema.shape.id.optional().openapi({
+ min_id: StatusSchema.shape.id.optional().meta({
description:
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
example: undefined,
}),
- local: zBoolean.default(false).openapi({
+ local: zBoolean.default(false).meta({
description: "Show only local statuses?",
}),
- remote: zBoolean.default(false).openapi({
+ remote: zBoolean.default(false).meta({
description: "Show only remote statuses?",
}),
- only_media: zBoolean.default(false).openapi({
+ only_media: zBoolean.default(false).meta({
description: "Show only statuses with media attached?",
}),
limit: z.coerce
@@ -90,7 +89,7 @@ export default apiRoute((app) =>
.min(1)
.max(40)
.default(20)
- .openapi({
+ .meta({
description: "Maximum number of results to return.",
}),
})
diff --git a/packages/api/routes/api/v2/filters/[id]/index.ts b/packages/api/routes/api/v2/filters/[id]/index.ts
index a43f0e05..da296aee 100644
--- a/packages/api/routes/api/v2/filters/[id]/index.ts
+++ b/packages/api/routes/api/v2/filters/[id]/index.ts
@@ -14,9 +14,8 @@ import {
import { db } from "@versia-server/kit/db";
import { FilterKeywords, Filters } from "@versia-server/kit/tables";
import { and, eq, inArray, type SQL } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) => {
app.get(
@@ -145,7 +144,7 @@ export default apiRoute((app) => {
.int()
.min(60)
.max(60 * 60 * 24 * 365 * 5)
- .openapi({
+ .meta({
description:
"How many seconds from now should the filter expire?",
}),
@@ -157,7 +156,7 @@ export default apiRoute((app) => {
})
.extend({
// biome-ignore lint/style/useNamingConvention: _destroy is a Mastodon API imposed variable name
- _destroy: zBoolean.default(false).openapi({
+ _destroy: zBoolean.default(false).meta({
description:
"If true, will remove the keyword with the given ID.",
}),
diff --git a/packages/api/routes/api/v2/filters/index.ts b/packages/api/routes/api/v2/filters/index.ts
index b55a02c4..fe96d954 100644
--- a/packages/api/routes/api/v2/filters/index.ts
+++ b/packages/api/routes/api/v2/filters/index.ts
@@ -14,9 +14,8 @@ import { db } from "@versia-server/kit/db";
import { FilterKeywords, Filters } from "@versia-server/kit/tables";
import { randomUUIDv7 } from "bun";
import { eq, type SQL } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) => {
app.get(
@@ -115,7 +114,7 @@ export default apiRoute((app) => {
.min(60)
.max(60 * 60 * 24 * 365 * 5)
.optional()
- .openapi({
+ .meta({
description:
"How many seconds from now should the filter expire?",
}),
diff --git a/packages/api/routes/api/v2/instance/index.ts b/packages/api/routes/api/v2/instance/index.ts
index 8f65cc6f..c464bc53 100644
--- a/packages/api/routes/api/v2/instance/index.ts
+++ b/packages/api/routes/api/v2/instance/index.ts
@@ -4,8 +4,7 @@ import { apiRoute } from "@versia-server/kit/api";
import { User } from "@versia-server/kit/db";
import { Users } from "@versia-server/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
import pkg from "../../../../../../package.json" with { type: "json" };
export default apiRoute((app) =>
diff --git a/packages/api/routes/api/v2/media/index.ts b/packages/api/routes/api/v2/media/index.ts
index 0c658805..5388e58a 100644
--- a/packages/api/routes/api/v2/media/index.ts
+++ b/packages/api/routes/api/v2/media/index.ts
@@ -6,9 +6,8 @@ import { config } from "@versia-server/config";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Media } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.post(
@@ -69,11 +68,11 @@ export default apiRoute((app) =>
validator(
"form",
z.object({
- file: z.instanceof(File).openapi({
+ file: z.file().meta({
description:
"The file to be attached, encoded using multipart form data. The file must have a MIME type.",
}),
- thumbnail: z.instanceof(File).optional().openapi({
+ thumbnail: z.file().optional().meta({
description:
"The custom thumbnail of the media to be attached, encoded using multipart form data.",
}),
@@ -84,7 +83,7 @@ export default apiRoute((app) =>
focus: z
.string()
.optional()
- .openapi({
+ .meta({
description:
"Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0. Used for media cropping on clients.",
externalDocs: {
diff --git a/packages/api/routes/api/v2/search/index.ts b/packages/api/routes/api/v2/search/index.ts
index da2eb7a4..cf679e08 100644
--- a/packages/api/routes/api/v2/search/index.ts
+++ b/packages/api/routes/api/v2/search/index.ts
@@ -14,9 +14,8 @@ import { parseUserAddress } from "@versia-server/kit/parsers";
import { searchManager } from "@versia-server/kit/search";
import { Instances, Notes, Users } from "@versia-server/kit/tables";
import { and, eq, inArray, isNull, sql } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
@@ -59,59 +58,53 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
- q: z.string().trim().openapi({
+ q: z.string().trim().meta({
description: "The search query.",
example: "versia",
}),
type: z
.enum(["accounts", "hashtags", "statuses"])
.optional()
- .openapi({
+ .meta({
description:
"Specify whether to search for only accounts, hashtags, statuses",
example: "accounts",
}),
- resolve: zBoolean.default(false).openapi({
+ resolve: zBoolean.default(false).meta({
description:
"Only relevant if type includes accounts. If true and (a) the search query is for a remote account (e.g., someaccount@someother.server) and (b) the local server does not know about the account, WebFinger is used to try and resolve the account at someother.server. This provides the best recall at higher latency. If false only accounts the server knows about are returned.",
}),
- following: zBoolean.default(false).openapi({
+ following: zBoolean.default(false).meta({
description:
"Only include accounts that the user is following?",
}),
- account_id: AccountSchema.shape.id.optional().openapi({
+ account_id: AccountSchema.shape.id.optional().meta({
description:
" If provided, will only return statuses authored by this account.",
}),
- exclude_unreviewed: zBoolean.default(false).openapi({
+ exclude_unreviewed: zBoolean.default(false).meta({
description:
"Filter out unreviewed tags? Use true when trying to find trending tags.",
}),
- max_id: Id.optional().openapi({
+ max_id: Id.optional().meta({
description:
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
}),
- since_id: Id.optional().openapi({
+ since_id: Id.optional().meta({
description:
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
example: undefined,
}),
- min_id: Id.optional().openapi({
+ min_id: Id.optional().meta({
description:
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
example: undefined,
}),
- limit: z.coerce
- .number()
- .int()
- .min(1)
- .max(40)
- .default(20)
- .openapi({
- description: "Maximum number of results to return.",
- }),
- offset: z.coerce.number().int().min(0).default(0).openapi({
+ limit: z.coerce.number().int().min(1).max(40).default(20).meta({
+ description: "Maximum number of results to return.",
+ }),
+ offset: z.coerce.number().int().min(0).default(0).meta({
description: "Skip the first n results.",
}),
}),
diff --git a/packages/api/routes/inbox/index.ts b/packages/api/routes/inbox/index.ts
index 248a49e3..ffcc0266 100644
--- a/packages/api/routes/inbox/index.ts
+++ b/packages/api/routes/inbox/index.ts
@@ -1,8 +1,7 @@
import { apiRoute, handleZodError } from "@versia-server/kit/api";
import { InboxJobType, inboxQueue } from "@versia-server/kit/queues/inbox";
-import { describeRoute } from "hono-openapi";
-import { validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.post(
@@ -22,7 +21,6 @@ export default apiRoute((app) =>
"versia-signature": z.string().optional(),
"versia-signed-at": z.coerce.number().optional(),
"versia-signed-by": z
- .string()
.url()
.or(z.string().startsWith("instance "))
.optional(),
diff --git a/packages/api/routes/likes/[uuid]/index.ts b/packages/api/routes/likes/[uuid]/index.ts
index 1da1b344..51e67065 100644
--- a/packages/api/routes/likes/[uuid]/index.ts
+++ b/packages/api/routes/likes/[uuid]/index.ts
@@ -6,9 +6,8 @@ import { apiRoute, handleZodError } from "@versia-server/kit/api";
import { Like, User } from "@versia-server/kit/db";
import { Likes } from "@versia-server/kit/tables";
import { and, eq, sql } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/media/[hash]/[name]/index.ts b/packages/api/routes/media/[hash]/[name]/index.ts
index 9581b2cb..071bb517 100644
--- a/packages/api/routes/media/[hash]/[name]/index.ts
+++ b/packages/api/routes/media/[hash]/[name]/index.ts
@@ -1,9 +1,8 @@
import { ApiError } from "@versia-server/kit";
import { apiRoute, handleZodError } from "@versia-server/kit/api";
import { file as bunFile } from "bun";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/media/proxy/[id].ts b/packages/api/routes/media/proxy/[id].ts
index 0757d040..3d3128a3 100644
--- a/packages/api/routes/media/proxy/[id].ts
+++ b/packages/api/routes/media/proxy/[id].ts
@@ -3,9 +3,8 @@ import { ApiError } from "@versia-server/kit";
import { apiRoute, handleZodError } from "@versia-server/kit/api";
import { proxy } from "hono/proxy";
import type { ContentfulStatusCode, StatusCode } from "hono/utils/http-status";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
@@ -34,19 +33,19 @@ export default apiRoute((app) =>
validator(
"param",
z.object({
- id: z
- .string()
- .transform((val) =>
- Buffer.from(val, "base64url").toString(),
- ),
+ id: z.base64url().meta({
+ description: "Base64url encoded URL to proxy",
+ type: "string",
+ }),
}),
handleZodError,
),
async (context) => {
const { id } = context.req.valid("param");
+ const url = Buffer.from(id, "base64url").toString();
// Check if URL is valid
- if (!URL.canParse(id)) {
+ if (!URL.canParse(url)) {
throw new ApiError(
400,
"Invalid URL",
@@ -54,7 +53,7 @@ export default apiRoute((app) =>
);
}
- const media = await proxy(id, {
+ const media = await proxy(url, {
// @ts-expect-error Proxy is a Bun-specific feature
proxy: config.http.proxy_address,
});
@@ -63,7 +62,7 @@ export default apiRoute((app) =>
// Cloudflare R2 serves those as application/xml
if (
media.headers.get("Content-Type") === "application/xml" &&
- id.endsWith(".svg")
+ url.endsWith(".svg")
) {
media.headers.set("Content-Type", "image/svg+xml");
}
@@ -71,7 +70,7 @@ export default apiRoute((app) =>
const realFilename =
media.headers
.get("Content-Disposition")
- ?.match(/filename="(.+)"/)?.[1] || id.split("/").pop();
+ ?.match(/filename="(.+)"/)?.[1] || url.split("/").pop();
if (!media.body) {
return context.body(null, media.status as StatusCode);
diff --git a/packages/api/routes/notes/[uuid]/index.ts b/packages/api/routes/notes/[uuid]/index.ts
index b6ba96da..a538d0af 100644
--- a/packages/api/routes/notes/[uuid]/index.ts
+++ b/packages/api/routes/notes/[uuid]/index.ts
@@ -6,9 +6,8 @@ import { apiRoute, handleZodError } from "@versia-server/kit/api";
import { Note } from "@versia-server/kit/db";
import { Notes } from "@versia-server/kit/tables";
import { and, eq, inArray } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/notes/[uuid]/quotes.ts b/packages/api/routes/notes/[uuid]/quotes.ts
index e91c0bfb..0ebfee27 100644
--- a/packages/api/routes/notes/[uuid]/quotes.ts
+++ b/packages/api/routes/notes/[uuid]/quotes.ts
@@ -7,9 +7,8 @@ import { apiRoute, handleZodError } from "@versia-server/kit/api";
import { db, Note } from "@versia-server/kit/db";
import { Notes } from "@versia-server/kit/tables";
import { and, eq, inArray } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/notes/[uuid]/replies.ts b/packages/api/routes/notes/[uuid]/replies.ts
index 8a755a0c..88cdb2f9 100644
--- a/packages/api/routes/notes/[uuid]/replies.ts
+++ b/packages/api/routes/notes/[uuid]/replies.ts
@@ -7,9 +7,8 @@ import { apiRoute, handleZodError } from "@versia-server/kit/api";
import { db, Note } from "@versia-server/kit/db";
import { Notes } from "@versia-server/kit/tables";
import { and, eq, inArray } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/notes/[uuid]/shares.ts b/packages/api/routes/notes/[uuid]/shares.ts
index 72ed8362..ec34cb56 100644
--- a/packages/api/routes/notes/[uuid]/shares.ts
+++ b/packages/api/routes/notes/[uuid]/shares.ts
@@ -7,9 +7,8 @@ import { apiRoute, handleZodError } from "@versia-server/kit/api";
import { db, Note } from "@versia-server/kit/db";
import { Notes } from "@versia-server/kit/tables";
import { and, eq, inArray } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/oauth.test.ts b/packages/api/routes/oauth.test.ts
index e881f460..ec96a0bc 100644
--- a/packages/api/routes/oauth.test.ts
+++ b/packages/api/routes/oauth.test.ts
@@ -5,7 +5,7 @@ import {
generateClient,
getTestUsers,
} from "@versia-server/tests";
-import type { z } from "zod";
+import type { z } from "zod/v4";
let clientId: string;
let clientSecret: string;
diff --git a/packages/api/routes/shares/[uuid]/index.ts b/packages/api/routes/shares/[uuid]/index.ts
index 9b2f6dc6..ecb500cb 100644
--- a/packages/api/routes/shares/[uuid]/index.ts
+++ b/packages/api/routes/shares/[uuid]/index.ts
@@ -6,9 +6,8 @@ import { apiRoute, handleZodError } from "@versia-server/kit/api";
import { Note } from "@versia-server/kit/db";
import { Notes } from "@versia-server/kit/tables";
import { and, eq, inArray } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/users/[uuid]/inbox/index.ts b/packages/api/routes/users/[uuid]/inbox/index.ts
index 04f0adab..2456ee0b 100644
--- a/packages/api/routes/users/[uuid]/inbox/index.ts
+++ b/packages/api/routes/users/[uuid]/inbox/index.ts
@@ -1,9 +1,8 @@
import { ApiError } from "@versia-server/kit";
import { apiRoute, handleZodError } from "@versia-server/kit/api";
import { InboxJobType, inboxQueue } from "@versia-server/kit/queues/inbox";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.post(
@@ -68,7 +67,7 @@ export default apiRoute((app) =>
validator(
"param",
z.object({
- uuid: z.string().uuid(),
+ uuid: z.uuid(),
}),
handleZodError,
),
@@ -78,7 +77,6 @@ export default apiRoute((app) =>
"versia-signature": z.string().optional(),
"versia-signed-at": z.coerce.number().optional(),
"versia-signed-by": z
- .string()
.url()
.or(z.string().startsWith("instance "))
.optional(),
diff --git a/packages/api/routes/users/[uuid]/index.ts b/packages/api/routes/users/[uuid]/index.ts
index 1bf4bd12..773632b8 100644
--- a/packages/api/routes/users/[uuid]/index.ts
+++ b/packages/api/routes/users/[uuid]/index.ts
@@ -2,9 +2,8 @@ import { UserSchema } from "@versia/sdk/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, handleZodError } from "@versia-server/kit/api";
import { User } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
@@ -39,7 +38,7 @@ export default apiRoute((app) =>
validator(
"param",
z.object({
- uuid: z.string().uuid(),
+ uuid: z.uuid(),
}),
handleZodError,
),
diff --git a/packages/api/routes/users/[uuid]/outbox/index.ts b/packages/api/routes/users/[uuid]/outbox/index.ts
index a30d7603..bd49b1b3 100644
--- a/packages/api/routes/users/[uuid]/outbox/index.ts
+++ b/packages/api/routes/users/[uuid]/outbox/index.ts
@@ -6,9 +6,8 @@ import { apiRoute, handleZodError } from "@versia-server/kit/api";
import { db, Note, User } from "@versia-server/kit/db";
import { Notes } from "@versia-server/kit/tables";
import { and, eq, inArray } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
const NOTES_PER_PAGE = 20;
@@ -52,7 +51,7 @@ export default apiRoute((app) =>
validator(
"param",
z.object({
- uuid: z.string().uuid(),
+ uuid: z.uuid(),
}),
handleZodError,
),
diff --git a/packages/api/routes/well-known/host-meta/index.ts b/packages/api/routes/well-known/host-meta/index.ts
index 16837899..81be321c 100644
--- a/packages/api/routes/well-known/host-meta/index.ts
+++ b/packages/api/routes/well-known/host-meta/index.ts
@@ -1,8 +1,7 @@
import { config } from "@versia-server/config";
import { apiRoute } from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/well-known/nodeinfo/2.0/index.ts b/packages/api/routes/well-known/nodeinfo/2.0/index.ts
index 725bffdb..589775a7 100644
--- a/packages/api/routes/well-known/nodeinfo/2.0/index.ts
+++ b/packages/api/routes/well-known/nodeinfo/2.0/index.ts
@@ -1,9 +1,8 @@
import { config } from "@versia-server/config";
import { apiRoute } from "@versia-server/kit/api";
import { Note, User } from "@versia-server/kit/db";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver } from "hono-openapi";
+import { z } from "zod/v4";
import manifest from "../../../../../../package.json" with { type: "json" };
export default apiRoute((app) =>
diff --git a/packages/api/routes/well-known/nodeinfo/index.ts b/packages/api/routes/well-known/nodeinfo/index.ts
index b087a8a7..fcc1657f 100644
--- a/packages/api/routes/well-known/nodeinfo/index.ts
+++ b/packages/api/routes/well-known/nodeinfo/index.ts
@@ -1,8 +1,7 @@
import { config } from "@versia-server/config";
import { apiRoute } from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/well-known/openid-configuration/index.ts b/packages/api/routes/well-known/openid-configuration/index.ts
index fecf9854..9917997b 100644
--- a/packages/api/routes/well-known/openid-configuration/index.ts
+++ b/packages/api/routes/well-known/openid-configuration/index.ts
@@ -1,8 +1,7 @@
import { config } from "@versia-server/config";
import { apiRoute } from "@versia-server/kit/api";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
diff --git a/packages/api/routes/well-known/versia.ts b/packages/api/routes/well-known/versia.ts
index 89bf672b..85598640 100644
--- a/packages/api/routes/well-known/versia.ts
+++ b/packages/api/routes/well-known/versia.ts
@@ -4,8 +4,7 @@ import { apiRoute } from "@versia-server/kit/api";
import { User } from "@versia-server/kit/db";
import { Users } from "@versia-server/kit/tables";
import { asc } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
+import { describeRoute, resolver } from "hono-openapi";
import { urlToContentFormat } from "@/content_types";
import pkg from "../../../../package.json" with { type: "json" };
diff --git a/packages/api/routes/well-known/webfinger/index.ts b/packages/api/routes/well-known/webfinger/index.ts
index d34cdc23..9b34783c 100644
--- a/packages/api/routes/well-known/webfinger/index.ts
+++ b/packages/api/routes/well-known/webfinger/index.ts
@@ -9,9 +9,8 @@ import { uuid, webfingerMention } from "@versia-server/kit/regex";
import { Users } from "@versia-server/kit/tables";
import { federationBridgeLogger } from "@versia-server/logging";
import { and, eq, isNull } from "drizzle-orm";
-import { describeRoute } from "hono-openapi";
-import { resolver, validator } from "hono-openapi/zod";
-import { z } from "zod";
+import { describeRoute, resolver, validator } from "hono-openapi";
+import { z } from "zod/v4";
export default apiRoute((app) =>
app.get(
diff --git a/packages/client/schemas/account-warning.ts b/packages/client/schemas/account-warning.ts
index 1447b12c..40c34924 100644
--- a/packages/client/schemas/account-warning.ts
+++ b/packages/client/schemas/account-warning.ts
@@ -1,11 +1,11 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { Account } from "./account.ts";
import { Appeal } from "./appeal.ts";
import { Id } from "./common.ts";
export const AccountWarning = z
.object({
- id: Id.openapi({
+ id: Id.meta({
description: "The ID of the account warning in the database.",
example: "0968680e-fd64-4525-b818-6e1c46fbdb28",
}),
@@ -19,40 +19,40 @@ export const AccountWarning = z
"silence",
"suspend",
])
- .openapi({
+ .meta({
description:
"Action taken against the account. 'none' = No action was taken, this is a simple warning; 'disable' = The account has been disabled; 'mark_statuses_as_sensitive' = Specific posts from the target account have been marked as sensitive; 'delete_statuses' = Specific statuses from the target account have been deleted; 'sensitive' = All posts from the target account are marked as sensitive; 'silence' = The target account has been limited; 'suspend' = The target account has been suspended.",
example: "none",
}),
- text: z.string().openapi({
+ text: z.string().meta({
description: "Message from the moderator to the target account.",
example: "Please adhere to our community guidelines.",
}),
status_ids: z
.array(Id)
.nullable()
- .openapi({
+ .meta({
description:
"List of status IDs that are relevant to the warning. When action is mark_statuses_as_sensitive or delete_statuses, those are the affected statuses.",
example: ["5ee59275-c308-4173-bb1f-58646204579b"],
}),
- target_account: Account.openapi({
+ target_account: Account.meta({
description:
"Account against which a moderation decision has been taken.",
}),
- appeal: Appeal.nullable().openapi({
+ appeal: Appeal.nullable().meta({
description: "Appeal submitted by the target account, if any.",
example: null,
}),
- created_at: z.string().datetime().openapi({
+ created_at: z.iso.datetime().meta({
description: "When the event took place.",
example: "2025-01-04T14:11:00Z",
}),
})
- .openapi({
+ .meta({
description: "Moderation warning against a particular account.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/AccountWarning",
},
- ref: "AccountWarning",
+ id: "AccountWarning",
});
diff --git a/packages/client/schemas/account.ts b/packages/client/schemas/account.ts
index 84f9c83c..f04141be 100644
--- a/packages/client/schemas/account.ts
+++ b/packages/client/schemas/account.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { userAddressRegex } from "../regex.ts";
import { iso631, zBoolean } from "./common.ts";
import { CustomEmoji } from "./emoji.ts";
@@ -10,7 +10,7 @@ export const Field = z
.string()
.trim()
.min(1)
- .openapi({
+ .meta({
description: "The key of a given field’s key-value pair.",
example: "Freak level",
externalDocs: {
@@ -21,18 +21,17 @@ export const Field = z
.string()
.trim()
.min(1)
- .openapi({
+ .meta({
description: "The value associated with the name key.",
example: "
High
",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#value",
},
}),
- verified_at: z
- .string()
+ verified_at: z.iso
.datetime()
.nullable()
- .openapi({
+ .meta({
description:
"Timestamp of when the server verified a URL value for a rel=“me” link.",
example: null,
@@ -41,11 +40,11 @@ export const Field = z
},
}),
})
- .openapi({ ref: "AccountField" });
+ .meta({ id: "AccountField" });
export const Source = z
.object({
- privacy: z.enum(["public", "unlisted", "private", "direct"]).openapi({
+ privacy: z.enum(["public", "unlisted", "private", "direct"]).meta({
description:
"The default post privacy to be used for new statuses.",
example: "unlisted",
@@ -53,7 +52,7 @@ export const Source = z
url: "https://docs.joinmastodon.org/entities/Account/#source-privacy",
},
}),
- sensitive: zBoolean.openapi({
+ sensitive: zBoolean.meta({
description:
"Whether new statuses should be marked sensitive by default.",
example: false,
@@ -61,7 +60,7 @@ export const Source = z
url: "https://docs.joinmastodon.org/entities/Account/#source-sensitive",
},
}),
- language: iso631.openapi({
+ language: iso631.meta({
description: "The default posting language for new statuses.",
example: "en",
externalDocs: {
@@ -73,7 +72,7 @@ export const Source = z
.int()
.nonnegative()
.optional()
- .openapi({
+ .meta({
description: "The number of pending follow requests.",
example: 3,
externalDocs: {
@@ -84,39 +83,35 @@ export const Source = z
.string()
.trim()
.min(0)
- .openapi({
+ .meta({
description: "Profile bio, in plain-text instead of in HTML.",
example: "ermmm what the meow meow",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#source-note",
},
}),
- fields: z.array(Field).openapi({
+ fields: z.array(Field).meta({
description: "Metadata about the account.",
}),
})
- .openapi({
+ .meta({
description:
"An extra attribute that contains source values to be used with API methods that verify credentials and update credentials.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#source",
},
- ref: "AccountSource",
+ id: "AccountSource",
});
-// Because Account has some recursive references, we need to define it like this
-const BaseAccount = z
+export const Account = z
.object({
- id: z
- .string()
- .uuid()
- .openapi({
- description: "The account ID in the database.",
- example: "9e84842b-4db6-4a9b-969d-46ab408278da",
- externalDocs: {
- url: "https://docs.joinmastodon.org/entities/Account/#id",
- },
- }),
+ id: z.uuid().meta({
+ description: "The account ID in the database.",
+ example: "9e84842b-4db6-4a9b-969d-46ab408278da",
+ externalDocs: {
+ url: "https://docs.joinmastodon.org/entities/Account/#id",
+ },
+ }),
username: z
.string()
.min(3)
@@ -125,7 +120,7 @@ const BaseAccount = z
/^[a-z0-9_-]+$/,
"Username can only contain letters, numbers, underscores and hyphens",
)
- .openapi({
+ .meta({
description:
"The username of the account, not including domain.",
example: "lexi",
@@ -138,7 +133,7 @@ const BaseAccount = z
.min(1)
.trim()
.regex(userAddressRegex, "Invalid user address")
- .openapi({
+ .meta({
description:
"The Webfinger account URI. Equal to username for local users, or username@domain for remote users.",
example: "lexi@beta.versia.social",
@@ -146,21 +141,18 @@ const BaseAccount = z
url: "https://docs.joinmastodon.org/entities/Account/#acct",
},
}),
- url: z
- .string()
- .url()
- .openapi({
- description: "The location of the user’s profile page.",
- example: "https://beta.versia.social/@lexi",
- externalDocs: {
- url: "https://docs.joinmastodon.org/entities/Account/#url",
- },
- }),
+ url: z.url().meta({
+ description: "The location of the user’s profile page.",
+ example: "https://beta.versia.social/@lexi",
+ externalDocs: {
+ url: "https://docs.joinmastodon.org/entities/Account/#url",
+ },
+ }),
display_name: z
.string()
.min(3)
.trim()
- .openapi({
+ .meta({
description: "The profile’s display name.",
example: "Lexi :flower:",
externalDocs: {
@@ -171,29 +163,26 @@ const BaseAccount = z
.string()
.min(0)
.trim()
- .openapi({
+ .meta({
description: "The profile’s bio or description.",
example: "
ermmm what the meow meow
",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#note",
},
}),
- avatar: z
- .string()
- .url()
- .openapi({
- description:
- "An image icon that is shown next to statuses and in the profile.",
- example:
- "https://cdn.versia.social/avatars/cff9aea0-0000-43fe-8b5e-e7c7ea69a488/lexi.webp",
- externalDocs: {
- url: "https://docs.joinmastodon.org/entities/Account/#avatar",
- },
- }),
+ avatar: z.url().meta({
+ description:
+ "An image icon that is shown next to statuses and in the profile.",
+ example:
+ "https://cdn.versia.social/avatars/cff9aea0-0000-43fe-8b5e-e7c7ea69a488/lexi.webp",
+ externalDocs: {
+ url: "https://docs.joinmastodon.org/entities/Account/#avatar",
+ },
+ }),
avatar_static: z
.string()
.url()
- .openapi({
+ .meta({
description:
"A static version of the avatar. Equal to avatar if its value is a static image; different if avatar is an animated GIF.",
example:
@@ -202,31 +191,25 @@ const BaseAccount = z
url: "https://docs.joinmastodon.org/entities/Account/#avatar_static",
},
}),
- header: z
- .string()
- .url()
- .openapi({
- description:
- "An image banner that is shown above the profile and in profile cards.",
- example:
- "https://cdn.versia.social/headers/a049f8e3-878c-4faa-ae4c-a6bcceddbd9d/femboy_2.webp",
- externalDocs: {
- url: "https://docs.joinmastodon.org/entities/Account/#header",
- },
- }),
- header_static: z
- .string()
- .url()
- .openapi({
- description:
- "A static version of the header. Equal to header if its value is a static image; different if header is an animated GIF.",
- example:
- "https://cdn.versia.social/headers/a049f8e3-878c-4faa-ae4c-a6bcceddbd9d/femboy_2.webp",
- externalDocs: {
- url: "https://docs.joinmastodon.org/entities/Account/#header_static",
- },
- }),
- locked: zBoolean.openapi({
+ header: z.url().meta({
+ description:
+ "An image banner that is shown above the profile and in profile cards.",
+ example:
+ "https://cdn.versia.social/headers/a049f8e3-878c-4faa-ae4c-a6bcceddbd9d/femboy_2.webp",
+ externalDocs: {
+ url: "https://docs.joinmastodon.org/entities/Account/#header",
+ },
+ }),
+ header_static: z.url().meta({
+ description:
+ "A static version of the header. Equal to header if its value is a static image; different if header is an animated GIF.",
+ example:
+ "https://cdn.versia.social/headers/a049f8e3-878c-4faa-ae4c-a6bcceddbd9d/femboy_2.webp",
+ externalDocs: {
+ url: "https://docs.joinmastodon.org/entities/Account/#header_static",
+ },
+ }),
+ locked: zBoolean.meta({
description:
"Whether the account manually approves follow requests.",
example: false,
@@ -234,21 +217,21 @@ const BaseAccount = z
url: "https://docs.joinmastodon.org/entities/Account/#locked",
},
}),
- fields: z.array(Field).openapi({
+ fields: z.array(Field).meta({
description:
"Additional metadata attached to a profile as name-value pairs.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#fields",
},
}),
- emojis: z.array(CustomEmoji).openapi({
+ emojis: z.array(CustomEmoji).meta({
description:
"Custom emoji entities to be used when rendering the profile.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#emojis",
},
}),
- bot: zBoolean.openapi({
+ bot: zBoolean.meta({
description:
"Indicates that the account may perform automated actions, may not be monitored, or identifies as a robot.",
example: false,
@@ -256,14 +239,14 @@ const BaseAccount = z
url: "https://docs.joinmastodon.org/entities/Account/#bot",
},
}),
- group: z.literal(false).openapi({
+ group: z.literal(false).meta({
description: "Indicates that the account represents a Group actor.",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#group",
},
}),
- discoverable: zBoolean.nullable().openapi({
+ discoverable: zBoolean.nullable().meta({
description:
"Whether the account has opted into discovery features such as the profile directory.",
example: true,
@@ -274,7 +257,7 @@ const BaseAccount = z
noindex: zBoolean
.nullable()
.optional()
- .openapi({
+ .meta({
description:
"Whether the local user has opted out of being indexed by search engines.",
example: false,
@@ -282,7 +265,7 @@ const BaseAccount = z
url: "https://docs.joinmastodon.org/entities/Account/#noindex",
},
}),
- suspended: zBoolean.optional().openapi({
+ suspended: zBoolean.optional().meta({
description:
"An extra attribute returned only when an account is suspended.",
example: false,
@@ -290,7 +273,7 @@ const BaseAccount = z
url: "https://docs.joinmastodon.org/entities/Account/#suspended",
},
}),
- limited: zBoolean.optional().openapi({
+ limited: zBoolean.optional().meta({
description:
"An extra attribute returned only when an account is silenced. If true, indicates that the account should be hidden behind a warning screen.",
example: false,
@@ -298,20 +281,17 @@ const BaseAccount = z
url: "https://docs.joinmastodon.org/entities/Account/#limited",
},
}),
- created_at: z
- .string()
- .datetime()
- .openapi({
- description: "When the account was created.",
- example: "2024-10-15T22:00:00.000Z",
- externalDocs: {
- url: "https://docs.joinmastodon.org/entities/Account/#created_at",
- },
- }),
+ created_at: z.iso.datetime().meta({
+ description: "When the account was created.",
+ example: "2024-10-15T22:00:00.000Z",
+ externalDocs: {
+ url: "https://docs.joinmastodon.org/entities/Account/#created_at",
+ },
+ }),
// TODO
last_status_at: z
.literal(null)
- .openapi({
+ .meta({
description: "When the most recent status was posted.",
example: null,
externalDocs: {
@@ -323,7 +303,7 @@ const BaseAccount = z
.number()
.int()
.nonnegative()
- .openapi({
+ .meta({
description: "How many statuses are attached to this account.",
example: 42,
externalDocs: {
@@ -334,7 +314,7 @@ const BaseAccount = z
.number()
.int()
.nonnegative()
- .openapi({
+ .meta({
description: "The reported followers of this profile.",
example: 6,
externalDocs: {
@@ -345,7 +325,7 @@ const BaseAccount = z
.number()
.int()
.nonnegative()
- .openapi({
+ .meta({
description: "The reported follows of this profile.",
example: 23,
externalDocs: {
@@ -353,7 +333,7 @@ const BaseAccount = z
},
}),
/* Versia Server API extension */
- uri: z.string().url().openapi({
+ uri: z.url().meta({
description:
"The location of the user's Versia profile page, as opposed to the local representation.",
example:
@@ -365,26 +345,25 @@ const BaseAccount = z
name: z.string(),
})
.optional(),
+ get moved() {
+ return Account.nullable()
+ .optional()
+ .meta({
+ description:
+ "Indicates that the profile is currently inactive and that its user has moved to a new account.",
+ example: null,
+ externalDocs: {
+ url: "https://docs.joinmastodon.org/entities/Account/#moved",
+ },
+ });
+ },
/* Versia Server API extension */
- roles: z.array(Role).openapi({
+ roles: z.array(Role).meta({
description: "Roles assigned to the account.",
}),
- mute_expires_at: z.string().datetime().nullable().openapi({
+ mute_expires_at: z.iso.datetime().nullable().meta({
description: "When a timed mute will expire, if applicable.",
example: "2025-03-01T14:00:00.000Z",
}),
})
- .openapi({ ref: "BaseAccount" });
-
-export const Account = BaseAccount.extend({
- moved: BaseAccount.nullable()
- .optional()
- .openapi({
- description:
- "Indicates that the profile is currently inactive and that its user has moved to a new account.",
- example: null,
- externalDocs: {
- url: "https://docs.joinmastodon.org/entities/Account/#moved",
- },
- }),
-}).openapi({ ref: "Account" });
+ .meta({ id: "Account" });
diff --git a/packages/client/schemas/appeal.ts b/packages/client/schemas/appeal.ts
index 4b626d6e..410b4c72 100644
--- a/packages/client/schemas/appeal.ts
+++ b/packages/client/schemas/appeal.ts
@@ -1,22 +1,22 @@
-import { z } from "zod";
+import { z } from "zod/v4";
export const Appeal = z
.object({
- text: z.string().openapi({
+ text: z.string().meta({
description:
"Text of the appeal from the moderated account to the moderators.",
example: "I believe this action was taken in error.",
}),
- state: z.enum(["approved", "rejected", "pending"]).openapi({
+ state: z.enum(["approved", "rejected", "pending"]).meta({
description:
"State of the appeal. 'approved' = The appeal has been approved by a moderator, 'rejected' = The appeal has been rejected by a moderator, 'pending' = The appeal has been submitted, but neither approved nor rejected yet.",
example: "pending",
}),
})
- .openapi({
+ .meta({
description: "Appeal against a moderation action.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Appeal",
},
- ref: "Appeal",
+ id: "Appeal",
});
diff --git a/packages/client/schemas/application.ts b/packages/client/schemas/application.ts
index afda6a91..685a6a00 100644
--- a/packages/client/schemas/application.ts
+++ b/packages/client/schemas/application.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
export const Application = z
.object({
@@ -7,7 +7,7 @@ export const Application = z
.trim()
.min(1)
.max(200)
- .openapi({
+ .meta({
description: "The name of your application.",
example: "Test Application",
externalDocs: {
@@ -17,7 +17,7 @@ export const Application = z
website: z
.string()
.nullable()
- .openapi({
+ .meta({
description: "The website associated with your application.",
example: "https://app.example",
externalDocs: {
@@ -27,7 +27,7 @@ export const Application = z
scopes: z
.array(z.string())
.default(["read"])
- .openapi({
+ .meta({
description:
"The scopes for your application. This is the registered scopes string split on whitespace.",
example: ["read", "write", "push"],
@@ -37,22 +37,18 @@ export const Application = z
}),
redirect_uris: z
.array(
- z
- .string()
- .url()
- .or(z.literal("urn:ietf:wg:oauth:2.0:oob"))
- .openapi({
- description: "URL or 'urn:ietf:wg:oauth:2.0:oob'",
- }),
+ z.url().or(z.literal("urn:ietf:wg:oauth:2.0:oob")).meta({
+ description: "URL or 'urn:ietf:wg:oauth:2.0:oob'",
+ }),
)
- .openapi({
+ .meta({
description:
"The registered redirection URI(s) for your application.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Application/#redirect_uris",
},
}),
- redirect_uri: z.string().openapi({
+ redirect_uri: z.string().meta({
deprecated: true,
description:
"The registered redirection URI(s) for your application. May contain \\n characters when multiple redirect URIs are registered.",
@@ -61,30 +57,30 @@ export const Application = z
},
}),
})
- .openapi({
- ref: "Application",
+ .meta({
+ id: "Application",
});
export const CredentialApplication = Application.extend({
- client_id: z.string().openapi({
+ client_id: z.string().meta({
description: "Client ID key, to be used for obtaining OAuth tokens",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/CredentialApplication/#client_id",
},
}),
- client_secret: z.string().openapi({
+ client_secret: z.string().meta({
description: "Client secret key, to be used for obtaining OAuth tokens",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/CredentialApplication/#client_secret",
},
}),
- client_secret_expires_at: z.string().openapi({
+ client_secret_expires_at: z.string().meta({
description:
"When the client secret key will expire at, presently this always returns 0 indicating that OAuth Clients do not expire",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/CredentialApplication/#client_secret_expires_at",
},
}),
-}).openapi({
- ref: "CredentialApplication",
+}).meta({
+ id: "CredentialApplication",
});
diff --git a/packages/client/schemas/attachment.ts b/packages/client/schemas/attachment.ts
index 76c9ce7e..d149cf1d 100644
--- a/packages/client/schemas/attachment.ts
+++ b/packages/client/schemas/attachment.ts
@@ -1,34 +1,34 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { Id } from "./common.ts";
export const Attachment = z
.object({
- id: Id.openapi({
+ id: Id.meta({
description: "The ID of the attachment in the database.",
example: "8c33d4c6-2292-4f4d-945d-261836e09647",
}),
- type: z.enum(["unknown", "image", "gifv", "video", "audio"]).openapi({
+ type: z.enum(["unknown", "image", "gifv", "video", "audio"]).meta({
description:
"The type of the attachment. 'unknown' = unsupported or unrecognized file type, 'image' = Static image, 'gifv' = Looping, soundless animation, 'video' = Video clip, 'audio' = Audio track.",
example: "image",
}),
- url: z.string().url().openapi({
+ url: z.url().meta({
description: "The location of the original full-size attachment.",
example:
"https://files.mastodon.social/media_attachments/files/022/345/792/original/57859aede991da25.jpeg",
}),
- preview_url: z.string().url().nullable().openapi({
+ preview_url: z.url().nullable().meta({
description:
"The location of a scaled-down preview of the attachment.",
example:
"https://files.mastodon.social/media_attachments/files/022/345/792/small/57859aede991da25.jpeg",
}),
- remote_url: z.string().url().nullable().openapi({
+ remote_url: z.url().nullable().meta({
description:
"The location of the full-size original attachment on the remote website, or null if the attachment is local.",
example: null,
}),
- meta: z.record(z.any()).openapi({
+ meta: z.any().meta({
description:
"Metadata. May contain subtrees like 'small' and 'original', and possibly a 'focus' object for smart thumbnail cropping.",
example: {
@@ -50,22 +50,22 @@ export const Attachment = z
},
},
}),
- description: z.string().trim().nullable().openapi({
+ description: z.string().trim().nullable().meta({
description:
"Alternate text that describes what is in the media attachment, to be used for the visually impaired or when media attachments do not load.",
example: "test media description",
}),
- blurhash: z.string().nullable().openapi({
+ blurhash: z.string().nullable().meta({
description:
"A hash computed by the BlurHash algorithm, for generating colorful preview thumbnails when media has not been downloaded yet.",
example: "UFBWY:8_0Jxv4mx]t8t64.%M-:IUWGWAt6M}",
}),
})
- .openapi({
+ .meta({
description:
"Represents a file or media attachment that can be added to a status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Attachment",
},
- ref: "Attachment",
+ id: "Attachment",
});
diff --git a/packages/client/schemas/card.ts b/packages/client/schemas/card.ts
index 3a7e804b..7389b0ff 100644
--- a/packages/client/schemas/card.ts
+++ b/packages/client/schemas/card.ts
@@ -1,97 +1,88 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { Account } from "./account.ts";
export const PreviewCardAuthor = z
.object({
- name: z.string().openapi({
+ name: z.string().meta({
description: "The original resource author’s name.",
example: "The Doubleclicks",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCardAuthor/#name",
},
}),
- url: z
- .string()
- .url()
- .openapi({
- description: "A link to the author of the original resource.",
- example: "https://www.youtube.com/user/thedoubleclicks",
- externalDocs: {
- url: "https://docs.joinmastodon.org/entities/PreviewCardAuthor/#url",
- },
- }),
- account: Account.nullable().openapi({
+ url: z.url().meta({
+ description: "A link to the author of the original resource.",
+ example: "https://www.youtube.com/user/thedoubleclicks",
+ externalDocs: {
+ url: "https://docs.joinmastodon.org/entities/PreviewCardAuthor/#url",
+ },
+ }),
+ account: Account.nullable().meta({
description: "The fediverse account of the author.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCardAuthor/#account",
},
}),
})
- .openapi({
- ref: "PreviewCardAuthor",
+ .meta({
+ id: "PreviewCardAuthor",
});
export const PreviewCard = z
.object({
- url: z
- .string()
- .url()
- .openapi({
- description: "Location of linked resource.",
- example: "https://www.youtube.com/watch?v=OMv_EPMED8Y",
- externalDocs: {
- url: "https://docs.joinmastodon.org/entities/PreviewCard/#url",
- },
- }),
+ url: z.url().meta({
+ description: "Location of linked resource.",
+ example: "https://www.youtube.com/watch?v=OMv_EPMED8Y",
+ externalDocs: {
+ url: "https://docs.joinmastodon.org/entities/PreviewCard/#url",
+ },
+ }),
title: z
.string()
.min(1)
- .openapi({
+ .meta({
description: "Title of linked resource.",
example: "♪ Brand New Friend (Christmas Song!)",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#title",
},
}),
- description: z.string().openapi({
+ description: z.string().meta({
description: "Description of preview.",
example: "",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#description",
},
}),
- type: z.enum(["link", "photo", "video"]).openapi({
+ type: z.enum(["link", "photo", "video"]).meta({
description: "The type of the preview card.",
example: "video",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#type",
},
}),
- authors: z.array(PreviewCardAuthor).openapi({
+ authors: z.array(PreviewCardAuthor).meta({
description:
"Fediverse account of the authors of the original resource.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#authors",
},
}),
- provider_name: z.string().openapi({
+ provider_name: z.string().meta({
description: "The provider of the original resource.",
example: "YouTube",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#provider_name",
},
}),
- provider_url: z
- .string()
- .url()
- .openapi({
- description: "A link to the provider of the original resource.",
- example: "https://www.youtube.com/",
- externalDocs: {
- url: "https://docs.joinmastodon.org/entities/PreviewCard/#provider_url",
- },
- }),
- html: z.string().openapi({
+ provider_url: z.url().meta({
+ description: "A link to the provider of the original resource.",
+ example: "https://www.youtube.com/",
+ externalDocs: {
+ url: "https://docs.joinmastodon.org/entities/PreviewCard/#provider_url",
+ },
+ }),
+ html: z.string().meta({
description: "HTML to be used for generating the preview card.",
example:
'',
@@ -102,7 +93,7 @@ export const PreviewCard = z
width: z
.number()
.int()
- .openapi({
+ .meta({
description: "Width of preview, in pixels.",
example: 480,
externalDocs: {
@@ -112,7 +103,7 @@ export const PreviewCard = z
height: z
.number()
.int()
- .openapi({
+ .meta({
description: "Height of preview, in pixels.",
example: 270,
externalDocs: {
@@ -120,10 +111,9 @@ export const PreviewCard = z
},
}),
image: z
- .string()
.url()
.nullable()
- .openapi({
+ .meta({
description: "Preview thumbnail.",
example:
"https://cdn.versia.social/preview_cards/images/014/179/145/original/9cf4b7cf5567b569.jpeg",
@@ -131,21 +121,18 @@ export const PreviewCard = z
url: "https://docs.joinmastodon.org/entities/PreviewCard/#image",
},
}),
- embed_url: z
- .string()
- .url()
- .openapi({
- description: "Used for photo embeds, instead of custom html.",
- example:
- "https://live.staticflickr.com/65535/49088768431_6a4322b3bb_b.jpg",
- externalDocs: {
- url: "https://docs.joinmastodon.org/entities/PreviewCard/#embed_url",
- },
- }),
+ embed_url: z.url().meta({
+ description: "Used for photo embeds, instead of custom html.",
+ example:
+ "https://live.staticflickr.com/65535/49088768431_6a4322b3bb_b.jpg",
+ externalDocs: {
+ url: "https://docs.joinmastodon.org/entities/PreviewCard/#embed_url",
+ },
+ }),
blurhash: z
.string()
.nullable()
- .openapi({
+ .meta({
description:
"A hash computed by the BlurHash algorithm, for generating colorful preview thumbnails when media has not been downloaded yet.",
example: "UvK0HNkV,:s9xBR%njog0fo2W=WBS5ozofV@",
@@ -154,11 +141,11 @@ export const PreviewCard = z
},
}),
})
- .openapi({
+ .meta({
description:
"Represents a rich preview card that is generated using OpenGraph tags from a URL.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard",
},
- ref: "PreviewCard",
+ id: "PreviewCard",
});
diff --git a/packages/client/schemas/common.ts b/packages/client/schemas/common.ts
index e3eeef6f..b93c57c6 100644
--- a/packages/client/schemas/common.ts
+++ b/packages/client/schemas/common.ts
@@ -1,22 +1,17 @@
import ISO6391 from "iso-639-1";
-import { z } from "zod";
-import "zod-openapi/extend";
+import { z } from "zod/v4";
-export const Id = z.string().uuid();
+export const Id = z.uuid();
export const iso631 = z
.enum(ISO6391.getAllCodes() as [string, ...string[]])
- .openapi({
+ .meta({
description: "ISO 639-1 language code",
example: "en",
externalDocs: {
url: "https://en.wikipedia.org/wiki/List_of_ISO_639-1_language_codes",
},
- ref: "ISO631",
+ id: "ISO631",
});
-export const zBoolean = z
- .string()
- .transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
- .openapi({ type: "boolean" })
- .or(z.boolean());
+export const zBoolean = z.stringbool().or(z.boolean());
diff --git a/packages/client/schemas/context.ts b/packages/client/schemas/context.ts
index 5916dd14..4344bec9 100644
--- a/packages/client/schemas/context.ts
+++ b/packages/client/schemas/context.ts
@@ -1,26 +1,26 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { Status } from "./status.ts";
export const Context = z
.object({
- ancestors: z.array(Status).openapi({
+ ancestors: z.array(Status).meta({
description: "Parents in the thread.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Context/#ancestors",
},
}),
- descendants: z.array(Status).openapi({
+ descendants: z.array(Status).meta({
description: "Children in the thread.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Context/#descendants",
},
}),
})
- .openapi({
+ .meta({
description:
"Represents the tree around a given status. Used for reconstructing threads of statuses.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Context/#context",
},
- ref: "Context",
+ id: "Context",
});
diff --git a/packages/client/schemas/emoji.ts b/packages/client/schemas/emoji.ts
index 5a1f8aae..00e49cbc 100644
--- a/packages/client/schemas/emoji.ts
+++ b/packages/client/schemas/emoji.ts
@@ -1,11 +1,11 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { emojiRegex } from "../regex.ts";
import { Id, zBoolean } from "./common.ts";
export const CustomEmoji = z
.object({
/* Versia Server API extension */
- id: Id.openapi({
+ id: Id.meta({
description: "ID of the custom emoji in the database.",
example: "af9ccd29-c689-477f-aa27-d7d95fd8fb05",
}),
@@ -17,36 +17,30 @@ export const CustomEmoji = z
emojiRegex,
"Shortcode must only contain letters (any case), numbers, dashes or underscores.",
)
- .openapi({
+ .meta({
description: "The name of the custom emoji.",
example: "blobaww",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/CustomEmoji/#shortcode",
},
}),
- url: z
- .string()
- .url()
- .openapi({
- description: "A link to the custom emoji.",
- example:
- "https://cdn.versia.social/emojis/images/000/011/739/original/blobaww.png",
- externalDocs: {
- url: "https://docs.joinmastodon.org/entities/CustomEmoji/#url",
- },
- }),
- static_url: z
- .string()
- .url()
- .openapi({
- description: "A link to a static copy of the custom emoji.",
- example:
- "https://cdn.versia.social/emojis/images/000/011/739/static/blobaww.png",
- externalDocs: {
- url: "https://docs.joinmastodon.org/entities/CustomEmoji/#static_url",
- },
- }),
- visible_in_picker: z.boolean().openapi({
+ url: z.url().meta({
+ description: "A link to the custom emoji.",
+ example:
+ "https://cdn.versia.social/emojis/images/000/011/739/original/blobaww.png",
+ externalDocs: {
+ url: "https://docs.joinmastodon.org/entities/CustomEmoji/#url",
+ },
+ }),
+ static_url: z.url().meta({
+ description: "A link to a static copy of the custom emoji.",
+ example:
+ "https://cdn.versia.social/emojis/images/000/011/739/static/blobaww.png",
+ externalDocs: {
+ url: "https://docs.joinmastodon.org/entities/CustomEmoji/#static_url",
+ },
+ }),
+ visible_in_picker: z.boolean().meta({
description:
"Whether this Emoji should be visible in the picker or unlisted.",
example: true,
@@ -59,7 +53,7 @@ export const CustomEmoji = z
.trim()
.max(64)
.nullable()
- .openapi({
+ .meta({
description: "Used for sorting custom emoji in the picker.",
example: "Blobs",
externalDocs: {
@@ -67,7 +61,7 @@ export const CustomEmoji = z
},
}),
/* Versia Server API extension */
- global: zBoolean.openapi({
+ global: zBoolean.meta({
description: "Whether this emoji is visible to all users.",
example: false,
}),
@@ -75,7 +69,7 @@ export const CustomEmoji = z
description: z
.string()
.nullable()
- .openapi({
+ .meta({
description:
"Emoji description for users using screen readers.",
example: "A cute blob.",
@@ -84,10 +78,10 @@ export const CustomEmoji = z
},
}),
})
- .openapi({
+ .meta({
description: "Represents a custom emoji.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/CustomEmoji",
},
- ref: "CustomEmoji",
+ id: "CustomEmoji",
});
diff --git a/packages/client/schemas/extended-description.ts b/packages/client/schemas/extended-description.ts
index a45c808e..7becfd2c 100644
--- a/packages/client/schemas/extended-description.ts
+++ b/packages/client/schemas/extended-description.ts
@@ -1,19 +1,16 @@
-import { z } from "zod";
+import { z } from "zod/v4";
export const ExtendedDescription = z
.object({
- updated_at: z
- .string()
- .datetime()
- .openapi({
- description:
- "A timestamp of when the extended description was last updated.",
- example: "2025-01-12T13:11:00Z",
- externalDocs: {
- url: "https://docs.joinmastodon.org/entities/ExtendedDescription/#updated_at",
- },
- }),
- content: z.string().openapi({
+ updated_at: z.iso.datetime().meta({
+ description:
+ "A timestamp of when the extended description was last updated.",
+ example: "2025-01-12T13:11:00Z",
+ externalDocs: {
+ url: "https://docs.joinmastodon.org/entities/ExtendedDescription/#updated_at",
+ },
+ }),
+ content: z.string().meta({
description:
"The rendered HTML content of the extended description.",
example: "
We love casting spells.
",
@@ -22,11 +19,11 @@ export const ExtendedDescription = z
},
}),
})
- .openapi({
+ .meta({
description:
"Represents an extended description for the instance, to be shown on its about page.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/ExtendedDescription",
},
- ref: "ExtendedDescription",
+ id: "ExtendedDescription",
});
diff --git a/packages/client/schemas/familiar-followers.ts b/packages/client/schemas/familiar-followers.ts
index cfeeee15..92c6a7ca 100644
--- a/packages/client/schemas/familiar-followers.ts
+++ b/packages/client/schemas/familiar-followers.ts
@@ -1,27 +1,27 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { Account } from "./account.ts";
export const FamiliarFollowers = z
.object({
- id: Account.shape.id.openapi({
+ id: Account.shape.id.meta({
description: "The ID of the Account in the database.",
example: "48214efb-1f3c-459a-abfa-618a5aeb2f7a",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FamiliarFollowers/#id",
},
}),
- accounts: z.array(Account).openapi({
+ accounts: z.array(Account).meta({
description: "Accounts you follow that also follow this account.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FamiliarFollowers/#accounts",
},
}),
})
- .openapi({
+ .meta({
description:
"Represents a subset of your follows who also follow some other user.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FamiliarFollowers",
},
- ref: "FamiliarFollowers",
+ id: "FamiliarFollowers",
});
diff --git a/packages/client/schemas/filters.ts b/packages/client/schemas/filters.ts
index 6e92ceb1..902f6f6f 100644
--- a/packages/client/schemas/filters.ts
+++ b/packages/client/schemas/filters.ts
@@ -1,16 +1,16 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { Id, zBoolean } from "./common.ts";
export const FilterStatus = z
.object({
- id: Id.openapi({
+ id: Id.meta({
description: "The ID of the FilterStatus in the database.",
example: "3b19ed7c-0c4b-45e1-8c75-e21dfc8e86c3",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterStatus/#id",
},
}),
- status_id: Id.openapi({
+ status_id: Id.meta({
description: "The ID of the Status that will be filtered.",
example: "4f941ac8-295c-4c2d-9300-82c162ac8028",
externalDocs: {
@@ -18,32 +18,32 @@ export const FilterStatus = z
},
}),
})
- .openapi({
+ .meta({
description:
"Represents a status ID that, if matched, should cause the filter action to be taken.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterStatus",
},
- ref: "FilterStatus",
+ id: "FilterStatus",
});
export const FilterKeyword = z
.object({
- id: Id.openapi({
+ id: Id.meta({
description: "The ID of the FilterKeyword in the database.",
example: "ca921e60-5b96-4686-90f3-d7cc420d7391",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterKeyword/#id",
},
}),
- keyword: z.string().openapi({
+ keyword: z.string().meta({
description: "The phrase to be matched against.",
example: "badword",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterKeyword/#keyword",
},
}),
- whole_word: zBoolean.openapi({
+ whole_word: zBoolean.meta({
description:
"Should the filter consider word boundaries? See implementation guidelines for filters.",
example: false,
@@ -52,18 +52,18 @@ export const FilterKeyword = z
},
}),
})
- .openapi({
+ .meta({
description:
"Represents a keyword that, if matched, should cause the filter action to be taken.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterKeyword",
},
- ref: "FilterKeyword",
+ id: "FilterKeyword",
});
export const Filter = z
.object({
- id: Id.openapi({
+ id: Id.meta({
description: "The ID of the Filter in the database.",
example: "6b8fa22f-b128-43c2-9a1f-3c0499ef3a51",
externalDocs: {
@@ -75,7 +75,7 @@ export const Filter = z
.trim()
.min(1)
.max(255)
- .openapi({
+ .meta({
description: "A title given by the user to name the filter.",
example: "Test filter",
externalDocs: {
@@ -93,7 +93,7 @@ export const Filter = z
]),
)
.default([])
- .openapi({
+ .meta({
description:
"The contexts in which the filter should be applied.",
example: ["home"],
@@ -104,7 +104,7 @@ export const Filter = z
expires_at: z
.string()
.nullable()
- .openapi({
+ .meta({
description: "When the filter should no longer be applied.",
example: "2026-09-20T17:27:39.296Z",
externalDocs: {
@@ -114,7 +114,7 @@ export const Filter = z
filter_action: z
.enum(["warn", "hide"])
.default("warn")
- .openapi({
+ .meta({
description:
"The action to be taken when a status matches this filter.",
example: "warn",
@@ -122,31 +122,31 @@ export const Filter = z
url: "https://docs.joinmastodon.org/entities/Filter/#filter_action",
},
}),
- keywords: z.array(FilterKeyword).openapi({
+ keywords: z.array(FilterKeyword).meta({
description: "The keywords grouped under this filter.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#keywords",
},
}),
- statuses: z.array(FilterStatus).openapi({
+ statuses: z.array(FilterStatus).meta({
description: "The statuses grouped under this filter.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#statuses",
},
}),
})
- .openapi({
+ .meta({
description:
"Represents a user-defined filter for determining which statuses should not be shown to the user.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter",
},
- ref: "Filter",
+ id: "Filter",
});
export const FilterResult = z
.object({
- filter: Filter.openapi({
+ filter: Filter.meta({
description: "The filter that was matched.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterResult/#filter",
@@ -155,7 +155,7 @@ export const FilterResult = z
keyword_matches: z
.array(z.string())
.nullable()
- .openapi({
+ .meta({
description: "The keyword within the filter that was matched.",
example: ["badword"],
externalDocs: {
@@ -165,7 +165,7 @@ export const FilterResult = z
status_matches: z
.array(Id)
.nullable()
- .openapi({
+ .meta({
description:
"The status ID within the filter that was matched.",
example: ["3819515a-5ceb-4078-8524-c939e38dcf8f"],
@@ -174,11 +174,11 @@ export const FilterResult = z
},
}),
})
- .openapi({
+ .meta({
description:
"Represents a filter whose keywords matched a given status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterResult",
},
- ref: "FilterResult",
+ id: "FilterResult",
});
diff --git a/packages/client/schemas/instance-v1.ts b/packages/client/schemas/instance-v1.ts
index 75e058df..0e0cddee 100644
--- a/packages/client/schemas/instance-v1.ts
+++ b/packages/client/schemas/instance-v1.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { Instance } from "./instance.ts";
import { SSOConfig } from "./versia.ts";
@@ -7,7 +7,7 @@ export const InstanceV1 = z
uri: Instance.shape.domain,
title: Instance.shape.title,
short_description: Instance.shape.description,
- description: z.string().openapi({
+ description: z.string().meta({
description: "An HTML-permitted description of the site.",
example: "
Join the world's smallest social network.
",
}),
@@ -20,29 +20,29 @@ export const InstanceV1 = z
streaming_api:
Instance.shape.configuration.shape.urls.shape.streaming,
})
- .openapi({
+ .meta({
description: "URLs of interest for clients apps.",
}),
stats: z
.object({
- user_count: z.number().openapi({
+ user_count: z.number().meta({
description: "Total users on this instance.",
example: 812303,
}),
- status_count: z.number().openapi({
+ status_count: z.number().meta({
description: "Total statuses on this instance.",
example: 38151616,
}),
- domain_count: z.number().openapi({
+ domain_count: z.number().meta({
description: "Total domains discovered by this instance.",
example: 25255,
}),
})
- .openapi({
+ .meta({
description:
"Statistics about how much information the instance contains.",
}),
- thumbnail: z.string().url().nullable().openapi({
+ thumbnail: z.url().nullable().meta({
description: "Banner image for the website.",
example:
"https://files.mastodon.social/site_uploads/files/000/000/001/original/vlcsnap-2018-08-27-16h43m11s127.png",
@@ -50,7 +50,7 @@ export const InstanceV1 = z
languages: Instance.shape.languages,
registrations: Instance.shape.registrations.shape.enabled,
approval_required: Instance.shape.registrations.shape.approval_required,
- invites_enabled: z.boolean().openapi({
+ invites_enabled: z.boolean().meta({
description: "Whether invites are enabled.",
example: true,
}),
@@ -62,7 +62,7 @@ export const InstanceV1 = z
Instance.shape.configuration.shape.accounts.shape
.max_featured_tags,
})
- .openapi({
+ .meta({
description: "Limits related to accounts.",
}),
statuses: z
@@ -77,7 +77,7 @@ export const InstanceV1 = z
Instance.shape.configuration.shape.statuses.shape
.characters_reserved_per_url,
})
- .openapi({
+ .meta({
description: "Limits related to authoring statuses.",
}),
media_attachments: z
@@ -101,7 +101,7 @@ export const InstanceV1 = z
Instance.shape.configuration.shape.media_attachments
.shape.video_matrix_limit,
})
- .openapi({
+ .meta({
description:
"Hints for which attachments will be accepted.",
}),
@@ -120,11 +120,11 @@ export const InstanceV1 = z
Instance.shape.configuration.shape.polls.shape
.max_expiration,
})
- .openapi({
+ .meta({
description: "Limits related to polls.",
}),
})
- .openapi({
+ .meta({
description: "Configured values and limits for this website.",
}),
contact_account: Instance.shape.contact.shape.account,
@@ -132,11 +132,11 @@ export const InstanceV1 = z
/* Versia Server API extension */
sso: SSOConfig,
})
- .openapi({
+ .meta({
description:
"Represents the software instance of Versia Server running on this domain.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/V1_Instance",
},
- ref: "InstanceV1",
+ id: "InstanceV1",
});
diff --git a/packages/client/schemas/instance.ts b/packages/client/schemas/instance.ts
index 0914138b..b72d4031 100644
--- a/packages/client/schemas/instance.ts
+++ b/packages/client/schemas/instance.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { Account } from "./account.ts";
import { iso631 } from "./common.ts";
import { Rule } from "./rule.ts";
@@ -6,50 +6,50 @@ import { SSOConfig } from "./versia.ts";
const InstanceIcon = z
.object({
- src: z.string().url().openapi({
+ src: z.url().meta({
description: "The URL of this icon.",
example:
"https://files.mastodon.social/site_uploads/files/000/000/003/36/accf17b0104f18e5.png",
}),
- size: z.string().openapi({
+ size: z.string().meta({
description:
"The size of this icon (in the form of 12x34, where 12 is the width and 34 is the height of the icon).",
example: "36x36",
}),
})
- .openapi({
+ .meta({
externalDocs: {
url: "https://docs.joinmastodon.org/entities/InstanceIcon",
},
- ref: "InstanceIcon",
+ id: "InstanceIcon",
});
export const Instance = z
.object({
- domain: z.string().openapi({
+ domain: z.string().meta({
description: "The domain name of the instance.",
example: "versia.social",
}),
- title: z.string().openapi({
+ title: z.string().meta({
description: "The title of the website.",
example: "Versia Social • Now with 100% more blobs!",
}),
- version: z.string().openapi({
+ version: z.string().meta({
description:
"Mastodon version that the API is compatible with. Used for compatibility with Mastodon clients.",
example: "4.3.0+glitch",
}),
/* Versia Server API extension */
- versia_version: z.string().openapi({
+ versia_version: z.string().meta({
description: "Versia Server version.",
example: "0.8.0",
}),
- source_url: z.string().url().openapi({
+ source_url: z.url().meta({
description:
"The URL for the source code of the software running on this instance, in keeping with AGPL license requirements.",
example: "https://github.com/versia-pub/server",
}),
- description: z.string().openapi({
+ description: z.string().meta({
description:
"A short, plain-text description defined by the admin.",
example: "The flagship Versia Server instance. Join for free hugs!",
@@ -58,74 +58,74 @@ export const Instance = z
.object({
users: z
.object({
- active_month: z.number().openapi({
+ active_month: z.number().meta({
description:
"The number of active users in the past 4 weeks.",
example: 1_261,
}),
})
- .openapi({
+ .meta({
description:
"Usage data related to users on this instance.",
}),
})
- .openapi({ description: "Usage data for this instance." }),
+ .meta({ description: "Usage data for this instance." }),
thumbnail: z
.object({
- url: z.string().url().openapi({
+ url: z.url().meta({
description: "The URL for the thumbnail image.",
example:
"https://files.mastodon.social/site_uploads/files/000/000/001/@1x/57c12f441d083cde.png",
}),
- blurhash: z.string().optional().openapi({
+ blurhash: z.string().optional().meta({
description:
"A hash computed by the BlurHash algorithm, for generating colorful preview thumbnails when media has not been downloaded yet.",
example: "UUKJMXv|x]t7^*t7Rjaz^jazRjaz",
}),
versions: z
.object({
- "@1x": z.string().url().optional().openapi({
+ "@1x": z.url().optional().meta({
description:
"The URL for the thumbnail image at 1x resolution.",
}),
- "@2x": z.string().url().optional().openapi({
+ "@2x": z.url().optional().meta({
description:
"The URL for the thumbnail image at 2x resolution.",
}),
})
.optional()
- .openapi({
+ .meta({
description:
"Links to scaled resolution images, for high DPI screens.",
}),
})
- .openapi({
+ .meta({
description: "An image used to represent this instance.",
}),
/* Versia Server API extension */
banner: z
.object({
- url: z.string().url().openapi({
+ url: z.url().meta({
description: "The URL for the banner image.",
example:
"https://files.mastodon.social/site_uploads/files/000/000/001/@1x/57c12f441d083cde.png",
}),
- blurhash: z.string().optional().openapi({
+ blurhash: z.string().optional().meta({
description:
"A hash computed by the BlurHash algorithm, for generating colorful preview thumbnails when media has not been downloaded yet.",
example: "UUKJMXv|x]t7^*t7Rjaz^jazRjaz",
}),
})
.optional()
- .openapi({
+ .meta({
description:
"A wide banner image used to represent this instance.",
}),
- icon: z.array(InstanceIcon).openapi({
+ icon: z.array(InstanceIcon).meta({
description:
"The list of available size variants for this instance configured icon.",
}),
- languages: z.array(iso631).openapi({
+ languages: z.array(iso631).meta({
description: "Primary languages of the website and its staff.",
example: ["en"],
}),
@@ -133,63 +133,63 @@ export const Instance = z
.object({
urls: z
.object({
- streaming: z.string().url().openapi({
+ streaming: z.url().meta({
description:
"The Websockets URL for connecting to the streaming API.",
example: "wss://versia.social",
}),
})
- .openapi({
+ .meta({
description: "URLs of interest for clients apps.",
}),
vapid: z
.object({
- public_key: z.string().openapi({
+ public_key: z.string().meta({
description:
"The instance's VAPID public key, used for push notifications, the same as WebPushSubscription#server_key.",
example:
"BCkMmVdKDnKYwzVCDC99Iuc9GvId-x7-kKtuHnLgfF98ENiZp_aj-UNthbCdI70DqN1zUVis-x0Wrot2sBagkMc=",
}),
})
- .openapi({ description: "VAPID configuration." }),
+ .meta({ description: "VAPID configuration." }),
accounts: z
.object({
- max_featured_tags: z.number().openapi({
+ max_featured_tags: z.number().meta({
description:
"The maximum number of featured tags allowed for each account.",
example: 10,
}),
- max_pinned_statuses: z.number().openapi({
+ max_pinned_statuses: z.number().meta({
description:
"The maximum number of pinned statuses for each account.",
example: 4,
}),
/* Versia Server API extension */
- max_displayname_characters: z.number().openapi({
+ max_displayname_characters: z.number().meta({
description:
"The maximum number of characters allowed in a display name.",
example: 30,
}),
/* Versia Server API extension */
- max_username_characters: z.number().openapi({
+ max_username_characters: z.number().meta({
description:
"The maximum number of characters allowed in a username.",
example: 30,
}),
/* Versia Server API extension */
- max_note_characters: z.number().openapi({
+ max_note_characters: z.number().meta({
description:
"The maximum number of characters allowed in an account's bio/note.",
example: 500,
}),
/* Versia Server API extension */
- avatar_limit: z.number().openapi({
+ avatar_limit: z.number().meta({
description:
"The maximum size of an avatar image, in bytes.",
example: 1048576,
}),
/* Versia Server API extension */
- header_limit: z.number().openapi({
+ header_limit: z.number().meta({
description:
"The maximum size of a header image, in bytes.",
example: 2097152,
@@ -197,206 +197,206 @@ export const Instance = z
/* Versia Server API extension */
fields: z
.object({
- max_fields: z.number().openapi({
+ max_fields: z.number().meta({
description:
"The maximum number of fields allowed per account.",
example: 4,
}),
- max_name_characters: z.number().openapi({
+ max_name_characters: z.number().meta({
description:
"The maximum number of characters allowed in a field name.",
example: 30,
}),
- max_value_characters: z.number().openapi({
+ max_value_characters: z.number().meta({
description:
"The maximum number of characters allowed in a field value.",
example: 100,
}),
})
- .openapi({
+ .meta({
description:
"Limits related to account fields.",
}),
})
- .openapi({ description: "Limits related to accounts." }),
+ .meta({ description: "Limits related to accounts." }),
statuses: z
.object({
- max_characters: z.number().openapi({
+ max_characters: z.number().meta({
description:
"The maximum number of allowed characters per status.",
example: 500,
}),
- max_media_attachments: z.number().openapi({
+ max_media_attachments: z.number().meta({
description:
"The maximum number of media attachments that can be added to a status.",
example: 4,
}),
- characters_reserved_per_url: z.number().openapi({
+ characters_reserved_per_url: z.number().meta({
description:
"Each URL in a status will be assumed to be exactly this many characters.",
example: 23,
}),
})
- .openapi({
+ .meta({
description: "Limits related to authoring statuses.",
}),
media_attachments: z
.object({
- supported_mime_types: z.array(z.string()).openapi({
+ supported_mime_types: z.array(z.string()).meta({
description:
"Contains MIME types that can be uploaded.",
example: ["image/jpeg", "image/png", "image/gif"],
}),
- description_limit: z.number().openapi({
+ description_limit: z.number().meta({
description:
"The maximum size of a description, in characters.",
example: 1500,
}),
- image_size_limit: z.number().openapi({
+ image_size_limit: z.number().meta({
description:
"The maximum size of any uploaded image, in bytes.",
example: 10485760,
}),
- image_matrix_limit: z.number().openapi({
+ image_matrix_limit: z.number().meta({
description:
"The maximum number of pixels (width times height) for image uploads.",
example: 16777216,
}),
- video_size_limit: z.number().openapi({
+ video_size_limit: z.number().meta({
description:
"The maximum size of any uploaded video, in bytes.",
example: 41943040,
}),
- video_frame_rate_limit: z.number().openapi({
+ video_frame_rate_limit: z.number().meta({
description:
"The maximum frame rate for any uploaded video.",
example: 60,
}),
- video_matrix_limit: z.number().openapi({
+ video_matrix_limit: z.number().meta({
description:
"The maximum number of pixels (width times height) for video uploads.",
example: 2304000,
}),
})
- .openapi({
+ .meta({
description:
"Hints for which attachments will be accepted.",
}),
/* Versia Server API extension */
emojis: z
.object({
- emoji_size_limit: z.number().openapi({
+ emoji_size_limit: z.number().meta({
description:
"The maximum size of an emoji image, in bytes.",
example: 1048576,
}),
- max_shortcode_characters: z.number().openapi({
+ max_shortcode_characters: z.number().meta({
description:
"The maximum number of characters allowed in an emoji shortcode.",
example: 30,
}),
- max_description_characters: z.number().openapi({
+ max_description_characters: z.number().meta({
description:
"The maximum number of characters allowed in an emoji description.",
example: 100,
}),
})
- .openapi({
+ .meta({
description: "Limits related to custom emojis.",
}),
polls: z
.object({
- max_options: z.number().openapi({
+ max_options: z.number().meta({
description:
"Each poll is allowed to have up to this many options.",
example: 4,
}),
- max_characters_per_option: z.number().openapi({
+ max_characters_per_option: z.number().meta({
description:
"Each poll option is allowed to have this many characters.",
example: 50,
}),
- min_expiration: z.number().openapi({
+ min_expiration: z.number().meta({
description:
"The shortest allowed poll duration, in seconds.",
example: 300,
}),
- max_expiration: z.number().openapi({
+ max_expiration: z.number().meta({
description:
"The longest allowed poll duration, in seconds.",
example: 2629746,
}),
})
- .openapi({ description: "Limits related to polls." }),
+ .meta({ description: "Limits related to polls." }),
translation: z
.object({
- enabled: z.boolean().openapi({
+ enabled: z.boolean().meta({
description:
"Whether the Translations API is available on this instance.",
example: true,
}),
})
- .openapi({ description: "Hints related to translation." }),
+ .meta({ description: "Hints related to translation." }),
})
- .openapi({
+ .meta({
description: "Configured values and limits for this website.",
}),
registrations: z
.object({
- enabled: z.boolean().openapi({
+ enabled: z.boolean().meta({
description: "Whether registrations are enabled.",
example: false,
}),
- approval_required: z.boolean().openapi({
+ approval_required: z.boolean().meta({
description:
"Whether registrations require moderator approval.",
example: false,
}),
- message: z.string().nullable().openapi({
+ message: z.string().nullable().meta({
description:
"A custom message to be shown when registrations are closed.",
}),
})
- .openapi({
+ .meta({
description: "Information about registering for this website.",
}),
api_versions: z
.object({
- mastodon: z.number().openapi({
+ mastodon: z.number().meta({
description:
"API version number that this server implements.",
example: 1,
}),
})
- .openapi({
+ .meta({
description:
"Information about which version of the API is implemented by this server.",
}),
contact: z
.object({
- email: z.string().email().openapi({
+ email: z.email().meta({
description:
"An email address that can be messaged regarding inquiries or issues.",
example: "contact@versia.social",
}),
- account: Account.nullable().openapi({
+ account: Account.nullable().meta({
description:
"An account that can be contacted regarding inquiries or issues.",
}),
})
- .openapi({
+ .meta({
description:
"Hints related to contacting a representative of the website.",
}),
- rules: z.array(Rule).openapi({
+ rules: z.array(Rule).meta({
description: "An itemized list of rules for this website.",
}),
/* Versia Server API extension */
sso: SSOConfig,
})
- .openapi({
+ .meta({
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Instance",
},
- ref: "Instance",
+ id: "Instance",
});
diff --git a/packages/client/schemas/marker.ts b/packages/client/schemas/marker.ts
index 74fe122e..99705921 100644
--- a/packages/client/schemas/marker.ts
+++ b/packages/client/schemas/marker.ts
@@ -1,27 +1,27 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { Id } from "./common.ts";
export const Marker = z
.object({
- last_read_id: Id.openapi({
+ last_read_id: Id.meta({
description: "The ID of the most recently viewed entity.",
example: "ead15c9d-8eda-4b2c-9546-ecbf851f001c",
}),
- version: z.number().openapi({
+ version: z.number().meta({
description:
"An incrementing counter, used for locking to prevent write conflicts.",
example: 462,
}),
- updated_at: z.string().datetime().openapi({
+ updated_at: z.iso.datetime().meta({
description: "The timestamp of when the marker was set.",
example: "2025-01-12T13:11:00Z",
}),
})
- .openapi({
+ .meta({
description:
"Represents the last read position within a user's timelines.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Marker",
},
- ref: "Marker",
+ id: "Marker",
});
diff --git a/packages/client/schemas/notification.ts b/packages/client/schemas/notification.ts
index 32f9318c..f7b3be22 100644
--- a/packages/client/schemas/notification.ts
+++ b/packages/client/schemas/notification.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { Account } from "./account.ts";
import { AccountWarning } from "./account-warning.ts";
import { Id } from "./common.ts";
@@ -7,7 +7,7 @@ import { Status } from "./status.ts";
export const Notification = z
.object({
- id: Id.openapi({
+ id: Id.meta({
description: "The ID of the notification in the database.",
example: "6405f495-da55-4ad7-b5d6-9a773360fc07",
}),
@@ -27,47 +27,46 @@ export const Notification = z
"severed_relationships",
"moderation_warning",
])
- .openapi({
+ .meta({
description:
"The type of event that resulted in the notification.",
example: "mention",
}),
- group_key: z.string().openapi({
+ group_key: z.string().meta({
description:
"Group key shared by similar notifications, to be used in the grouped notifications feature.",
example: "ungrouped-34975861",
}),
- created_at: z.string().datetime().openapi({
+ created_at: z.iso.datetime().meta({
description: "The timestamp of the notification.",
example: "2025-01-12T13:11:00Z",
}),
- account: Account.openapi({
+ account: Account.meta({
description:
"The account that performed the action that generated the notification.",
}),
- status: Status.optional().openapi({
+ status: Status.optional().meta({
description:
"Status that was the object of the notification. Attached when type of the notification is favourite, reblog, status, mention, poll, or update.",
}),
- report: Report.optional().openapi({
+ report: Report.optional().meta({
description:
"Report that was the object of the notification. Attached when type of the notification is admin.report.",
}),
- event: z.undefined().openapi({
+ event: z.undefined().meta({
description:
"Versia Server does not sever relationships, so this field is always empty.",
- type: "null",
}),
- moderation_warning: AccountWarning.optional().openapi({
+ moderation_warning: AccountWarning.optional().meta({
description:
"Moderation warning that caused the notification. Attached when type of the notification is moderation_warning.",
}),
})
- .openapi({
+ .meta({
description:
"Represents a notification of an event relevant to the user.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Notification",
},
- ref: "Notification",
+ id: "Notification",
});
diff --git a/packages/client/schemas/poll.ts b/packages/client/schemas/poll.ts
index a2643727..b3fe20f9 100644
--- a/packages/client/schemas/poll.ts
+++ b/packages/client/schemas/poll.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { Id } from "./common.ts";
import { CustomEmoji } from "./emoji.ts";
@@ -8,7 +8,7 @@ export const PollOption = z
.string()
.trim()
.min(1)
- .openapi({
+ .meta({
description: "The text value of the poll option.",
example: "yes",
externalDocs: {
@@ -20,7 +20,7 @@ export const PollOption = z
.int()
.nonnegative()
.nullable()
- .openapi({
+ .meta({
description:
"The total number of received votes for this option.",
example: 6,
@@ -29,41 +29,40 @@ export const PollOption = z
},
}),
})
- .openapi({
+ .meta({
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#Option",
},
- ref: "PollOption",
+ id: "PollOption",
});
export const Poll = z
.object({
- id: Id.openapi({
+ id: Id.meta({
description: "ID of the poll in the database.",
example: "d87d230f-e401-4282-80c7-2044ab989662",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#id",
},
}),
- expires_at: z
- .string()
+ expires_at: z.iso
.datetime()
.nullable()
- .openapi({
+ .meta({
description: "When the poll ends.",
example: "2025-01-07T14:11:00.000Z",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#expires_at",
},
}),
- expired: z.boolean().openapi({
+ expired: z.boolean().meta({
description: "Is the poll currently expired?",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#expired",
},
}),
- multiple: z.boolean().openapi({
+ multiple: z.boolean().meta({
description: "Does the poll allow multiple-choice answers?",
example: false,
externalDocs: {
@@ -74,7 +73,7 @@ export const Poll = z
.number()
.int()
.nonnegative()
- .openapi({
+ .meta({
description: "How many votes have been received.",
example: 6,
externalDocs: {
@@ -86,7 +85,7 @@ export const Poll = z
.int()
.nonnegative()
.nullable()
- .openapi({
+ .meta({
description:
"How many unique accounts have voted on a multiple-choice poll.",
example: 3,
@@ -94,13 +93,13 @@ export const Poll = z
url: "https://docs.joinmastodon.org/entities/Poll/#voters_count",
},
}),
- options: z.array(PollOption).openapi({
+ options: z.array(PollOption).meta({
description: "Possible answers for the poll.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#options",
},
}),
- emojis: z.array(CustomEmoji).openapi({
+ emojis: z.array(CustomEmoji).meta({
description: "Custom emoji to be used for rendering poll options.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#emojis",
@@ -109,7 +108,7 @@ export const Poll = z
voted: z
.boolean()
.optional()
- .openapi({
+ .meta({
description:
"When called with a user token, has the authorized user voted?",
example: true,
@@ -120,7 +119,7 @@ export const Poll = z
own_votes: z
.array(z.number().int())
.optional()
- .openapi({
+ .meta({
description:
"When called with a user token, which options has the authorized user chosen? Contains an array of index values for options.",
example: [0],
@@ -129,10 +128,10 @@ export const Poll = z
},
}),
})
- .openapi({
+ .meta({
description: "Represents a poll attached to a status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll",
},
- ref: "Poll",
+ id: "Poll",
});
diff --git a/packages/client/schemas/preferences.ts b/packages/client/schemas/preferences.ts
index 34fb609f..a5766ee4 100644
--- a/packages/client/schemas/preferences.ts
+++ b/packages/client/schemas/preferences.ts
@@ -1,23 +1,23 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { Source } from "./account.ts";
export const Preferences = z
.object({
- "posting:default:visibility": Source.shape.privacy.openapi({
+ "posting:default:visibility": Source.shape.privacy.meta({
description: "Default visibility for new posts.",
example: "public",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Preferences/#posting-default-visibility",
},
}),
- "posting:default:sensitive": Source.shape.sensitive.openapi({
+ "posting:default:sensitive": Source.shape.sensitive.meta({
description: "Default sensitivity flag for new posts.",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Preferences/#posting-default-sensitive",
},
}),
- "posting:default:language": Source.shape.language.nullable().openapi({
+ "posting:default:language": Source.shape.language.nullable().meta({
description: "Default language for new posts.",
example: null,
externalDocs: {
@@ -26,7 +26,7 @@ export const Preferences = z
}),
"reading:expand:media": z
.enum(["default", "show_all", "hide_all"])
- .openapi({
+ .meta({
description:
"Whether media attachments should be automatically displayed or blurred/hidden.",
example: "default",
@@ -34,7 +34,7 @@ export const Preferences = z
url: "https://docs.joinmastodon.org/entities/Preferences/#reading-expand-media",
},
}),
- "reading:expand:spoilers": z.boolean().openapi({
+ "reading:expand:spoilers": z.boolean().meta({
description: "Whether CWs should be expanded by default.",
example: false,
externalDocs: {
@@ -42,10 +42,10 @@ export const Preferences = z
},
}),
})
- .openapi({
+ .meta({
description: "Represents a user's preferences.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Preferences",
},
- ref: "Preferences",
+ id: "Preferences",
});
diff --git a/packages/client/schemas/privacy-policy.ts b/packages/client/schemas/privacy-policy.ts
index c057249f..4b3fb9da 100644
--- a/packages/client/schemas/privacy-policy.ts
+++ b/packages/client/schemas/privacy-policy.ts
@@ -1,19 +1,16 @@
-import { z } from "zod";
+import { z } from "zod/v4";
export const PrivacyPolicy = z
.object({
- updated_at: z
- .string()
- .datetime()
- .openapi({
- description:
- "A timestamp of when the privacy policy was last updated.",
- example: "2025-01-12T13:11:00Z",
- externalDocs: {
- url: "https://docs.joinmastodon.org/entities/PrivacyPolicy/#updated_at",
- },
- }),
- content: z.string().openapi({
+ updated_at: z.iso.datetime().meta({
+ description:
+ "A timestamp of when the privacy policy was last updated.",
+ example: "2025-01-12T13:11:00Z",
+ externalDocs: {
+ url: "https://docs.joinmastodon.org/entities/PrivacyPolicy/#updated_at",
+ },
+ }),
+ content: z.string().meta({
description: "The rendered HTML content of the privacy policy.",
example: "
Privacy Policy
None, good luck.
",
externalDocs: {
@@ -21,10 +18,10 @@ export const PrivacyPolicy = z
},
}),
})
- .openapi({
+ .meta({
description: "Represents the privacy policy of the instance.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PrivacyPolicy",
},
- ref: "PrivacyPolicy",
+ id: "PrivacyPolicy",
});
diff --git a/packages/client/schemas/pushsubscription.ts b/packages/client/schemas/pushsubscription.ts
index 5236ea7c..3ec03837 100644
--- a/packages/client/schemas/pushsubscription.ts
+++ b/packages/client/schemas/pushsubscription.ts
@@ -1,64 +1,64 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { Id } from "./common.ts";
export const WebPushSubscription = z
.object({
- id: Id.openapi({
+ id: Id.meta({
example: "24eb1891-accc-43b4-b213-478e37d525b4",
description: "The ID of the Web Push subscription in the database.",
}),
- endpoint: z.string().url().openapi({
+ endpoint: z.url().meta({
example: "https://yourdomain.example/listener",
description: "Where push alerts will be sent to.",
}),
alerts: z
.object({
- mention: z.boolean().optional().openapi({
+ mention: z.boolean().optional().meta({
example: true,
description: "Receive mention notifications?",
}),
- favourite: z.boolean().optional().openapi({
+ favourite: z.boolean().optional().meta({
example: true,
description: "Receive favourite notifications?",
}),
- reblog: z.boolean().optional().openapi({
+ reblog: z.boolean().optional().meta({
example: true,
description: "Receive reblog notifications?",
}),
- follow: z.boolean().optional().openapi({
+ follow: z.boolean().optional().meta({
example: true,
description: "Receive follow notifications?",
}),
- poll: z.boolean().optional().openapi({
+ poll: z.boolean().optional().meta({
example: false,
description: "Receive poll notifications?",
}),
- follow_request: z.boolean().optional().openapi({
+ follow_request: z.boolean().optional().meta({
example: false,
description: "Receive follow request notifications?",
}),
- status: z.boolean().optional().openapi({
+ status: z.boolean().optional().meta({
example: false,
description:
"Receive new subscribed account notifications?",
}),
- update: z.boolean().optional().openapi({
+ update: z.boolean().optional().meta({
example: false,
description: "Receive status edited notifications?",
}),
- "admin.sign_up": z.boolean().optional().openapi({
+ "admin.sign_up": z.boolean().optional().meta({
example: false,
description:
"Receive new user signup notifications? Must have a role with the appropriate permissions.",
}),
- "admin.report": z.boolean().optional().openapi({
+ "admin.report": z.boolean().optional().meta({
example: false,
description:
"Receive new report notifications? Must have a role with the appropriate permissions.",
}),
})
.default({})
- .openapi({
+ .meta({
example: {
mention: true,
favourite: true,
@@ -74,62 +74,57 @@ export const WebPushSubscription = z
description:
"Which alerts should be delivered to the endpoint.",
}),
- server_key: z.string().openapi({
+ server_key: z.string().meta({
example:
"BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
description: "The streaming server’s VAPID key.",
}),
})
- .openapi({ ref: "WebPushSubscription" });
+ .meta({ id: "WebPushSubscription" });
-export const WebPushSubscriptionInput = z
- .object({
- subscription: z.object({
- endpoint: z.string().url().openapi({
- example: "https://yourdomain.example/listener",
- description: "Where push alerts will be sent to.",
- }),
- keys: z
- .object({
- p256dh: z.string().base64url().openapi({
- description:
- "User agent public key. Base64url encoded string of a public key from a ECDH keypair using the prime256v1 curve.",
- example:
- "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoKCJeHCy69ywHcb3dAR/T8Sud5ljSFHJkuiR6it1ycqAjGTe5F1oZ0ef5QiMX/zdQ+d4jSKiO7RztIz+o/eGuQ==",
- }),
- auth: z.string().base64url().openapi({
- description:
- "Auth secret. Base64url encoded string of 16 bytes of random data.",
- example: "u67u09PXZW4ncK9l9mAXkA==",
- }),
- })
- .strict(),
+export const WebPushSubscriptionInput = z.strictObject({
+ subscription: z.object({
+ endpoint: z.url().meta({
+ example: "https://yourdomain.example/listener",
+ description: "Where push alerts will be sent to.",
}),
- policy: z
- .enum(["all", "followed", "follower", "none"])
- .default("all")
- .openapi({
+ keys: z.strictObject({
+ p256dh: z.base64url().meta({
description:
- "Specify whether to receive push notifications from all, followed, follower, or none users.",
+ "User agent public key. Base64url encoded string of a public key from a ECDH keypair using the prime256v1 curve.",
+ example:
+ "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoKCJeHCy69ywHcb3dAR/T8Sud5ljSFHJkuiR6it1ycqAjGTe5F1oZ0ef5QiMX/zdQ+d4jSKiO7RztIz+o/eGuQ==",
}),
- data: z
- .object({
- alerts: WebPushSubscription.shape.alerts,
- })
- .strict()
- .default({
- alerts: {
- mention: false,
- favourite: false,
- reblog: false,
- follow: false,
- poll: false,
- follow_request: false,
- status: false,
- update: false,
- "admin.sign_up": false,
- "admin.report": false,
- },
+ auth: z.base64url().meta({
+ description:
+ "Auth secret. Base64url encoded string of 16 bytes of random data.",
+ example: "u67u09PXZW4ncK9l9mAXkA==",
}),
- })
- .strict();
+ }),
+ }),
+ policy: z
+ .enum(["all", "followed", "follower", "none"])
+ .default("all")
+ .meta({
+ description:
+ "Specify whether to receive push notifications from all, followed, follower, or none users.",
+ }),
+ data: z
+ .strictObject({
+ alerts: WebPushSubscription.shape.alerts,
+ })
+ .default({
+ alerts: {
+ mention: false,
+ favourite: false,
+ reblog: false,
+ follow: false,
+ poll: false,
+ follow_request: false,
+ status: false,
+ update: false,
+ "admin.sign_up": false,
+ "admin.report": false,
+ },
+ }),
+});
diff --git a/packages/client/schemas/relationship.ts b/packages/client/schemas/relationship.ts
index f24c7cbe..f70707e2 100644
--- a/packages/client/schemas/relationship.ts
+++ b/packages/client/schemas/relationship.ts
@@ -1,75 +1,75 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { Id, iso631 } from "./common.ts";
export const Relationship = z
.object({
- id: Id.openapi({
+ id: Id.meta({
description: "The account ID.",
example: "51f34c31-c8c6-4dc2-9df1-3704fcdde9b6",
}),
- following: z.boolean().openapi({
+ following: z.boolean().meta({
description: "Are you following this user?",
example: true,
}),
- showing_reblogs: z.boolean().openapi({
+ showing_reblogs: z.boolean().meta({
description:
"Are you receiving this user’s boosts in your home timeline?",
example: true,
}),
- notifying: z.boolean().openapi({
+ notifying: z.boolean().meta({
description: "Have you enabled notifications for this user?",
example: false,
}),
- languages: z.array(iso631).openapi({
+ languages: z.array(iso631).meta({
description: "Which languages are you following from this user?",
example: ["en"],
}),
- followed_by: z.boolean().openapi({
+ followed_by: z.boolean().meta({
description: "Are you followed by this user?",
example: true,
}),
- blocking: z.boolean().openapi({
+ blocking: z.boolean().meta({
description: "Are you blocking this user?",
example: false,
}),
- blocked_by: z.boolean().openapi({
+ blocked_by: z.boolean().meta({
description: "Is this user blocking you?",
example: false,
}),
- muting: z.boolean().openapi({
+ muting: z.boolean().meta({
description: "Are you muting this user?",
example: false,
}),
- muting_notifications: z.boolean().openapi({
+ muting_notifications: z.boolean().meta({
description: "Are you muting notifications from this user?",
example: false,
}),
- requested: z.boolean().openapi({
+ requested: z.boolean().meta({
description: "Do you have a pending follow request for this user?",
example: false,
}),
- requested_by: z.boolean().openapi({
+ requested_by: z.boolean().meta({
description: "Has this user requested to follow you?",
example: false,
}),
- domain_blocking: z.boolean().openapi({
+ domain_blocking: z.boolean().meta({
description: "Are you blocking this user’s domain?",
example: false,
}),
- endorsed: z.boolean().openapi({
+ endorsed: z.boolean().meta({
description: "Are you featuring this user on your profile?",
example: false,
}),
- note: z.string().min(0).max(5000).trim().openapi({
+ note: z.string().min(0).max(5000).trim().meta({
description: "This user’s profile bio",
example: "they also like Kerbal Space Program",
}),
})
- .openapi({
+ .meta({
description:
"Represents the relationship between accounts, such as following / blocking / muting / etc.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Relationship",
},
- ref: "Relationship",
+ id: "Relationship",
});
diff --git a/packages/client/schemas/report.ts b/packages/client/schemas/report.ts
index adef106f..7c48a1d5 100644
--- a/packages/client/schemas/report.ts
+++ b/packages/client/schemas/report.ts
@@ -1,60 +1,60 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { Account } from "./account.ts";
import { Id } from "./common.ts";
export const Report = z
.object({
- id: Id.openapi({
+ id: Id.meta({
description: "The ID of the report in the database.",
example: "9b0cd757-324b-4ea6-beab-f6226e138886",
}),
- action_taken: z.boolean().openapi({
+ action_taken: z.boolean().meta({
description: "Whether an action was taken yet.",
example: false,
}),
- action_taken_at: z.string().datetime().nullable().openapi({
+ action_taken_at: z.iso.datetime().nullable().meta({
description: "When an action was taken against the report.",
example: null,
}),
- category: z.enum(["spam", "violation", "other"]).openapi({
+ category: z.enum(["spam", "violation", "other"]).meta({
description:
"The generic reason for the report. 'spam' = Unwanted or repetitive content, 'violation' = A specific rule was violated, 'other' = Some other reason.",
example: "spam",
}),
- comment: z.string().openapi({
+ comment: z.string().meta({
description: "The reason for the report.",
example: "Spam account",
}),
- forwarded: z.boolean().openapi({
+ forwarded: z.boolean().meta({
description: "Whether the report was forwarded to a remote domain.",
example: false,
}),
- created_at: z.string().datetime().openapi({
+ created_at: z.iso.datetime().meta({
description: "When the report was created.",
example: "2024-12-31T23:59:59.999Z",
}),
status_ids: z
.array(Id)
.nullable()
- .openapi({
+ .meta({
description:
"IDs of statuses that have been attached to this report for additional context.",
example: ["1abf027c-af03-46ff-8d17-9ee799a17ca7"],
}),
- rule_ids: z.array(z.string()).nullable().openapi({
+ rule_ids: z.array(z.string()).nullable().meta({
description:
"IDs of the rules that have been cited as a violation by this report.",
example: null,
}),
- target_account: Account.openapi({
+ target_account: Account.meta({
description: "The account that was reported.",
}),
})
- .openapi({
+ .meta({
description:
"Reports filed against users and/or statuses, to be taken action on by moderators.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Report",
},
- ref: "Report",
+ id: "Report",
});
diff --git a/packages/client/schemas/rule.ts b/packages/client/schemas/rule.ts
index 692a30a7..3131dc89 100644
--- a/packages/client/schemas/rule.ts
+++ b/packages/client/schemas/rule.ts
@@ -1,24 +1,24 @@
-import { z } from "zod";
+import { z } from "zod/v4";
export const Rule = z
.object({
- id: z.string().openapi({
+ id: z.string().meta({
description: "The identifier for the rule.",
example: "1",
}),
- text: z.string().openapi({
+ text: z.string().meta({
description: "The rule to be followed.",
example: "Do not spam pictures of skibidi toilet.",
}),
- hint: z.string().optional().openapi({
+ hint: z.string().optional().meta({
description: "Longer-form description of the rule.",
example: "Please, we beg you.",
}),
})
- .openapi({
+ .meta({
description: "Represents a rule that server users should follow.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Rule",
},
- ref: "Rule",
+ id: "Rule",
});
diff --git a/packages/client/schemas/search.ts b/packages/client/schemas/search.ts
index 46bd9664..cb9ffe7d 100644
--- a/packages/client/schemas/search.ts
+++ b/packages/client/schemas/search.ts
@@ -1,24 +1,24 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { Account } from "./account.ts";
import { Status } from "./status.ts";
import { Tag } from "./tag.ts";
export const Search = z
.object({
- accounts: z.array(Account).openapi({
+ accounts: z.array(Account).meta({
description: "Accounts which match the given query",
}),
- statuses: z.array(Status).openapi({
+ statuses: z.array(Status).meta({
description: "Statuses which match the given query",
}),
- hashtags: z.array(Tag).openapi({
+ hashtags: z.array(Tag).meta({
description: "Hashtags which match the given query",
}),
})
- .openapi({
+ .meta({
description: "Represents the results of a search.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Search",
},
- ref: "Search",
+ id: "Search",
});
diff --git a/packages/client/schemas/status.ts b/packages/client/schemas/status.ts
index 1165ebe5..50271f10 100644
--- a/packages/client/schemas/status.ts
+++ b/packages/client/schemas/status.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { Account } from "./account.ts";
import { Attachment } from "./attachment.ts";
import { PreviewCard } from "./card.ts";
@@ -11,28 +11,28 @@ import { NoteReaction } from "./versia.ts";
export const Mention = z
.object({
- id: Account.shape.id.openapi({
+ id: Account.shape.id.meta({
description: "The account ID of the mentioned user.",
example: "b9dcb548-bd4d-42af-8b48-3693e6d298e6",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Mention-id",
},
}),
- username: Account.shape.username.openapi({
+ username: Account.shape.username.meta({
description: "The username of the mentioned user.",
example: "lexi",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Mention-username",
},
}),
- url: Account.shape.url.openapi({
+ url: Account.shape.url.meta({
description: "The location of the mentioned user’s profile.",
example: "https://beta.versia.social/@lexi",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Mention-url",
},
}),
- acct: Account.shape.acct.openapi({
+ acct: Account.shape.acct.meta({
description:
"The webfinger acct: URI of the mentioned user. Equivalent to username for local users, or username@domain for remote users.",
example: "lexi@beta.versia.social",
@@ -41,64 +41,59 @@ export const Mention = z
},
}),
})
- .openapi({
+ .meta({
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Mention",
},
- ref: "Mention",
+ id: "Mention",
});
export const StatusSource = z
.object({
- id: Id.openapi({
+ id: Id.meta({
description: "ID of the status in the database.",
example: "c7db92a4-e472-4e94-a115-7411ee934ba1",
}),
- text: z.string().trim().openapi({
+ text: z.string().trim().meta({
description: "The plain text used to compose the status.",
example: "this is a status that will be edited",
}),
// min(0) because some masto-fe clients send empty spoiler_text
// when they don't want to set it.
- spoiler_text: z.string().trim().min(0).max(1024).openapi({
+ spoiler_text: z.string().trim().min(0).max(1024).meta({
description:
"The plain text used to compose the status’s subject or content warning.",
example: "",
}),
})
- .openapi({
+ .meta({
externalDocs: {
url: "https://docs.joinmastodon.org/entities/StatusSource",
},
- ref: "StatusSource",
+ id: "StatusSource",
});
-// Because Status has some recursive references, we need to define it like this
-const BaseStatus = z
+export const Status = z
.object({
- id: Id.openapi({
+ id: Id.meta({
description: "ID of the status in the database.",
example: "2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#id",
},
}),
- uri: z
- .string()
- .url()
- .openapi({
- description: "URI of the status used for federation.",
- example:
- "https://beta.versia.social/@lexi/2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
- externalDocs: {
- url: "https://docs.joinmastodon.org/entities/Status/#uri",
- },
- }),
+ uri: z.url().meta({
+ description: "URI of the status used for federation.",
+ example:
+ "https://beta.versia.social/@lexi/2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
+ externalDocs: {
+ url: "https://docs.joinmastodon.org/entities/Status/#uri",
+ },
+ }),
url: z
- .string()
.url()
.nullable()
- .openapi({
+ .meta({
description: "A link to the status’s HTML representation.",
example:
"https://beta.versia.social/@lexi/2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
@@ -106,20 +101,20 @@ const BaseStatus = z
url: "https://docs.joinmastodon.org/entities/Status/#url",
},
}),
- account: Account.openapi({
+ account: Account.meta({
description: "The account that authored this status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#account",
},
}),
- in_reply_to_id: Id.nullable().openapi({
+ in_reply_to_id: Id.nullable().meta({
description: "ID of the status being replied to.",
example: "c41c9fe9-919a-4d35-a921-d3e79a5c95f8",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#in_reply_to_id",
},
}),
- in_reply_to_account_id: Account.shape.id.nullable().openapi({
+ in_reply_to_account_id: Account.shape.id.nullable().meta({
description:
"ID of the account that authored the status being replied to.",
example: "7b9b3ec6-1013-4cc6-8902-94ad00cf2ccc",
@@ -128,35 +123,31 @@ const BaseStatus = z
},
}),
- content: z.string().openapi({
+ content: z.string().meta({
description: "HTML-encoded status content.",
example: "
hello world
",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#content",
},
}),
- created_at: z
- .string()
- .datetime()
- .openapi({
- description: "The date when this status was created.",
- example: "2025-01-07T14:11:00.000Z",
- externalDocs: {
- url: "https://docs.joinmastodon.org/entities/Status/#created_at",
- },
- }),
- edited_at: z
- .string()
+ created_at: z.iso.datetime().meta({
+ description: "The date when this status was created.",
+ example: "2025-01-07T14:11:00.000Z",
+ externalDocs: {
+ url: "https://docs.joinmastodon.org/entities/Status/#created_at",
+ },
+ }),
+ edited_at: z.iso
.datetime()
.nullable()
- .openapi({
+ .meta({
description: "Timestamp of when the status was last edited.",
example: "2025-01-07T14:11:00.000Z",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#edited_at",
},
}),
- emojis: z.array(CustomEmoji).openapi({
+ emojis: z.array(CustomEmoji).meta({
description:
"Custom emoji to be used when rendering status content.",
externalDocs: {
@@ -167,7 +158,7 @@ const BaseStatus = z
.number()
.int()
.nonnegative()
- .openapi({
+ .meta({
description: "How many replies this status has received.",
example: 1,
externalDocs: {
@@ -178,7 +169,7 @@ const BaseStatus = z
.number()
.int()
.nonnegative()
- .openapi({
+ .meta({
description: "How many boosts this status has received.",
example: 6,
externalDocs: {
@@ -189,14 +180,14 @@ const BaseStatus = z
.number()
.int()
.nonnegative()
- .openapi({
+ .meta({
description: "How many favourites this status has received.",
example: 11,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#favourites_count",
},
}),
- reblogged: zBoolean.optional().openapi({
+ reblogged: zBoolean.optional().meta({
description:
"If the current token has an authorized user: Have you boosted this status?",
example: false,
@@ -204,7 +195,7 @@ const BaseStatus = z
url: "https://docs.joinmastodon.org/entities/Status/#reblogged",
},
}),
- favourited: zBoolean.optional().openapi({
+ favourited: zBoolean.optional().meta({
description:
"If the current token has an authorized user: Have you favourited this status?",
example: true,
@@ -212,7 +203,7 @@ const BaseStatus = z
url: "https://docs.joinmastodon.org/entities/Status/#favourited",
},
}),
- muted: zBoolean.optional().openapi({
+ muted: zBoolean.optional().meta({
description:
"If the current token has an authorized user: Have you muted notifications for this status’s conversation?",
example: false,
@@ -220,14 +211,14 @@ const BaseStatus = z
url: "https://docs.joinmastodon.org/entities/Status/#muted",
},
}),
- sensitive: zBoolean.openapi({
+ sensitive: zBoolean.meta({
description: "Is this status marked as sensitive content?",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#sensitive",
},
}),
- spoiler_text: z.string().openapi({
+ spoiler_text: z.string().meta({
description:
"Subject or summary line, below which status content is collapsed until expanded.",
example: "lewd text",
@@ -235,41 +226,39 @@ const BaseStatus = z
url: "https://docs.joinmastodon.org/entities/Status/#spoiler_text",
},
}),
- visibility: z
- .enum(["public", "unlisted", "private", "direct"])
- .openapi({
- description: "Visibility of this status.",
- example: "public",
- externalDocs: {
- url: "https://docs.joinmastodon.org/entities/Status/#visibility",
- },
- }),
- media_attachments: z.array(Attachment).openapi({
+ visibility: z.enum(["public", "unlisted", "private", "direct"]).meta({
+ description: "Visibility of this status.",
+ example: "public",
+ externalDocs: {
+ url: "https://docs.joinmastodon.org/entities/Status/#visibility",
+ },
+ }),
+ media_attachments: z.array(Attachment).meta({
description: "Media that is attached to this status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#media_attachments",
},
}),
- mentions: z.array(Mention).openapi({
+ mentions: z.array(Mention).meta({
description: "Mentions of users within the status content.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#mentions",
},
}),
- tags: z.array(Tag).openapi({
+ tags: z.array(Tag).meta({
description: "Hashtags used within the status content.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#tags",
},
}),
- card: PreviewCard.nullable().openapi({
+ card: PreviewCard.nullable().meta({
description:
"Preview card for links included within status content.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#card",
},
}),
- poll: Poll.nullable().openapi({
+ poll: Poll.nullable().meta({
description: "The poll attached to the status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#poll",
@@ -277,7 +266,7 @@ const BaseStatus = z
}),
application: z
.object({
- name: z.string().openapi({
+ name: z.string().meta({
description:
"The name of the application that posted this status.",
externalDocs: {
@@ -285,10 +274,9 @@ const BaseStatus = z
},
}),
website: z
- .string()
.url()
.nullable()
- .openapi({
+ .meta({
description:
"The website associated with the application that posted this status.",
externalDocs: {
@@ -297,30 +285,41 @@ const BaseStatus = z
}),
})
.optional()
- .openapi({
+ .meta({
description: "The application used to post this status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#application",
},
}),
- language: iso631.nullable().openapi({
+ language: iso631.nullable().meta({
description: "Primary language of this status.",
example: "en",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#language",
},
}),
+ get reblog() {
+ return Status.nullable().meta({
+ description: "The status being reblogged.",
+ externalDocs: {
+ url: "https://docs.joinmastodon.org/entities/Status/#reblog",
+ },
+ });
+ },
+ get quote() {
+ return Status.nullable();
+ },
text: z
.string()
.nullable()
- .openapi({
+ .meta({
description:
"Plain-text source of a status. Returned instead of content when status is deleted, so the user may redraft from the source text without the client having to reverse-engineer the original text from the HTML content.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#text",
},
}),
- pinned: zBoolean.optional().openapi({
+ pinned: zBoolean.optional().meta({
description:
"If the current token has an authorized user: Have you pinned this status? Only appears if the status is pinnable.",
example: true,
@@ -328,8 +327,8 @@ const BaseStatus = z
url: "https://docs.joinmastodon.org/entities/Status/#pinned",
},
}),
- reactions: z.array(NoteReaction).openapi({}),
- bookmarked: zBoolean.optional().openapi({
+ reactions: z.array(NoteReaction).meta({}),
+ bookmarked: zBoolean.optional().meta({
description:
"If the current token has an authorized user: Have you bookmarked this status?",
example: false,
@@ -340,7 +339,7 @@ const BaseStatus = z
filtered: z
.array(FilterResult)
.optional()
- .openapi({
+ .meta({
description:
"If the current token has an authorized user: The filter and keywords that matched this status.",
externalDocs: {
@@ -348,35 +347,23 @@ const BaseStatus = z
},
}),
})
- .openapi({
- ref: "BaseStatus",
+ .meta({
+ id: "Status",
});
-export const Status = BaseStatus.extend({
- reblog: BaseStatus.nullable().openapi({
- description: "The status being reblogged.",
- externalDocs: {
- url: "https://docs.joinmastodon.org/entities/Status/#reblog",
- },
- }),
- quote: BaseStatus.nullable(),
-}).openapi({
- ref: "Status",
-});
-
export const ScheduledStatus = z
.object({
- id: Id.openapi({
+ id: Id.meta({
description: "ID of the scheduled status in the database.",
example: "2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
}),
- scheduled_at: z.string().datetime().openapi({
+ scheduled_at: z.iso.datetime().meta({
description: "When the status will be scheduled.",
example: "2025-01-07T14:11:00.000Z",
}),
media_attachments: Status.shape.media_attachments,
params: z.object({
- text: z.string().openapi({
+ text: z.string().meta({
description: "Text to be used as status content.",
example: "Hello, world!",
}),
@@ -384,7 +371,7 @@ export const ScheduledStatus = z
media_ids: z
.array(Id)
.nullable()
- .openapi({
+ .meta({
description:
"IDs of the MediaAttachments that will be attached to the status.",
example: ["1234567890", "1234567891"],
@@ -394,22 +381,22 @@ export const ScheduledStatus = z
visibility: Status.shape.visibility,
in_reply_to_id: Status.shape.in_reply_to_id,
/** Versia Server API Extension */
- quote_id: z.string().openapi({
+ quote_id: z.string().meta({
description: "ID of the status being quoted.",
example: "c5d62a13-f340-4e7d-8942-7fd14be688dc",
}),
language: Status.shape.language,
- scheduled_at: z.null().openapi({
+ scheduled_at: z.null().meta({
description:
"When the status will be scheduled. This will be null because the status is only scheduled once.",
example: null,
}),
- idempotency: z.string().nullable().openapi({
+ idempotency: z.string().nullable().meta({
description: "Idempotency key to prevent duplicate statuses.",
example: "1234567890",
}),
}),
})
- .openapi({
- ref: "ScheduledStatus",
+ .meta({
+ id: "ScheduledStatus",
});
diff --git a/packages/client/schemas/tag.ts b/packages/client/schemas/tag.ts
index b6cd1eba..cc91300f 100644
--- a/packages/client/schemas/tag.ts
+++ b/packages/client/schemas/tag.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
export const Tag = z
.object({
@@ -6,27 +6,24 @@ export const Tag = z
.string()
.min(1)
.max(128)
- .openapi({
+ .meta({
description: "The value of the hashtag after the # sign.",
example: "versia",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Tag-name",
},
}),
- url: z
- .string()
- .url()
- .openapi({
- description: "A link to the hashtag on the instance.",
- example: "https://beta.versia.social/tags/versia",
- externalDocs: {
- url: "https://docs.joinmastodon.org/entities/Status/#Tag-url",
- },
- }),
+ url: z.url().meta({
+ description: "A link to the hashtag on the instance.",
+ example: "https://beta.versia.social/tags/versia",
+ externalDocs: {
+ url: "https://docs.joinmastodon.org/entities/Status/#Tag-url",
+ },
+ }),
})
- .openapi({
+ .meta({
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Tag",
},
- ref: "Tag",
+ id: "Tag",
});
diff --git a/packages/client/schemas/token.ts b/packages/client/schemas/token.ts
index cc83657d..bbd80c79 100644
--- a/packages/client/schemas/token.ts
+++ b/packages/client/schemas/token.ts
@@ -1,30 +1,30 @@
-import { z } from "zod";
+import { z } from "zod/v4";
export const Token = z
.object({
- access_token: z.string().openapi({
+ access_token: z.string().meta({
description: "An OAuth token to be used for authorization.",
example: "ZA-Yj3aBD8U8Cm7lKUp-lm9O9BmDgdhHzDeqsY8tlL0",
}),
- token_type: z.string().openapi({
+ token_type: z.string().meta({
description: "The OAuth token type. Versia uses Bearer tokens.",
example: "Bearer",
}),
- scope: z.string().openapi({
+ scope: z.string().meta({
description:
"The OAuth scopes granted by this token, space-separated.",
example: "read write follow push",
}),
- created_at: z.number().nonnegative().openapi({
+ created_at: z.number().nonnegative().meta({
description: "When the token was generated. UNIX timestamp.",
example: 1573979017,
}),
})
- .openapi({
+ .meta({
description:
"Represents an OAuth token used for authenticating with the API and performing actions.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Token",
},
- ref: "Token",
+ id: "Token",
});
diff --git a/packages/client/schemas/tos.ts b/packages/client/schemas/tos.ts
index bf47ed98..36a1e96b 100644
--- a/packages/client/schemas/tos.ts
+++ b/packages/client/schemas/tos.ts
@@ -1,17 +1,17 @@
-import { z } from "zod";
+import { z } from "zod/v4";
export const TermsOfService = z
.object({
- updated_at: z.string().datetime().openapi({
+ updated_at: z.iso.datetime().meta({
description: "A timestamp of when the ToS was last updated.",
example: "2025-01-12T13:11:00Z",
}),
- content: z.string().openapi({
+ content: z.string().meta({
description: "The rendered HTML content of the ToS.",
example: "
ToS
None, have fun.
",
}),
})
- .openapi({
+ .meta({
description: "Represents the ToS of the instance.",
- ref: "TermsOfService",
+ id: "TermsOfService",
});
diff --git a/packages/client/schemas/versia.ts b/packages/client/schemas/versia.ts
index 4606bfe8..120d8c32 100644
--- a/packages/client/schemas/versia.ts
+++ b/packages/client/schemas/versia.ts
@@ -1,86 +1,78 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { Id } from "./common.ts";
import { RolePermission } from "./permissions.ts";
/* Versia Server API extension */
export const Role = z
.object({
- id: Id.openapi({
+ id: Id.meta({
description: "The role ID in the database.",
example: "b4a7e0f0-8f6a-479b-910b-9265c070d5bd",
}),
- name: z.string().min(1).max(128).trim().openapi({
+ name: z.string().min(1).max(128).trim().meta({
description: "The name of the role.",
example: "Moderator",
}),
- permissions: z
- .array(z.nativeEnum(RolePermission))
- .transform(
- // Deduplicate permissions
- (permissions) => Array.from(new Set(permissions)),
- )
- .default([])
- .openapi({
- description: "The permissions granted to the role.",
- example: [
- RolePermission.ManageEmojis,
- RolePermission.ManageAccounts,
- ],
- type: "array",
- }),
- priority: z.number().int().default(0).openapi({
+ permissions: z.array(z.enum(RolePermission)).meta({
+ description: "The permissions granted to the role.",
+ example: [
+ RolePermission.ManageEmojis,
+ RolePermission.ManageAccounts,
+ ],
+ }),
+ priority: z.number().int().meta({
description:
"Role priority. Higher priority roles allow overriding lower priority roles.",
example: 100,
}),
- description: z.string().min(0).max(1024).trim().optional().openapi({
+ description: z.string().min(0).max(1024).trim().optional().meta({
description: "Short role description.",
example: "Allows managing emojis and accounts.",
}),
- visible: z.boolean().default(true).openapi({
+ visible: z.boolean().default(true).meta({
description: "Whether the role should be shown in the UI.",
}),
- icon: z.string().url().optional().openapi({
+ icon: z.url().optional().meta({
description: "URL to the role icon.",
example: "https://example.com/role-icon.png",
}),
})
- .openapi({
+ .meta({
description:
"Information about a role in the system, as well as its permissions.",
- ref: "Role",
+ id: "Role",
});
/* Versia Server API extension */
export const NoteReaction = z
.object({
- name: z.string().min(1).trim().openapi({
+ name: z.string().min(1).trim().meta({
description: "Custom Emoji shortcode or Unicode emoji.",
example: "blobfox_coffee",
}),
- count: z.number().int().nonnegative().openapi({
+ count: z.number().int().nonnegative().meta({
description: "Number of users who reacted with this emoji.",
example: 5,
}),
- remote: z.boolean().openapi({
+ remote: z.boolean().meta({
description:
"Whether this reaction is from a remote instance (federated).",
example: false,
}),
- me: z.boolean().optional().openapi({
+ me: z.boolean().optional().meta({
description:
"Whether the current authenticated user reacted with this emoji.",
example: true,
}),
})
- .openapi({
+ .meta({
description: "Information about a reaction to a note.",
- ref: "NoteReaction",
+ id: "NoteReaction",
});
/* Versia Server API extension */
export const NoteReactionWithAccounts = NoteReaction.extend({
- account_ids: z.array(Id).openapi({
+ account_ids: z.array(Id).meta({
description: "Array of user IDs who reacted with this emoji.",
example: [
"1d0185bc-d949-4ff5-8a15-1d691b256489",
@@ -88,14 +80,14 @@ export const NoteReactionWithAccounts = NoteReaction.extend({
"1f0c4eb9-a742-4c82-96c9-697a39831cd1",
],
}),
-}).openapi({
+}).meta({
description: "Information about a reaction to a note with account IDs.",
- ref: "NoteReactionWithAccounts",
+ id: "NoteReactionWithAccounts",
});
/* Versia Server API extension */
export const SSOConfig = z.object({
- forced: z.boolean().openapi({
+ forced: z.boolean().meta({
description:
"If this is enabled, normal identifier/password login is disabled and login must be done through SSO.",
example: false,
@@ -103,21 +95,21 @@ export const SSOConfig = z.object({
providers: z
.array(
z.object({
- id: z.string().min(1).openapi({
+ id: z.string().min(1).meta({
description: "The ID of the provider.",
example: "google",
}),
- name: z.string().min(1).openapi({
+ name: z.string().min(1).meta({
description: "Human-readable provider name.",
example: "Google",
}),
- icon: z.string().url().optional().openapi({
+ icon: z.url().optional().meta({
description: "URL to the provider icon.",
example: "https://cdn.versia.social/google-icon.png",
}),
}),
)
- .openapi({
+ .meta({
description:
"An array of external OpenID Connect providers that users can link their accounts to.",
}),
@@ -126,32 +118,32 @@ export const SSOConfig = z.object({
/* Versia Server API extension */
export const Challenge = z
.object({
- id: Id.openapi({}).openapi({
+ id: Id.meta({}).meta({
description: "Challenge ID in the database.",
example: "b4a7e0f0-8f6a-479b-910b-9265c070d5bd",
}),
- algorithm: z.enum(["SHA-1", "SHA-256", "SHA-512"]).openapi({
+ algorithm: z.enum(["SHA-1", "SHA-256", "SHA-512"]).meta({
description: "Algorithm used to generate the challenge.",
example: "SHA-1",
}),
- challenge: z.string().openapi({
+ challenge: z.string().meta({
description: "Challenge to solve.",
example: "1234567890",
}),
- maxnumber: z.number().int().nonnegative().optional().openapi({
+ maxnumber: z.number().int().nonnegative().optional().meta({
description: "Maximum number to solve the challenge.",
example: 100,
}),
- salt: z.string().openapi({
+ salt: z.string().meta({
description: "Salt used to generate the challenge.",
example: "1234567890",
}),
- signature: z.string().openapi({
+ signature: z.string().meta({
description: "Signature of the challenge.",
example: "1234567890",
}),
})
- .openapi({
+ .meta({
description: "A cryptographic challenge to solve. Used for Captchas.",
- ref: "Challenge",
+ id: "Challenge",
});
diff --git a/packages/client/versia/client.ts b/packages/client/versia/client.ts
index 70bf94fb..bb085443 100644
--- a/packages/client/versia/client.ts
+++ b/packages/client/versia/client.ts
@@ -1,5 +1,5 @@
import { OAuth2Client } from "@badgateway/oauth2-client";
-import type { z } from "zod";
+import type { z } from "zod/v4";
import type { Account } from "../schemas/account.ts";
import type { CredentialApplication } from "../schemas/application.ts";
import type { Attachment } from "../schemas/attachment.ts";
diff --git a/packages/config/index.ts b/packages/config/index.ts
index 6d859989..31b17271 100644
--- a/packages/config/index.ts
+++ b/packages/config/index.ts
@@ -5,7 +5,7 @@ import { parseTOML } from "confbox";
import ISO6391 from "iso-639-1";
import { types as mimeTypes } from "mime-types";
import { generateVAPIDKeys } from "web-push";
-import { z } from "zod";
+import { z } from "zod/v4";
import { fromZodError } from "zod-validation-error";
export class ProxiableUrl extends URL {
@@ -133,11 +133,7 @@ export const sensitiveString = z
.refine(
(text) =>
text.startsWith("PATH:") ? fileFromPathString(text).exists() : true,
- (text) => ({
- message: `Path ${
- fileFromPathString(text).name
- } does not exist, is a directory or is not accessible`,
- }),
+ "Path does not exist, is a directory or is not accessible",
)
.transform((text) =>
text.startsWith("PATH:") ? fileFromPathString(text).text() : text,
@@ -149,9 +145,7 @@ export const filePathString = z
.transform((s) => file(s))
.refine(
(file) => file.exists(),
- (file) => ({
- message: `Path ${file.name} does not exist, is a directory or is not accessible`,
- }),
+ "Path does not exist, is a directory or is not accessible",
)
.transform(async (file) => ({
content: await file.text(),
@@ -181,8 +175,8 @@ export const keyPair = z
).toString("base64");
ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: `Public and private keys are not set. Here are generated keys for you to copy.\n\nPublic: ${publicKey}\nPrivate: ${privateKey}`,
+ code: "custom",
+ error: `Public and private keys are not set. Here are generated keys for you to copy.\n\nPublic: ${publicKey}\nPrivate: ${privateKey}`,
});
return z.NEVER;
@@ -201,8 +195,8 @@ export const keyPair = z
);
} catch {
ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: "Public key is invalid",
+ code: "custom",
+ error: "Public key is invalid",
});
return z.NEVER;
@@ -218,8 +212,8 @@ export const keyPair = z
);
} catch {
ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: "Private key is invalid",
+ code: "custom",
+ error: "Private key is invalid",
});
return z.NEVER;
@@ -242,8 +236,8 @@ export const vapidKeyPair = z
const keys = generateVAPIDKeys();
ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: `VAPID keys are not set. Here are generated keys for you to copy.\n\nPublic: ${keys.publicKey}\nPrivate: ${keys.privateKey}`,
+ code: "custom",
+ error: `VAPID keys are not set. Here are generated keys for you to copy.\n\nPublic: ${keys.publicKey}\nPrivate: ${keys.privateKey}`,
});
return z.NEVER;
@@ -268,8 +262,8 @@ export const hmacKey = sensitiveString.transform(async (text, ctx) => {
const base64 = Buffer.from(exported).toString("base64");
ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: `HMAC key is not set. Here is a generated key for you to copy: ${base64}`,
+ code: "custom",
+ error: `HMAC key is not set. Here is a generated key for you to copy: ${base64}`,
});
return z.NEVER;
@@ -288,8 +282,8 @@ export const hmacKey = sensitiveString.transform(async (text, ctx) => {
);
} catch {
ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: "HMAC key is invalid",
+ code: "custom",
+ error: "HMAC key is invalid",
});
return z.NEVER;
@@ -445,9 +439,7 @@ export const ConfigSchema = z
"When send_emails is enabled, SMTP configuration must be set",
),
media: z.strictObject({
- backend: z
- .nativeEnum(MediaBackendType)
- .default(MediaBackendType.Local),
+ backend: z.enum(MediaBackendType).default(MediaBackendType.Local),
uploads_path: z.string().min(1).default("uploads"),
conversion: z.strictObject({
convert_images: z.boolean().default(false),
@@ -498,33 +490,35 @@ export const ConfigSchema = z
.default(5_000_000),
disallowed_usernames: z
.array(regex)
- .default([
- "well-known",
- "about",
- "activities",
- "api",
- "auth",
- "dev",
- "inbox",
- "internal",
- "main",
- "media",
- "nodeinfo",
- "notice",
- "oauth",
- "objects",
- "proxy",
- "push",
- "registration",
- "relay",
- "settings",
- "status",
- "tag",
- "users",
- "web",
- "search",
- "mfa",
- ]),
+ .default(
+ [
+ "well-known",
+ "about",
+ "activities",
+ "api",
+ "auth",
+ "dev",
+ "inbox",
+ "internal",
+ "main",
+ "media",
+ "nodeinfo",
+ "notice",
+ "oauth",
+ "objects",
+ "proxy",
+ "push",
+ "registration",
+ "relay",
+ "settings",
+ "status",
+ "tag",
+ "users",
+ "web",
+ "search",
+ "mfa",
+ ].map((s) => new RegExp(`^${s}$`, "i")),
+ ),
max_field_count: z.number().int().default(10),
max_field_name_characters: z.number().int().default(1000),
max_field_value_characters: z.number().int().default(1000),
@@ -709,7 +703,6 @@ export const ConfigSchema = z
.describe("Primary instance languages. ISO 639-1 codes."),
contact: z.strictObject({
email: z
- .string()
.email()
.describe("Email to contact the instance administration"),
}),
@@ -735,13 +728,9 @@ export const ConfigSchema = z
keys: keyPair,
}),
permissions: z.strictObject({
- anonymous: z
- .array(z.nativeEnum(RolePermission))
- .default(DEFAULT_ROLES),
- default: z
- .array(z.nativeEnum(RolePermission))
- .default(DEFAULT_ROLES),
- admin: z.array(z.nativeEnum(RolePermission)).default(ADMIN_ROLES),
+ anonymous: z.array(z.enum(RolePermission)).default(DEFAULT_ROLES),
+ default: z.array(z.enum(RolePermission)).default(DEFAULT_ROLES),
+ admin: z.array(z.enum(RolePermission)).default(ADMIN_ROLES),
}),
logging: z.strictObject({
file: z
diff --git a/packages/config/package.json b/packages/config/package.json
index 3e053c12..7ef42bc5 100644
--- a/packages/config/package.json
+++ b/packages/config/package.json
@@ -20,7 +20,6 @@
"web-push": "catalog:",
"iso-639-1": "catalog:",
"mime-types": "catalog:",
- "@versia/client": "workspace:*",
- "zod-to-json-schema": "catalog:"
+ "@versia/client": "workspace:*"
}
}
diff --git a/packages/config/to-json-schema.ts b/packages/config/to-json-schema.ts
index 6c4cf474..38e8d246 100644
--- a/packages/config/to-json-schema.ts
+++ b/packages/config/to-json-schema.ts
@@ -1,6 +1,6 @@
-import { zodToJsonSchema } from "zod-to-json-schema";
+import * as z from "zod/v4";
import { ConfigSchema } from "./index.ts";
-const jsonSchema = zodToJsonSchema(ConfigSchema, {});
+const jsonSchema = z.toJSONSchema(ConfigSchema);
console.write(`${JSON.stringify(jsonSchema, null, 4)}\n`);
diff --git a/packages/kit/api-error.ts b/packages/kit/api-error.ts
index c4b89c16..8f63b97a 100644
--- a/packages/kit/api-error.ts
+++ b/packages/kit/api-error.ts
@@ -1,8 +1,8 @@
import type { ContentfulStatusCode } from "hono/utils/http-status";
import type { JSONObject } from "hono/utils/types";
import type { DescribeRouteOptions } from "hono-openapi";
-import { resolver } from "hono-openapi/zod";
-import { z } from "zod";
+import { resolver } from "hono-openapi";
+import { z } from "zod/v4";
/**
* API Error
diff --git a/packages/kit/api.ts b/packages/kit/api.ts
index a77309b9..f31e6a7f 100644
--- a/packages/kit/api.ts
+++ b/packages/kit/api.ts
@@ -1,4 +1,4 @@
-import type { Hook } from "@hono/zod-validator";
+import type { Hook } from "@hono/standard-validator";
import type { RolePermission } from "@versia/client/schemas";
import { config } from "@versia-server/config";
import { serverLogger } from "@versia-server/logging";
@@ -8,9 +8,9 @@ import { eq, type SQL } from "drizzle-orm";
import type { Context, Hono, MiddlewareHandler, ValidationTargets } from "hono";
import { every } from "hono/combine";
import { createMiddleware } from "hono/factory";
-import { validator } from "hono-openapi/zod";
+import { validator } from "hono-openapi";
import { type ParsedQs, parse } from "qs";
-import { type ZodAny, type ZodError, z } from "zod";
+import { type ZodAny, ZodError, z } from "zod/v4";
import { fromZodError } from "zod-validation-error";
import type { AuthData, HonoEnv } from "~/types/api";
import { ApiError } from "./api-error.ts";
@@ -31,9 +31,11 @@ export const handleZodError: Hook<
keyof ValidationTargets
> = (result, context): Response | undefined => {
if (!result.success) {
+ const issues = result.error as z.core.$ZodIssue[];
+
return context.json(
{
- error: fromZodError(result.error as ZodError).message,
+ error: fromZodError(new ZodError(issues)).message,
},
422,
);
@@ -204,7 +206,7 @@ type WithIdParam = {
* @returns MiddlewareHandler
*/
export const withNoteParam = every(
- validator("param", z.object({ id: z.string().uuid() }), handleZodError),
+ validator("param", z.object({ id: z.uuid() }), handleZodError),
createMiddleware<
HonoEnv & {
Variables: {
@@ -242,7 +244,7 @@ export const withNoteParam = every(
* @returns MiddlewareHandler
*/
export const withUserParam = every(
- validator("param", z.object({ id: z.string().uuid() }), handleZodError),
+ validator("param", z.object({ id: z.uuid() }), handleZodError),
createMiddleware<
HonoEnv & {
Variables: {
@@ -278,7 +280,7 @@ export const withUserParam = every(
* @returns
*/
export const withEmojiParam = every(
- validator("param", z.object({ id: z.string().uuid() }), handleZodError),
+ validator("param", z.object({ id: z.uuid() }), handleZodError),
createMiddleware<
HonoEnv & {
Variables: {
diff --git a/packages/kit/db/application.ts b/packages/kit/db/application.ts
index c2a8df76..15314c66 100644
--- a/packages/kit/db/application.ts
+++ b/packages/kit/db/application.ts
@@ -10,7 +10,7 @@ import {
inArray,
type SQL,
} from "drizzle-orm";
-import type { z } from "zod";
+import type { z } from "zod/v4";
import { db } from "../tables/db.ts";
import { Applications } from "../tables/schema.ts";
import { BaseInterface } from "./base.ts";
diff --git a/packages/kit/db/emoji.ts b/packages/kit/db/emoji.ts
index 5dfcccc0..e4a3fc08 100644
--- a/packages/kit/db/emoji.ts
+++ b/packages/kit/db/emoji.ts
@@ -16,7 +16,7 @@ import {
isNull,
type SQL,
} from "drizzle-orm";
-import type { z } from "zod";
+import type { z } from "zod/v4";
import { db } from "../tables/db.ts";
import { Emojis, type Instances, type Medias } from "../tables/schema.ts";
import { BaseInterface } from "./base.ts";
@@ -194,7 +194,7 @@ export class Emoji extends BaseInterface {
global: this.data.ownerId === null,
description:
this.media.data.content[this.media.getPreferredMimeType()]
- .description ?? null,
+ ?.description ?? null,
};
}
diff --git a/packages/kit/db/media.ts b/packages/kit/db/media.ts
index eb21ab3f..a680c262 100644
--- a/packages/kit/db/media.ts
+++ b/packages/kit/db/media.ts
@@ -16,7 +16,7 @@ import {
type SQL,
} from "drizzle-orm";
import sharp from "sharp";
-import type { z } from "zod";
+import type { z } from "zod/v4";
import { mimeLookup } from "@/content_types.ts";
import { getMediaHash } from "../../../classes/media/media-hasher.ts";
import { ApiError } from "../api-error.ts";
@@ -297,7 +297,7 @@ export class Media extends BaseInterface {
const content = await Media.fileToContentFormat(file, url, {
description:
this.data.content[Object.keys(this.data.content)[0]]
- .description || undefined,
+ ?.description || undefined,
});
await this.update({
@@ -319,7 +319,7 @@ export class Media extends BaseInterface {
remote: true,
description:
this.data.content[Object.keys(this.data.content)[0]]
- .description || undefined,
+ ?.description || undefined,
},
};
@@ -363,7 +363,7 @@ export class Media extends BaseInterface {
content[type] = {
...content[type],
...metadata,
- };
+ } as (typeof content)[keyof typeof content];
}
await this.update({
@@ -490,6 +490,14 @@ export class Media extends BaseInterface {
public toApiMeta(): z.infer {
const type = this.getPreferredMimeType();
const data = this.data.content[type];
+
+ if (!data) {
+ throw new ApiError(
+ 500,
+ `No content for type ${type} in attachment ${this.id}`,
+ );
+ }
+
const size =
data.width && data.height
? `${data.width}x${data.height}`
@@ -533,7 +541,7 @@ export class Media extends BaseInterface {
? new ProxiableUrl(thumbnailData.content).proxied
: null,
meta: this.toApiMeta(),
- description: data.description || null,
+ description: data?.description || null,
blurhash: this.data.blurhash,
};
}
diff --git a/packages/kit/db/note.ts b/packages/kit/db/note.ts
index bad30145..73589953 100644
--- a/packages/kit/db/note.ts
+++ b/packages/kit/db/note.ts
@@ -20,7 +20,7 @@ import {
} from "drizzle-orm";
import { htmlToText } from "html-to-text";
import { createRegExp, exactly, global } from "magic-regexp";
-import type { z } from "zod";
+import type { z } from "zod/v4";
import { mergeAndDeduplicate } from "@/lib.ts";
import { sanitizedHtmlStrip } from "@/sanitization";
import { versiaTextToHtml } from "../parsers.ts";
diff --git a/packages/kit/db/notification.ts b/packages/kit/db/notification.ts
index 2d954212..ecfc4205 100644
--- a/packages/kit/db/notification.ts
+++ b/packages/kit/db/notification.ts
@@ -7,7 +7,7 @@ import {
inArray,
type SQL,
} from "drizzle-orm";
-import type { z } from "zod";
+import type { z } from "zod/v4";
import { db } from "../tables/db.ts";
import { Notifications } from "../tables/schema.ts";
import { BaseInterface } from "./base.ts";
@@ -189,6 +189,7 @@ export class Notification extends BaseInterface<
created_at: new Date(this.data.createdAt).toISOString(),
id: this.data.id,
type: this.data.type,
+ event: undefined,
status: this.data.status
? await new Note(this.data.status).toApi(account)
: undefined,
diff --git a/packages/kit/db/pushsubscription.ts b/packages/kit/db/pushsubscription.ts
index 682dd496..2f7b3654 100644
--- a/packages/kit/db/pushsubscription.ts
+++ b/packages/kit/db/pushsubscription.ts
@@ -7,7 +7,7 @@ import {
inArray,
type SQL,
} from "drizzle-orm";
-import type { z } from "zod";
+import type { z } from "zod/v4";
import { db } from "../tables/db.ts";
import { PushSubscriptions, Tokens } from "../tables/schema.ts";
import { BaseInterface } from "./base.ts";
diff --git a/packages/kit/db/relationship.ts b/packages/kit/db/relationship.ts
index 8256cd85..4fd9b7ba 100644
--- a/packages/kit/db/relationship.ts
+++ b/packages/kit/db/relationship.ts
@@ -10,7 +10,7 @@ import {
type SQL,
sql,
} from "drizzle-orm";
-import { z } from "zod";
+import { z } from "zod/v4";
import { db } from "../tables/db.ts";
import { Relationships, Users } from "../tables/schema.ts";
import { BaseInterface } from "./base.ts";
diff --git a/packages/kit/db/role.ts b/packages/kit/db/role.ts
index ae4080a3..c813e6ac 100644
--- a/packages/kit/db/role.ts
+++ b/packages/kit/db/role.ts
@@ -12,7 +12,7 @@ import {
inArray,
type SQL,
} from "drizzle-orm";
-import type { z } from "zod";
+import type { z } from "zod/v4";
import { db } from "../tables/db.ts";
import { Roles, RoleToUsers } from "../tables/schema.ts";
import { BaseInterface } from "./base.ts";
diff --git a/packages/kit/db/token.ts b/packages/kit/db/token.ts
index 871e780b..fdfd9e43 100644
--- a/packages/kit/db/token.ts
+++ b/packages/kit/db/token.ts
@@ -7,7 +7,7 @@ import {
inArray,
type SQL,
} from "drizzle-orm";
-import type { z } from "zod";
+import type { z } from "zod/v4";
import { db } from "../tables/db.ts";
import { Tokens } from "../tables/schema.ts";
import type { Application } from "./application.ts";
diff --git a/packages/kit/db/user.ts b/packages/kit/db/user.ts
index af301652..30130e32 100644
--- a/packages/kit/db/user.ts
+++ b/packages/kit/db/user.ts
@@ -30,7 +30,7 @@ import {
sql,
} from "drizzle-orm";
import { htmlToText } from "html-to-text";
-import type { z } from "zod";
+import type { z } from "zod/v4";
import { getBestContentType } from "@/content_types";
import { randomString } from "@/math";
import type { HttpVerb, KnownEntity } from "~/types/api.ts";
diff --git a/packages/kit/example.ts b/packages/kit/example.ts
index 8ebb14cf..1b1119e6 100644
--- a/packages/kit/example.ts
+++ b/packages/kit/example.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { Hooks } from "./hooks.ts";
import { Plugin } from "./plugin.ts";
diff --git a/packages/kit/inbox-processor.ts b/packages/kit/inbox-processor.ts
index faced9f2..39d6983a 100644
--- a/packages/kit/inbox-processor.ts
+++ b/packages/kit/inbox-processor.ts
@@ -245,7 +245,8 @@ export class InboxProcessor {
// If note has a blocked word
if (
Object.values(note.content?.data ?? {})
- .flatMap((c) => c.content)
+ .flatMap((c) => c?.content)
+ .filter((content) => content !== undefined)
.some((content) =>
config.validation.filters.note_content.some((filter) =>
filter.test(content),
@@ -281,7 +282,8 @@ export class InboxProcessor {
if (
Object.values(user.bio?.data ?? {})
- .flatMap((c) => c.content)
+ .flatMap((c) => c?.content)
+ .filter((content) => content !== undefined)
.some((content) =>
config.validation.filters.bio.some((filter) =>
filter.test(content),
diff --git a/packages/kit/json-schema.ts b/packages/kit/json-schema.ts
index 333f9dcb..20d791c6 100644
--- a/packages/kit/json-schema.ts
+++ b/packages/kit/json-schema.ts
@@ -1,6 +1,6 @@
-import { zodToJsonSchema } from "zod-to-json-schema";
+import * as z from "zod/v4";
import { manifestSchema } from "./schema.ts";
-const jsonSchema = zodToJsonSchema(manifestSchema);
+const jsonSchema = z.toJSONSchema(manifestSchema);
console.write(`${JSON.stringify(jsonSchema, null, 4)}\n`);
diff --git a/packages/kit/package.json b/packages/kit/package.json
index 34e54d3d..0e15c802 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -39,7 +39,6 @@
"hono": "catalog:",
"mitt": "catalog:",
"zod": "catalog:",
- "zod-to-json-schema": "catalog:",
"zod-validation-error": "catalog:",
"chalk": "catalog:",
"@versia/client": "workspace:*",
@@ -52,7 +51,7 @@
"altcha-lib": "catalog:",
"hono-openapi": "catalog:",
"qs": "catalog:",
- "@hono/zod-validator": "catalog:",
+ "@hono/standard-validator": "catalog:",
"ioredis": "catalog:",
"linkify-html": "catalog:",
"markdown-it": "catalog:",
diff --git a/packages/kit/plugin.ts b/packages/kit/plugin.ts
index bd408022..22e7e780 100644
--- a/packages/kit/plugin.ts
+++ b/packages/kit/plugin.ts
@@ -1,6 +1,6 @@
import type { Hono, MiddlewareHandler } from "hono";
import { createMiddleware } from "hono/factory";
-import type { z } from "zod";
+import type { z } from "zod/v4";
import { fromZodError, type ZodError } from "zod-validation-error";
import type { HonoEnv } from "~/types/api";
import type { ServerHooks } from "./hooks.ts";
diff --git a/packages/kit/schema.ts b/packages/kit/schema.ts
index fb8eac32..473211a8 100644
--- a/packages/kit/schema.ts
+++ b/packages/kit/schema.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
export const manifestSchema = z.object({
// biome-ignore lint/style/useNamingConvention: JSON schema requires this to be $schema
@@ -15,8 +15,8 @@ export const manifestSchema = z.object({
.array(
z.object({
name: z.string().min(1).max(100),
- email: z.string().email().optional(),
- url: z.string().url().optional(),
+ email: z.email().optional(),
+ url: z.url().optional(),
}),
)
.optional(),
@@ -47,7 +47,7 @@ export const manifestSchema = z.object({
"other",
])
.optional(),
- url: z.string().url().optional(),
+ url: z.url().optional(),
})
.optional(),
});
diff --git a/packages/kit/tables/schema.ts b/packages/kit/tables/schema.ts
index de246312..7f0c8e17 100644
--- a/packages/kit/tables/schema.ts
+++ b/packages/kit/tables/schema.ts
@@ -25,7 +25,7 @@ import {
uniqueIndex,
uuid,
} from "drizzle-orm/pg-core";
-import type { z } from "zod";
+import type { z } from "zod/v4";
const createdAt = () =>
timestamp("created_at", { precision: 3, mode: "string" })
diff --git a/packages/sdk/entities/collection.ts b/packages/sdk/entities/collection.ts
index a1093b29..acf9a191 100644
--- a/packages/sdk/entities/collection.ts
+++ b/packages/sdk/entities/collection.ts
@@ -1,4 +1,4 @@
-import type { z } from "zod";
+import type { z } from "zod/v4";
import {
CollectionSchema,
URICollectionSchema,
diff --git a/packages/sdk/entities/contentformat.ts b/packages/sdk/entities/contentformat.ts
index 93999f1c..69e4760e 100644
--- a/packages/sdk/entities/contentformat.ts
+++ b/packages/sdk/entities/contentformat.ts
@@ -1,4 +1,4 @@
-import type { z } from "zod";
+import type { z } from "zod/v4";
import {
AudioContentFormatSchema,
ContentFormatSchema,
diff --git a/packages/sdk/entities/delete.ts b/packages/sdk/entities/delete.ts
index 6df3d896..717ba2bc 100644
--- a/packages/sdk/entities/delete.ts
+++ b/packages/sdk/entities/delete.ts
@@ -1,4 +1,4 @@
-import type { z } from "zod";
+import type { z } from "zod/v4";
import { DeleteSchema } from "../schemas/delete.ts";
import type { JSONObject } from "../types.ts";
import { Entity } from "./entity.ts";
diff --git a/packages/sdk/entities/extensions/likes.ts b/packages/sdk/entities/extensions/likes.ts
index f8db6885..d9c93dae 100644
--- a/packages/sdk/entities/extensions/likes.ts
+++ b/packages/sdk/entities/extensions/likes.ts
@@ -1,4 +1,4 @@
-import type { z } from "zod";
+import type { z } from "zod/v4";
import { DislikeSchema, LikeSchema } from "../../schemas/extensions/likes.ts";
import type { JSONObject } from "../../types.ts";
import { Entity } from "../entity.ts";
diff --git a/packages/sdk/entities/extensions/polls.ts b/packages/sdk/entities/extensions/polls.ts
index 995e324d..6b32b26c 100644
--- a/packages/sdk/entities/extensions/polls.ts
+++ b/packages/sdk/entities/extensions/polls.ts
@@ -1,4 +1,4 @@
-import type { z } from "zod";
+import type { z } from "zod/v4";
import { VoteSchema } from "../../schemas/extensions/polls.ts";
import type { JSONObject } from "../../types.ts";
import { Entity } from "../entity.ts";
diff --git a/packages/sdk/entities/extensions/reactions.ts b/packages/sdk/entities/extensions/reactions.ts
index 5f3f785e..8ce921cf 100644
--- a/packages/sdk/entities/extensions/reactions.ts
+++ b/packages/sdk/entities/extensions/reactions.ts
@@ -1,4 +1,4 @@
-import type { z } from "zod";
+import type { z } from "zod/v4";
import { ReactionSchema } from "../../schemas/extensions/reactions.ts";
import type { JSONObject } from "../../types.ts";
import { Entity } from "../entity.ts";
diff --git a/packages/sdk/entities/extensions/reports.ts b/packages/sdk/entities/extensions/reports.ts
index d15cfe48..faa79532 100644
--- a/packages/sdk/entities/extensions/reports.ts
+++ b/packages/sdk/entities/extensions/reports.ts
@@ -1,4 +1,4 @@
-import type { z } from "zod";
+import type { z } from "zod/v4";
import { ReportSchema } from "../../schemas/extensions/reports.ts";
import type { JSONObject } from "../../types.ts";
import { Entity } from "../entity.ts";
diff --git a/packages/sdk/entities/extensions/share.ts b/packages/sdk/entities/extensions/share.ts
index f3817744..617a4308 100644
--- a/packages/sdk/entities/extensions/share.ts
+++ b/packages/sdk/entities/extensions/share.ts
@@ -1,4 +1,4 @@
-import type { z } from "zod";
+import type { z } from "zod/v4";
import { ShareSchema } from "../../schemas/extensions/share.ts";
import type { JSONObject } from "../../types.ts";
import { Entity } from "../entity.ts";
diff --git a/packages/sdk/entities/follow.ts b/packages/sdk/entities/follow.ts
index d38d9728..149b10bc 100644
--- a/packages/sdk/entities/follow.ts
+++ b/packages/sdk/entities/follow.ts
@@ -1,4 +1,4 @@
-import type { z } from "zod";
+import type { z } from "zod/v4";
import {
FollowAcceptSchema,
FollowRejectSchema,
diff --git a/packages/sdk/entities/instancemetadata.ts b/packages/sdk/entities/instancemetadata.ts
index d95f44ab..b17700c2 100644
--- a/packages/sdk/entities/instancemetadata.ts
+++ b/packages/sdk/entities/instancemetadata.ts
@@ -1,4 +1,4 @@
-import type { z } from "zod";
+import type { z } from "zod/v4";
import { InstanceMetadataSchema } from "../schemas/instance.ts";
import type { JSONObject } from "../types.ts";
import { ImageContentFormat } from "./contentformat.ts";
diff --git a/packages/sdk/entities/note.ts b/packages/sdk/entities/note.ts
index 7314e763..e995e94d 100644
--- a/packages/sdk/entities/note.ts
+++ b/packages/sdk/entities/note.ts
@@ -1,4 +1,4 @@
-import type { z } from "zod";
+import type { z } from "zod/v4";
import { NoteSchema } from "../schemas/note.ts";
import type { JSONObject } from "../types.ts";
import { NonTextContentFormat, TextContentFormat } from "./contentformat.ts";
diff --git a/packages/sdk/entities/user.ts b/packages/sdk/entities/user.ts
index 0d744632..b809aba1 100644
--- a/packages/sdk/entities/user.ts
+++ b/packages/sdk/entities/user.ts
@@ -1,4 +1,4 @@
-import type { z } from "zod";
+import type { z } from "zod/v4";
import { UserSchema } from "../schemas/user.ts";
import type { JSONObject } from "../types.ts";
import { ImageContentFormat, TextContentFormat } from "./contentformat.ts";
diff --git a/packages/sdk/schemas/collection.ts b/packages/sdk/schemas/collection.ts
index 92accefb..80758c63 100644
--- a/packages/sdk/schemas/collection.ts
+++ b/packages/sdk/schemas/collection.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { u64, url } from "./common.ts";
export const CollectionSchema = z.strictObject({
diff --git a/packages/sdk/schemas/common.ts b/packages/sdk/schemas/common.ts
index 63cc13ba..995ddc0e 100644
--- a/packages/sdk/schemas/common.ts
+++ b/packages/sdk/schemas/common.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
export const f64 = z
.number()
@@ -11,4 +11,4 @@ export const u64 = z
.nonnegative()
.max(2 ** 64 - 1);
-export const url = z.string().url();
+export const url = z.url();
diff --git a/packages/sdk/schemas/contentformat.ts b/packages/sdk/schemas/contentformat.ts
index 5605a9fc..c24f85b4 100644
--- a/packages/sdk/schemas/contentformat.ts
+++ b/packages/sdk/schemas/contentformat.ts
@@ -1,5 +1,5 @@
import { types } from "mime-types";
-import { z } from "zod";
+import { z } from "zod/v4";
import { f64, u64 } from "./common.ts";
const hashSizes = {
@@ -39,10 +39,10 @@ const audioMimeTypes = Object.values(types).filter((v) =>
v.startsWith("audio/"),
) as [string, ...string[]];
-export const ContentFormatSchema = z.record(
+export const ContentFormatSchema = z.partialRecord(
z.enum(allMimeTypes),
z.strictObject({
- content: z.string().or(z.string().url()),
+ content: z.string().or(z.url()),
remote: z.boolean(),
description: z.string().nullish(),
size: u64.nullish(),
@@ -64,9 +64,9 @@ export const ContentFormatSchema = z.record(
}),
);
-export const TextContentFormatSchema = z.record(
+export const TextContentFormatSchema = z.partialRecord(
z.enum(textMimeTypes),
- ContentFormatSchema.valueSchema
+ ContentFormatSchema.valueType
.pick({
content: true,
remote: true,
@@ -77,9 +77,9 @@ export const TextContentFormatSchema = z.record(
}),
);
-export const NonTextContentFormatSchema = z.record(
+export const NonTextContentFormatSchema = z.partialRecord(
z.enum(nonTextMimeTypes),
- ContentFormatSchema.valueSchema
+ ContentFormatSchema.valueType
.pick({
content: true,
remote: true,
@@ -91,27 +91,27 @@ export const NonTextContentFormatSchema = z.record(
height: true,
})
.extend({
- content: z.string().url(),
+ content: z.url(),
remote: z.literal(true),
}),
);
-export const ImageContentFormatSchema = z.record(
+export const ImageContentFormatSchema = z.partialRecord(
z.enum(imageMimeTypes),
- NonTextContentFormatSchema.valueSchema,
+ NonTextContentFormatSchema.valueType,
);
-export const VideoContentFormatSchema = z.record(
+export const VideoContentFormatSchema = z.partialRecord(
z.enum(videoMimeTypes),
- NonTextContentFormatSchema.valueSchema.extend({
- duration: ContentFormatSchema.valueSchema.shape.duration,
- fps: ContentFormatSchema.valueSchema.shape.fps,
+ NonTextContentFormatSchema.valueType.extend({
+ duration: ContentFormatSchema.valueType.shape.duration,
+ fps: ContentFormatSchema.valueType.shape.fps,
}),
);
-export const AudioContentFormatSchema = z.record(
+export const AudioContentFormatSchema = z.partialRecord(
z.enum(audioMimeTypes),
- NonTextContentFormatSchema.valueSchema.extend({
- duration: ContentFormatSchema.valueSchema.shape.duration,
+ NonTextContentFormatSchema.valueType.extend({
+ duration: ContentFormatSchema.valueType.shape.duration,
}),
);
diff --git a/packages/sdk/schemas/delete.ts b/packages/sdk/schemas/delete.ts
index 9cd7d9a8..1cbbd9c2 100644
--- a/packages/sdk/schemas/delete.ts
+++ b/packages/sdk/schemas/delete.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { url } from "./common.ts";
import { EntitySchema } from "./entity.ts";
diff --git a/packages/sdk/schemas/entity.ts b/packages/sdk/schemas/entity.ts
index 9a7ba8c9..3bd5738d 100644
--- a/packages/sdk/schemas/entity.ts
+++ b/packages/sdk/schemas/entity.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { isISOString } from "../regex.ts";
import { url } from "./common.ts";
import { CustomEmojiExtensionSchema } from "./extensions/emojis.ts";
@@ -12,7 +12,7 @@ export const ExtensionPropertySchema = z
export const EntitySchema = z.strictObject({
// biome-ignore lint/style/useNamingConvention: required for JSON schema
- $schema: z.string().url().nullish(),
+ $schema: z.url().nullish(),
id: z.string().max(512),
created_at: z
.string()
diff --git a/packages/sdk/schemas/extensions/emojis.ts b/packages/sdk/schemas/extensions/emojis.ts
index d075f9b4..cba32ee8 100644
--- a/packages/sdk/schemas/extensions/emojis.ts
+++ b/packages/sdk/schemas/extensions/emojis.ts
@@ -4,7 +4,7 @@
* @see module:federation/schemas/base
* @see https://versia.pub/extensions/custom-emojis
*/
-import { z } from "zod";
+import { z } from "zod/v4";
import { emojiRegex } from "../../regex.ts";
import { ImageContentFormatSchema } from "../contentformat.ts";
diff --git a/packages/sdk/schemas/extensions/groups.ts b/packages/sdk/schemas/extensions/groups.ts
index 8c246056..1fef7a79 100644
--- a/packages/sdk/schemas/extensions/groups.ts
+++ b/packages/sdk/schemas/extensions/groups.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { url } from "../common.ts";
import { TextContentFormatSchema } from "../contentformat.ts";
import { EntitySchema } from "../entity.ts";
diff --git a/packages/sdk/schemas/extensions/likes.ts b/packages/sdk/schemas/extensions/likes.ts
index 75208fb7..33c6d0c8 100644
--- a/packages/sdk/schemas/extensions/likes.ts
+++ b/packages/sdk/schemas/extensions/likes.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { url } from "../common.ts";
import { EntitySchema } from "../entity.ts";
diff --git a/packages/sdk/schemas/extensions/migration.ts b/packages/sdk/schemas/extensions/migration.ts
index da215ce3..8d276542 100644
--- a/packages/sdk/schemas/extensions/migration.ts
+++ b/packages/sdk/schemas/extensions/migration.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { url } from "../common.ts";
import { EntitySchema } from "../entity.ts";
diff --git a/packages/sdk/schemas/extensions/polls.ts b/packages/sdk/schemas/extensions/polls.ts
index 09741b8c..1fbbc800 100644
--- a/packages/sdk/schemas/extensions/polls.ts
+++ b/packages/sdk/schemas/extensions/polls.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { isISOString } from "../../regex.ts";
import { u64, url } from "../common.ts";
import { TextContentFormatSchema } from "../contentformat.ts";
diff --git a/packages/sdk/schemas/extensions/reactions.ts b/packages/sdk/schemas/extensions/reactions.ts
index 99b7162d..c2645588 100644
--- a/packages/sdk/schemas/extensions/reactions.ts
+++ b/packages/sdk/schemas/extensions/reactions.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { url } from "../common.ts";
import { EntitySchema } from "../entity.ts";
diff --git a/packages/sdk/schemas/extensions/reports.ts b/packages/sdk/schemas/extensions/reports.ts
index c49bc8db..8c6886aa 100644
--- a/packages/sdk/schemas/extensions/reports.ts
+++ b/packages/sdk/schemas/extensions/reports.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { url } from "../common.ts";
import { EntitySchema } from "../entity.ts";
diff --git a/packages/sdk/schemas/extensions/share.ts b/packages/sdk/schemas/extensions/share.ts
index 20968a08..a9acd249 100644
--- a/packages/sdk/schemas/extensions/share.ts
+++ b/packages/sdk/schemas/extensions/share.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { url } from "../common.ts";
import { EntitySchema } from "../entity.ts";
diff --git a/packages/sdk/schemas/extensions/vanity.ts b/packages/sdk/schemas/extensions/vanity.ts
index 6d47136a..e9e49c0a 100644
--- a/packages/sdk/schemas/extensions/vanity.ts
+++ b/packages/sdk/schemas/extensions/vanity.ts
@@ -5,7 +5,7 @@
* @see https://versia.pub/extensions/vanity
*/
-import { z } from "zod";
+import { z } from "zod/v4";
import { ianaTimezoneRegex, isISOString } from "../../regex.ts";
import { url } from "../common.ts";
import {
diff --git a/packages/sdk/schemas/follow.ts b/packages/sdk/schemas/follow.ts
index 62c89c32..1b9b8a0e 100644
--- a/packages/sdk/schemas/follow.ts
+++ b/packages/sdk/schemas/follow.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { url } from "./common.ts";
import { EntitySchema } from "./entity.ts";
diff --git a/packages/sdk/schemas/instance.ts b/packages/sdk/schemas/instance.ts
index e62648b5..0ed5b8f4 100644
--- a/packages/sdk/schemas/instance.ts
+++ b/packages/sdk/schemas/instance.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { extensionRegex, semverRegex } from "../regex.ts";
import { url } from "./common.ts";
import { ImageContentFormatSchema } from "./contentformat.ts";
diff --git a/packages/sdk/schemas/note.ts b/packages/sdk/schemas/note.ts
index 9d58beca..2afeacd6 100644
--- a/packages/sdk/schemas/note.ts
+++ b/packages/sdk/schemas/note.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { url } from "./common.ts";
import {
NonTextContentFormatSchema,
diff --git a/packages/sdk/schemas/user.ts b/packages/sdk/schemas/user.ts
index aeaf1256..763ca7b6 100644
--- a/packages/sdk/schemas/user.ts
+++ b/packages/sdk/schemas/user.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { url } from "./common.ts";
import {
ImageContentFormatSchema,
diff --git a/packages/sdk/schemas/webfinger.ts b/packages/sdk/schemas/webfinger.ts
index 524d2f00..3b97e2e2 100644
--- a/packages/sdk/schemas/webfinger.ts
+++ b/packages/sdk/schemas/webfinger.ts
@@ -1,4 +1,4 @@
-import { z } from "zod";
+import { z } from "zod/v4";
import { url } from "./common.ts";
export const WebFingerSchema = z.object({
diff --git a/types/api.ts b/types/api.ts
index 637f6d19..ce9736c2 100644
--- a/types/api.ts
+++ b/types/api.ts
@@ -4,7 +4,7 @@ import type { Application, Token, User } from "@versia-server/kit/db";
import type { SocketAddress } from "bun";
import type { Hono } from "hono";
import type { RouterRoute } from "hono/types";
-import type { z } from "zod";
+import type { z } from "zod/v4";
export type HttpVerb = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS";
diff --git a/utils/content_types.ts b/utils/content_types.ts
index 7d78e1c9..86f2423e 100644
--- a/utils/content_types.ts
+++ b/utils/content_types.ts
@@ -2,7 +2,7 @@ import type { ContentFormatSchema } from "@versia/sdk/schemas";
import { config } from "@versia-server/config";
import { htmlToText as htmlToTextLib } from "html-to-text";
import { lookup } from "mime-types";
-import type { z } from "zod";
+import type { z } from "zod/v4";
export const getBestContentType = (
content?: z.infer | null,
diff --git a/utils/rss.ts b/utils/rss.ts
index 31316119..1633dd57 100644
--- a/utils/rss.ts
+++ b/utils/rss.ts
@@ -70,11 +70,11 @@ export const getFeed = async (user: User, page = 0): Promise => {
url: image.getUrl().href,
title:
image.data.content[image.getPreferredMimeType()]
- .description ?? undefined,
+ ?.description ?? undefined,
type: image.getPreferredMimeType(),
length:
image.data.content[image.getPreferredMimeType()]
- .size ?? undefined,
+ ?.size ?? undefined,
}
: undefined,
video: video
@@ -82,14 +82,14 @@ export const getFeed = async (user: User, page = 0): Promise => {
url: video.getUrl().href,
title:
video.data.content[video.getPreferredMimeType()]
- .description ?? undefined,
+ ?.description ?? undefined,
type: video.getPreferredMimeType(),
duration:
video.data.content[video.getPreferredMimeType()]
- .duration ?? undefined,
+ ?.duration ?? undefined,
length:
video.data.content[video.getPreferredMimeType()]
- .size ?? undefined,
+ ?.size ?? undefined,
}
: undefined,
audio: audio
@@ -97,14 +97,14 @@ export const getFeed = async (user: User, page = 0): Promise => {
url: audio.getUrl().href,
title:
audio.data.content[audio.getPreferredMimeType()]
- .description ?? undefined,
+ ?.description ?? undefined,
type: audio.getPreferredMimeType(),
duration:
audio.data.content[audio.getPreferredMimeType()]
- .duration ?? undefined,
+ ?.duration ?? undefined,
length:
audio.data.content[audio.getPreferredMimeType()]
- .size ?? undefined,
+ ?.size ?? undefined,
}
: undefined,
});
diff --git a/utils/server.ts b/utils/server.ts
index 831c5122..fb4ba853 100644
--- a/utils/server.ts
+++ b/utils/server.ts
@@ -2,7 +2,7 @@ import type { ConfigSchema } from "@versia-server/config";
import { debugResponse } from "@versia-server/kit/api";
import { type Server, serve } from "bun";
import type { Hono } from "hono";
-import type { z } from "zod";
+import type { z } from "zod/v4";
import type { HonoEnv } from "~/types/api";
export const createServer = (