refactor(api): ♻️ Make SDK and client package only use resources in their own package
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 1s
Build Docker Images / lint (push) Failing after 7s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 1s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s

This commit is contained in:
Jesse Wierzbinski 2025-05-13 11:51:59 +02:00
parent c0060f1baf
commit 5dfcfc548f
No known key found for this signature in database
25 changed files with 256 additions and 260 deletions

View file

@ -1,50 +0,0 @@
name: Staging build bundle
on:
push:
branches: ["staging"]
jobs:
tests:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install NPM packages
run: |
bun install
- name: Build dist
run: |
bun run build
- name: Bundle
run: |
mkdir bundle
cp -r dist bundle/
cp -r config bundle/
cp -r docs bundle/
cp -r CODE_OF_CONDUCT.md bundle/
cp -r CONTRIBUTING.md bundle/
cp -r README.md bundle/
cp -r flake.nix bundle/
cp -r shell.nix bundle/
cp -r flake.lock bundle/
cp -r LICENSE bundle/
cp -r SECURITY.md bundle/
tar cfJ archive.tar.xz bundle/
- name: Upload
uses: actions/upload-artifact@v4
with:
name: staging-dist
path: archive.tar.xz

33
.github/workflows/test-publish.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: Test Publish
on:
push:
permissions:
contents: read
# For provenance generation
id-token: write
jobs:
# Build job
build:
runs-on: ubuntu-latest
strategy:
matrix:
package: ["sdk", "client"]
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Install
run: bun install
- name: Publish to NPM
run: bun publish --dry-run
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish to JSR
run: bunx jsr publish --allow-slow-types --allow-dirty --dry-run

View file

@ -52,17 +52,56 @@ export default apiRoute((app) =>
"json", "json",
z z
.object({ .object({
display_name: AccountSchema.shape.display_name.openapi({ display_name: AccountSchema.shape.display_name
description: "The display name to use for the profile.", .openapi({
description:
"The display name to use for the profile.",
example: "Lexi", example: "Lexi",
}), })
username: AccountSchema.shape.username.openapi({ .max(
config.validation.accounts
.max_displayname_characters,
)
.refine(
(s) =>
!config.validation.filters.displayname.some(
(filter) => filter.test(s),
),
"Display name contains blocked words",
),
username: AccountSchema.shape.username
.openapi({
description: "The username to use for the profile.", description: "The username to use for the profile.",
example: "lexi", example: "lexi",
}), })
note: AccountSchema.shape.note.openapi({ .max(config.validation.accounts.max_username_characters)
description: "The account bio. Markdown is supported.", .refine(
}), (s) =>
!config.validation.filters.username.some(
(filter) => filter.test(s),
),
"Username contains blocked words",
)
.refine(
(s) =>
!config.validation.accounts.disallowed_usernames.some(
(u) => u.test(s),
),
"Username is disallowed",
),
note: AccountSchema.shape.note
.openapi({
description:
"The account bio. Markdown is supported.",
})
.max(config.validation.accounts.max_bio_characters)
.refine(
(s) =>
!config.validation.filters.bio.some((filter) =>
filter.test(s),
),
"Bio contains blocked words",
),
avatar: z avatar: z
.string() .string()
.url() .url()
@ -150,10 +189,14 @@ export default apiRoute((app) =>
fields_attributes: z fields_attributes: z
.array( .array(
z.object({ z.object({
name: AccountSchema.shape.fields.element.shape name: AccountSchema.shape.fields.element.shape.name.max(
.name, config.validation.accounts
value: AccountSchema.shape.fields.element.shape .max_field_name_characters,
.value, ),
value: AccountSchema.shape.fields.element.shape.value.max(
config.validation.accounts
.max_field_value_characters,
),
}), }),
) )
.max(config.validation.accounts.max_field_count), .max(config.validation.accounts.max_field_count),

View file

@ -113,7 +113,9 @@ export default apiRoute((app) => {
"json", "json",
z z
.object({ .object({
shortcode: CustomEmojiSchema.shape.shortcode, shortcode: CustomEmojiSchema.shape.shortcode.max(
config.validation.emojis.max_shortcode_characters,
),
element: z element: z
.string() .string()
.url() .url()
@ -136,7 +138,12 @@ export default apiRoute((app) => {
), ),
), ),
category: CustomEmojiSchema.shape.category.optional(), category: CustomEmojiSchema.shape.category.optional(),
alt: CustomEmojiSchema.shape.description.optional(), alt: CustomEmojiSchema.shape.description
.unwrap()
.max(
config.validation.emojis.max_description_characters,
)
.optional(),
global: CustomEmojiSchema.shape.global.default(false), global: CustomEmojiSchema.shape.global.default(false),
}) })
.partial(), .partial(),

View file

@ -45,7 +45,9 @@ export default apiRoute((app) =>
validator( validator(
"json", "json",
z.object({ z.object({
shortcode: CustomEmojiSchema.shape.shortcode, shortcode: CustomEmojiSchema.shape.shortcode.max(
config.validation.emojis.max_shortcode_characters,
),
element: z element: z
.string() .string()
.url() .url()
@ -68,7 +70,10 @@ export default apiRoute((app) =>
), ),
), ),
category: CustomEmojiSchema.shape.category.optional(), category: CustomEmojiSchema.shape.category.optional(),
alt: CustomEmojiSchema.shape.description.optional(), alt: CustomEmojiSchema.shape.description
.unwrap()
.max(config.validation.emojis.max_description_characters)
.optional(),
global: CustomEmojiSchema.shape.global.default(false), global: CustomEmojiSchema.shape.global.default(false),
}), }),
handleZodError, handleZodError,

View file

@ -8,6 +8,7 @@ import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod"; import { z } from "zod";
import { apiRoute, auth, handleZodError } from "@/api"; import { apiRoute, auth, handleZodError } from "@/api";
import { ApiError } from "~/classes/errors/api-error"; import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/config";
export default apiRoute((app) => { export default apiRoute((app) => {
app.get( app.get(
@ -109,7 +110,10 @@ export default apiRoute((app) => {
description: description:
"The custom thumbnail of the media to be attached, encoded using multipart form data.", "The custom thumbnail of the media to be attached, encoded using multipart form data.",
}), }),
description: AttachmentSchema.shape.description, description: AttachmentSchema.shape.description
.unwrap()
.max(config.validation.media.max_description_characters)
.optional(),
focus: z.string().openapi({ focus: z.string().openapi({
description: description:
"Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0. Used for media cropping on clients.", "Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0. Used for media cropping on clients.",

View file

@ -8,6 +8,7 @@ import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod"; import { z } from "zod";
import { apiRoute, auth, handleZodError } from "@/api"; import { apiRoute, auth, handleZodError } from "@/api";
import { ApiError } from "~/classes/errors/api-error"; import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/config";
export default apiRoute((app) => export default apiRoute((app) =>
app.post( app.post(
@ -67,7 +68,10 @@ export default apiRoute((app) =>
description: description:
"The custom thumbnail of the media to be attached, encoded using multipart form data.", "The custom thumbnail of the media to be attached, encoded using multipart form data.",
}), }),
description: AttachmentSchema.shape.description.optional(), description: AttachmentSchema.shape.description
.unwrap()
.max(config.validation.media.max_description_characters)
.optional(),
focus: z focus: z
.string() .string()
.optional() .optional()

View file

@ -25,7 +25,17 @@ import * as VersiaEntities from "~/packages/sdk/entities";
const schema = z const schema = z
.object({ .object({
status: StatusSourceSchema.shape.text.optional().openapi({ status: StatusSourceSchema.shape.text
.max(config.validation.notes.max_characters)
.refine(
(s) =>
!config.validation.filters.note_content.some((filter) =>
filter.test(s),
),
"Status contains blocked words",
)
.optional()
.openapi({
description: description:
"The text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided.", "The text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided.",
}), }),
@ -54,7 +64,11 @@ const schema = z
}), }),
language: StatusSchema.shape.language.optional(), language: StatusSchema.shape.language.optional(),
"poll[options]": z "poll[options]": z
.array(PollOption.shape.title) .array(
PollOption.shape.title.max(
config.validation.polls.max_option_characters,
),
)
.max(config.validation.polls.max_options) .max(config.validation.polls.max_options)
.optional() .optional()
.openapi({ .openapi({

View file

@ -20,7 +20,17 @@ import * as VersiaEntities from "~/packages/sdk/entities";
const schema = z const schema = z
.object({ .object({
status: StatusSourceSchema.shape.text.optional().openapi({ status: StatusSourceSchema.shape.text
.max(config.validation.notes.max_characters)
.refine(
(s) =>
!config.validation.filters.note_content.some((filter) =>
filter.test(s),
),
"Status contains blocked words",
)
.optional()
.openapi({
description: description:
"The text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided.", "The text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided.",
}), }),
@ -49,7 +59,11 @@ const schema = z
}), }),
language: StatusSchema.shape.language.optional(), language: StatusSchema.shape.language.optional(),
"poll[options]": z "poll[options]": z
.array(PollOption.shape.title) .array(
PollOption.shape.title.max(
config.validation.polls.max_option_characters,
),
)
.max(config.validation.polls.max_options) .max(config.validation.polls.max_options)
.optional() .optional()
.openapi({ .openapi({

View file

@ -8,6 +8,7 @@ import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod"; import { z } from "zod";
import { apiRoute, auth, handleZodError } from "@/api"; import { apiRoute, auth, handleZodError } from "@/api";
import { ApiError } from "~/classes/errors/api-error"; import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/config";
export default apiRoute((app) => export default apiRoute((app) =>
app.post( app.post(
@ -76,7 +77,10 @@ export default apiRoute((app) =>
description: description:
"The custom thumbnail of the media to be attached, encoded using multipart form data.", "The custom thumbnail of the media to be attached, encoded using multipart form data.",
}), }),
description: AttachmentSchema.shape.description.optional(), description: AttachmentSchema.shape.description
.unwrap()
.max(config.validation.media.max_description_characters)
.optional(),
focus: z focus: z
.string() .string()
.optional() .optional()

View file

@ -3,6 +3,7 @@ import {
Id, Id,
RolePermission, RolePermission,
Search as SearchSchema, Search as SearchSchema,
userAddressRegex,
zBoolean, zBoolean,
} from "@versia/client/schemas"; } from "@versia/client/schemas";
import { db, Note, User } from "@versia/kit/db"; import { db, Note, User } from "@versia/kit/db";
@ -11,13 +12,7 @@ import { and, eq, inArray, isNull, sql } from "drizzle-orm";
import { describeRoute } from "hono-openapi"; import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod"; import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod"; import { z } from "zod";
import { import { apiRoute, auth, handleZodError, parseUserAddress } from "@/api";
apiRoute,
auth,
handleZodError,
parseUserAddress,
userAddressValidator,
} from "@/api";
import { ApiError } from "~/classes/errors/api-error"; import { ApiError } from "~/classes/errors/api-error";
import { searchManager } from "~/classes/search/search-manager"; import { searchManager } from "~/classes/search/search-manager";
import { config } from "~/config.ts"; import { config } from "~/config.ts";
@ -151,7 +146,7 @@ export default apiRoute((app) =>
if (!type || type === "accounts") { if (!type || type === "accounts") {
// Check if q is matching format username@domain.com or @username@domain.com // Check if q is matching format username@domain.com or @username@domain.com
const accountMatches = q?.trim().match(userAddressValidator); const accountMatches = q?.trim().match(userAddressRegex);
if (accountMatches) { if (accountMatches) {
// Remove leading @ if it exists // Remove leading @ if it exists
if (accountMatches[0].startsWith("@")) { if (accountMatches[0].startsWith("@")) {

View file

@ -92,6 +92,7 @@
"dependencies": { "dependencies": {
"@badgateway/oauth2-client": "^3.0.0", "@badgateway/oauth2-client": "^3.0.0",
"iso-639-1": "^3.1.5", "iso-639-1": "^3.1.5",
"magic-regexp": "^0.10.0",
"zod": "^3.24.2", "zod": "^3.24.2",
"zod-openapi": "^4.2.4", "zod-openapi": "^4.2.4",
}, },

View file

@ -2,8 +2,7 @@ import { type BunFile, env, file } from "bun";
import ISO6391 from "iso-639-1"; import ISO6391 from "iso-639-1";
import { types as mimeTypes } from "mime-types"; import { types as mimeTypes } from "mime-types";
import { generateVAPIDKeys } from "web-push"; import { generateVAPIDKeys } from "web-push";
import { ZodError, z } from "zod"; import { z } from "zod";
import { fromZodError } from "zod-validation-error";
import { ProxiableUrl } from "~/classes/media/url.ts"; import { ProxiableUrl } from "~/classes/media/url.ts";
import { RolePermission } from "~/packages/client/schemas/permissions.ts"; import { RolePermission } from "~/packages/client/schemas/permissions.ts";
@ -271,16 +270,6 @@ export const hmacKey = sensitiveString.transform(async (text, ctx) => {
return text; return text;
}); });
try {
console.info();
} catch (e) {
if (e instanceof ZodError) {
throw fromZodError(e);
}
throw e;
}
export const ConfigSchema = z export const ConfigSchema = z
.strictObject({ .strictObject({
postgres: z postgres: z

View file

@ -1,4 +1,8 @@
import type { CustomEmoji } from "@versia/client/schemas"; import {
type CustomEmoji,
emojiWithColonsRegex,
emojiWithIdentifiersRegex,
} from "@versia/client/schemas";
import { db, type Instance, Media } from "@versia/kit/db"; import { db, type Instance, Media } from "@versia/kit/db";
import { Emojis, type Instances, type Medias } from "@versia/kit/tables"; import { Emojis, type Instances, type Medias } from "@versia/kit/tables";
import { randomUUIDv7 } from "bun"; import { randomUUIDv7 } from "bun";
@ -13,7 +17,6 @@ import {
type SQL, type SQL,
} from "drizzle-orm"; } from "drizzle-orm";
import type { z } from "zod"; import type { z } from "zod";
import { emojiValidatorWithColons, emojiValidatorWithIdentifiers } from "@/api";
import * as VersiaEntities from "~/packages/sdk/entities/index.ts"; import * as VersiaEntities from "~/packages/sdk/entities/index.ts";
import type { ImageContentFormatSchema } from "~/packages/sdk/schemas/index.ts"; import type { ImageContentFormatSchema } from "~/packages/sdk/schemas/index.ts";
import { BaseInterface } from "./base.ts"; import { BaseInterface } from "./base.ts";
@ -162,7 +165,7 @@ export class Emoji extends BaseInterface<typeof Emojis, EmojiType> {
* @returns An array of emojis * @returns An array of emojis
*/ */
public static parseFromText(text: string): Promise<Emoji[]> { public static parseFromText(text: string): Promise<Emoji[]> {
const matches = text.match(emojiValidatorWithColons); const matches = text.match(emojiWithColonsRegex);
if (!matches || matches.length === 0) { if (!matches || matches.length === 0) {
return Promise.resolve([]); return Promise.resolve([]);
} }
@ -213,9 +216,8 @@ export class Emoji extends BaseInterface<typeof Emojis, EmojiType> {
instance: Instance, instance: Instance,
): Promise<Emoji> { ): Promise<Emoji> {
// Extracts the shortcode from the emoji name (e.g. :shortcode: -> shortcode) // Extracts the shortcode from the emoji name (e.g. :shortcode: -> shortcode)
const shortcode = [ const shortcode = [...emoji.name.matchAll(emojiWithIdentifiersRegex)][0]
...emoji.name.matchAll(emojiValidatorWithIdentifiers), .groups.shortcode;
][0].groups.shortcode;
if (!shortcode) { if (!shortcode) {
throw new Error("Could not extract shortcode from emoji name"); throw new Error("Could not extract shortcode from emoji name");

View file

@ -65,6 +65,7 @@
"dependencies": { "dependencies": {
"@badgateway/oauth2-client": "^3.0.0", "@badgateway/oauth2-client": "^3.0.0",
"iso-639-1": "^3.1.5", "iso-639-1": "^3.1.5",
"magic-regexp": "^0.10.0",
"zod": "^3.24.2", "zod": "^3.24.2",
"zod-openapi": "^4.2.4" "zod-openapi": "^4.2.4"
} }

48
packages/client/regex.ts Normal file
View file

@ -0,0 +1,48 @@
import {
anyOf,
caseInsensitive,
charIn,
charNotIn,
createRegExp,
digit,
exactly,
global,
letter,
maybe,
not,
oneOrMore,
} from "magic-regexp";
export const userAddressRegex = createRegExp(
maybe("@"),
oneOrMore(anyOf(letter.lowercase, digit, charIn("-_"))).groupedAs(
"username",
),
maybe(
exactly("@"),
oneOrMore(anyOf(letter, digit, charIn("_-.:"))).groupedAs("domain"),
),
[global],
);
export const emojiRegex = createRegExp(
// A-Z a-z 0-9 _ -
oneOrMore(letter.or(digit).or(charIn("_-"))),
[caseInsensitive, global],
);
export const emojiWithColonsRegex = createRegExp(
exactly(":"),
oneOrMore(letter.or(digit).or(charIn("_-"))),
exactly(":"),
[caseInsensitive, global],
);
export const emojiWithIdentifiersRegex = createRegExp(
exactly(
exactly(not.letter.or(not.digit).or(charNotIn("_-"))).times(1),
oneOrMore(letter.or(digit).or(charIn("_-"))).groupedAs("shortcode"),
exactly(not.letter.or(not.digit).or(charNotIn("_-"))).times(1),
),
[caseInsensitive, global],
);

View file

@ -1,3 +1,9 @@
export {
emojiRegex,
emojiWithColonsRegex,
emojiWithIdentifiersRegex,
userAddressRegex,
} from "./regex.ts";
export { Account, Field, Source } from "./schemas/account.ts"; export { Account, Field, Source } from "./schemas/account.ts";
export { AccountWarning } from "./schemas/account-warning.ts"; export { AccountWarning } from "./schemas/account-warning.ts";
export { Appeal } from "./schemas/appeal.ts"; export { Appeal } from "./schemas/appeal.ts";

View file

@ -1,6 +1,5 @@
import { z } from "zod"; import { z } from "zod";
import { userAddressValidator } from "@/api.ts"; import { userAddressRegex } from "../regex.ts";
import { config } from "~/config.ts";
import { iso631, zBoolean } from "./common.ts"; import { iso631, zBoolean } from "./common.ts";
import { CustomEmoji } from "./emoji.ts"; import { CustomEmoji } from "./emoji.ts";
import { Role } from "./versia.ts"; import { Role } from "./versia.ts";
@ -11,7 +10,6 @@ export const Field = z
.string() .string()
.trim() .trim()
.min(1) .min(1)
.max(config.validation.accounts.max_field_name_characters)
.openapi({ .openapi({
description: "The key of a given fields key-value pair.", description: "The key of a given fields key-value pair.",
example: "Freak level", example: "Freak level",
@ -23,7 +21,6 @@ export const Field = z
.string() .string()
.trim() .trim()
.min(1) .min(1)
.max(config.validation.accounts.max_field_value_characters)
.openapi({ .openapi({
description: "The value associated with the name key.", description: "The value associated with the name key.",
example: "<p>High</p>", example: "<p>High</p>",
@ -87,14 +84,6 @@ export const Source = z
.string() .string()
.trim() .trim()
.min(0) .min(0)
.max(config.validation.accounts.max_bio_characters)
.refine(
(s) =>
!config.validation.filters.bio.some((filter) =>
filter.test(s),
),
"Bio contains blocked words",
)
.openapi({ .openapi({
description: "Profile bio, in plain-text instead of in HTML.", description: "Profile bio, in plain-text instead of in HTML.",
example: "ermmm what the meow meow", example: "ermmm what the meow meow",
@ -102,10 +91,7 @@ export const Source = z
url: "https://docs.joinmastodon.org/entities/Account/#source-note", url: "https://docs.joinmastodon.org/entities/Account/#source-note",
}, },
}), }),
fields: z fields: z.array(Field).openapi({
.array(Field)
.max(config.validation.accounts.max_field_count)
.openapi({
description: "Metadata about the account.", description: "Metadata about the account.",
}), }),
}) })
@ -135,25 +121,10 @@ const BaseAccount = z
.string() .string()
.min(3) .min(3)
.trim() .trim()
.max(config.validation.accounts.max_username_characters)
.regex( .regex(
/^[a-z0-9_-]+$/, /^[a-z0-9_-]+$/,
"Username can only contain letters, numbers, underscores and hyphens", "Username can only contain letters, numbers, underscores and hyphens",
) )
.refine(
(s) =>
!config.validation.filters.username.some((filter) =>
filter.test(s),
),
"Username contains blocked words",
)
.refine(
(s) =>
!config.validation.accounts.disallowed_usernames.some((u) =>
u.test(s),
),
"Username is disallowed",
)
.openapi({ .openapi({
description: description:
"The username of the account, not including domain.", "The username of the account, not including domain.",
@ -166,7 +137,7 @@ const BaseAccount = z
.string() .string()
.min(1) .min(1)
.trim() .trim()
.regex(userAddressValidator, "Invalid user address") .regex(userAddressRegex, "Invalid user address")
.openapi({ .openapi({
description: description:
"The Webfinger account URI. Equal to username for local users, or username@domain for remote users.", "The Webfinger account URI. Equal to username for local users, or username@domain for remote users.",
@ -189,14 +160,6 @@ const BaseAccount = z
.string() .string()
.min(3) .min(3)
.trim() .trim()
.max(config.validation.accounts.max_displayname_characters)
.refine(
(s) =>
!config.validation.filters.displayname.some((filter) =>
filter.test(s),
),
"Display name contains blocked words",
)
.openapi({ .openapi({
description: "The profiles display name.", description: "The profiles display name.",
example: "Lexi :flower:", example: "Lexi :flower:",
@ -207,15 +170,7 @@ const BaseAccount = z
note: z note: z
.string() .string()
.min(0) .min(0)
.max(config.validation.accounts.max_bio_characters)
.trim() .trim()
.refine(
(s) =>
!config.validation.filters.bio.some((filter) =>
filter.test(s),
),
"Bio contains blocked words",
)
.openapi({ .openapi({
description: "The profiles bio or description.", description: "The profiles bio or description.",
example: "<p>ermmm what the meow meow</p>", example: "<p>ermmm what the meow meow</p>",
@ -279,10 +234,7 @@ const BaseAccount = z
url: "https://docs.joinmastodon.org/entities/Account/#locked", url: "https://docs.joinmastodon.org/entities/Account/#locked",
}, },
}), }),
fields: z fields: z.array(Field).openapi({
.array(Field)
.max(config.validation.accounts.max_field_count)
.openapi({
description: description:
"Additional metadata attached to a profile as name-value pairs.", "Additional metadata attached to a profile as name-value pairs.",
externalDocs: { externalDocs: {

View file

@ -1,5 +1,4 @@
import { z } from "zod"; import { z } from "zod";
import { config } from "~/config.ts";
import { Id } from "./common.ts"; import { Id } from "./common.ts";
export const Attachment = z export const Attachment = z
@ -51,12 +50,7 @@ export const Attachment = z
}, },
}, },
}), }),
description: z description: z.string().trim().nullable().openapi({
.string()
.trim()
.max(config.validation.media.max_description_characters)
.nullable()
.openapi({
description: description:
"Alternate text that describes what is in the media attachment, to be used for the visually impaired or when media attachments do not load.", "Alternate text that describes what is in the media attachment, to be used for the visually impaired or when media attachments do not load.",
example: "test media description", example: "test media description",

View file

@ -1,6 +1,5 @@
import { z } from "zod"; import { z } from "zod";
import { emojiValidator } from "@/api.ts"; import { emojiRegex } from "../regex.ts";
import { config } from "~/config.ts";
import { Id, zBoolean } from "./common.ts"; import { Id, zBoolean } from "./common.ts";
export const CustomEmoji = z export const CustomEmoji = z
@ -14,9 +13,8 @@ export const CustomEmoji = z
.string() .string()
.trim() .trim()
.min(1) .min(1)
.max(config.validation.emojis.max_shortcode_characters)
.regex( .regex(
emojiValidator, emojiRegex,
"Shortcode must only contain letters (any case), numbers, dashes or underscores.", "Shortcode must only contain letters (any case), numbers, dashes or underscores.",
) )
.openapi({ .openapi({
@ -76,7 +74,6 @@ export const CustomEmoji = z
/* Versia Server API extension */ /* Versia Server API extension */
description: z description: z
.string() .string()
.max(config.validation.emojis.max_description_characters)
.nullable() .nullable()
.openapi({ .openapi({
description: description:

View file

@ -1,5 +1,4 @@
import { z } from "zod"; import { z } from "zod";
import pkg from "~/package.json";
import { Account } from "./account.ts"; import { Account } from "./account.ts";
import { iso631 } from "./common.ts"; import { iso631 } from "./common.ts";
import { Rule } from "./rule.ts"; import { Rule } from "./rule.ts";
@ -48,7 +47,7 @@ export const Instance = z
source_url: z.string().url().openapi({ source_url: z.string().url().openapi({
description: description:
"The URL for the source code of the software running on this instance, in keeping with AGPL license requirements.", "The URL for the source code of the software running on this instance, in keeping with AGPL license requirements.",
example: pkg.repository.url, example: "https://github.com/versia-pub/server",
}), }),
description: z.string().openapi({ description: z.string().openapi({
description: description:

View file

@ -1,5 +1,4 @@
import { z } from "zod"; import { z } from "zod";
import { config } from "~/config.ts";
import { Id } from "./common.ts"; import { Id } from "./common.ts";
import { CustomEmoji } from "./emoji.ts"; import { CustomEmoji } from "./emoji.ts";
@ -9,7 +8,6 @@ export const PollOption = z
.string() .string()
.trim() .trim()
.min(1) .min(1)
.max(config.validation.polls.max_option_characters)
.openapi({ .openapi({
description: "The text value of the poll option.", description: "The text value of the poll option.",
example: "yes", example: "yes",

View file

@ -1,5 +1,4 @@
import { z } from "zod"; import { z } from "zod";
import { config } from "~/config.ts";
import { Account } from "./account.ts"; import { Account } from "./account.ts";
import { Attachment } from "./attachment.ts"; import { Attachment } from "./attachment.ts";
import { PreviewCard } from "./card.ts"; import { PreviewCard } from "./card.ts";
@ -55,18 +54,7 @@ export const StatusSource = z
description: "ID of the status in the database.", description: "ID of the status in the database.",
example: "c7db92a4-e472-4e94-a115-7411ee934ba1", example: "c7db92a4-e472-4e94-a115-7411ee934ba1",
}), }),
text: z text: z.string().trim().openapi({
.string()
.max(config.validation.notes.max_characters)
.trim()
.refine(
(s) =>
!config.validation.filters.note_content.some((filter) =>
filter.test(s),
),
"Status contains blocked words",
)
.openapi({
description: "The plain text used to compose the status.", description: "The plain text used to compose the status.",
example: "this is a status that will be edited", example: "this is a status that will be edited",
}), }),

View file

@ -1,5 +1,4 @@
import { z } from "zod"; import { z } from "zod";
import { config } from "~/config.ts";
import { Id } from "./common.ts"; import { Id } from "./common.ts";
import { RolePermission } from "./permissions.ts"; import { RolePermission } from "./permissions.ts";
@ -55,12 +54,7 @@ export const Role = z
/* Versia Server API extension */ /* Versia Server API extension */
export const NoteReaction = z export const NoteReaction = z
.object({ .object({
name: z name: z.string().min(1).trim().openapi({
.string()
.min(1)
.max(config.validation.emojis.max_shortcode_characters)
.trim()
.openapi({
description: "Custom Emoji shortcode or Unicode emoji.", description: "Custom Emoji shortcode or Unicode emoji.",
example: "blobfox_coffee", example: "blobfox_coffee",
}), }),

View file

@ -14,14 +14,12 @@ import {
anyOf, anyOf,
caseInsensitive, caseInsensitive,
charIn, charIn,
charNotIn,
createRegExp, createRegExp,
digit, digit,
exactly, exactly,
global, global,
letter, letter,
maybe, maybe,
not,
oneOrMore, oneOrMore,
} from "magic-regexp"; } from "magic-regexp";
import { type ParsedQs, parse } from "qs"; import { type ParsedQs, parse } from "qs";
@ -49,28 +47,6 @@ export const idValidator = createRegExp(
[caseInsensitive], [caseInsensitive],
); );
export const emojiValidator = createRegExp(
// A-Z a-z 0-9 _ -
oneOrMore(letter.or(digit).or(charIn("_-"))),
[caseInsensitive, global],
);
export const emojiValidatorWithColons = createRegExp(
exactly(":"),
oneOrMore(letter.or(digit).or(charIn("_-"))),
exactly(":"),
[caseInsensitive, global],
);
export const emojiValidatorWithIdentifiers = createRegExp(
exactly(
exactly(not.letter.or(not.digit).or(charNotIn("_-"))).times(1),
oneOrMore(letter.or(digit).or(charIn("_-"))).groupedAs("shortcode"),
exactly(not.letter.or(not.digit).or(charNotIn("_-"))).times(1),
),
[caseInsensitive, global],
);
export const mentionValidator = createRegExp( export const mentionValidator = createRegExp(
exactly("@"), exactly("@"),
oneOrMore(anyOf(letter.lowercase, digit, charIn("-_"))).groupedAs( oneOrMore(anyOf(letter.lowercase, digit, charIn("-_"))).groupedAs(
@ -83,28 +59,6 @@ export const mentionValidator = createRegExp(
[global], [global],
); );
export const userAddressValidator = createRegExp(
maybe("@"),
oneOrMore(anyOf(letter.lowercase, digit, charIn("-_"))).groupedAs(
"username",
),
maybe(
exactly("@"),
oneOrMore(anyOf(letter, digit, charIn("_-.:"))).groupedAs("domain"),
),
[global],
);
export const userAddressValidatorRemote = createRegExp(
maybe("@"),
oneOrMore(anyOf(letter.lowercase, digit, charIn("-_"))).groupedAs(
"username",
),
exactly("@"),
oneOrMore(anyOf(letter, digit, charIn("_-.:"))).groupedAs("domain"),
[global],
);
export const webfingerMention = createRegExp( export const webfingerMention = createRegExp(
exactly("acct:"), exactly("acct:"),
oneOrMore(anyOf(letter, digit, charIn("-_"))).groupedAs("username"), oneOrMore(anyOf(letter, digit, charIn("-_"))).groupedAs("username"),