refactor: ⬆️ Upgrade to Zod v4 and hono-openapi 0.5.0

This commit is contained in:
Jesse Wierzbinski 2025-07-07 03:42:35 +02:00
parent add2429606
commit 24d4150da4
No known key found for this signature in database
209 changed files with 1331 additions and 1622 deletions

View file

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

View file

@ -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=="],

View file

@ -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:*",

View file

@ -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<Hono<HonoEnv>> => {
const app = new Hono<HonoEnv>({
@ -127,22 +125,23 @@ export const appFactory = async (): Promise<Hono<HonoEnv>> => {
(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",

View file

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

View file

@ -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:"

View file

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

View file

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

View file

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

View file

@ -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 => {

View file

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

View file

@ -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 => {

View file

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

View file

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

View file

@ -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 => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 accounts 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 accounts posts in all languages.",
example: ["en", "fr"],

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<typeof MarkerResponseSchema> = {
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.",
}),
})

View file

@ -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: {

View file

@ -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: {

View file

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

View file

@ -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<typeof Notification>[] = [];

View file

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

View file

@ -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<typeof Notification>[] = [];

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more