+
+ This endpoint allows you to retrieve a paginated list of all your attachments (in a conversation if a conversation id is provided). By default, a maximum of ten attachments are shown per page.
+
+ ### Optional attributes
+
+
+
+ Limit to attachments from a given conversation.
+
+
+ Limit the number of attachments returned.
+
+
+
+
+
+
+ This endpoint allows you to upload a new attachment to a conversation. See the code examples for how to send the file to the Protocol API.
+
+ ### Required attributes
+
+
+
+ The file you want to add as an attachment.
+
+
+
+
+
+
+ This endpoint allows you to retrieve an attachment by providing the attachment id. Refer to [the list](#the-attachment-model) at the top of this page to see which properties are included with attachment objects.
+
+
+
+
+ This endpoint allows you to perform an update on an attachment. Currently, the only supported type of update is changing the filename.
+
+ ### Optional attributes
+
+
+
+ The new filename for the attachment.
+
+
+
+
+
+
+ This endpoint allows you to delete attachments. Note: This will permanently delete the file.
+
+
+
+
+
+
+ ```bash {{ title: 'cURL' }}
+ curl -X DELETE https://api.protocol.chat/v1/attachments/Nc6yKKMpcxiiFxp6 \
+ -H "Authorization: Bearer {token}"
+ ```
+
+ ```js
+ import ApiClient from '@example/protocol-api'
+
+ const client = new ApiClient(token)
+
+ await client.attachments.delete('Nc6yKKMpcxiiFxp6')
+ ```
+
+ ```python
+ from protocol_api import ApiClient
+
+ client = ApiClient(token)
+
+ client.attachments.delete("Nc6yKKMpcxiiFxp6")
+ ```
+
+ ```php
+ $client = new \Protocol\ApiClient($token);
+
+ $client->attachments->delete('Nc6yKKMpcxiiFxp6');
+ ```
+
+
+
+
+
diff --git a/app/authentication/page.mdx b/app/authentication/page.mdx
new file mode 100644
index 0000000..625f0af
--- /dev/null
+++ b/app/authentication/page.mdx
@@ -0,0 +1,41 @@
+export const metadata = {
+ title: 'Authentication',
+ description:
+ 'In this guide, we’ll look at how authentication works. Protocol offers two ways to authenticate your API requests: Basic authentication and OAuth2 with a token.',
+}
+
+# Authentication
+
+You'll need to authenticate your requests to access any of the endpoints in the Protocol API. In this guide, we'll look at how authentication works. Protocol offers two ways to authenticate your API requests: Basic authentication and OAuth2 with a token — OAuth2 is the recommended way. {{ className: 'lead' }}
+
+## Basic authentication
+
+With basic authentication, you use your username and password to authenticate your HTTP requests. Unless you have a very good reason, you probably shouldn't use basic auth. Here's how to authenticate using cURL:
+
+```bash {{ title: 'Example request with basic auth' }}
+curl https://api.protocol.chat/v1/conversations \
+ -u username:password
+```
+
+Please don't commit your Protocol password to GitHub!
+
+## OAuth2 with bearer token
+
+The recommended way to authenticate with the Protocol API is by using OAuth2. When establishing a connection using OAuth2, you will need your access token — you will find it in the [Protocol dashboard](#) under API settings. Here's how to add the token to the request header using cURL:
+
+```bash {{ title: 'Example request with bearer token' }}
+curl https://api.protocol.chat/v1/conversations \
+ -H "Authorization: Bearer {token}"
+```
+
+Always keep your token safe and reset it if you suspect it has been compromised.
+
+## Using an SDK
+
+If you use one of our official SDKs, you won't have to worry about any of the above — fetch your access token from the [Protocol dashboard](#) under API settings, and the client library will take care of the rest. All the client libraries use OAuth2 behind the scenes.
+
+
+
+
diff --git a/app/contacts/page.mdx b/app/contacts/page.mdx
new file mode 100644
index 0000000..b75afa9
--- /dev/null
+++ b/app/contacts/page.mdx
@@ -0,0 +1,394 @@
+export const metadata = {
+ title: 'Contacts',
+ description:
+ 'On this page, we’ll dive into the different contact endpoints you can use to manage contacts programmatically.',
+}
+
+# Contacts
+
+As the name suggests, contacts are a core part of Protocol — the very reason Protocol exists is so you can have secure conversations with your contacts. On this page, we'll dive into the different contact endpoints you can use to manage contacts programmatically. We'll look at how to query, create, update, and delete contacts. {{ className: 'lead' }}
+
+## The contact model
+
+The contact model contains all the information about your contacts, such as their username, avatar, and phone number. It also contains a reference to the conversation between you and the contact and information about when they were last active on Protocol.
+
+### Properties
+
+
+
+ Unique identifier for the contact.
+
+
+ The username for the contact.
+
+
+ The phone number for the contact.
+
+
+ The avatar image URL for the contact.
+
+
+ The contact display name in the contact list. By default, this is just the
+ username.
+
+
+ Unique identifier for the conversation associated with the contact.
+
+
+ Timestamp of when the contact was last active on the platform.
+
+
+ Timestamp of when the contact was created.
+
+
+
+---
+
+## List all contacts {{ tag: 'GET', label: '/v1/contacts' }}
+
+
+
+
+ This endpoint allows you to retrieve a paginated list of all your contacts. By default, a maximum of ten contacts are shown per page.
+
+ ### Optional attributes
+
+
+
+ Limit the number of contacts returned.
+
+
+
+
+
+
+ This endpoint allows you to add a new contact to your contact list in Protocol. To add a contact, you must provide their Protocol username and phone number.
+
+ ### Required attributes
+
+
+
+ The username for the contact.
+
+
+ The phone number for the contact.
+
+
+
+ ### Optional attributes
+
+
+
+ The avatar image URL for the contact.
+
+
+ The contact display name in the contact list. By default, this is just the username.
+
+
+
+
+
+
+ This endpoint allows you to retrieve a contact by providing their Protocol id. Refer to [the list](#the-contact-model) at the top of this page to see which properties are included with contact objects.
+
+
+
+
+ This endpoint allows you to perform an update on a contact. Currently, the only attribute that can be updated on contacts is the `display_name` attribute which controls how a contact appears in your contact list in Protocol.
+
+ ### Optional attributes
+
+
+
+ The contact display name in the contact list. By default, this is just the username.
+
+
+
+
+
+
+ This endpoint allows you to delete contacts from your contact list in Protocol. Note: This will also delete your conversation with the given contact.
+
+
+
+
+
+
+ ```bash {{ title: 'cURL' }}
+ curl -X DELETE https://api.protocol.chat/v1/contacts/WAz8eIbvDR60rouK \
+ -H "Authorization: Bearer {token}"
+ ```
+
+ ```js
+ import ApiClient from '@example/protocol-api'
+
+ const client = new ApiClient(token)
+
+ await client.contacts.delete('WAz8eIbvDR60rouK')
+ ```
+
+ ```python
+ from protocol_api import ApiClient
+
+ client = ApiClient(token)
+
+ client.contacts.delete("WAz8eIbvDR60rouK")
+ ```
+
+ ```php
+ $client = new \Protocol\ApiClient($token);
+
+ $client->contacts->delete('WAz8eIbvDR60rouK');
+ ```
+
+
+
+
+
diff --git a/app/conversations/page.mdx b/app/conversations/page.mdx
new file mode 100644
index 0000000..87fae52
--- /dev/null
+++ b/app/conversations/page.mdx
@@ -0,0 +1,407 @@
+export const metadata = {
+ title: 'Conversations',
+ description:
+ 'On this page, we’ll dive into the different conversation endpoints you can use to manage conversations programmatically.',
+}
+
+# Conversations
+
+Conversations are an essential part of Protocol — they are the containers for the messages between you, your contacts, and groups. On this page, we’ll dive into the different conversation endpoints you can use to manage conversations programmatically. We'll look at how to query, create, update, and delete conversations. {{ className: 'lead' }}
+
+## The conversation model
+
+The conversation model contains all the information about the conversations between you and your contacts. In addition, conversations can also be group-based with more than one contact, they can have a pinned message, and they can be muted.
+
+### Properties
+
+
+
+ Unique identifier for the conversation.
+
+
+ Unique identifier for the other contact in the conversation.
+
+
+ Unique identifier for the group that the conversation belongs to.
+
+
+ Unique identifier for the pinned message.
+
+
+ Whether or not the conversation has been pinned.
+
+
+ Whether or not the conversation has been muted.
+
+
+ Timestamp of when the conversation was last active.
+
+
+ Timestamp of when the conversation was last opened by the authenticated
+ user.
+
+
+ Timestamp of when the conversation was created.
+
+
+ Timestamp of when the conversation was archived.
+
+
+
+---
+
+## List all conversations {{ tag: 'GET', label: '/v1/conversations' }}
+
+
+
+
+ This endpoint allows you to retrieve a paginated list of all your conversations. By default, a maximum of ten conversations are shown per page.
+
+ ### Optional attributes
+
+
+
+ Limit the number of conversations returned.
+
+
+ Only show conversations that are muted when set to `true`.
+
+
+ Only show conversations that are archived when set to `true`.
+
+
+ Only show conversations that are pinned when set to `true`.
+
+
+ Only show conversations for the specified group.
+
+
+
+
+
+
+ This endpoint allows you to add a new conversation between you and a contact or group. A contact or group id is required to create a conversation.
+
+ ### Required attributes
+
+
+
+ Unique identifier for the other contact in the conversation.
+
+
+ Unique identifier for the group that the conversation belongs to.
+
+
+
+
+
+
+ This endpoint allows you to retrieve a conversation by providing the conversation id. Refer to [the list](#the-conversation-model) at the top of this page to see which properties are included with conversation objects.
+
+
+
+
+ This endpoint allows you to perform an update on a conversation. Examples of updates are pinning a message, muting or archiving the conversation, or pinning the conversation itself.
+
+ ### Optional attributes
+
+
+
+ Unique identifier for the pinned message.
+
+
+ Whether or not the conversation has been pinned.
+
+
+ Whether or not the conversation has been muted.
+
+
+ Timestamp of when the conversation was archived.
+
+
+
+
+
+
+ This endpoint allows you to delete your conversations in Protocol. Note: This will permanently delete the conversation and all its messages — archive it instead if you want to be able to restore it later.
+
+
+
+
+
+
+ ```bash {{ title: 'cURL' }}
+ curl -X DELETE https://api.protocol.chat/v1/conversations/xgQQXg3hrtjh7AvZ \
+ -H "Authorization: Bearer {token}"
+ ```
+
+ ```js
+ import ApiClient from '@example/protocol-api'
+
+ const client = new ApiClient(token)
+
+ await client.conversations.delete('xgQQXg3hrtjh7AvZ')
+ ```
+
+ ```python
+ from protocol_api import ApiClient
+
+ client = ApiClient(token)
+
+ client.conversations.delete("xgQQXg3hrtjh7AvZ")
+ ```
+
+ ```php
+ $client = new \Protocol\ApiClient($token);
+
+ $client->conversations->delete('xgQQXg3hrtjh7AvZ');
+ ```
+
+
+
+
+
diff --git a/app/errors/page.mdx b/app/errors/page.mdx
new file mode 100644
index 0000000..15f070b
--- /dev/null
+++ b/app/errors/page.mdx
@@ -0,0 +1,70 @@
+export const metadata = {
+ title: 'Errors',
+ description:
+ 'In this guide, we will talk about what happens when something goes wrong while you work with the API.',
+}
+
+# Errors
+
+In this guide, we will talk about what happens when something goes wrong while you work with the API. Mistakes happen, and mostly they will be yours, not ours. Let's look at some status codes and error types you might encounter. {{ className: 'lead' }}
+
+You can tell if your request was successful by checking the status code when receiving an API response. If a response comes back unsuccessful, you can use the error type and error message to figure out what has gone wrong and do some rudimentary debugging (before contacting support).
+
+
+ Before reaching out to support with an error, please be aware that 99% of all
+ reported errors are, in fact, user errors. Therefore, please carefully check
+ your code before contacting Protocol support.
+
+
+---
+
+## Status codes
+
+Here is a list of the different categories of status codes returned by the Protocol API. Use these to understand if a request was successful.
+
+
+
+ A 2xx status code indicates a successful response.
+
+
+ A 4xx status code indicates a client error — this means it's a _you_
+ problem.
+
+
+ A 5xx status code indicates a server error — you won't be seeing these.
+
+
+
+---
+
+## Error types
+
+
+
+
+ Whenever a request is unsuccessful, the Protocol API will return an error response with an error type and message. You can use this information to understand better what has gone wrong and how to fix it. Most of the error messages are pretty helpful and actionable.
+
+ Here is a list of the two error types supported by the Protocol API — use these to understand what you have done wrong.
+
+
+
+ This means that we made an error, which is highly speculative and unlikely.
+
+
+ This means that you made an error, which is much more likely.
+
+
+
+
+
+
+ ```bash {{ title: "Error response" }}
+ {
+ "type": "api_error",
+ "message": "No way this is happening!?",
+ "documentation_url": "https://protocol.chat/docs/errors/api_error"
+ }
+ ```
+
+
+
diff --git a/app/favicon.ico b/app/favicon.ico
new file mode 100644
index 0000000..2deafb7
Binary files /dev/null and b/app/favicon.ico differ
diff --git a/app/groups/page.mdx b/app/groups/page.mdx
new file mode 100644
index 0000000..e304787
--- /dev/null
+++ b/app/groups/page.mdx
@@ -0,0 +1,448 @@
+export const metadata = {
+ title: 'Groups',
+ description:
+ 'On this page, we’ll dive into the different group endpoints you can use to manage groups programmatically.',
+}
+
+# Groups
+
+Groups are where communities live in Protocol — they are a collection of contacts you're talking to all at once. On this page, we'll dive into the different group endpoints you can use to manage groups programmatically. We'll look at how to query, create, update, and delete groups. {{ className: 'lead' }}
+
+## The group model
+
+The group model contains all the information about your groups, including what contacts are in the group and the group's name, description, and avatar.
+
+### Properties
+
+
+
+ Unique identifier for the group.
+
+
+ The name for the group.
+
+
+ The description for the group.
+
+
+ The avatar image URL for the group.
+
+
+ Unique identifier for the conversation that belongs to the group.
+
+
+ An array of contact objects that are members of the group.
+
+
+ Timestamp of when the group was created.
+
+
+ Timestamp of when the group was archived.
+
+
+
+---
+
+## List all groups {{ tag: 'GET', label: '/v1/groups' }}
+
+
+
+
+ This endpoint allows you to retrieve a paginated list of all your groups. By default, a maximum of ten groups are shown per page.
+
+ ### Optional attributes
+
+
+
+ Limit the number of groups returned.
+
+
+ Only show groups that are archived when set to `true`.
+
+
+
+
+
+
+ This endpoint allows you to create a new group conversation between you and a group of your Protocol contacts.
+
+ ### Required attributes
+
+
+
+ The name for the group.
+
+
+
+ ### Optional attributes
+
+
+
+ The description for the group.
+
+
+ The avatar image URL for the group.
+
+
+ An array of contact objects that are members of the group.
+
+
+
+
+
+
+ This endpoint allows you to retrieve a group by providing the group id. Refer to [the list](#the-group-model) at the top of this page to see which properties are included with group objects.
+
+
+
+
+ This endpoint allows you to perform an update on a group. Examples of updates are changing the name, description, and avatar or adding and removing contacts from the group.
+
+ ### Optional attributes
+
+
+
+ The new name for the group.
+
+
+ The new description for the group.
+
+
+ The new avatar image URL for the group.
+
+
+ An array of contact objects that are members of the group.
+
+
+ Timestamp of when the group was archived.
+
+
+
+
+
+
+ This endpoint allows you to delete groups. Note: This will permanently delete the group, including the messages — archive it instead if you want to be able to restore it later.
+
+
+
+
+
+
+ );
+}
diff --git a/app/messages/page.mdx b/app/messages/page.mdx
new file mode 100644
index 0000000..e3cbca0
--- /dev/null
+++ b/app/messages/page.mdx
@@ -0,0 +1,441 @@
+export const metadata = {
+ title: 'Messages',
+ description:
+ 'On this page, we’ll dive into the different message endpoints you can use to manage messages programmatically.',
+}
+
+# Messages
+
+Messages are what conversations are made of in Protocol — they are the basic building blocks of your conversations with your Protocol contacts. On this page, we'll dive into the different message endpoints you can use to manage messages programmatically. We'll look at how to query, send, update, and delete messages. {{ className: 'lead' }}
+
+## The message model
+
+The message model contains all the information about the messages and attachments you send to your contacts and groups, including how your contacts have reacted to them.
+
+### Properties
+
+
+
+ Unique identifier for the message.
+
+
+ Unique identifier for the conversation the message belongs to.
+
+
+ The contact object for the contact who sent the message.
+
+
+ The message content.
+
+
+ An array of reaction objects associated with the message.
+
+
+ An array of attachment objects associated with the message.
+
+
+ Timestamp of when the message was read.
+
+
+ Timestamp of when the message was created.
+
+
+ Timestamp of when the message was last updated.
+
+
+
+---
+
+## List all messages {{ tag: 'GET', label: '/v1/messages' }}
+
+
+
+
+ This endpoint allows you to retrieve a paginated list of all your messages (in a conversation if a conversation id is provided). By default, a maximum of ten messages are shown per page.
+
+ ### Optional attributes
+
+
+
+ Limit to messages from a given conversation.
+
+
+ Limit the number of messages returned.
+
+
+
+
+
+
+ This endpoint allows you to send a new message to one of your conversations.
+
+ ### Required attributes
+
+
+
+ Unique identifier for the conversation the message belongs to.
+
+
+ The message content.
+
+
+
+ ### Optional attributes
+
+
+
+ An array of attachment objects associated with the message.
+
+
+
+
+
+
+ This endpoint allows you to retrieve a message by providing the message id. Refer to [the list](#the-message-model) at the top of this page to see which properties are included with message objects.
+
+
+
+
+ This endpoint allows you to perform an update on a message. Examples of updates are adding a reaction, editing the message, or adding an attachment.
+
+ ### Optional attributes
+
+
+
+ The message content.
+
+
+ An array of reaction objects associated with the message.
+
+
+ An array of attachment objects associated with the message.
+
+
+
+
+
+ Sorry, we couldn’t find the page you’re looking for.
+
+
+
+ >
+ );
+}
diff --git a/app/page.mdx b/app/page.mdx
new file mode 100644
index 0000000..d3999f7
--- /dev/null
+++ b/app/page.mdx
@@ -0,0 +1,43 @@
+import { Guides } from '@/components/Guides'
+import { Resources } from '@/components/Resources'
+import { HeroPattern } from '@/components/HeroPattern'
+
+export const metadata = {
+ title: 'API Documentation',
+ description:
+ 'Learn everything there is to know about the Protocol API and integrate Protocol into your product.',
+}
+
+export const sections = [
+ { title: 'Guides', id: 'guides' },
+ { title: 'Resources', id: 'resources' },
+]
+
+
+
+# API Documentation
+
+Use the Protocol API to access contacts, conversations, group messages, and more and seamlessly integrate your product into the workflows of dozens of devoted Protocol users. {{ className: 'lead' }}
+
+
+
+
+
+
+## Getting started {{ anchor: false }}
+
+To get started, create a new application in your [developer settings](#), then read about how to make requests for the resources you need to access using our HTTP APIs or dedicated client SDKs. When your integration is ready to go live, publish it to our [integrations directory](#) to reach the Protocol community. {{ className: 'lead' }}
+
+
+
+
+
+
+
+
diff --git a/app/pagination/page.mdx b/app/pagination/page.mdx
new file mode 100644
index 0000000..8bec705
--- /dev/null
+++ b/app/pagination/page.mdx
@@ -0,0 +1,63 @@
+export const metadata = {
+ title: 'Pagination',
+ description:
+ 'In this guide, we will look at how to work with paginated responses when querying the Protocol API',
+}
+
+# Pagination
+
+In this guide, we will look at how to work with paginated responses when querying the Protocol API. By default, all responses limit results to ten. However, you can go as high as 100 by adding a `limit` parameter to your requests. If you are using one of the official Protocol API client libraries, you don't need to worry about pagination, as it's all being taken care of behind the scenes. {{ className: 'lead' }}
+
+When an API response returns a list of objects, no matter the amount, pagination is supported. In paginated responses, objects are nested in a `data` attribute and have a `has_more` attribute that indicates whether you have reached the end of the last page. You can use the `starting_after` and `endding_before` query parameters to browse pages.
+
+## Example using cursors
+
+
+
+
+ In this example, we request the page that starts after the conversation with id `s4WycXedwhQrEFuM`. As a result, we get a list of three conversations and can tell by the `has_more` attribute that we have reached the end of the resultset.
+
+
+
+ The last ID on the page you're currently on when you want to fetch the next page.
+
+
+ The first ID on the page you're currently on when you want to fetch the previous page.
+
+
+ Limit the number of items returned.
+
+
+
+
+
+
+ ```bash {{ title: 'Manual pagination using cURL' }}
+ curl -G https://api.protocol.chat/v1/conversations \
+ -H "Authorization: Bearer {token}" \
+ -d starting_after="s4WycXedwhQrEFuM" \
+ -d limit=10
+ ```
+
+ ```json {{ title: 'Paginated response' }}
+ {
+ "has_more": false,
+ "data": [
+ {
+ "id": "WAz8eIbvDR60rouK",
+ // ...
+ },
+ {
+ "id": "hSIhXBhNe8X1d8Et"
+ // ...
+ },
+ {
+ "id": "fbwYwpi9C2ybt6Yb"
+ // ...
+ }
+ ]
+ }
+ ```
+
+
+
diff --git a/app/providers.tsx b/app/providers.tsx
new file mode 100644
index 0000000..6742e48
--- /dev/null
+++ b/app/providers.tsx
@@ -0,0 +1,37 @@
+"use client";
+
+import { ThemeProvider, useTheme } from "next-themes";
+import { type ReactNode, useEffect } from "react";
+
+function ThemeWatcher() {
+ const { resolvedTheme, setTheme } = useTheme();
+
+ useEffect(() => {
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
+
+ function onMediaChange() {
+ const systemTheme = media.matches ? "dark" : "light";
+ if (resolvedTheme === systemTheme) {
+ setTheme("system");
+ }
+ }
+
+ onMediaChange();
+ media.addEventListener("change", onMediaChange);
+
+ return () => {
+ media.removeEventListener("change", onMediaChange);
+ };
+ }, [resolvedTheme, setTheme]);
+
+ return null;
+}
+
+export function Providers({ children }: { children: ReactNode }) {
+ return (
+
+
+ {children}
+
+ );
+}
diff --git a/app/quickstart/page.mdx b/app/quickstart/page.mdx
new file mode 100644
index 0000000..7bf8aea
--- /dev/null
+++ b/app/quickstart/page.mdx
@@ -0,0 +1,98 @@
+export const metadata = {
+ title: 'Quickstart',
+ description:
+ 'This guide will get you all set up and ready to use the Protocol API. We’ll cover how to get started an API client and how to make your first API request.',
+}
+
+# Quickstart
+
+This guide will get you all set up and ready to use the Protocol API. We'll cover how to get started using one of our API clients and how to make your first API request. We'll also look at where to go next to find all the information you need to take full advantage of our powerful REST API. {{ className: 'lead' }}
+
+
+ Before you can make requests to the Protocol API, you will need to grab your
+ API key from your dashboard. You find it under [Settings » API](#).
+
+
+## Choose your client
+
+Before making your first API request, you need to pick which API client you will use. In addition to good ol' cURL HTTP requests, Protocol offers clients for JavaScript, Python, and PHP. In the following example, you can see how to install each client.
+
+
+
+```bash {{ title: 'cURL' }}
+# cURL is most likely already installed on your machine
+curl --version
+```
+
+```bash {{ language: 'js' }}
+# Install the Protocol JavaScript SDK
+npm install @example/protocol-api --save
+```
+
+```bash {{ language: 'python' }}
+# Install the Protocol Python SDK
+pip install protocol_api
+```
+
+```bash {{ language: 'php' }}
+# Install the Protocol PHP SDK
+composer require protocol/sdk
+```
+
+
+
+
+
+
+
+## Making your first API request
+
+After picking your preferred client, you are ready to make your first call to the Protocol API. Below, you can see how to send a GET request to the Conversations endpoint to get a list of all your conversations. In the cURL example, results are limited to ten conversations, the default page length for each client.
+
+
+
+```bash {{ title: 'cURL' }}
+curl -G https://api.protocol.chat/v1/conversations \
+ -H "Authorization: Bearer {token}" \
+ -d limit=10
+```
+
+```js
+import ApiClient from '@example/protocol-api'
+
+const client = new ApiClient(token)
+
+await client.conversations.list()
+```
+
+```python
+from protocol_api import ApiClient
+
+client = ApiClient(token)
+
+client.conversations.list()
+```
+
+```php
+$client = new \Protocol\ApiClient($token);
+
+$client->conversations->list();
+```
+
+
+
+
+
+
+
+## What's next?
+
+Great, you're now set up with an API client and have made your first request to the API. Here are a few links that might be handy as you venture further into the Protocol API:
+
+- [Grab your API key from the Protocol dashboard](#)
+- [Check out the Conversations endpoint](/conversations)
+- [Learn about the different error messages in Protocol](/errors)
diff --git a/app/sdks/page.mdx b/app/sdks/page.mdx
new file mode 100644
index 0000000..ec7acd1
--- /dev/null
+++ b/app/sdks/page.mdx
@@ -0,0 +1,17 @@
+import { Libraries } from '@/components/Libraries'
+
+export const metadata = {
+ title: 'Protocol SDKs',
+ description:
+ 'Protocol offers fine-tuned JavaScript, Ruby, PHP, Python, and Go libraries to make your life easier and give you the best experience when consuming the API.',
+}
+
+export const sections = [
+ { title: 'Official libraries', id: 'official-libraries' },
+]
+
+# Protocol SDKs
+
+The recommended way to interact with the Protocol API is by using one of our official SDKs. Today, Protocol offers fine-tuned JavaScript, Ruby, PHP, Python, and Go libraries to make your life easier and give you the best experience when consuming the API. {{ className: 'lead' }}
+
+
diff --git a/app/webhooks/page.mdx b/app/webhooks/page.mdx
new file mode 100644
index 0000000..d8a568d
--- /dev/null
+++ b/app/webhooks/page.mdx
@@ -0,0 +1,172 @@
+export const metadata = {
+ title: 'Webhooks',
+ description:
+ 'In this guide, we will look at how to register and consume webhooks to integrate your app with Protocol.',
+}
+
+# Webhooks
+
+In this guide, we will look at how to register and consume webhooks to integrate your app with Protocol. With webhooks, your app can know when something happens in Protocol, such as someone sending a message or adding a contact. {{ className: 'lead' }}
+
+## Registering webhooks
+
+To register a new webhook, you need to have a URL in your app that Protocol can call. You can configure a new webhook from the Protocol dashboard under [API settings](#). Give your webhook a name, pick the [events](#event-types) you want to listen for, and add your URL.
+
+Now, whenever something of interest happens in your app, a webhook is fired off by Protocol. In the next section, we'll look at how to consume webhooks.
+
+## Consuming webhooks
+
+When your app receives a webhook request from Protocol, check the `type` attribute to see what event caused it. The first part of the event type will tell you the payload type, e.g., a conversation, message, etc.
+
+```json {{ title: 'Example webhook payload' }}
+{
+ "id": "a056V7R7NmNRjl70",
+ "type": "conversation.updated",
+ "payload": {
+ "id": "WAz8eIbvDR60rouK"
+ // ...
+ }
+}
+```
+
+In the example above, a conversation was `updated`, and the payload type is a `conversation`.
+
+
+
+
+
+---
+
+## Event types
+
+
+
+
+
+
+ A new contact was created.
+
+
+ An existing contact was updated.
+
+
+ A contact was successfully deleted.
+
+
+ A new conversation was created.
+
+
+ An existing conversation was updated.
+
+
+ A conversation was successfully deleted.
+
+
+ A new message was created.
+
+
+ An existing message was updated.
+
+
+ A message was successfully deleted.
+
+
+ A new group was created.
+
+
+ An existing group was updated.
+
+
+ A group was successfully deleted.
+
+
+ A new attachment was created.
+
+
+ An existing attachment was updated.
+
+
+ An attachment was successfully deleted.
+
+
+
+
+
+
+ ```json {{ 'title': 'Example payload' }}
+ {
+ "id": "a056V7R7NmNRjl70",
+ "type": "message.updated",
+ "payload": {
+ "id": "SIuAFUNKdSYHZF2w",
+ "conversation_id": "xgQQXg3hrtjh7AvZ",
+ "contact": {
+ "id": "WAz8eIbvDR60rouK",
+ "username": "KevinMcCallister",
+ "phone_number": "1-800-759-3000",
+ "avatar_url": "https://assets.protocol.chat/avatars/kevin.jpg",
+ "last_active_at": 705103200,
+ "created_at": 692233200
+ },
+ "message": "I’m traveling with my dad. He’s at a meeting. I hate meetings.",
+ "reactions": [],
+ "attachments": [],
+ "read_at": 705103200,
+ "created_at": 692233200,
+ "updated_at": 692233200
+ }
+ }
+ ```
+
+
+
+
+---
+
+## Security
+
+To know for sure that a webhook was, in fact, sent by Protocol instead of a malicious actor, you can verify the request signature. Each webhook request contains a header named `x-protocol-signature`, and you can verify this signature by using your secret webhook key. The signature is an HMAC hash of the request payload hashed using your secret key. Here is an example of how to verify the signature in your app:
+
+
+
+```js
+const signature = req.headers['x-protocol-signature']
+const hash = crypto.createHmac('sha256', secret).update(payload).digest('hex')
+
+if (hash === signature) {
+ // Request is verified
+} else {
+ // Request could not be verified
+}
+```
+
+```python
+from flask import request
+import hashlib
+import hmac
+
+signature = request.headers.get("x-protocol-signature")
+hash = hmac.new(bytes(secret, "ascii"), bytes(payload, "ascii"), hashlib.sha256)
+
+if hash.hexdigest() == signature:
+ # Request is verified
+else:
+ # Request could not be verified
+```
+
+```php
+$signature = $request['headers']['x-protocol-signature'];
+$hash = hash_hmac('sha256', $payload, $secret);
+
+if (hash_equals($hash, $signature)) {
+ // Request is verified
+} else {
+ // Request could not be verified
+}
+```
+
+
+
+If your generated signature matches the `x-protocol-signature` header, you can be sure that the request was truly coming from Protocol. It's essential to keep your secret webhook key safe — otherwise, you can no longer be sure that a given webhook was sent by Protocol. Don't commit your secret webhook key to GitHub!
diff --git a/biome.json b/biome.json
index ef6b12d..4c705e6 100644
--- a/biome.json
+++ b/biome.json
@@ -1,20 +1,91 @@
{
- "$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
+ "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
"organizeImports": {
- "enabled": true,
- "ignore": ["node_modules", "dist", "cache"]
+ "enabled": true
},
"linter": {
"enabled": true,
"rules": {
- "recommended": true
- },
- "ignore": ["node_modules", "dist", "cache"]
+ "all": true,
+ "correctness": {
+ "noNodejsModules": "off"
+ },
+ "complexity": {
+ "noExcessiveCognitiveComplexity": "off"
+ },
+ "style": {
+ "noDefaultExport": "off",
+ "noParameterProperties": "off",
+ "noNamespaceImport": "off",
+ "useFilenamingConvention": "off",
+ "useNamingConvention": {
+ "level": "warn",
+ "options": {
+ "requireAscii": false,
+ "strictCase": false,
+ "conventions": [
+ {
+ "selector": {
+ "kind": "typeProperty"
+ },
+ "formats": [
+ "camelCase",
+ "CONSTANT_CASE",
+ "PascalCase",
+ "snake_case"
+ ]
+ },
+ {
+ "selector": {
+ "kind": "objectLiteralProperty",
+ "scope": "any"
+ },
+ "formats": [
+ "camelCase",
+ "CONSTANT_CASE",
+ "PascalCase",
+ "snake_case"
+ ]
+ },
+ {
+ "selector": {
+ "kind": "classMethod",
+ "scope": "any"
+ },
+ "formats": ["camelCase", "PascalCase"]
+ },
+ {
+ "selector": {
+ "kind": "functionParameter",
+ "scope": "any"
+ },
+ "formats": ["snake_case", "camelCase"]
+ }
+ ]
+ }
+ }
+ },
+ "nursery": {
+ "noDuplicateElseIf": "warn",
+ "noDuplicateJsonKeys": "warn",
+ "noEvolvingTypes": "warn",
+ "noYodaExpression": "warn",
+ "useConsistentBuiltinInstantiation": "warn",
+ "useErrorMessage": "warn",
+ "useImportExtensions": "off",
+ "useThrowNewError": "warn"
+ }
+ }
},
"formatter": {
"enabled": true,
"indentStyle": "space",
- "indentWidth": 4,
- "ignore": ["node_modules", "dist", "cache"]
+ "indentWidth": 4
+ },
+ "javascript": {
+ "globals": ["Bun"]
+ },
+ "files": {
+ "ignore": ["node_modules", ".next", ".output"]
}
}
diff --git a/bun.lockb b/bun.lockb
index bd34815..90d4c9c 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/components/Banner.vue b/components/Banner.vue
deleted file mode 100644
index fd74c25..0000000
--- a/components/Banner.vue
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
+ )}
+
+ );
+}
+
+export function Code({ children, ...props }: ComponentPropsWithoutRef<"code">) {
+ const isGrouped = useContext(CodeGroupContext);
+
+ if (isGrouped) {
+ if (typeof children !== "string") {
+ throw new Error(
+ "`Code` children must be a string when nested inside a `CodeGroup`.",
+ );
+ }
+ return (
+ // biome-ignore lint/security/noDangerouslySetInnerHtml:
+ // biome-ignore lint/style/useNamingConvention:
+
+ );
+ }
+
+ return {children};
+}
+
+export function Pre({
+ children,
+ ...props
+}: ComponentPropsWithoutRef) {
+ const isGrouped = useContext(CodeGroupContext);
+
+ if (isGrouped) {
+ return children;
+ }
+
+ return {children};
+}
diff --git a/components/Features.vue b/components/Features.vue
deleted file mode 100644
index 46edf53..0000000
--- a/components/Features.vue
+++ /dev/null
@@ -1,77 +0,0 @@
-
-
-
-
Made by developers
-
- Lysand is designed and maintained by the developers of the Lysand Server, which uses Lysand for
- federation. This community could include you! Check out our Git repository to see how you can contribute.
-
+
+ {
+ if (
+ event.key === "Escape" &&
+ !autocompleteState.isOpen &&
+ autocompleteState.query === ""
+ ) {
+ // In Safari, closing the dialog with the escape key can sometimes cause the scroll position to jump to the
+ // bottom of the page. This is a workaround for that until we can figure out a proper fix in Headless UI.
+ if (document.activeElement instanceof HTMLElement) {
+ document.activeElement.blur();
+ }
+
+ onClose();
+ } else {
+ inputProps.onKeyDown(event);
+ }
+ }}
+ />
+ {autocompleteState.status === "stalled" && (
+
- The Lysand project is made possible by the hard work of our contributors. Here are some of the people
- who
- have helped make Lysand what it is today.
-
+ );
+}
diff --git a/docs/extensions.md b/docs/extensions.md
deleted file mode 100644
index e4770c3..0000000
--- a/docs/extensions.md
+++ /dev/null
@@ -1,95 +0,0 @@
-# Protocol Extensions
-
-Lysand accommodates protocol extensions, which are enhancements to the protocol that are not part of the core protocol. These extensions are designed to augment the protocol with additional features and are namespaced.
-
-Protocol extensions can be incorporated into an object as follows:
-
-```json5
-{
- // ...
- "extensions": {
- "com.organization.name:key": "value"
- }
- // ...
-}
-```
-
-The `extensions` field is an object that comprises a list of extensions. Each extension is a key-value pair, where the key represents the extension name, and the value signifies the extension value.
-
-The extension name **MUST** be a string that includes the reverse domain name of the organization that devised the extension, followed by a colon, and then the name of the extension. For instance, `com.example:extension_name`.
-
-The extension name **MUST** be unique within the organization namespace (i.e., it should be unique for each organization).
-
-The extension value **MAY** be any valid JSON value. The decision to implement extensions is at the discretion of the servers.
-
-For instance, a server might implement an extension that enables users to geotag an object. The extension name could be `org.geotagger:geotag`, and the extension value might be a string that contains the geotag.
-```json5
-{
- // ...
- "extensions": {
- "org.geotagger:geotag": "40.7128° N, 74.0060° W"
- }
- // ...
-}
-```
-
-Lysand strongly advocates that extensions are documented and standardized, and that servers refrain from implementing extensions that are not documented or standardized by their author. Moreover, official extensions of the Lysand protocol should take precedence over custom extensions. (Third-party extensions may be incorporated into the official spec if necessary).
-
-## Adding New Object Types
-
-Lysand supports the addition of new object types via extensions. This is beneficial for introducing new types of objects to the protocol, such as polls or events.
-
-Every new object type added **MUST** have `Extension` as its object type, and **MUST** have an `extension_type` field that contains the extension name of the object type.
-
-The extension name of the object type is formatted as follows:
-
-```
-com.organization.name:extension/Type
-```
-
-For instance, if a server wishes to add a new object type named `Poll`, the extension name would be `com.example:poll/Poll`.
-
-Custom types **MUST** commence with a capital letter, **MUST** be alphanumeric values (with PascalCase used instead of spaces) and **MUST** be unique across all extensions.
-
-Custom types **MUST** be unique within their organization namespace (i.e., it should be unique for each organization).
-
-An example is provided in the following object:
-```json5
-{
- "type": "Extension",
- "extension_type": "com.example:poll/Poll",
- "id": "6f27bc77-58ee-4c9b-b804-8cc1c1182fa9",
- "uri": "https://example.com/actions/6f27bc77-58ee-4c9b-b804-8cc1c1182fa9",
- "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe",
- "created_at": "2021-01-01T00:00:00.000Z",
- "question": "What is your favourite colour?",
- "options": [
- "Red",
- "Blue",
- "Green"
- ]
-}
-```
-
-## Official Protocol Extensions
-
-Lysand has a selection of official extensions that are part of the core protocol. These extensions are standardized and documented, and servers **SHOULD** implement them if they implement the core protocol (however, they are not obligated to do so).
-
-These include:
-
-- [Custom Emojis](/extensions/custom-emojis)
-- [Reactions](/extensions/reactions)
-- [Polls](/extensions/polls)
-- [Is Cat](/extensions/is-cat)
-- [Server Endorsement](/extensions/server-endorsement)
-- [Reports](/extensions/reports)
-
-## Types
-
-```typescript
-// Specific extension types will extend from this
-interface Extension extends Entity {
- type: "Extension";
- extension_type: string;
-}
-```
\ No newline at end of file
diff --git a/docs/extensions/custom-emojis.md b/docs/extensions/custom-emojis.md
deleted file mode 100644
index ef9728e..0000000
--- a/docs/extensions/custom-emojis.md
+++ /dev/null
@@ -1,57 +0,0 @@
-# Custom Emojis
-
-For detailed information about the Custom Emoji type, refer to [Custom Emojis](../structures/custom-emoji). The implementation of the extension is as follows:
-
-```json5
-{
- // ...
- "extensions": {
- "org.lysand:custom_emojis": {
- "emojis": [
- {
- "name": "happy_face",
- "url": {
- "image/png": {
- "content": "https://cdn.example.com/emojis/happy_face.png",
- "content_type": "image/png"
- }
- }
- },
- // ...
- ]
- }
- }
- // ...
-}
-```
-
-In this context, the extension name is `org.lysand:custom_emojis`, and the extension value is an object that includes an array of emojis.
-
-## Utilizing Custom Emojis
-
-Clients are **required** to implement custom emojis in any text field where their presence is plausible. This includes, but is not limited to, status text, display names, alt text, and bio fields. However, this does not extend to an [Actor](../objects/actors)'s username, for instance.
-
-A custom emoji is represented within a text string as follows:
-```
-:emoji_name:
-```
-
-For instance, to use the `happy_face` emoji, a user would type:
-```
-:happy_face:
-```
-
-Clients are **required** to substitute the `:emoji_name:` with the corresponding inline emoji. If the client does not support custom emojis, it **should** display the `:emoji_name:` as it is.
-
-If the client supports Custom Emojis, but does not support a specific emoji that the user is attempting to use (such as with an incompatible MIME type), it **should** display the `:emoji_name:` as it is.
-
-When rendered as images, Custom Emojis **should** have appropriate alt text for accessibility. The alt text **should** be the alt text of the emoji, if it exists. If the emoji does not have an alt text, the alt text **should** be the name of the emoji.
-
-### Styling Custom Emojis
-
-If the styling system, such as CSS, supports it, clients **should** style the emoji to match the height of the text but allow it to take as much width as necessary, instead of treating it as a square and potentially distorting the image.
-
-Example in HTML:
-```html
-Hello, world!
-```
\ No newline at end of file
diff --git a/docs/extensions/events.md b/docs/extensions/events.md
deleted file mode 100644
index ea0ed86..0000000
--- a/docs/extensions/events.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Events
-
-With the Events extension, users can create events. This is useful for creating gatherings, such as meetups or parties.
-
-This extension is planned but not yet drafted.
\ No newline at end of file
diff --git a/docs/extensions/interactivity.md b/docs/extensions/interactivity.md
deleted file mode 100644
index 2a8cf1b..0000000
--- a/docs/extensions/interactivity.md
+++ /dev/null
@@ -1,10 +0,0 @@
-# Interactivity
-
-> [!WARNING]
-> This extension is a work in progress and is not to be used in any production system. The specification is subject to change.
-
-On platforms like Discord, users can interact with messages with custom fields like buttons or dropdowns. This extension allows you to define these fields in your messages.
-
-
-
-...
\ No newline at end of file
diff --git a/docs/extensions/is-cat.md b/docs/extensions/is-cat.md
deleted file mode 100644
index 9e06169..0000000
--- a/docs/extensions/is-cat.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# IsCat
-
-> [!NOTE]
-> This is a **light-hearted** extension designed for amusement and not intended for serious application.
-
-> [!WARNING]
-> This extension may be superseded by the upcoming [Vanity Profiles](./vanity) extension.
-
-The IsCat feature allows users to communicate their cat status to others, similar to Misskey's "IsCat" feature.
-
-A user, referred to as an Actor, can specify their feline status using the following field:
-
-```json5
-{
- "type": "User",
- // ...
- "extensions": {
- "org.lysand:is_cat": {
- "cat": true,
- // Potential additional fields
- // "dog": true
- }
- }
-}
-```
-
-Clients **SHOULD** display an appropriate graphic to signify a user's cat status, such as adding cat ears to the user's avatar.
\ No newline at end of file
diff --git a/docs/extensions/microblogging.md b/docs/extensions/microblogging.md
deleted file mode 100644
index 4f894ed..0000000
--- a/docs/extensions/microblogging.md
+++ /dev/null
@@ -1,66 +0,0 @@
-# Microblogging
-
-> [!WARNING]
->
-> Before Lysand 3.0, microblogging was directly integrated into the core spec. As of Lysand 3.0, microblogging has been moved to an extension, as part of a larger modularization effort. This document describes the new microblogging extension.
-
-The Microblogging extension allows users to perform certain tasks related to microblogging, such as "boosting" (reposting) posts.
-
-## Announce
-
-The `Announce` action signifies a user's intent to broadcast or share an object with their followers. This action is analogous to the "retweet" function on Twitter.
-
-
-`Announce`s can of course be deleted ("unboosting") with a classic [Undo](../objects/undo) object.
-
-Here's an example of an `Announce` action:
-
-```json5
-{
- "id": "3e7e4750-afd4-4d99-a256-02f0710a0520",
- "type": "Extension",
- "extension_type": "org.lysand:microblogging/Announce",
- "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe",
- "uri": "https://example.com/actions/3e7e4750-afd4-4d99-a256-02f0710a0520",
- "created_at": "2021-01-01T00:00:00.000Z",
- "object": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19"
-}
-```
-
-### Fields
-
-#### Author
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| author | String | Yes |
-
-URI of the [Actor](../objects/actors) who initiated the action.
-
-#### Object
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| object | String | Yes |
-
-URI of the object being announced. Must be of type [Note](../objects/note)
-
-#### Implementation
-
-When a [Note](../objects/note) object is announced, the client **SHOULD** display the original note with an indicator that it has been announced. The client **SHOULD** also display the number of times the note has been announced, such as a number next to a small icon like such on [Mastodon](https://joinmastodon.org/):
-
-
-
-Furthermore, users should be notified when their notes are announced by other users.
-
-
-## Types
-
-```typescript
-interface Announce extends Extension {
- extension_type: "org.lysand:microblogging/Announce";
- author: string;
- object: string;
-}
-```
-
diff --git a/docs/extensions/migration.md b/docs/extensions/migration.md
deleted file mode 100644
index d7591ed..0000000
--- a/docs/extensions/migration.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# Migration
-
-Sometimes, users may wish to move from one instance to another. This could be due to a change in administration, a desire to be closer to friends, or any other reason. This document outlines an extension to make the process of moving instances easier.
-
-## User migrations
-
-The following object is used to represent a user migration:
-
-```json5
-{
- "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
- "type": "Extension",
- "extension_type": "org.lysand:migration/Migration",
- "author": "https://example.com/users/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
- "uri": "https://example.com/actions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
- "created_at": "2021-01-01T00:00:00.000Z",
- "destination": "https://otherinstance.social/users/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
-}
-```
-
-### Fields
-
-#### Author
-
-| Name | Type | Required |
-| :----- | :--- | :------- |
-| author | URI | Yes |
-
-URI of the [Actor](../objects/actors) who initiated the action. This Actor will be the user migrating.
-
-#### Destination
-
-| Name | Type | Required |
-| :---------- | :--- | :------- |
-| destination | URI | Yes |
-
-URI of the user's new account.
-
-### Implementation
-
-When an instance receives a `Migration` object, the client **SHOULD** display a notification to all followers of the migrating user. This notification **SHOULD** include a link to the user's new account.
-
-Furthermore, all following relationships of the migrating user **SHOULD** be transferred to the new account. This includes followers, following, and any other relationship that may exist. The old account **SHOULD** be marked as inactive and display a message indicating that the user has migrated to a new account.
-
-#### Server Actors
-
-If the user in question is a server actor, then it should be considered that the entire instance is migrating to a new address. In this case, the above process should be applied to all users on the instance.
\ No newline at end of file
diff --git a/docs/extensions/polls.md b/docs/extensions/polls.md
deleted file mode 100644
index eb3b7e9..0000000
--- a/docs/extensions/polls.md
+++ /dev/null
@@ -1,249 +0,0 @@
-# Polls
-
-The Polls extension enables users to generate polls/surveys, a valuable tool for soliciting feedback or opinions from users, such as "What is your preferred color?".
-
-Polls are incorporated as a new field under the [Note](../objects/note) Extensions, named `polls`. This field is an object that encapsulates the poll details.
-
-```json5
-{
- "id": "f08a124e-fe90-439e-8be4-15a428a72a19",
- "type": "Note",
- // ...
- "extensions": {
- "org.lysand:polls": {
- "poll": {
- "options": [
- {
- "text/plain": {
- "content": "Red"
- }
- },
- {
- "text/plain": {
- "content": "Blue"
- }
- },
- {
- "text/plain": {
- "content": "Green"
- }
- }
- ],
- "votes": [
- 9,
- 5,
- 0
- ],
- "multiple_choice": false,
- "expires_at": "2021-01-04T00:00:00.000Z"
- }
- }
- }
- // ...
-}
-```
-
-The fields are explained below.
-
-> [!NOTE]
-> There is no `question` field, as it is presumed that the question will be included in the `contents` field of the associated [Note](../objects/note). Clients are anticipated to render surveys adjacent to the contents of the [Note](../objects/note) itself.
-
-## Fields
-
-### Options
-
-| Name | Type | Required |
-| :------ | :--------------------- | :------- |
-| options | Array of ContentFormat | Yes |
-
-Displays the various options users can vote for.
-
-**MUST** contain at least 2 options, but does not have an upper limit for the number of options.
-
-> [!NOTE]
-> Servers should limit the number of options to a reasonable number, preferably in a configurable manner, such as 40. This is to prevent abuse of the protocol by sending a large number of options, as they are not paginated.
-
-### Votes
-
-| Name | Type | Required |
-| :---- | :--------------- | :------- |
-| votes | Array of Integer | Yes |
-
-Contains the number of votes cast for each option. The index of the array corresponds to the index of the option in the `options` array.
-
-Votes should not be public: the server should hide the users that casted votes and only show the total amount.
-
-### Multiple Choice
-
-| Name | Type | Required |
-| :-------------- | :------ | :------- |
-| multiple_choice | Boolean | No |
-
-Indicates whether the poll is multiple choice. If true, users can vote for multiple options. If false, users can only vote for one option.
-
-If not provided, it is assumed that the poll is not multiple choice.
-
-### Expiration
-
-| Name | Type | Required |
-| :--------- | :----- | :------- |
-| expires_at | String | Yes |
-
-The date and time when the poll expires. After this time, the poll is closed and no more votes can be cast.
-
-Clients **SHOULD** display the time remaining until the poll expires.
-
-### Integration With Custom Emojis
-
-If you implement both the Polls and the [Custom Emojis](./custom-emojis) extensions, you can use the Custom Emojis extension to add emojis to poll options.
-
-Example:
-```json5
-{
- // ...
- "extensions": {
- "org.lysand:polls": {
- "poll": {
- "options": [
- {
- "text/plain": {
- "content": "Red :red:"
- }
- },
- {
- "text/plain": {
- "content": "Blue :blue:"
- }
- },
- {
- "text/plain": {
- "content": "Green :green:"
- }
- }
- ],
- "votes": [
- 9,
- 5,
- 0
- ],
- "multiple_choice": false,
- "expires_at": "2021-01-04T00:00:00.000Z"
- }
- },
- "org.lysand:custom_emojis": {
- "emojis": [
- {
- "name": "red",
- "url": {
- "image/webp": {
- "content": "https://cdn.example.com/emojis/red.webp"
- }
- }
- },
- {
- "name": "blue",
- "url": {
- "image/webp": {
- "content": "https://cdn.example.com/emojis/blue.webp"
- }
- }
- },
- {
- "name": "green",
- "url": {
- "image/webp": {
- "content": "https://cdn.example.com/emojis/green.webp"
- }
- }
- }
- ]
- }
- }
- // ...
-}
-```
-
-When rendering the poll options, clients **SHOULD** display emojis as recommended by the [Custom Emojis](./custom-emojis) extension.
-
-### Poll Results
-
-Clients **SHOULD** display poll results as a percentage of votes. For example, if 10 users voted for the first option, and 5 users voted for the second option, the first option should be displayed as 66.67%, and the second option should be displayed as 33.33%. (with the third option being 0%)
-
-Clients **SHOULD** display the number of votes for each option, and the total number of votes.
-
-### Sending Votes
-
-Clients **SHOULD** allow users to vote on polls. When a user votes on a poll, the client **MUST** send a `POST` request to the poll's [Note](../objects/note) URI with the following JSON object in the body:
-
-```json5
-{
- "type": "Extension",
- "extension_type": "org.lysand:polls/Vote",
- "id": "6b1586cf-1f83-4d85-8d70-a5dc9f213b3e",
- "uri": "https://example.com/actions/6b1586cf-1f83-4d85-8d70-a5dc9f213b3e",
- "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe",
- "poll": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19",
- "option": 0
-}
-```
-
-The `option` field **MUST** be the index of the option in the `options` array that the user is voting for.
-
-In return, the server **MUST** respond with a `200 OK` response code, and a JSON object in the body, unless there is an error. This JSON object **MUST** be a valid `VoteResult` object.
-
-```json5
-{
- "type": "Extension",
- "extension_type": "org.lysand:polls/VoteResult",
- "id": "d6eb84ea-cd13-43f9-9c54-01244da8e5e3",
- "uri": "https://example.com/actions/d6eb84ea-cd13-43f9-9c54-01244da8e5e3",
- "poll": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19",
- "votes": [
- 10,
- 5,
- 0
- ]
-}
-```
-
-Each number in the `votes` array corresponds to the number of votes for each option. The index of the array corresponds to the index of the poll option in the original Poll object.
-
-If the poll is closed, the server **MUST** respond with a `403 Forbidden` response code.
-
-The total amount of votes can be calculated by summing the `votes` array.
-
-This amount **MUST** include the user's vote, and **SHOULD** be displayed to the user after voting.
-
-### Poll Events
-
-When a poll ends, a user that has voted in it **SHOULD** be notified of the results by the server.
-
-The server **MAY** send a `GET` request to the poll's Publication URI to update its internal database.
-
-## Types
-
-```typescript
-interface Poll extends Extension {
- extension_type: "org.lysand:polls/Poll";
- options: ContentFormat[];
- votes: number[]; // unsigned 64-bit integer
- multiple_choice?: boolean;
- expires_at: string;
-}
-```
-
-```typescript
-interface Vote extends Extension {
- extension_type: "org.lysand:polls/Vote";
- poll: string;
- option: number; // unsigned 64-bit integer
-}
-```
-
-```typescript
-interface VoteResult extends Extension {
- extension_type: "org.lysand:polls/VoteResult";
- poll: string;
- votes: number[]; // unsigned 64-bit integer
-}
-```
diff --git a/docs/extensions/reactions.md b/docs/extensions/reactions.md
deleted file mode 100644
index 37bc253..0000000
--- a/docs/extensions/reactions.md
+++ /dev/null
@@ -1,151 +0,0 @@
-# Emoji Reactions
-
-The Emoji Reactions extension allows users to express their responses to objects using emojis, similar to the functionality provided by platforms like Facebook and Discord.
-
-Here's an example of a reaction to a post using this extension:
-
-```json5
-{
- "type": "Extension",
- "extension_type": "org.lysand:reactions/Reaction",
- "id": "6f27bc77-58ee-4c9b-b804-8cc1c1182fa9",
- "uri": "https://example.com/actions/6f27bc77-58ee-4c9b-b804-8cc1c1182fa9",
- "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe",
- "created_at": "2021-01-01T00:00:00.000Z",
- "object": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19",
- "content": "😀",
-}
-```
-## Fields
-
-### Type
-
-The `type` field **MUST BE** `Extension`.
-
-The `extension_type` field **MUST** be `org.lysand:reactions/Reaction`.
-
-### Object
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| object | String | Yes |
-
-URI of the object that the user is reacting to. This **MUST** be a [Note](../objects/note) object.
-
-### Content
-
-| Name | Type | Required |
-| :------ | :----- | :------- |
-| content | String | Yes |
-
-Emoji that the user is reacting with.
-
-Clients **SHOULD** check if the value of `content` is an emoji: if it is not an emoji and instead is text, depending on which other extensions are implemented, it **MAY** be a [Custom Emoji](./custom-emojis).
-
-> [!NOTE]
-> If this field is not recognized as an emoji or [Custom Emoji](./custom-emojis), the whole Reaction object can be discarded as it is invalid.
->
-> Please see [Reactions With Custom Emojis](#reactions-with-custom-emojis) for more information about custom emoji reactions.
-
-
-### Retrieving Reactions
-
-Clients can retrieve reactions to an object by sending a `GET` request to the reaction [Collection](../structures/collection)'s URI.
-
-The URI of the reaction [Collection](../structures/collection) **MUST** be specified as follows, inside a [Note](../objects/note):
-```json5
-{
- // ...
- "extensions": {
- "org.lysand:reactions": {
- "reactions": "https://example.com/notes/f08a124e-fe90-439e-8be4-15a428a72a19/reactions"
- }
- }
-}
-```
-
-The `reactions` field is the URI of the reaction `Collection`.
-
-The server **MUST** respond with a [Collection](../structures/collection) object that contains a list of `Reaction` objects, as such:
-
-```json5
-{
- "type": "Collection",
- "first": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19/reactions?page=1",
- "last": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19/reactions?page=1",
- "total_count": 1,
- // "author": ... (for signatures)
- "items": [
- {
- "type": "Extension",
- "extension_type": "org.lysand:reactions/Reaction",
- "id": "6f27bc77-58ee-4c9b-b804-8cc1c1182fa9",
- "uri": "https://example.com/actions/6f27bc77-58ee-4c9b-b804-8cc1c1182fa9",
- "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe",
- "created_at": "2021-01-01T00:00:00.000Z",
- "object": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19",
- "content": "😀",
- }
- ]
-}
-```
-
-### Public Reaction Federation
-
-If a user reacts to a [Note](../objects/note), the user's server **MUST** federate the reaction to all its followers. This is to ensure that all users see the reaction.
-
-Note, however, that this does not mean that the reaction will be displayed to users. If the [Note](../objects/note), that was reacted to is not visible to a user, the reaction **MUST NOT** be displayed to the user, even if the user follows the user that reacted to the [Note](../objects/note),.
-
-### Private Reaction Federation
-
-If a user reacts to a [Note](../objects/note),, the user's server **MUST** federate the reaction to the author of the [Note](../objects/note),. This is to ensure that the author of the [Note](../objects/note), sees the reaction.
-
-### Reactions With Custom Emojis
-
-If you implement both the Reactions and the Custom Emojis extensions, you can use the Custom Emojis extension to react with custom emojis.
-
-The Reaction object needs to be modified as such:
-
-```json5
-{
- "type": "Extension",
- "extension_type": "org.lysand:reactions/Reaction",
- "id": "6f27bc77-58ee-4c9b-b804-8cc1c1182fa9",
- "uri": "https://example.com/actions/6f27bc77-58ee-4c9b-b804-8cc1c1182fa9",
- "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe",
- "created_at": "2021-01-01T00:00:00.000Z",
- "object": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19",
- "content": ":happy_face:",
- "extensions": {
- "org.lysand:custom_emojis": {
- "emojis": [
- {
- "name": "happy_face",
- "url": {
- "image/webp": {
- "content": "https://cdn.example.com/emojis/happy_face.webp",
- }
- }
- },
- // ...
- ]
- }
- }
-}
-```
-
-The only addition to the Reaction object is the `extensions` field, which contains the [Custom Emojis](./custom-emojis) extension.
-
-When rendering the Reaction object, clients **MUST** replace the `:emoji_name:` with the appropriate emoji. If the client does not support custom emojis, it **MUST** display the `:emoji_name:` as-is.
-
-This emoji must be rendered according to the rules of the [Custom Emojis](./custom-emojis) extension.
-
-## Types
-
-```typescript
-interface Reaction extends Extension {
- extension_type: "org.lysand:reactions/Reaction";
- object: string;
- content: string;
-}
-```
\ No newline at end of file
diff --git a/docs/extensions/reports.md b/docs/extensions/reports.md
deleted file mode 100644
index 5ca4823..0000000
--- a/docs/extensions/reports.md
+++ /dev/null
@@ -1,70 +0,0 @@
-# Reports
-
-The Reports extension enables users to flag content or users that violate the server's rules. This feature is important for maintaining a safe community environment.
-
-If the reporting user (reporter) and the reported user (reportee) are on the same server, the report can be handled directly without the need for federation. However, if the reporter and reportee are on different servers, the report **MUST** be federated to the reportee's server.
-
-## Report Object
-
-The report object encapsulates the details of the report. It is structured as follows:
-
-```json5
-{
- "type": "Extension",
- "extension_type": "org.lysand:reports/Report",
- "author": "https://example.com/users/6f3001a1-641b-4763-a9c4-a089852eec84",
- "id": "6f3001a1-641b-4763-a9c4-a089852eec84",
- "uri": "https://example.com/actions/f7bbf7fc-88d2-47dd-b241-5d1f770a10f0",
- "objects": [
- "https://test.com/publications/46f936a3-9a1e-4b02-8cde-0902a89769fa",
- "https://test.com/publications/213d7c56-fb9b-4646-a4d2-7d70aa7d106a"
- ],
- "reason": "spam",
- "comment": "This is spam."
-}
-```
-
-Report events **MUST** be sent to the server actor's inbox.
-
-## Fields
-
-### Objects
-
-| Name | Type | Required |
-| :------ | :-------------- | :------- |
-| objects | Array of String | Yes |
-
-URIs of the objects that are being reported.
-
-If `objects` contains Actors, then these Actors **MUST** be treated as the reported users.
-
-If `objects` contains Notes, then these Notes **MUST** be treated as the reported content.
-
-`objects` can contain any URI to any kind of objects, however, typically only Actors or Notes should be reportable.
-
-### Reason
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| reason | String | Yes |
-
-The reason for the report. This should be a concise summary of the report, such as `"spam"`, `"hate speech"`, `"tos violation"`, etc.
-
-### Comment
-
-| Name | Type | Required |
-| :------ | :----- | :------- |
-| comment | String | No |
-
-Additional comments about the report. This is meant to provide a more detailed description of the report, such as `"This user has been spamming my inbox with advertisements."`.
-
-## Types
-
-```typescript
-interface Report extends Extension {
- extension_type: "org.lysand:reports/Report";
- objects: string[];
- reason: string;
- comment?: string;
-}
-```
\ No newline at end of file
diff --git a/docs/extensions/server-endorsement.md b/docs/extensions/server-endorsement.md
deleted file mode 100644
index a202b65..0000000
--- a/docs/extensions/server-endorsement.md
+++ /dev/null
@@ -1,78 +0,0 @@
-# Server Endorsement
-
-The Server Endorsement extension provides a mechanism for servers to vouch for the credibility of other servers. This is a valuable alternative to unrestricted federation, enabling servers to endorse those they deem trustworthy. Federation will only occur with endorsed servers and their subsequent endorsements, up to an admin-defined depth.
-
-## Endorsement Entity
-
-The endorsement entity encapsulates the details of an endorsement. It adheres to the following structure:
-
-```json5
-{
- "type": "Extension",
- "extension_type": "org.lysand:server_endorsement/Endorsement",
- "author": "https://example.com/actor", // The endorsing server's actor
- "id": "ed480922-b095-4f09-9da5-c995be8f5960",
- "uri": "https://example.com/actions/ed480922-b095-4f09-9da5-c995be8f5960",
- "server": "https://example.com",
-}
-```
-
-Endorsement entities **MUST** be dispatched to the endorsing server actor's inbox whenever the server administrators create a new endorsement.
-
-### Server
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| server | String | Yes |
-
-URI of the endorsed server. This URI **MUST** correspond to the server's root endpoint, such as `https://example.com`.
-
-This URI **MUST NOT** be an IP address, except for development purposes.
-
-### Author
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| author | String | Yes |
-
-The `author` field **MUST** represent the endorsing server actor. This requirement ensures that endorsements can be cryptographically signed using the server actor's `public_key`.
-
-## Endorsement Collection
-
-The URI for the endorsement collection can be specified within the server's metadata, under `extensions`:
-
-```json5
-{
- // ...
- "extensions": {
- "org.lysand:server_endorsement": {
- "endorsements": "https://example.com/endorsements"
- }
- }
- // ...
-}
-```
-
-This should return a [Collection](../structures/collection) of `Endorsement` entities.
-
-## Endorsement Protocol Behavior
-
-Upon receiving an endorsement, a server **MUST** validate the endorsement's signature. If the signature is invalid, the server **MUST** disregard the endorsement. This also applies when fetching the endorsement collection.
-
-The server has the discretion to decide how to handle the endorsement. Endorsements are intended to serve as a dynamic whitelist for servers, with administrators initially selecting a few trusted servers and progressively adding more.
-
-Servers **SHOULD** display received endorsements to their administrators, allowing them to accept or reject the endorsement. Ultimately, the decision on how to handle the endorsement lies with the administrators.
-
-Endorsements should be viewed as a seal of trust: If a server endorses another, it is expressing its trust in that server. Servers should refrain from endorsing servers they do not trust.
-
-Lastly, servers **MAY** verify the endorsements of the servers they have endorsed, up to a user-defined depth. This creates a trust chain, where servers endorse those they trust, and those servers, in turn, endorse servers they trust, and so forth.
-
-## Types
-
-```typescript
-interface Endorsement extends Extension {
- extension_type: "org.lysand:server_endorsement/Endorsement";
- author: string;
- server: string;
-}
-```
\ No newline at end of file
diff --git a/docs/extensions/vanity.md b/docs/extensions/vanity.md
deleted file mode 100644
index 3b20094..0000000
--- a/docs/extensions/vanity.md
+++ /dev/null
@@ -1,184 +0,0 @@
-# Vanity
-
-The Vanity extension allows users to customize their profile in a more in-depth manner.
-
-Here is an example object:
-```json5
-{
- // ...
- "type": "User",
- // ...
- "extensions": {
- "org.lysand:vanity": {
- "avatar_overlay": {
- "image/png": {
- "content": "https://cdn.example.com/ab5081cf-b11f-408f-92c2-7c246f290593/cat_ears.png",
- "content_type": "image/png"
- }
- },
- "avatar_mask": {
- "image/png": {
- "content": "https://cdn.example.com/d8c42be1-d0f7-43ef-b4ab-5f614e1beba4/rounded_square.jpeg",
- "content_type": "image/jpeg"
- }
- },
- "background": {
- "image/png": {
- "content": "https://cdn.example.com/6492ddcd-311e-4921-9567-41b497762b09/untitled-file-0019822.png",
- "content_type": "image/png"
- }
- },
- "audio": {
- "audio/mpeg": {
- "content": "https://cdn.example.com/4da2f0d4-4728-4819-83e4-d614e4c5bebc/michael-jackson-thriller.mp3",
- "content_type": "audio/mpeg"
- }
- },
- "pronouns": {
- "en-us": [
- "he/him",
- {
- "subject": "they",
- "object": "them",
- "dependent_possessive": "their",
- "independent_possessive": "theirs",
- "reflexive": "themself"
- },
- ]
- },
- "birthday": "1998-04-12",
- "location": "+40.6894-074.0447/",
- "activitypub": [
- "@erikuden@mastodon.de"
- ],
- "aliases": [
- "https://burger.social/accounts/349ee237-c672-41c1-aadc-677e185f795a",
- "https://social.lysand.org/users/f565ef02-035d-4974-ba5e-f62a8558331d"
- ]
- }
- }
-}
-```
-
-The extension name is `org.lysand:vanity`. All properties are optional.
-
-## Fields
-
-### Avatar Overlay
-
-| Name | Type | Required |
-| :------------- | :------------ | :------- |
-| avatar_overlay | ContentFormat | No |
-
-An overlay to be placed on top of the user's avatar. This can be used to add accessories, such as hats or glasses. Overlay should always be a transparent image.
-
-### Avatar Mask
-
-| Name | Type | Required |
-| :---------- | :------------ | :------- |
-| avatar_mask | ContentFormat | No |
-
-A mask to be applied to the user's avatar. This can be used to change the shape of the avatar, such as making it a circle or a rounded square. Mask should be a fully black (#000000) image with the shape of the mask being transparent. As such, a rounded square mask should have a fully black square with rounded corners.
-
-### Background
-
-| Name | Type | Required |
-| :--------- | :------------ | :------- |
-| background | ContentFormat | No |
-
-A background image to be displayed on the user's profile. This should be a full-width high-resolution image, preferably at least 1920x1080 pixels.
-
-Space-efficient formats such as WebP are recommended.
-
-### Audio
-
-| Name | Type | Required |
-| :---- | :------------ | :------- |
-| audio | ContentFormat | No |
-
-An audio file to be played on the user's profile. This can be used to add a theme song or a voice introduction.
-
-> [!WARNING]
-> Audio files should be muted by default and should not autoplay. Users should have the option to play the audio file, or disable them entirely.
->
-> Furthermore, audio file support in this extension should be toggleable per server, as it can be a potential vector for abuse.
-
-### Pronouns
-
-| Name | Type | Required |
-| :------- | :----------------------------------- | :------- |
-| pronouns | Array of ShortPronoun or LongPronoun | No |
-
-An array of pronouns the user uses. Pronouns can be represented as a string or an object.
-
-See [Types](#types) for a full description of the `ShortPronoun` and `LongPronoun` types.
-
-### Birthday
-
-| Name | Type | Required |
-| :------- | :----- | :------- |
-| birthday | String | No |
-
-The user's birthday. This should be in the format `YYYY-MM-DD` (ISO 8601). If the year is set to `0000`, clients should not display the year.
-
-Clients might choose to display the user's age as well when the year is present.
-
-### Location
-
-| Name | Type | Required |
-| :------- | :----- | :------- |
-| location | String | No |
-
-The user's location. This should be a string of GPS data as defined in [ISO 6709 Annex H](https://en.wikipedia.org/wiki/ISO_6709#String_expression_(Annex_H)), or alternatively a raw string such as "New York, NY". GPS data does not need to be precise, and can be as simple as `+46+002/` (France) or `+48.52+002.20/` (Paris, France).
-
-Clients might choose to display a map of the user's location.
-
-### ActivityPub
-
-| Name | Type | Required |
-| :--------- | :----- | :------- |
-| activitypub | Array of String | No |
-
-The user's ActivityPub profile. This should be an array of strings in the format `@username@domain`.
-
-Servers are expected to use standard WebFinger resolution to fetch the user's ActivityPub profile if needed.
-
-### Aliases
-
-| Name | Type | Required |
-| :------ | :----- | :------- |
-| aliases | Array of String | No |
-
-Aliases to the user's profile on other Lysand-compatible servers. This should be an array of URIs resolving to the user's Lysand object.
-
-## Types
-
-```typescript
-interface VanityExtension {
- avatar_overlay?: ContentFormat;
- avatar_mask?: ContentFormat;
- background?: ContentFormat;
- audio?: ContentFormat;
- pronouns?: {
- [language: string]: (ShortPronoun | LongPronoun)[];
- };
- birthday?: string;
- location?: string;
- activitypub?: string[];
- aliases?: string[];
-}
-```
-
-```typescript
-type ShortPronoun = string;
-```
-
-```typescript
-interface LongPronoun {
- subject: string;
- object: string;
- dependent_possessive: string;
- independent_possessive: string;
- reflexive: string;
-}
-```
\ No newline at end of file
diff --git a/docs/federation/endpoints.md b/docs/federation/endpoints.md
deleted file mode 100644
index f0ac925..0000000
--- a/docs/federation/endpoints.md
+++ /dev/null
@@ -1,198 +0,0 @@
-# Federation
-
-This section explains how federation operates within Lysand.
-
-Lysand's federation is built upon the HTTP stack. Servers interact with each other by exchanging HTTP requests.
-
-These requests are predominantly `POST` requests that carry a JSON object in the body. This JSON object **MUST** conform to the Lysand object schema.
-
-Servers that receive non-conforming Lysand objects **SHOULD** disregard these objects as invalid, and return a `400 Bad Request` response code (these could include debugging information in the response body).
-
-> [!NOTE]
-> Values such as `https://example.com/users/uuid` are example implementations and not a guide on how an implementation should format a URI. These must follow the rules outlined in [the base spec](../spec.md).
->
-## User Actor Endpoints
-
-When a server aims to retrieve the profile of a user on a differentserver, it follows the process outlined in [User Discovery](user-discovery).
-
-Upon discovering the target server's endpoints, the requesting server can issue a `GET` request to the user's endpoint to retrieve the user's actor.
-
-For instance, to fetch user information, the requesting server **MUST** send a `GET` request to the endpoint `https://example.com/users/uuid` with the required headers. The server can use either the [Server Actor](/federation/server-actor) or a requesting user's actor to sign the request, as appropriate depending on which actor the request is associated with.
-
-The server **MUST** respond with a `200 OK` response code, and a JSON object in the body. This JSON object **MUST** be a valid [Actor](../objects/actors) object.
-
-> [!NOTE]
-> While servers are not obligated to implement functionality between certain endpoints such as dislikes or featured notes, they **MUST** at least return an empty collection for these endpoints. Servers may also disregard any objects they do not handle, but should return a success response code.
-
-## User Inbox
-
-After the requesting server has discovered the target server's endpoints, it can issue a `POST` request to the `inbox` endpoint to transmit an object to the user. This process mirrors how objects are sent in ActivityPub.
-
-Typically, the inbox can be found at the same URL as the user's actor, but this is not a requirement. The server **MUST** specify the inbox URL in the actor object.
-
-Example inbox URL: `https://example.com/users/uuid/inbox`
-
-The requesting server **MUST** send a `POST` request to the endpoint `https://example.com/users/uuid/inbox` with the headers `Content-Type: application/json` and `Accept: application/json`.
-
-The body of the request **MUST** contain a valid Lysand object.
-
-Example with cURL (without signature):
-```bash
-curl -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d '{ \
- "type":"Publication", \
- "id":"6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", \
- "uri":"https://example.com/publications/6f27bc77-58ee-4c9b-b804-8cc1c1182fa9", \
- "author":"https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe", \
- "created_at":"2021-01-01T00:00:00.000Z", \
- "contents":"Hello, world!" \
-}' https://example.com/users/uuid/inbox
-```
-
-The server **MUST** respond with a `201 CREATED` response code if the operation was successful.
-
-## User Outbox
-
-In Lysand, users have an outbox, which is a list of objects that the user has posted. This is akin to the outbox in ActivityPub.
-
-The server **MUST** specify the outbox URL in the actor object.
-
-Example outbox URL: `https://example.com/users/uuid/outbox`
-
-The requesting server **MUST** send a `GET` request to the outbox endpoint (`https://example.com/users/uuid/outbox`) with the headers `Accept: application/json`.
-
-The server **MUST** respond with a `200 OK` response code, and a JSON object in the body. This JSON object **MUST** be a valid [Collection](../structures/collection) object containing [Publications](../objects/publications).
-
-Example:
-
-```json5
-{
- "first": "https://example.com/users/uuid/outbox?page=1",
- "last": "https://example.com/users/uuid/outbox?page=1",
- // No next or prev attribute in this case, but they can exist
- "total_items": 1,
- "items": [
- {
- "type": "Note",
- "id": "6f27bc77-58ee-4c9b-b804-8cc1c1182fa9",
- "uri": "https://example.com/publications/6f27bc77-58ee-4c9b-b804-8cc1c1182fa9",
- "author": "https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755",
- "created_at": "2021-01-01T00:00:00.000Z",
- "contents": [
- {
- "content": "Hello, world!",
- "content_type": "text/plain"
- }
- ],
- }
- ]
-}
-```
-
-These publications **MUST BE** ordered from newest to oldest, in descending order.
-
-## User Followers
-
-Users in Lysand have a list of followers, which is a list of users that follow the user. This is similar to the followers list in ActivityPub.
-
-> [!NOTE]
-> If you prefer not to display this list publicly, you can configure the followers endpoint to return an empty collection.
-
-The server **MUST** specify the followers URL in the actor object.
-
-Example followers URL: `https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755/followers`
-
-The requesting server **MUST** send a `GET` request to the followers endpoint (`https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755/followers`) with the headers `Accept: application/json`.
-
-The server **MUST** respond with a `200 OK` response code, and a JSON object in the body. This JSON object **MUST** be a valid [Collection](../structures/collection) object containing [Actors](../objects/actors). This collection may be empty.
-
-## User Following
-
-Users in Lysand have a followlist, which is a list of users that the user follows. This is similar to the following list in ActivityPub.
-
-> [!NOTE]
-> If you prefer not to display this list publicly, you can configure the following endpoint to return an empty collection.
-
-The server **MUST** specify the following URL in the actor object.
-
-Example following URL: `https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755/following`
-
-The requesting server **MUST** send a `GET` request to the following endpoint (`https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755/following`) with the headers `Accept: application/json`.
-
-The server **MUST** respond with a `200 OK` response code, and a JSON object in the body. This JSON object **MUST** be a valid [Collection](../structures/collection) object containing [Actors](../objects/actors). This collection may be empty.
-
-## User Featured Publications
-
-Users in Lysand have a list of featured publications, which is a list of publications that the user has pinned or deemed important. This is similar to the featured publications list in ActivityPub.
-
-> [!NOTE]
-> If you prefer not to display this list publicly, you can configure the featured publications endpoint to return an empty collection.
-
-The server **MUST** specify the featured publications URL in the actor object.
-
-Example featured publications URL: `https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755/featured`
-
-The requesting server **MUST** send a `GET` request to the featured publications endpoint (`https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755/featured`) with the headers `Accept: application/json`.
-
-The server **MUST** respond with a `200 OK` response code, and a JSON object in the body. This JSON object **MUST** be a valid [Collection](../structures/collection) object containing [Publications](../objects/publications). This collection may be empty.
-
-## User Likes
-
-Users in Lysand have a list of likes, which is a list of posts that the user has liked. This is similar to the likes list in ActivityPub.
-
-> [!NOTE]
-> If you prefer not to display this list publicly, you can configure the likes endpoint to return an empty collection.
-
-The server **MUST** specify the likes URL in the actor object.
-
-Example likes URL: `https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755/likes`
-
-The requesting server **MUST** send a `GET` request to the likes endpoint (`https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755/likes`) with the headers `Accept: application/json`.
-
-The server **MUST** respond with a `200 OK` response code, and a JSON object in the body. This JSON object **MUST** be a valid [Collection](../structures/collection) object containing [Publications](../objects/publications). This collection may be empty.
-
-
-## User Dislikes
-
-Users in Lysand have a list of dislikes, which is a list of posts that the user has disliked. This is similar to the dislikes list in ActivityPub.
-
-> [!NOTE]
-> If you prefer not to display this list publicly, you can configure the dislikes endpoint to return an empty collection.
-
-The server **MUST** specify the dislikes URL in the actor object.
-
-Example dislikes URL: `https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755/dislikes`
-
-The requesting server **MUST** send a `GET` request to the dislikes endpoint (`https://example.com/users/731bae4a-9279-4b11-bd8f-f30af7bec755/dislikes`) with the headers `Accept: application/json`.
-
-The server **MUST** respond with a `200 OK` response code, and a JSON object in the body. This JSON object **MUST** be a valid [Collection](../structures/collection) object containing [Publications](../objects/publications). This collection may be empty.
-
-## Server Discovery
-
-> [!NOTE]
-> The terms "the server" and "the requesting server" are used in this section. "The server" refers to the server that is being discovered, and "the requesting server" refers to the server that is attempting to discover the server.
-
-To retrieve the metadata of a server, the requesting server **MUST** send a `GET` request to the endpoint `https://example.com/.well-known/lysand`.
-
-The requesting server **MUST** send the following headers with the request:
-```
-Accept: application/json
-```
-
-The server **MUST** respond with a `200 OK` response code, and a JSON object in the body. This JSON object **MUST** be a valid [ServerMetadata](../objects/server-metadata) object.
-
-Example:
-
-```json5
-{
- "type": "ServerMetadata",
- "name": "Example",
- "uri": "https://example.com",
- "version": "1.0.0",
- "supported_extensions": [
- "org.lysand:reactions",
- "org.lysand:polls",
- "org.lysand:custom_emojis",
- "org.lysand:is_cat"
- ]
-}
-```
diff --git a/docs/federation/server-actor.md b/docs/federation/server-actor.md
deleted file mode 100644
index 7bf937c..0000000
--- a/docs/federation/server-actor.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# Server Actor
-
-Servers **MUST** have an Actor object that represents the server. This Actor object **MUST** be a valid User object.
-
-The Actor object can be found by sending a WebFinger request to the server's WebFinger endpoint for `actor@server.com`. For more information about WebFinger, please see [User Discovery](/federation/user-discovery).
-
-The Actor object **MUST** contain a `public_key` field that contains the public key of the server. This public key **MUST** be used to sign all requests sent by the server when the `author` field of an object is the server actor.
-
-The server actor **MUST** be used to sign all requests sent by the server when the `author` field of an object is the server actor.
-
-The server actor **SHOULD** contain empty data fields, such as `display_name` and `bio`. However, if the server actor does contain data fields, they **MUST** be valid, as with any actor.
\ No newline at end of file
diff --git a/docs/federation/user-discovery.md b/docs/federation/user-discovery.md
deleted file mode 100644
index 96ce849..0000000
--- a/docs/federation/user-discovery.md
+++ /dev/null
@@ -1,78 +0,0 @@
-# User Discovery
-
-> [!NOTE]
-> The terms "the server" and "the requesting server" are used in this section. "The server" refers to the server that is being discovered, and "the requesting server" refers to the server that is trying to discover the server.
-
-Servers **MUST** implement the [WebFinger](https://tools.ietf.org/html/rfc7033) protocol to allow other servers to discover their endpoints. This is done by serving a `host-meta` file at the address `/.well-known/host-meta`.
-
-The document **MUST** contain the following information, as specified by the WebFinger protocol:
-
-```xml
-
-
-
-
-```
-
-The `template` field **MUST** be the URI of the server's WebFinger endpoint, which is usually `https://example.com/.well-known/webfinger?resource={uri}`.
-
-The `resource` field **MUST** be the URI of the user that the server is trying to discover (in the format `acct:identifier@example.com`)
-
-Breaking down this URI, we get the following:
-
-- `acct`: The protocol of the URI. This is always `acct` for Lysand.
-- `identifier`: Either the UUID or the username of the user that the server is trying to discover.
-- `example.com`: The domain of the server that the user is on. This is usually the domain of the server. This can also be a subdomain of the server, such as `lysand.example.com`.
-
-This format is reminiscent of the `acct` format used by ActivityPub, but with either a UUID or a username instead of just an username. Users will typically not use the `id` of an actor to identify it, but instead its `username`: servers **MUST** only use the `id` to identify actors.
-
----
-
-Once the server's WebFinger endpoint has been discovered, it can receive a `GET` request to the endpoint to discover the endpoints of the user.
-
-The requesting server **MUST** send a `GET` request to the endpoint `https://example.com/.well-known/webfinger`.
-
-The requesting server **MUST** send the following headers with the request:
-
-- `Accept: application/jrd+json`
-- `Accept: application/json`
-
-The requestinng server **MUST** send the following query parameters with the request:
-
-- `resource`: The URI of the user that the server is trying to discover (in the format `acct:identifier@example.com` (replace `identifier` with the user's ID or username)
-
----
-
-The server **MUST** respond with a `200 OK` response code, and a JSON object in the body. This JSON object **MUST** contain the following information, as specified by the WebFinger protocol:
-
-```json5
-{
- "subject": "acct:identifier@example.com",
- "links": [
- {
- "rel": "self",
- "type": "application/json",
- "href": "https://example.com/users/uuid"
- },
- // The following links are optional but could be added to server software to display XML feeds and an HTML profile page
- {
- "rel": "http://webfinger.net/rel/profile-page",
- "type": "text/html",
- "href": "https://example.com/users/uuid"
- },
- {
- "rel": "http://schemas.google.com/g/2010#updates-from",
- "type": "application/atom+xml",
- "href": "https://example.com/users/uuid"
- },
- ]
-}
-```
-
-> [!NOTE]
-> The `subject` field **MUST** be the same as the `resource` field in the request.
-
-> [!NOTE]
-> The server implementation is free to add any additional links to the `links` array, such as for compatibility with other federation protocols. However, the links specified above **MUST** be included.
->
-> The `href` values of these links can be anything as long as it includes the `uuid` of the user, such as `https://example.com/accounts/uuid` or `https://example.com/uuid.`.
diff --git a/docs/groups.md b/docs/groups.md
deleted file mode 100644
index 55c1c76..0000000
--- a/docs/groups.md
+++ /dev/null
@@ -1,67 +0,0 @@
-# Groups
-
-Groups are a way to organize the visibility of objects on the server. Groups can be thought of as something similar to a Matrix room or a Discord channel, while also being similar to a Mastodon list.
-
-> [!NOTE]
-> Groups replace the old "visibility" system for Notes, which was designed for a microblogging context. Groups are more flexible and can be used for any application.
->
-> Notes can still use visibility in cases where groups are not needed with the `followers` and `public` values where you'd typically put a group URI (for example, in a [Publication](./objects/publications.md)'s `group` field').
-
-# Group Entity
-
-The group entity encapsulates the details of a group. It adheres to the following structure:
-
-```json5
-{
- "type": "Group",
- "id": "ed480922-b095-4f09-9da5-c995be8f5960",
- "uri": "https://example.com/groups/ed480922-b095-4f09-9da5-c995be8f5960",
- "name": {
- "text/html": {
- "content": "The Woozy fan club"
- }
- },
- "description": {
- "text/plain": {
- "content": "A group for fans of the Woozy emoji."
- }
- },
- "members": "https://example.com/groups/ed480922-b095-4f09-9da5-c995be8f5960/members",
-}
-```
-
-## Fields
-
-### Name
-
-| Name | Type | Required |
-| :--- | :------------ | :------- |
-| name | ContentFormat | No |
-
-The name of the group. This field is optional. Can contain custom emojis, like most other text fields.
-
-### Description
-
-| Name | Type | Required |
-| :---------- | :------------ | :------- |
-| description | ContentFormat | No |
-
-A description of the group. This field is optional. Can contain custom emojis, like most other text fields.
-
-### Members
-
-| Name | Type | Required |
-| :------ | :----- | :------- |
-| members | String | Yes |
-
-The URI of the group's members list. This field is required. Resolves to a [Collection](./structures/collection) of [User](./objects/user) objects.
-
-## Implementation
-
-`Note` objects can be posted to groups by setting the `group` field to the URI of the group. If there is no `group` field, the note is posted to whoever is mentioned in the `to` field.
-
-Other values for `group` are:
-- `public` for public notes, which can be seen by anyone.
-- `followers` for notes that can be seen by the author's followers only.
-
-If the `group` field is empty, and nobody is mentioned in the `to` field, the note is only visible to the author.
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
deleted file mode 100644
index ef819a6..0000000
--- a/docs/index.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-# https://vitepress.dev/reference/default-theme-home-page
-layout: home
-
-hero:
- name: "Lysand"
- text: "Federation, simpler"
- tagline: A simple to implement and complete federation protocol
- image:
- src: https://cdn.lysand.org/logo.webp
- alt: Lysand Logo
- actions:
- - theme: brand
- text: Protocol Docs
- link: /spec
- - theme: alt
- text: Lysand Server
- link: https://github.com/lysand-org/lysand
----
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/objects.md b/docs/objects.md
deleted file mode 100644
index 2d1db00..0000000
--- a/docs/objects.md
+++ /dev/null
@@ -1,62 +0,0 @@
-# Data Entities
-
-Lysand employs JSON (JavaScript Object Notation) entities for its data structure. This format is designed to be straightforward, facilitating easy implementation and comprehension.
-
-All JSON entities such as [Publications](objects/publications), [Actors](objects/actors), and [Actions](objects/actions) **MUST** include the following attributes:
-
-## Identifier (ID)
-
-The `id` attribute of an Entity is a string that serves as the unique identifier of the entity. It is utilized to distinguish the entity, and **MUST** be unique among all entities on the same server.
-
-While the `id` attribute is not mandated to be unique across the entire network, it is advisable to do so. Servers **MUST** employ UUIDs or a UUID-compatible system for the `id` attribute.
-
-## Creation Timestamp
-
-The `created_at` attribute of an entity is a string that signifies the date and time when the entity was created. It is used to sequence the entities. The data **MUST** adhere to the ISO 8601 format.
-
-Example: `2021-01-01T00:00:00.000Z`
-
-> [!NOTE]
-> The `created_at` attribute should reflect the actual date and time of the post, but it is not mandatory. Any ISO 8601 date is permissible in the `created_at` field. Servers have the discretion to process dates they deem invalid, such as future dates.
-
-## Uniform Resource Identifier (URI)
-
-The `uri` attribute of an entity is a string that signifies the URI of the entity. It is used to identify the entity, and **MUST** be unique among all entities. This URI **MUST** be unique across the entire network, and include the `id` of the entity in the URI.
-
-URIs must adhere to the rules defined [here](spec).
-
-## Entity Type
-
-The `type` attribute of an entity is a string that signifies the type of the entity. It is used to determine how the entity should be presented to the user.
-
-The `type` attribute **MUST** a type officially defined in the Lysand protocol. Extension types are **NOT** permitted and should instead use the [Extension System](extensions.md).
-
-# Types
-
-This document uses TypeScript to define the types of the entities in a clear and universal manner. TypeScript is a superset of JavaScript that adds static type definitions to the language. The types are defined in the following format:
-
-```typescript
-interface Entity {
- id: string;
- created_at: string;
- uri: string;
- type: string;
- extensions?: {
- "org.lysand:custom_emojis"?: {
- emojis: Emoji[];
- };
- [key: string]: object | undefined;
- };
-}
-```
-
-The `Entity` type is the base type for all entities in the Lysand protocol. It includes the `id`, `created_at`, `uri`, and `type` attributes.
-
-Other entities described in other parts of this documentation will extend the `Entity` type to include additional attributes, such as:
-
-```typescript
-interface ImaginaryNote extends Entity {
- content: string;
- mentions: string[];
-};
-```
\ No newline at end of file
diff --git a/docs/objects/actions.md b/docs/objects/actions.md
deleted file mode 100644
index 733a72d..0000000
--- a/docs/objects/actions.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# User Interactions
-
-User interactions in the Lysand protocol are primarily facilitated through Actions. These are JSON objects that encapsulate a user's intended operation, such as favouriting an object or initiating a follow request to another user.
-
-Actions are a broad category encompassing various types of objects, rather than being a specific object type.
-
-Here's an example of an Action:
-
-```json5
-{
- "type": "Like",
- "id": "3e7e4750-afd4-4d99-a256-02f0710a0520",
- "uri": "https://example.com/actions/3e7e4750-afd4-4d99-a256-02f0710a0520",
- "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe",
- "created_at": "2021-01-01T00:00:00.000Z",
- "object": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19"
-}
-```
-
-## Action Types
-
-The currently supported action types include:
-- [`Like`](./like)
-- [`Dislike`](./dislike)
-- [`Follow`](./follow)
-- [`FollowAccept`](./follow-accept)
-- [`FollowReject`](./follow-reject)
-- [`Announce`](./announce)
-- [`Undo`](./undo)
-
-Notably, the Lysand protocol does not include a `Block` action. This is because Lysand does not inherently support the concept of blocking users. Instead, the decision to display or hide content from a particular user should be done server-side.
-
-This approach helps prevent potential misuse of the protocol to determine if a user has been blocked by another, thereby addressing a significant privacy concern.
-
-## Fields
-
-### Author
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| author | String | Yes |
-
-URI of the [Actor](./actors) who initiated the action.
-
-## Types
-
-```typescript
-interface Action extends Entity {
- type: "Like" | "Dislike" | "Follow" | "FollowAccept" | "FollowReject" | "Announce" | "Undo";
- author: string
-}
-```
\ No newline at end of file
diff --git a/docs/objects/actors.md b/docs/objects/actors.md
deleted file mode 100644
index 7f31639..0000000
--- a/docs/objects/actors.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# Actors
-
-Actors are the primary users of the Lysand protocol. They are JSON objects that symbolize a user, akin to ActivityPub's `Actor` objects.
-
-Actors encompass two distinct object types currently, namely [Users](./user) and [Server Actors](../federation/server-actor).
-
-Here is a sample Actor:
-
-```json5
-{
- "type": "User",
- "id": "02e1e3b2-cb1f-4e4a-b82e-98866bee5de7",
- "uri": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7",
- "created_at": "2021-01-01T00:00:00.000Z",
- "display_name": "Gordon Ramsay",
- "username": "gordonramsay",
- "avatar": {
- "image/png": {
- "content": "https://cdn.test.com/avatars/ab5081cf-b11f-408f-92c2-7c246f290593.png",
- },
- },
- "header": {
- "image/png": {
- "content": "https://cdn.test.com/banners/ab5081cf-b11f-408f-92c2-7c246f290593.png",
- },
- },
- "indexable": true,
- "public_key": {
- "public_key": "...",
- "actor": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7"
- },
- "bio": {
- "text/plain": {
- "content": "Hello!",
- },
- },
- "fields": [],
- "featured": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/featured",
- "followers": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/followers",
- "following": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/following",
- "likes": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/likes",
- "dislikes": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/dislikes",
- "inbox": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/inbox",
- "outbox": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/outbox",
-}
-```
-
-For detailed information on their structure, please refer to the respective documentation for [User](./user) and [Server Actor](../federation/server-actor).
\ No newline at end of file
diff --git a/docs/objects/announce.md b/docs/objects/announce.md
deleted file mode 100644
index ecb8901..0000000
--- a/docs/objects/announce.md
+++ /dev/null
@@ -1 +0,0 @@
-This page has been moved to the [Microblogging Extension](../extensions/microblogging#announce).
diff --git a/docs/objects/dislike.md b/docs/objects/dislike.md
deleted file mode 100644
index 8b7002c..0000000
--- a/docs/objects/dislike.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Dislike
-
-The Dislike action signifies a user's negative sentiment towards an object. It is a frequently used action in the Lysand protocol.
-
-Example:
-```json5
-{
- "type": "Dislike",
- "id": "3e7e4750-afd4-4d99-a256-02f0710a0520",
- "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe",
- "uri": "https://example.com/actions/3e7e4750-afd4-4d99-a256-02f0710a0520",
- "created_at": "2021-01-01T00:00:00.000Z",
- "object": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19"
-}
-```
-
-## Fields
-
-### Author
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| author | String | Yes |
-
-URI of the [Actor](./actors) who initiated the action.
-
-### Object
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| object | String | Yes |
-
-URI of the object being disliked. Must be of type [Note](./note)
-
-## Types
-
-```typescript
-interface Dislike extends Action {
- type: "Dislike";
- object: string;
-}
-```
\ No newline at end of file
diff --git a/docs/objects/follow-accept.md b/docs/objects/follow-accept.md
deleted file mode 100644
index d595c28..0000000
--- a/docs/objects/follow-accept.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Follow Accept
-
-A FollowAccept action symbolizes a user's consent to a follow request from another user. Upon acceptance, the user will start receiving the other user's posts in their feed.
-
-Example:
-```json5
-{
- "type": "FollowAccept",
- "id": "3e7e4750-afd4-4d99-a256-02f0710a0520",
- "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe",
- "uri": "https://example.com/actions/3e7e4750-afd4-4d99-a256-02f0710a0520",
- "created_at": "2021-01-01T00:00:00.000Z",
- "follower": "https://example.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7"
-}
-```
-
-## Fields
-
-### Author
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| author | String | Yes |
-
-URI of the [Actor](./actors) who was being follow requested
-
-### Follower
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| follower | String | Yes |
-
-URI of the [User](./user) who tried to follow the author
-
-## Types
-
-```typescript
-interface FollowAccept extends Action {
- type: "FollowAccept";
- follower: string;
-}
-```
\ No newline at end of file
diff --git a/docs/objects/follow-reject.md b/docs/objects/follow-reject.md
deleted file mode 100644
index aed6449..0000000
--- a/docs/objects/follow-reject.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Follow Reject
-
-A FollowReject action signifies a user's decision to decline a follow request from another user. This rejection prevents the requesting user's posts from appearing in the recipient's feed.
-
-Example:
-```json5
-{
- "type": "FollowReject",
- "id": "3e7e4750-afd4-4d99-a256-02f0710a0520",
- "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe",
- "uri": "https://example.com/actions/3e7e4750-afd4-4d99-a256-02f0710a0520",
- "created_at": "2021-01-01T00:00:00.000Z",
- "follower": "https://example.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7"
-}
-```
-
-## Fields
-
-### Author
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| author | String | Yes |
-
-URI of the [Actor](./actors) who was being follow requested.
-
-### Follower
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| follower | String | Yes |
-
-URI of the [User](./user) who tried to follow the author.
-
-## Types
-
-```typescript
-interface FollowReject extends Action {
- type: "FollowReject";
- follower: string;
-}
-```
diff --git a/docs/objects/follow.md b/docs/objects/follow.md
deleted file mode 100644
index 7d907bd..0000000
--- a/docs/objects/follow.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Follow
-
-A Follow action embodies a user's intent to follow another user. Upon successful execution of this action, the follower will start receiving the followed user's posts in their feed. This action facilitates the creation of a personalized content stream for each user.
-
-Example:
-```json5
-{
- "type": "Follow",
- "id": "3e7e4750-afd4-4d99-a256-02f0710a0520",
- "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe",
- "uri": "https://example.com/actions/3e7e4750-afd4-4d99-a256-02f0710a0520",
- "created_at": "2021-01-01T00:00:00.000Z",
- "followee": "https://example.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7"
-}
-```
-
-## Fields
-
-### Author
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| author | String | Yes |
-
-URI of the [Actor](./actors) who initiated the action.
-
-### Followee
-
-| Name | Type | Required |
-| :------- | :----- | :------- |
-| followee | String | Yes |
-
-URI of the [User](./user) who is being follow requested.
-
-## Types
-
-```typescript
-interface Follow extends Action {
- type: "Follow";
- followee: string;
-}
-```
diff --git a/docs/objects/like.md b/docs/objects/like.md
deleted file mode 100644
index b80ebba..0000000
--- a/docs/objects/like.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Like
-
-The Dislike action signifies a user's positive sentiment towards an object, akin to a favourite. It is a frequently used action in the Lysand protocol.
-
-Example:
-```json5
-{
- "type": "Like",
- "id": "3e7e4750-afd4-4d99-a256-02f0710a0520",
- "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe",
- "uri": "https://example.com/actions/3e7e4750-afd4-4d99-a256-02f0710a0520",
- "created_at": "2021-01-01T00:00:00.000Z",
- "object": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19"
-}
-```
-
-## Fields
-
-### Author
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| author | String | Yes |
-
-URI of the [Actor](./actors) who initiated the action.
-
-### Object
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| object | String | Yes |
-
-URI of the object being liked. Must be of type [Note](./note)
-
-## Types
-
-```typescript
-interface Like extends Action {
- type: "Like";
- object: string;
-}
-```
diff --git a/docs/objects/note.md b/docs/objects/note.md
deleted file mode 100644
index fb71073..0000000
--- a/docs/objects/note.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Note
-
-A `Note` object symbolizes a basic post or publication within the Lysand protocol. It is the most frequently used object type.
-
-`Note` objects extend all properties from the [Publication](./publications) object, thereby inheriting its characteristics and behaviors.
-
-### Types
-
-```typescript
-interface Note extends Publication {
- type: "Note";
-}
-```
\ No newline at end of file
diff --git a/docs/objects/patch.md b/docs/objects/patch.md
deleted file mode 100644
index cc93488..0000000
--- a/docs/objects/patch.md
+++ /dev/null
@@ -1,65 +0,0 @@
-# Patch
-
-A `Patch` object represents a modification to a [Note](./note). It is primarily used to update a [Note](./note), for instance, to correct a typographical error.
-
-`Patch` objects are intended for internal server use and are not designed to be displayed to the user.
-
-Each subsequent patch is applied to the original object, not the preceding patch. The server is responsible for presenting the most recent patch stored to the client.
-
-> [!NOTE]
-> A `Patch` object should replace the object it is patching when displayed to the client. Therefore, if a Patch object lacks some fields from the previous object, these fields should be removed in the edit.
-
-Here is a sample `Patch` for the aforementioned object:
-
-```json5
-{
- "type": "Patch",
- "id": "4c21fdea-1318-4d14-b3aa-1ac2f3db2e53",
- "uri": "https://example.com/publications/4c21fdea-1318-4d14-b3aa-1ac2f3db2e53",
- "patched_id": "f08a124e-fe90-439e-8be4-15a428a72a19",
- "patched_at": "2021-01-01T00:00:00.000Z",
- "contents": [
- {
- "content": "This is patched!",
- "content_type": "text/plain"
- },
- ],
- // ...
-}
-```
-
-## Fields
-
-### ID
-
-This ID must be distinct from the original Note object, but it does not replace the original Note object's ID. It serves to identify the Patch object.
-
-### Patched ID
-
-| Name | Type | Required |
-| :--------- | :----- | :------- |
-| patched_id | String | Yes |
-
-This is the URI of the object being patched. It must be a [Note](./note).
-
-### Patched At
-
-| Name | Type | Required |
-| :--------- | :----- | :------- |
-| patched_at | String | Yes |
-
-This is the date and time when the object was patched. It must be in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format.
-
-### Other
-
-`Patch` objects inherit all other properties from [Publications](./publications).
-
-## Types
-
-```typescript
-interface Patch extends Publication {
- type: "Patch";
- patched_id: string;
- patched_at: string;
-}
-```
diff --git a/docs/objects/publications.md b/docs/objects/publications.md
deleted file mode 100644
index 235608c..0000000
--- a/docs/objects/publications.md
+++ /dev/null
@@ -1,346 +0,0 @@
-# Publications
-
-Publications are the main building blocks of the Lysand protocol. They are JSON objects that represent a publication, such as a post or a comment.
-
-Here is an example publication:
-```json5
-{
- "type": "Note",
- "id": "f08a124e-fe90-439e-8be4-15a428a72a19",
- "uri": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19",
- "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe",
- "created_at": "2021-01-01T00:00:00.000Z",
- "content": {
- "text/plain": {
- "content": "Hello, world! I own this website: https://google.com"
- },
- "text/html": {
- "content": "Hello, world! I own this website! https://google.com"
- }
- },
- "category": "microblog",
- "device": {
- "name": "Megalodon for Android",
- "version": "1.3.89",
- "url": "https://sk22.github.io/megalodon"
- },
- "previews": [
- {
- "link": "https://google.com",
- "title": "Google",
- "description": "The world's most popular search engine",
- "image": "https://cdn.example.com/previews/6e0204a2-746c-4972-8602-c4f37fc63bbe.png",
- "icon": "https://google.com/favicon.ico"
- }
- ],
- "group": "public",
- "attachments": [
- {
- "image/png": {
- "content": "https://cdn.example.com/attachments/ece2f9d9-27d7-457d-b657-4ce9172bdcf8.png"
- },
- "image/webp": {
- "content": "https://cdn.example.com/attachments/ece2f9d9-27d7-457d-b657-4ce9172bdcf8.webp"
- }
- }
- ],
- "replies_to": "https://test.com/publications/0b6ecf49-2959-4590-afb6-232f57036fa6",
- "mentions": [
- "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7"
- ],
-}
-```
-
-## Fields
-
-### Type
-
-Currently available types are:
-- [`Note`](./note)
-- [`Patch`](./patch)
-
-### Author
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| author | String | Yes |
-
-URI of the user that created the object.
-
-### Content
-
-| Name | Type | Required |
-| :------ | :------------ | :------- |
-| content | ContentFormat | No |
-
-Content of the Publication, such as note text for [Notes](./note). If it is not provided, it is assumed that the publication does not have any content.
-
-It is recommended that servers limit the length of the content from 500 to a couple thousand characters, but it is up to the server to decide how long the content can be. The protocol does not have an upper limit for the length of the content.
-
-The `content` field **MUST** be a text format, such as `text/plain` or `text/html`. The `content` field **MUST NOT** be a binary format, such as `image/png` or `video/mp4`. Platforms such as video sharing sites should use the `attachments` field for media instead.
-
-An example value for the `content` field would be:
-```json5
-{
- // ...
- "content": {
- "text/plain": {
- "content": "Hello, world!"
- },
- "text/html": {
- "content": "Hello, world!"
- }
- }
- // ...
-}
-```
-
-> [!NOTE]
-> Lysand heavily recommends that servers support the `text/html` content type, as it is the richest content type that is supported by most clients.
->
-> Lysand also recommends that servers always include a `text/plain` version of each object, as it is the most basic content type that is supported by all clients, such as command line clients.
-
-> [!WARNING]
-> Servers should not trust the `text/html` content type, as it could contain malicious code. Servers should always sanitize the content before displaying it to the user.
->
-> Additionally, frontends should warn users before clicking on links that do not match the link text, such as `https://google.com`
-
-It is up to the client to choose which content format to display to the user. The client may choose to display the first content format that it supports, or it may choose to display the content format that it thinks is the most appropriate.
-
-Clients should display the richest content format that they support, such as HTML or more exotic formats such as MFM.
-
-### Category
-
-| Name | Type | Required |
-| :------- | :----------- | :------- |
-| category | CategoryType | No |
-
-Category of the publication. Used for clients to possibly display notes in different ways, for example a note with the `microblog` category could be displayed in a timeline, while a note with the `forum` category could be displayed Reddit-style.
-
-See [the Types section](#types) for more information on the `CategoryType` enum.
-
-### Device
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| device | Device | No |
-
-Device that the publication was created on. If it is not provided, it is assumed that the publication was created on a generic device.
-
-Servers should avoid collecting any information that could be used to identify the user, such as IP addresses or user agents. A simple name is recommended.
-
-### Previews
-
-| Name | Type | Required |
-| :------- | :------------------- | :------- |
-| previews | Array of LinkPreview | No |
-
-Previews for links in the publication. Optional. This is to avoid the [stampeding mastodon problem](https://github.com/mastodon/mastodon/issues/23662) where a link preview is fetched by every server that sees the publication, creating an accidental DDOS attack.
-
-> [!WARNING]
-> Servers should make sure not to trust the previews, as they could be faked by remote servers. This is not a very good attack vector, but it is still possible to redirect users to malicious links.
-
-### Group
-
-| Name | Type | Required |
-| :---- | :----- | :------- |
-| group | String | No |
-
-URI of a [Group](../groups.md), or `public` or `followers`.
-
-Refer to the [Groups](../groups.md) page for more information on groups, their implementation and what to do if this value is not provided.
-
-### Attachments
-
-| Name | Type | Required |
-| :---------- | :--------------------- | :------- |
-| attachments | Array of ContentFormat | No |
-
-Contains list of attachments for the publication in [ContentFormat](../structures/content-format) structure. f it is not provided, it is assumed that the publication does not have any attachments.
-
-It is recommended that servers limit the number of attachments to 20, but it is up to the server to decide how many attachments a publication can have. The protocol does not have an upper limit for the number of attachments.
-
-The `attachments` field **MAY** be in any format, such as `image/png`, `image/webp`, `video/mp4`, or `audio/mpeg`. It is up to the server to decide which formats are allowed.
-
-> [!NOTE]
-> Lysand recommends that servers let users upload any file as an attachment, but clients should warn users before downloading potentially malicious files, such as `.exe` files.
-
-### Replies To
-
-| Name | Type | Required |
-| :--------- | :----- | :------- |
-| replies_to | String | No |
-
-URI of the `Note` that the publication is replying to, if any. Used to determine the reply chain of an object.
-
-### Quotes
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| quotes | String | No |
-
-URI of the `Note` that the publication is quoting, if any. It is used to determine the quote chain of an object.
-
-Quoting is similar to replying, but it does not (by default) notify the user that they were quoted. It is meant to be used to comment or add context to another publication.
-
-Example of quoting:
-```json5
-{
- // ...
- "quotes": "https://test.com/publications/5f886c84-f8f7-4f65-8ac2-4691d385c509",
- // ...
-}
-```
-
-Quoting **SHOULD BE** rendered differently from replying, such as by adding a quote block to the publication or including the quoted post in the publication.
-
-### Mentions
-
-| Name | Type | Required |
-| :------- | :-------------- | :------- |
-| mentions | Array of String | No |
-
-URIs of users that the publication is mentioning. (such as `@username@server.social` in a note). If it is not provided, it is assumed that the publication is not mentioning any other users.
-
-An example value for `mentions` would be:
-```json5
-{
- // ...
- "mentions": [
- "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7"
- ]
- // ...
-}
-```
-
-### Subject
-
-| Name | Type | Required |
-| :------ | :----- | :------- |
-| subject | String | No |
-
-Subject of the publication. May be used as a content warning or spoiler warning. If not provided or is empty, there is no subject.
-
-It is recommended that servers limit the length of the subject from 1 to 300 characters, but it is up to the server to decide how long the subject can be. The protocol does not have an upper limit for the length of the subject.
-
-The `subject` field **MUST NOT** be a `ContentFormat` object. It **MUST** be a string, and **MUST** be plain text. It **MUST NOT** contain any HTML or other markup.
-
-See [ContentFormat](/structures/content-format) for more information on `ContentFormat` objects.
-
-Client extensions are welcome to add support for HTML or other markup in the `subject` field, but it is not made this way by design (although [Custom Emojis](../extensions/custom-emojis) are supported)
-
-An example value for `subject` would be:
-```json5
-{
- // ...
- "subject": "This is a subject!"
- // ...
-}
-```
-
-### Is Sensitive
-
-| Name | Type | Required |
-| :----------- | :----- | :------- |
-| is_sensitive | String | No |
-
-Whether or not the publication contains sensitive content, whether in the content or in attachments or emojis. May be used as a content warning or spoiler warning. If not provided, it is assumed that the publication is not sensitive.
-
-An example value for `is_sensitive` would be:
-```json5
-{
- // ...
- "is_sensitive": true
- // ...
-}
-```
-
-### Visibility
-
-| Name | Type | Required |
-| :--------- | :--------- | :------- |
-| visibility | Visibility | Yes |
-
-Can be `public`, `unlisted`, `followers`, or `direct`. If not provided, it is assumed that the publication is public.
-
-- `public`: The publication is visible to everyone, including anonymous users.
-- `unlisted`: The publication is visible to everyone, but it should not appear in public timelines or search results.
-- `followers`: The publication is visible to followers only.
-- `direct`: The publication is a direct message, and is visible only to the mentioned users.
-
-Servers **MUST** respect the visibility of the publication and **MUST NOT** show the publication to users who are not allowed to see it.
-
-## Types
-
-```typescript
-interface Publication extends Entity {
- type: "Note" | "Patch";
- author: string;
- content?: ContentFormat;
- category?: CategoryType;
- device?: Device;
- previews?: LinkPreview[];
- group?: string | "public" | "followers";
- attachments?: ContentFormat[];
- replies_to?: string;
- quotes?: string;
- mentions?: string[];
- subject?: string;
- is_sensitive?: boolean;
- visibility: Visibility;
- extensions?: Entity["extensions"] & {
- "org.lysand:reactions"?: {
- reactions: string;
- };
- "org.lysand:polls"?: {
- poll: {
- options: ContentFormat[];
- votes: number[]; // unsigned 64-bit integer
- multiple_choice?: boolean;
- expires_at: string;
- };
- };
- };
-}
-```
-
-```typescript
-enum Visibility {
- Public = "public",
- Unlisted = "unlisted",
- Followers = "followers",
- Direct = "direct"
-}
-```
-
-```typescript
-interface LinkPreview {
- link: string;
- title: string;
- description?: string;
- image?: string;
- icon?: string;
-}
-```
-
-```typescript
-interface Device {
- name: string;
- version?: string;
- url?: string;
-}
-```
-
-```typescript
-/*
- * UI examples for each category:
- * microblog -> Twitter, Mastodon
- * forum -> Reddit
- * blog -> Wordpress, WriteFreely
- * image -> Instagram
- * video -> YouTube
- * audio -> SoundCloud, Spotify
- * messaging -> Discord, Matrix, Signal
- */
-type CategoryType = "microblog" | "forum" | "blog" | "image" | "video" | "audio" | "messaging"
-```
diff --git a/docs/objects/server-metadata.md b/docs/objects/server-metadata.md
deleted file mode 100644
index 376069d..0000000
--- a/docs/objects/server-metadata.md
+++ /dev/null
@@ -1,174 +0,0 @@
-# Server Metadata
-
-Server metadata is metadata that servers can provide to clients to help them determine how to interact with the server. It is meant to be a simple way for servers to provide information to other servers and clients.
-
-Unlike other objects, server metadata is not meant to be federated. The `id`, `uri` and `created_at` fields are not required on server metadata objects.
-
-Here is an example server metadata object:
-```json5
-{
- "type": "ServerMetadata",
- "name": "Example Server",
- "version": "1.0.0",
- "description": "This is an example server.",
- "website": "https://example.com",
- "moderators": [
- "https://example.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7",
- // ...
- ],
- "admins": [
- // ...
- ],
- "logo": {
- "image/png": {
- "content": "https://cdn.example.com/logo.png"
- },
- "image/webp": {
- "content": "https://cdn.example.com/logo.webp"
- }
- },
- "banner": {
- "image/png": {
- "content": "https://cdn.example.com/banner.png"
- },
- "image/webp": {
- "content": "https://cdn.example.com/banner.webp"
- }
- },
- "supported_extensions": [ "org.lysand:reactions" ],
- "extensions": {
- // Example extension
- "org.joinmastodon:monthly_active_users": 1000
- }
-}
-```
-
-## Fields
-
-### Type
-
-| Name | Type | Required | Notes |
-| :--- | :----- | :------- | ------------------------ |
-| type | String | Yes | Must be "ServerMetadata" |
-
-### Name
-
-| Name | Type | Required |
-| :--- | :----- | :------- |
-| name | String | Yes |
-
-Represents the name of the server. The `name` field is required on all Server Metadata objects. This should be the name of the server instance itself, such as "Rosie's Lysand Server", not the software name such as "Mastodon".
-
-It is recommended that servers limit the length of the name from 1 to 50 characters, but it is up to the server to decide how long the name can be. The protocol does not have an upper limit for the length of the name.
-
-### Version
-
-| Name | Type | Required | Notes |
-| :------ | :----- | :------- | ----------------------- |
-| version | String | Yes | Should be SemVer format |
-
-Represents the version of the server software. It is recommended that servers use [SemVer](https://semver.org) to version their servers, but it is not required.
-
-### Description
-
-| Name | Type | Required |
-| :---------- | :----- | :------- |
-| description | String | No |
-
-
-This is a short description of this particular server. It should include information about the server, such as what it is about and what it is used for.
-
-For example, a server focused on a specific topic may include information about that topic in the description.
-
-It is recommended that servers limit the length of the description from 1 to 500 characters, but it is up to the server to decide how long the description can be. The protocol does not have an upper limit for the length of the description.
-
-### Website
-
-| Name | Type | Required |
-| :------ | :----- | :------- |
-| website | String | No |
-
-Represents the website of the server. This may be used to link to the server's website, such as a status page or a public modlog.
-
-### Moderators
-
-| Name | Type | Required |
-| :--------- | :-------------- | :------- |
-| moderators | Array of String | No |
-
-Rrray of URIs to the server moderators.
-
-If it is not provided, it is assumed that the server does not have any moderators, or is not willing to provide a list.
-
-### Admins
-
-| Name | Type | Required |
-| :----- | :-------------- | :------- |
-| admins | Array of String | No |
-
-The `admins` field on a Server Metadata object is an array of URIs that represent the admins of the server.
-
-The `admins` field is not required on all Server Metadata objects. If it is not provided, it is assumed that the server does not have any admins, or is not willing to provide a list.
-
-### Logo
-
-| Name | Type | Required |
-| :--- | :------------ | :------- |
-| logo | ContentFormat | No |
-
-
-The `logo` field on a Server Metadata is a [ContentFormat](../structures/content-format) object.
-
-The logo content_type **MUST** be an image format, such as `image/png` or `image/jpeg` (animated images are permitted). The logo content_type **MUST NOT** be a video format, such as `video/mp4` or `video/webm`.
-
-Lysand heavily recommends that servers provide both the original format and a modern format for each logo, such as WebP, AVIF, JXL, or HEIF. This is to reduce bandwidth usage and improve performance for clients.
-
-Clients should display the most modern format that they support, such as WebP, AVIF, JXL, or HEIF. If the client does not support any modern formats, it should display the original format.
-
-> [!NOTE]
-> Servers may find it useful to use a CDN that can automatically convert images to modern formats, such as Cloudflare. This will offload image processing from the server, and improve performance for clients.
-
-### Banner
-
-| Name | Type | Required |
-| :----- | :------------ | :------- |
-| banner | ContentFormat | No |
-
-The `banner` field on a Server Metadata is a [ContentFormat](../structures/content-format) object.
-
-The banner content_type **MUST** be an image format, such as `image/png` or `image/jpeg` (animated images are permitted). The banner content_type **MUST NOT** be a video format, such as `video/mp4` or `video/webm`.
-
-Lysand heavily recommends that servers provide both the original format and a modern format for each banner, such as WebP, AVIF, JXL, or HEIF. This is to reduce bandwidth usage and improve performance for clients.
-
-Clients should display the most modern format that they support, such as WebP, AVIF, JXL, or HEIF. If the client does not support any modern formats, it should display the original format.
-
-> [!NOTE]
-> Servers may find it useful to use a CDN that can automatically convert images to modern formats, such as Cloudflare. This will offload image processing from the server, and improve performance for clients.
-
-### Supported Extensions
-
-| Name | Type | Required |
-| :------------------- | :-------------- | :----------------------------- |
-| supported_extensions | Array of String | Yes, can be empty array (`[]`) |
-
-List of extension names that the server supports, in namespaced format (`"org.lysand:reactions"`).
-
-## Types
-
-```typescript
-interface ServerMetadata {
- type: "ServerMetadata";
- name: string;
- version: string;
- description?: string;
- website?: string;
- moderators?: string[];
- admins?: string[];
- logo?: ContentFormat;
- banner?: ContentFormat;
- supported_extensions: string[];
- extensions?: {
- [key: string]: object | undefined;
- };
-}
-```
\ No newline at end of file
diff --git a/docs/objects/undo.md b/docs/objects/undo.md
deleted file mode 100644
index 0643fb0..0000000
--- a/docs/objects/undo.md
+++ /dev/null
@@ -1,53 +0,0 @@
-# Undo
-
-An `Undo` object signifies the reversal of a previously executed action by a user. It is primarily used to revoke an action or remove an existing object.
-
-An `Undo` object **MUST** contain an `object` field that holds the URI of the object being reversed.
-
-Servers **MUST NOT** permit users to reverse actions that they did not initiate.
-
-Upon receiving `Undo` actions, servers **MUST** reverse the action being undone. For instance, if a user expresses liking a post and subsequently undoes the like action, the server **MUST** eliminate the like from the post. Similarly, if an `Undo` action is received for a `Follow` action, the server **MUST** cease following the user.
-
-If the `Undo` action has a Publication or another object as the `object` field, the server **MUST** discontinue displaying the object to users. It is recommended, but not mandatory, to delete the original object.
-
-An `Undo` action on a `Patch` object **MUST** be interpreted as the cancellation of the `Note` object, not the patch itself.
-
-Example:
-```json5
-{
- "type": "Undo",
- "id": "3e7e4750-afd4-4d99-a256-02f0710a0520",
- "author": "https://example.com/users/6e0204a2-746c-4972-8602-c4f37fc63bbe",
- "uri": "https://example.com/actions/3e7e4750-afd4-4d99-a256-02f0710a0520",
- "created_at": "2021-01-01T00:00:00.000Z",
- "object": "https://example.com/publications/f08a124e-fe90-439e-8be4-15a428a72a19"
-}
-```
-
-## Fields
-
-### Author
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| author | String | Yes |
-
-URI of the [Actor](./actors) who initiated the action.
-
-### Object
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| object | String | Yes |
-
-URI of the object being undone. The object **MUST** be an [Action](./actions) or a [Note](./note). To undo [Patch](./patch) objects, use a subsequent [Patch](./patch) or delete the original [Note](./note).
-
-## Types
-
-```typescript
-interface Undo extends Entity {
- type: "Undo";
- author: string;
- object: string;
-}
-```
\ No newline at end of file
diff --git a/docs/objects/user.md b/docs/objects/user.md
deleted file mode 100644
index db7f73f..0000000
--- a/docs/objects/user.md
+++ /dev/null
@@ -1,355 +0,0 @@
-# User
-
-Users, represented as Actors, are unique entities on the server. They are the sole type of Actor.
-
-Here is an example user:
-
-```json5
-{
- "type": "User",
- "id": "02e1e3b2-cb1f-4e4a-b82e-98866bee5de7",
- "uri": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7",
- "created_at": "2021-01-01T00:00:00.000Z",
- "display_name": "Gordon Ramsay",
- "username": "gordonramsay",
- "avatar": {
- "image/png": {
- "content": "https://cdn.test.com/avatars/ab5081cf-b11f-408f-92c2-7c246f290593.png",
- },
- "image/webp": {
- "content": "https://cdn.test.com/avatars/ab5081cf-b11f-408f-92c2-7c246f290593.webp",
- }
- },
- "header": {
- "image/png": {
- "content": "https://cdn.test.com/banners/ab5081cf-b11f-408f-92c2-7c246f290593.png",
- },
- "image/webp": {
- "content": "https://cdn.test.com/banners/ab5081cf-b11f-408f-92c2-7c246f290593.webp",
- }
- },
- "indexable": true,
- "manually_approves_followers": false,
- "public_key": {
- "public_key": "...",
- "actor": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7"
- },
- "bio": {
- "text/plain": {
- "content": "My name is Gordon Ramsay, I'm a silly quirky little pony that LOVES to roleplay in the bedroom!",
- },
- "text/html": {
- "content": "My name is Gordon Ramsay, I'm a silly quirky little pony that LOVES to roleplay in the bedroom!",
- }
- },
- "fields": [
- {
- "key": {
- "text/plain": {
- "content": "Where I live",
- }
- },
- "value": {
- "text/plain": {
- "content": "Portland, Oregon",
- }
- }
- }
- ],
- "featured": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/featured",
- "followers": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/followers",
- "following": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/following",
- "likes": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/likes",
- "dislikes": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/dislikes",
- "inbox": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/inbox",
- "outbox": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7/outbox",
-}
-```
-
-## Fields
-
-All text fields can incorporate [Custom Emojis](../extensions/custom-emojis) as part of the Custom Emojis protocol extension. If a server doesn't support the Custom Emojis extension, no additional work is required to ensure text field compatibility: custom emojis are denoted as part of the plaintext content using `:emoji-name:` syntax, which won't disrupt text formatting.
-
-### Type
-
-The `type` of a `User` is invariably `User`.
-
-### Public Key
-
-| Name | Type | Required |
-| :--------- | :------------------------------------- | :------- |
-| public_key | [ActorPublicKeyData](../security/keys) | Yes |
-
-Author public key. Used to authenticate the actor's identity for their posts. The key **MUST** be encoded in base64.
-
-All actors **MUST** have a `public_key` field. All servers **SHOULD** authenticate the actor's identity using the `public_key` field, which is used to encode any HTTP requests emitted on behalf of the actor.
-
-For more information on cryptographic signing, please see the [Signing](/security/signing) page.
-
-Example of encoding the key in TypeScript:
-```ts
-// Where keyPair is your key pair
-const publicKey = btoa(String.fromCharCode(...new Uint8Array(await crypto.subtle.exportKey("spki", keyPair.publicKey))));
-```
-
-### Display Name
-
-| Name | Type | Required |
-| :----------- | :----- | :------- |
-| display_name | String | No |
-
-User's display name. If it is not provided, it is assumed that the actor does not have a display name, and the actor's username should be used instead as a fallback.
-
-Display names **MUST** be treated as mutable, and **MUST NOT** be used to identify the actor.
-
-It is recommended that servers limit the display name length from 1 to 50 characters, but the server has the discretion to decide the display name length. The protocol does not impose an upper limit for the display name length.
-
-### Username
-
-| Name | Type | Required |
-| :------- | :----- | :------- |
-| username | String | Yes |
-
-Actor's username (`@cpluspatch` for example). It is used to loosely identify the actor, and **MUST** be unique across all actors of a server.
-
-The `username` field **MUST NOT** be used to identify the actor internally or across the protocol. It is only meant to be used as a display name, and as such is mutable by the user.
-
-The `username` field **MUST** be a string that contains only alphanumeric lowercase characters, underscores, and dashes. It **MUST NOT** contain any spaces or other special characters.
-
-It **MUST** match this regex: `/^[a-z0-9_-]+$/`
-
-It is recommended that servers limit the username length from 1 to 20 characters, but the server has the discretion to decide the username length. The protocol does not impose an upper limit for the username length.
-
-#### Implementation Details
-
-Usernames are intended to be mutable, but clients should guard this action with warnings and confirmations to prevent accidental changes. Servers should also rate limit username changes to prevent abuse.
-
-Since user search is done via the username, servers could implement a username history system to be able to find users by their old usernames. However, users might not want to be found by their old usernames, so this feature should be implemented with privacy in mind.
-
-### Indexable
-
-| Name | Type | Required |
-| :-------- | :------ | :------- |
-| indexable | Boolean | Yes |
-
-Whether or not the actor should be indexed by search engines.
-
-Servers and search engines should respect the `indexable` field, and **SHOULD NOT** index the actor if the `indexable` field is set to `false`. This is to protect the privacy of users that do not want to be indexed by search engines.
-
-#### Implementation Details
-
-This field should also trigger a change in the `robots.txt` file of the server, to prevent search engines from indexing the user's profile, or some other kind of mechanism to prevent indexing (some search engines support using meta tags or headers for example)
-
-### Manually Approves Followers
-
-| Name | Type | Required |
-| :-------------------------- | :------ | :------- |
-| manually_approves_followers | Boolean | Yes |
-
-Whether or not the actor manually approves followers. This is meant to be used for clients to know if follow requests are automatically accepted or if they need to be manually approved by the user.
-
-### Avatar
-
-| Name | Type | Required |
-| :----- | :------------ | :------- |
-| avatar | ContentFormat | No |
-
-Profile picture for users. If it is not provided, it is assumed that the actor does not have an avatar.
-
-The avatar content_type **MUST** be an image format, such as `image/png` or `image/jpeg`. The avatar content_type **MUST NOT** be a video format, such as `video/mp4` or `video/webm`.
-
-It is strongly recommended that servers provide both the original format and a modern format for each avatar, such as WebP, AVIF, JXL, or HEIF. This is to reduce bandwidth usage and improve performance for clients.
-
-Clients should display the most modern format that they support, such as WebP, AVIF, JXL, or HEIF. If the client does not support any modern formats, it should display the original format.
-
-### Header
-
-| Name | Type | Required |
-| :----- | :------------ | :------- |
-| header | ContentFormat | No |
-
-Banner for users. If it is not provided, it is assumed that the actor does not have a header.
-
-The header content_type **MUST** be an image format, such as `image/png` or `image/jpeg` (animated images are permitted). The header content_type **MUST NOT** be a video format, such as `video/mp4` or `video/webm`.
-
-It is strongly recommended that servers provide both the original format and a modern format for each header, such as WebP, AVIF, JXL, or HEIF. This is to reduce bandwidth usage and improve performance for clients.
-
-Clients should display the most modern format that they support, such as WebP, AVIF, JXL, or HEIF. If the client does not support any modern formats, it should display the original format.
-
-### Bio
-
-| Name | Type | Required |
-| :--- | :------------ | :------- |
-| bio | ContentFormat | No |
-
-Used to display a short description of the actor to cleints. It is recommended that servers limit the bio length from 500 to a couple thousand characters, but the server has the discretion to decide the bio length. The protocol does not impose an upper limit for the bio length.
-
-The `bio` **MUST** be a text format, such as `text/plain` or `text/html`. The `bio` **MUST NOT** be a binary format, such as `image/png` or `video/mp4`.
-
-An example value for the `bio` field would be:
-```json5
-{
- // ...
- "bio": {
- "text/plain": {
- "content": "This is my bio!",
- },
- "text/html": {
- "content": "This is my bio!",
- }
- }
- // ...
-}
-```
-
-> [!NOTE]
-> Lysand heavily recommends that servers support the `text/html` content type, as it is the most rich content type that is supported by most clients.
->
-> Lysand also recommends that servers always include a `text/plain` version of each object, as it is the most basic content type that is supported by all clients, such as command line clients.
-
-It is up to the client to choose which content format to display to the user. The client may choose to display the first content format that it supports, or it may choose to display the content format that it thinks is the most appropriate.
-
-### Fields
-
-| Name | Type | Required |
-| :----- | :------------- | :------- |
-| fields | Array of Field | No |
-
-Custom key-value pairs for clients, such as additional metadata. If not provided, it is assumed that the actor does not have any fields.
-
-An example value for the `fields` field would be:
-```json5
-{
- // ...
- "fields": [
- {
- "key": {
- "text/plain": {
- "content": "Where I live",
- }
- },
- "value": {
- "text/plain": {
- "content": "Portland, Oregon",
- }
- }
- }
- // Other fields...
- ]
- // ...
-}
-```
-
-Fields are formatted as follows:
-```ts
-interface Field {
- key: ContentFormat;
- value: ContentFormat;
-}
-```
-
-Both `key` and `value` should be presented to the user as a pair.
-
-The `key` and `value` fields **MUST** be text formats, such as `text/plain` or `text/html`. They **MUST NOT** be binary formats, such as `image/png` or `video/mp4`.
-
-### Featured
-
-| Name | Type | Required |
-| :------- | :----- | :------- |
-| featured | String | Yes |
-
-Please refer to [Featured Publications](../federation/endpoints) for more information.
-
-### Followers
-
-| Name | Type | Required |
-| :-------- | :----- | :------- |
-| followers | String | Yes |
-
-Please refer to [User Followers](../federation/endpoints) for more information.
-
-### Following
-
-| Name | Type | Required |
-| :-------- | :----- | :------- |
-| following | String | Yes |
-
-Please refer to [User Following](../federation/endpoints) for more information.
-
-### Likes
-
-| Name | Type | Required |
-| :---- | :----- | :------- |
-| likes | String | Yes |
-
-Please refer to [User Likes](../federation/endpoints) for more information.
-
-### Dislikes
-
-| Name | Type | Required |
-| :------- | :----- | :------- |
-| dislikes | String | Yes |
-
-Please refer to [User Dislikes](../federation/endpoints) for more information.
-
-### Inbox
-
-| Name | Type | Required |
-| :---- | :----- | :------- |
-| inbox | String | Yes |
-
-The `inbox` field on an Actor is a string that displays the URI of the actor's inbox. It is used to identify the actor's inbox for federation.
-
-Please refer to [Inbox](../federation/endpoints) for more information.
-
-### Outbox
-
-| Name | Type | Required |
-| :----- | :----- | :------- |
-| outbox | String | Yes |
-
-The `outbox` field on an Actor is a string that displays the URI of the actor's outbox. It is used to identify the actor's outbox for federation.
-
-Please refer to [Outbox](../federation/endpoints) for more information.
-
-## Related Extensions
-
-These extensions might add or affect the User object if used:
-- [Custom Emojis](../extensions/custom-emojis)
-- [Vanity Profile](../extensions/vanity)
-
-## Types
-
-```typescript
-interface User extends Entity {
- type: "User";
- id: string;
- uri: string;
- created_at: string;
- display_name?: string;
- username: string;
- avatar?: ContentFormat;
- header?: ContentFormat;
- indexable: boolean;
- public_key: ActorPublicKeyData;
- bio?: ContentFormat;
- fields?: Field[];
- featured: string;
- followers: string;
- following: string;
- likes: string;
- dislikes: string;
- inbox: string;
- outbox: string;
- extensions?: Entity["extensions"] & {
- "org.lysand:vanity"?: VanityExtension;
- };
-}
-```
-
-```typescript
-interface Field {
- key: ContentFormat;
- value: ContentFormat;
-}
-```
\ No newline at end of file
diff --git a/docs/public/assets/boosting.png b/docs/public/assets/boosting.png
deleted file mode 100644
index 94fd6e8..0000000
Binary files a/docs/public/assets/boosting.png and /dev/null differ
diff --git a/docs/public/assets/discord-buttons.webp b/docs/public/assets/discord-buttons.webp
deleted file mode 100644
index 593b25a..0000000
Binary files a/docs/public/assets/discord-buttons.webp and /dev/null differ
diff --git a/docs/public/favicon.png b/docs/public/favicon.png
deleted file mode 100644
index 5bfb829..0000000
Binary files a/docs/public/favicon.png and /dev/null differ
diff --git a/docs/security/api.md b/docs/security/api.md
deleted file mode 100644
index 10b9427..0000000
--- a/docs/security/api.md
+++ /dev/null
@@ -1,116 +0,0 @@
-# API Security
-
-This document details the security requirements for Lysand API implementations.
-
-It is a **MUST** for all Lysand-compatible servers to adhere to the guidelines marked as `Server API`.
-
-The guidelines marked as `Client API` are optional but recommended for client software.
-
-## Server API
-
-**Server API routes** are the endpoints of the server used by federation. These endpoints must **ONLY** be accessible by other servers and not by client software.
-
-> [!NOTE]
-> You may notice that most of these guidelines are redundant or useless for a simple JSON API system. However, they are mandated to encourage good security practices, so that developers don't overlook them on the important Client API routes.
-
-### HTTP Security
-
-All HTTP requests/responses **MUST** be transmitted using the **Hypertext Transfer Protocol Secure Extension** (HTTPS). Servers **MUST NOT** send responses without TLS (HTTPS), except for development purposes (e.g., if a server is operating on localhost or another local network).
-
-Servers should support HTTP/2 and HTTP/3 for enhanced performance and security. Servers **MUST** support HTTP/1.1 at a minimum, however TLS 1.2 is not allowed.
-
-Additionally, IPv6 is **RECOMMENDED** for all servers for enhanced security and performance. In the (far away) future, IPv4 will be removed, and servers that do not support IPv6 may face connectivity issues. (Whenever possible, servers should support both IPv4 and IPv6.)
-
-### Content Security Policy
-
-Servers **MUST** set a Content Security Policy (CSP) header to all their Server API routes to prevent XSS attacks. The CSP must be as restrictive as possible:
-
-```
-Content-Security-Policy: default-src 'none'; frame-ancestors 'none'
-```
-
-### Security headers
-
-Servers **MUST** set the following security headers to all their Lysand API routes:
-
-```
-X-Content-Type-Options: nosniff
-X-Frame-Options: DENY
-Referrer-Policy: no-referrer
-Strict-Transport-Security: max-age=31536000;
-```
-
-## Object Storage
-
-Object storage may be abused to store fake Lysand objects if the object storage is on the same origin as the server. To prevent this, servers must sign all valid objects with the author's private key, in the same way as described in the [Signing](signing.md) spec for outbound requests. This signature **MUST** be verified by any requesting server before accepting the object.
-
-This behaviour is also documented in the [Signing](signing.md) spec and [general spec](../spec.md). It is duplicated here in case you missed it the first time.
-
-## Client API
-
-**Client API routes** are the endpoints of the server used by client software. These endpoints must **ONLY** be accessible by client software and not by other servers. As an example, the [Mastodon API](https://docs.joinmastodon.org/api/) is a Client API.
-
-### Rate Limiting
-
-Servers **SHOULD** implement rate limiting on all Client API routes to prevent abuse. The rate limit **SHOULD** be set to a reasonable value, such as 100 requests per minute per IP address. This is left to the server administrator's discretion.
-
-### Authentication
-
-Client API routes **SHOULD** require authentication to prevent unauthorized access. The authentication method **SHOULD** be OAuth 2.0, as it is a widely-used and secure authentication method.
-
-Servers should also use either cryptographically secure random access tokens (via OAuth 2.0) or JWTs for authentication. The access tokens **MUST** be stored securely and **MUST NOT** be exposed to unauthorized parties.
-
-### Content Security Policy
-
-Servers **SHOULD** set a Content Security Policy (CSP) header to all their Client API routes to prevent XSS attacks. The CSP must be as restrictive as possible.
-
-No example is provided here, as this specification does not mandate a specific client API for servers.
-
-### Security headers
-
-Servers **SHOULD** set the following security headers to all their Client API routes:
-
-```
-X-Content-Type-Options: nosniff
-X-Frame-Options: DENY
-Referrer-Policy: no-referrer
-```
-
-If the server supports CORS, the `Access-Control-Allow-Origin` header **SHOULD** be set (usually to `*`), and the `Access-Control-Allow-Methods` header **SHOULD** be set to the allowed methods.
-
-`Permissions-Policy` headers are **RECOMMENDED** for all Client API routes that serve JS/HTML content (the "frontend"). The permissions policy should be as restrictive as possible.
-
-## Security Considerations
-
-When implementing security in your server, it is important to consider the following security considerations:
-
-### Authentication
-
-- Tokens/JWTs should expire after a reasonable amount of time (e.g., a week) to prevent unauthorized access. Additionally, they should be invalidated after a user logs out or changes their password.
-- Passwords **SHOULD** be hashed using a secure hashing algorithm, such as Argon2 or bcrypt. They **SHOULD NOT** be stored in plaintext or using weak hashing algorithms such as MD5 or SHA-1. Be also aware of weak default rounds for these algorithms.
-- Servers **SHOULD** implement multi-factor authentication (MFA) to provide an additional layer of security for users.
- - Passkeys/WebAuthn are **RECOMMENDED** for MFA, as they are more secure than SMS or email-based MFA.
-- Servers **SHOULD** implement very strict rate limiting on login attempts to prevent brute force attacks.
-- CSRF tokens **SHOULD** be used to prevent CSRF attacks on sensitive endpoints.
-
-### Key Management
-
-- Ensure that private keys are stored securely and are not exposed to unauthorized parties.
-- Allow exporting private keys by users in secure formats, such as encrypted files. Do not allow exporting private keys to untrusted environments. Additionally, indicate that this is a security-sensitive operation.
-
-> [!NOTE]
-> The importation of private keys is not recommended, as it can lead to security issues. However, if you choose to implement this feature, warn any users that this is probably a bad idea.
-
-### Cryptography
-
-- Do not roll your own security, but instead use well-established libraries such as the [WebCrypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API).
-- Cryptographic libraries written in unsafe languages, such as C, or that are a frequent source of security issues (e.g., OpenSSL) should be avoided.
-- Configure your server to only accept TLS 1.3 or higher, as older versions of TLS are vulnerable to attacks.
-
-### General Security
-
-- Have your server regularly audited for security vulnerabilities by professionals.
-- Keep all packages, dependencies, and libraries up-to-date. This also includes OS libraries (OSes that don't update packages often except for security patches such as Debian can be a risk, as often times a lot of vulnerabilities are missed).
-- Consider providing a container image for your server that does not run as the root user, and has all the necessary security configurations in place.
-- Open-source your server software, as it allows for more eyes on the code and can help identify security vulnerabilities faster.
-
diff --git a/docs/security/keys.md b/docs/security/keys.md
deleted file mode 100644
index 04d3597..0000000
--- a/docs/security/keys.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# Public Key Cryptography
-
-Lysand employs public key cryptography for object signing, ensuring the authenticity of the object's origin.
-
-All public keys in Lysand **MUST** be encoded using the [ed25519](https://ed25519.cr.yp.to/) algorithm. This algorithm is favored due to its speed, security, and compact key size. Legacy algorithms such as RSA are not supported and **SHOULD NOT** be implemented using extensions due to security considerations.
-
-While it's technically possible to implement other encryption algorithms using extensions, it's generally discouraged.
-
-In the near future, Lysand will also support quantum-resistant algorithms, once they are incorporated into popular libraries.
-
-Here's an example of generating a public-private key pair in TypeScript using the WebCrypto API:
-
-```ts
-const keyPair = await crypto.subtle.generateKey(
- "Ed25519",
- true,
- ["sign", "verify"]
-);
-
-// Encode both to base64 (Buffer is a Node.js API, replace with btoa and atob for browser environments)
-const privateKey = Buffer.from(
- await crypto.subtle.exportKey("pkcs8", keys.privateKey),
-).toString("base64");
-
-const publicKey = Buffer.from(
- await crypto.subtle.exportKey("spki", keyPair.publicKey),
-).toString("base64");
-
-// Store the public and private key somewhere in your user data
-```
-
-> [!WARNING]
-> Support for Ed25519 in the WebCrypto API is a recent addition and may not be available in some older runtimes, such as Node.js or older browsers.
-
-Public key data is represented as follows across the protocol:
-
-```ts
-interface ActorPublicKeyData {
- public_key: string;
- actor: string;
-}
-```
-
-The `public_key` field is a string that contains the user's public key. It **MUST** be encoded using base64.
-
-Base64 encoding of public and private keys is defined as follows:
-- The public key **MUST** be encoded using the `spki` format.
-- The private key **MUST** be encoded using the `pkcs8` format.
-- Both keys **MUST** be turned from raw bytes to base64 by turning the bytes into a sequence of UTF-16 code units, then encoding them as base64 (as shown in the example above).
-
-The `actor` field is a string that contains the user's URI. This field is mandatory.
\ No newline at end of file
diff --git a/docs/security/signing.md b/docs/security/signing.md
deleted file mode 100644
index 27c1f8c..0000000
--- a/docs/security/signing.md
+++ /dev/null
@@ -1,184 +0,0 @@
-# HTTP Signatures
-
-Lysand employs cryptography to safeguard objects from being altered during transit. This is achieved by signing objects using a private key, and then verifying the signature with a corresponding public key.
-
-> [!NOTE]
-> The 'author' of the object refers to the entity (usually an [Actor](../objects/actors)) that created the object. This is indicated by the `author` property on the object body.
-
-> [!NOTE]
-> Please see the [API Security](api.md) document for security guidelines.
-
-## Creating a Signature
-
-Prerequisites:
-- A private key for the author of the object.
-- The object to be signed, serialized as a string.
-
-### Signature
-
-The `Signature` is a string, typically sent as part of the `Signature` HTTP header. It contains a signed string signed with a private key.
-
-It is formatted as follows:
-```
-Signature: keyId="$0",algorithm="ed25519",headers="(request-target) host date digest",signature="$1"
-```
-
-- `$0` is the URI of the user that is sending the request. (e.g., `https://example.com/users/uuid`)
-- `$1` is the base64-encoded signed string.
-
-The signed string is calculated as follows:
-
-1. Create a string that contains the following, replacing the placeholders with the actual values of the request:
-```
-(request-target): post $2
-host: $3
-date: $4
-digest: SHA-256=$5
-```
-
-- `$2` is the path of the request (e.g., `/users/uuid/inbox`).
-- `$3` is the host of the server that is receiving the request.
-- `$4` is the date and time that the request was sent (ISO 8601, e.g. `2024-04-10T01:27:24.880Z`).
-- `$5` is the SHA-256 digest of the request body, base64-encoded.
-
-> [!WARNING]
-> The last line of the signed string **MUST** be terminated with a newline character (`\n`).
-
-2. Sign the string with the user's private key.
-
-2. Base64-encode the signature.
-
-#### Example
-
-Let's imagine a user at `sender.com` wants to send something to a user at `receiver.com`'s inbox.
-
-Here is an example of signing a request using TypeScript and the WebCrypto API.
-
-```typescript
-const privateKey = ... // CryptoKey
-const body = {...} // Object to be signed
-const date = new Date();
-
-const digest = await crypto.subtle.digest(
- "SHA-256",
- // Make sure to follow the JSON object handling guidelines
- // This just uses JSON.stringify as an example
- new TextEncoder().encode(JSON.stringify(body)),
-);
-
-const userInbox = new URL(
- "https://receiver.com/users/22a56612-9909-48ca-84af-548b28db6fd5/inbox"
-);
-
-const date = new Date();
-
-// Note: the Buffer class is from the Node.js Buffer API, this can be replaced with btoa and atob magic in the browser
-const signature = await crypto.subtle.sign(
- "Ed25519",
- privateKey,
- new TextEncoder().encode(
- `(request-target): post ${userInbox.pathname}\n` +
- `host: ${userInbox.host}\n` +
- `date: ${date.toISOString()}\n` +
- `digest: SHA-256=${Buffer.from(new Uint8Array(digest)).toString(
- "base64",
- )}\n`,
- ),
-);
-
-const signatureBase64 = Buffer.from(new Uint8Array(signature)).toString(
- "base64",
-);
-```
-
-> [!WARNING]
-> Support for Ed25519 in the WebCrypto API is recent and may not be available in some older runtimes, such as Node.js or older browsers.
-
-The request can then be sent with the `Signature`, `Origin` and `Date` headers as follows:
-```ts
-await fetch("https://receiver.com/users/22a56612-9909-48ca-84af-548b28db6fd5/inbox", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Date: date.toISOString(),
- Origin: "sender.com",
- Signature: `keyId="https://sender.com/users/caf18716-800d-4c88-843d-4947ab39ca0f",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`,
- },
- // Once again, make sure to follow the JSON object handling guidelines
- body: JSON.stringify(body),
-});
-```
-
-Example of validation on the receiving server side:
-
-```typescript
-// req is a Request object
-const signatureHeader = req.headers.get("Signature");
-const origin = req.headers.get("Origin");
-const date = req.headers.get("Date");
-
-if (!signatureHeader) {
- return errorResponse("Missing Signature header", 400);
-}
-
-if (!origin) {
- return errorResponse("Missing Origin header", 400);
-}
-
-if (!date) {
- return errorResponse("Missing Date header", 400);
-}
-
-const signature = signatureHeader
- .split("signature=")[1]
- .replace(/"/g, "");
-
-const digest = await crypto.subtle.digest(
- "SHA-256",
- new TextEncoder().encode(JSON.stringify(body)),
-);
-
-const keyId = signatureHeader
- .split("keyId=")[1]
- .split(",")[0]
- .replace(/"/g, "");
-
-// TODO: Fetch sender using WebFinger if not found
-const sender = ... // Get sender from your database via its URI (inside the keyId variable)
-
-const public_key = await crypto.subtle.importKey(
- "spki",
- // Buffer is a Node.js API, this can be modified to work in browser too
- Buffer.from(sender.publicKey, "base64"),
- "Ed25519",
- false,
- ["verify"],
-);
-
-
-const expectedSignedString =
- `(request-target): ${req.method.toLowerCase()} ${
- new URL(req.url).pathname
- }\n` +
- `host: ${new URL(req.url).host}\n` +
- `date: ${date}\n` +
- `digest: SHA-256=${Buffer.from(new Uint8Array(digest)).toString(
- "base64",
- )}\n`;
-
-// Check if signed string is valid
-const isValid = await crypto.subtle.verify(
- "Ed25519",
- public_key,
- Buffer.from(signature, "base64"),
- new TextEncoder().encode(expectedSignedString),
-);
-
-if (!isValid) {
- throw new Error("Invalid signature");
-}
-```
-
-Signature is **REQUIRED** on **ALL** outbound and inbound requests. If the request is not signed, the server **MUST** respond with a `401 Unauthorized` response code. However, the receiving server is not required to validate the signature, it just must be provided.
-
-If a request is made by the server and not by a user, the [Server Actor](/federation/server-actor) **MUST** be used in the `author` field.
\ No newline at end of file
diff --git a/docs/spec.md b/docs/spec.md
deleted file mode 100644
index b1b92dc..0000000
--- a/docs/spec.md
+++ /dev/null
@@ -1,102 +0,0 @@
-# Introduction
-
-> [!NOTE]
-> You are looking at the documentation for `Lysand 3.1`, released in July 2024
->
-> Small changes may still be made before the final release.
->
-> Previous versions:
-> - `Lysand 3.0`, released in May 2024.
-> - `Lysand 2.0`, released in March 2024.
-> - `Lysand 1.0`, published in September 2023.
-
-The Lysand Protocol is designed as a communication medium for federated applications, leveraging the HTTP stack. Its simplicity ensures ease of implementation and comprehension.
-
-Distinct from ActivityPub, Lysand incorporates an extensive range of built-in features tailored for social media applications. It prioritizes security and privacy by default.
-
-Lysand aims for standardization, discouraging vendor-specific implementations, as seen in Mastodon's adaptation of ActivityPub. It relies on straightforward JSON objects and HTTP requests, eliminating the need for complex serialization formats.
-
-This repository provides TypeScript types for every object in the protocol, facilitating easy adaptation to other languages.
-
-# Design Goals
-
-While Lysand draws parallels with popular protocols like ActivityPub and ActivityStreams, it is not compatible with either. It also does not support ActivityPub's JSON-LD serialization format.
-
-Lysand-compatible servers may choose to implement other protocols, such as ActivityPub, but it is not a requirement.
-
-# Vocabulary
-
-The words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and **MAY** are used in this document as defined in [RFC 2119](https://datatracker.ietf.org/doc/html/rfc2119).
-
-- **Actor**: An individual or entity utilizing the Lysand protocol, analogous to ActivityPub's `Actor` objects. An actor could be a [Server Actor](federation/server-actor), representing a server, or a [User Actor](objects/actors).
-- **Server**: A server that deploys the Lysand protocol, referred to as an **implementation**. Servers are also known as **instances** when referring to the deployed form.
-- **Entity**: A generic term for any object in the Lysand protocol, such as an [Actor](objects/actors), [Note](objects/publications), or [Like](objects/like).
-
-# Implementation Requirements
-
-Servers **MUST** reject any requests that fail to respect the Lysand specification in any way. This includes, but is not limited to, incorrect JSON object handling, incorrect HTTP headers, and incorrect URI normalization.
-
-## For strictly-typed languages (e.g. Rust)
-
-All numbers are to be treated as 64-bit integer or floats (depending on whether a valid value would be int or float). If a valid value cannot be negative, it must also be treated as unsigned.
-
-Examples:
-- A `size` (bytes) property on a file object should be treated as an unsigned 64-bit integer.
-- A `duration` property on a video object should be treated as an unsigned 64-bit float.
-
-## Optional Fields
-
-Fields marked as "optional" may be set to `null` or omitted entirely. If a field is omitted, it is assumed to be `null`, unless it is not in the object's schema.
-
-## HTTP
-
-All HTTP request and response bodies **MUST** be encoded as UTF-8 JSON, with the `Content-Type` header set to `application/json; charset=utf-8`. Appropriate signatures must be included in the `Signature` header for **every request and response**.
-
-Servers **MUST** use UUIDs or a UUID-compatible system for the `id` field. Any valid UUID is acceptable, but it **should** be unique across the entire known network if possible. However, uniqueness across the server is the only requirement.
-
-> [!NOTE]
-> Protocol implementers may prefer [UUIDv7](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7) over the popular UUIDv4 for their internal databases, as UUIDv7 is lexicographically sortable by time generated. A PostgreSQL extension is available [here](https://github.com/fboulnois/pg_uuidv7).
-
-All URIs **MUST** be absolute and HTTPS, except for development purposes. They **MUST** be unique across the entire network and **must not** contain mutable data, such as the actor's `username`.
-
-All URIs **MUST** be normalized and **MUST NOT** contain any query parameters, except where explicitely allowed. URI normalization is defined in [RFC 3986 Section 6](https://datatracker.ietf.org/doc/html/rfc3986#section-6).
-
-### Requests
-
-All requests **MUST** include at least the following headers:
-- `Accept: application/json`
-- `Content-Type: application/json; charset=utf-8` if the request contains a body
-- `Signature` if the request body is signed (which is typically the case)
-
-Additionally, requests **SHOULD** include the following headers (though not mandated by the protocol):
-- `User-Agent` with a value that identifies the client software
-
-### Responses
-
-All responses **MUST** include at least the following headers:
-- `Content-Type: application/json; charset=utf-8` if the response contains a body
-- `Signature` if the response body is signed (which is typically the case)
-- `Cache-Control: no-store` on entities that can be edited directly without using a [Patch](objects/patch), such as [Actors](objects/actors)
-- A cache header with a `max-age` of at least 5 minutes for entities that are not expected to change frequently, such as [Notes](objects/publications)
-- A cache header with a large `max-age` for media files when served by a CDN or other caching service under the server's control
-
-
-## JSON Object Handling
-
-All JSON objects disseminated during federation **MUST** be handled as follows:
-- The object's keys **MUST** be arranged in lexicographical order.
-- The object **MUST** be serialized using the [Canonical JSON](https://datatracker.ietf.org/doc/html/rfc8785) format.
-- The object **MUST** be encoded using UTF-8.
-- The object **MUST** be signed using either the [Server Actor](federation/server-actor) or the [Actor](objects/actors) object's private key, depending on the context. (Signatures and keys are governed by the rules outlined in the [Keys](security/keys) and [Signing](security/signing) spec). Signatures are encoded using request/response headers, not within the JSON object itself.
-
-## API Security
-
-All servers **MUST** adhere to the security guidelines outlined in the [API Security](security/api) document.
-
----
-
-## Appendix
-
-> [This document is dedicated to all citizens of planet Earth. You deserve freedom of communication; we hope we have contributed in some part, however small, towards that goal and right.](https://w3c.github.io/activitypub/#acknowledgements)
-
-Signed by the maintainers.
diff --git a/docs/structures/collection.md b/docs/structures/collection.md
deleted file mode 100644
index 9b698e4..0000000
--- a/docs/structures/collection.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Collections
-
-Collections are structured JSON objects that encapsulate a group of related items. They are typically used to represent a set of similar objects, such as a series of publications or a group of users.
-
-Here's how a Collections can be represented in TypeScript:
-
-```ts
-interface Collections {
- first: string;
- last: string;
- total_count: number; // unsigned 64-bit integer
- author: string;
- next?: string;
- prev?: string;
- items: T[];
-}
-```
-
-Collections are intended to be served in a paginated format, with a set of items and links to the next and previous sets of items. For example, if a user has a collection of Notes that is several thousand items long, the server can serve the first 50 items when requested and provide a link to the next 50 items.
-
-Collections **MUST** include:
-- A `first` field that holds the URI of the first page of collection items, and a `last` field that holds the URI of the last page of collection items. In the case that there is only one page, `first` and `last` should be the same.
-- A `total_count` field that holds the total number of items in the set, across all pages.
-- A `next` field that holds the URI of the next set of items, and a `prev` field that holds the URI of the previous set of items. These fields are optional if the user is on the first or last set of items.
-- An `items` field that holds a paginated array of items in the set. (for example, a series of publications or a group of users)
-- An `author` field that holds the URI of the entity that created the set (such as the [Actor](../objects/actors) that creates posts). This is used to identify the creator of the set for signing. If the set is generated by the server and not by a specific user (such as the Endorsement set with the [ServerEndorsement Extension](/extensions/server-endorsement)), the `author` should be the server actor's URI.
-
-`first`, `last`, `prev` and `next` URIs may use query strings to specify the range of items to be returned. For example, a `first` URI may look like `https://example.com/users/uuid/notes?start=0&end=50`.
-
-It is recommended that pages are limited to a hundred elements at most, but also at least twenty elements, so as to not overload federation with large payloads.
-
-> [!WARNING]
-> Like any other payload, Collections are to be signed using the author's private key. Unsigned collections are not valid.
\ No newline at end of file
diff --git a/docs/structures/content-format.md b/docs/structures/content-format.md
deleted file mode 100644
index baf5ed5..0000000
--- a/docs/structures/content-format.md
+++ /dev/null
@@ -1,100 +0,0 @@
-# ContentFormat
-
-The `ContentFormat` structure, as represented below in TypeScript, is a flexible and robust way to handle various types of content. It is designed to accommodate a wide range of content types and provide additional metadata about the content.
-
-```ts
-interface ContentFormat {
- [contentType: string]: {
- content: string;
- description?: string;
- size?: number; // unsigned 64-bit integer
- hash?: {
- sha256?: string;
- sha512?: string;
- [key: string]: string | undefined;
- };
- blurhash?: string;
- fps?: number; // unsigned 64-bit integer
- width?: number; // unsigned 64-bit integer
- height?: number; // unsigned 64-bit integer
- duration?: number; // unsigned 64-bit float
- }
-}
-```
-
-Here's an example of how this structure can be used:
-
-```json
-{
- "text/plain": {
- "content": "Hello, world!"
- }
-}
-```
-
-And another example:
-
-```json
-{
- "image/png": {
- "content": "https://cdn.example.com/attachments/ece2f9d9-27d7-457d-b657-4ce9172bdcf8.png",
- "description": "A jolly horse running through mountains",
- "size": 123456,
- "hash": {
- "sha256": "91714fc336210d459d4f9d9233de663be2b87ffe923f1cfd76ece9d06f7c965d"
- }
- }
-}
-```
-
-The `content` field holds the actual content of the object. This content can be either a string containing plaintext, or a URI to the actual content when it cannot be encoded as this format.
-
-The `description` field provides a brief summary of the content. This is particularly useful for accessibility purposes, such as for visually impaired users, or when the content fails to load. It's an optional field, and if not provided, it's assumed that the content doesn't have a description.
-
-The `size` field, also optional, indicates the size of the content in bytes. While it's not necessary for text content, it's recommended for binary content like images, videos, and audio. This information helps clients decide whether to download the content based on its size.
-
-The `ContentFormat` structure is designed to handle multiple formats of the same file. For instance, a PNG image and a WebP image. However, it's not intended for formats that can't be converted to others, like PDFs. These should only be stored once.
-
-Here's an acceptable use case:
-
-```json
-{
- "image/png": {
- "content": "https://cdn.example.com/attachments/ece2f9d9-27d7-457d-b657-4ce9172bdcf8.png",
- "description": "A jolly horse running through mountains",
- "hash": {
- "sha256": "91714fc336210d459d4f9d9233de663be2b87ffe923f1cfd76ece9d06f7c965d"
- }
- },
- "image/webp": {
- "content": "https://cdn.example.com/attachments/ece2f9d9-27d7-457d-b657-4ce9172bdcf8.webp",
- "description": "A jolly horse running through mountains",
- "hash": {
- "sha256": "b493d48364afe44d11c0165cf470a4164d1e2609911ef998be868d46ade3de4e"
- }
- }
-}
-```
-
-However, this is not:
-
-```json
-{
- "image/png": {
- "content": "https://cdn.example.com/attachments/ece2f9d9-27d7-457d-b657-4ce9172bdcf8.png",
- "description": "A jolly horse running through mountains"
- },
- "image/webp": {
- "content": "https://cdn.example.com/attachments/ece2f9d9-27d7-457d-b657-4ce9172bdcf8.webp",
- "description": "A jolly horse running through mountains"
- },
- "application/pdf": {
- "content": "https://cdn.example.com/attachments/anotherfile.pdf",
- "description": "An informative PDF document on macroeconomics"
- }
-}
-```
-
-Each `ContentFormat` object should be treated as a **single file in multiple optional formats**, not as multiple files. The multiple formats are intended to optimize bandwidth usage.
-
-If optional fields are provided for one object in the `ContentFormat`, they should be provided for all objects in the `ContentFormat`. For instance, if the `description` field is provided for one object, it should be provided for all objects, as they represent the same file.
\ No newline at end of file
diff --git a/docs/structures/custom-emoji.md b/docs/structures/custom-emoji.md
deleted file mode 100644
index 0c3ae44..0000000
--- a/docs/structures/custom-emoji.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Custom Emojis in Lysand
-
-Lysand supports the use of custom emojis. Here's how they are represented in TypeScript:
-
-```ts
-interface Emoji {
- name: string;
- alt?: string;
- url: ContentFormat;
-}
-```
-
-Custom emojis in Lysand are part of an optional extension to the protocol. For more details, refer to the [Protocol Extensions](../extensions) section.
-
-While servers have the discretion to implement custom emojis, it is highly recommended for a richer user interaction.
-
-Here's an example of a custom emoji representation:
-
-```json
-{
- "name": "happy_face",
- "alt": "A happy face emoji.",
- "url": {
- "image/webp": {
- "content": "https://cdn.example.com/emojis/happy_face.webp",
- }
- }
-}
-```
-
-The `name` field is a string that should only contain alphanumeric characters, underscores, and dashes. Spaces or other special characters are not allowed. It should match the following regex: `/^[a-zA-Z0-9_-]+$/`.
-
-The `url` field is a [ContentFormat](./content-format), serving as a list of URLs where the emoji can be accessed. It is mandatory for all emojis and should contain at least one URL. The `url` field should be a binary image format, such as `image/png` or `image/jpeg`. Text formats like `text/plain` or `text/html` are not acceptable.
-
-The `alt` field is an optional string that provides the alt text for the emoji. This is particularly useful for visually impaired users or when the emoji fails to load. If not provided, it's assumed that the emoji doesn't have an alt text.
-
-While emojis are typically small and don't consume much bandwidth, servers may choose to transcode emojis into more modern formats like WebP, AVIF, JXL, or HEIF for optimization. Clients should display the most modern format they support. If they don't support any modern formats, they should display the original format.
-
-> [!NOTE]
-> Servers might find it beneficial to use a CDN like Cloudflare that can automatically convert images to modern formats. This approach offloads image processing from the server and enhances performance for clients.
-
-The size of emojis is not standardized and is left to the server's discretion. Servers may choose to limit the size of emojis, but it's not mandatory. As a general guideline, an upper limit of a few hundred kilobytes is recommended to avoid excessive bandwidth usage.
\ No newline at end of file
diff --git a/images/logos/go.svg b/images/logos/go.svg
new file mode 100644
index 0000000..7f7b19d
--- /dev/null
+++ b/images/logos/go.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/images/logos/node.svg b/images/logos/node.svg
new file mode 100644
index 0000000..1d09de2
--- /dev/null
+++ b/images/logos/node.svg
@@ -0,0 +1,4 @@
+
+
+
diff --git a/images/logos/php.svg b/images/logos/php.svg
new file mode 100644
index 0000000..0a9ac46
--- /dev/null
+++ b/images/logos/php.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/images/logos/python.svg b/images/logos/python.svg
new file mode 100644
index 0000000..9bceb58
--- /dev/null
+++ b/images/logos/python.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/images/logos/ruby.svg b/images/logos/ruby.svg
new file mode 100644
index 0000000..b22a5bf
--- /dev/null
+++ b/images/logos/ruby.svg
@@ -0,0 +1,4 @@
+
+
+
diff --git a/lib/remToPx.ts b/lib/remToPx.ts
new file mode 100644
index 0000000..48e2748
--- /dev/null
+++ b/lib/remToPx.ts
@@ -0,0 +1,10 @@
+export function remToPx(remValue: number) {
+ const rootFontSize =
+ typeof window === "undefined"
+ ? 16
+ : Number.parseFloat(
+ window.getComputedStyle(document.documentElement).fontSize,
+ );
+
+ return remValue * rootFontSize;
+}
diff --git a/mdx-components.tsx b/mdx-components.tsx
new file mode 100644
index 0000000..4688405
--- /dev/null
+++ b/mdx-components.tsx
@@ -0,0 +1,10 @@
+import type { MDXComponents } from "mdx/types";
+
+import * as mdxComponents from "./components/mdx";
+
+export function useMDXComponents(components: MDXComponents) {
+ return {
+ ...components,
+ ...mdxComponents,
+ };
+}
diff --git a/mdx/recma.mjs b/mdx/recma.mjs
new file mode 100644
index 0000000..237ac11
--- /dev/null
+++ b/mdx/recma.mjs
@@ -0,0 +1,3 @@
+import { mdxAnnotations } from "mdx-annotations";
+
+export const recmaPlugins = [mdxAnnotations.recma];
diff --git a/mdx/rehype.mjs b/mdx/rehype.mjs
new file mode 100644
index 0000000..66999d1
--- /dev/null
+++ b/mdx/rehype.mjs
@@ -0,0 +1,129 @@
+import { slugifyWithCounter } from "@sindresorhus/slugify";
+import * as acorn from "acorn";
+import { toString as mdastToString } from "mdast-util-to-string";
+import { mdxAnnotations } from "mdx-annotations";
+import shiki from "shiki";
+import { visit } from "unist-util-visit";
+
+function rehypeParseCodeBlocks() {
+ return (tree) => {
+ // biome-ignore lint/style/useNamingConvention:
+ visit(tree, "element", (node, _, parentNode) => {
+ if (node.tagName === "code" && node.properties.className) {
+ parentNode.properties.language =
+ node.properties.className[0]?.replace(/^language-/, "");
+ }
+ });
+ };
+}
+
+let highlighter;
+
+function rehypeShiki() {
+ return async (tree) => {
+ highlighter =
+ highlighter ??
+ (await shiki.getHighlighter({ theme: "css-variables" }));
+
+ visit(tree, "element", (node) => {
+ if (
+ node.tagName === "pre" &&
+ node.children[0]?.tagName === "code"
+ ) {
+ const codeNode = node.children[0];
+ const textNode = codeNode.children[0];
+
+ node.properties.code = textNode.value;
+
+ if (node.properties.language) {
+ const tokens = highlighter.codeToThemedTokens(
+ textNode.value,
+ node.properties.language,
+ );
+
+ textNode.value = shiki.renderToHtml(tokens, {
+ elements: {
+ pre: ({ children }) => children,
+ code: ({ children }) => children,
+ line: ({ children }) => `${children}`,
+ },
+ });
+ }
+ }
+ });
+ };
+}
+
+function rehypeSlugify() {
+ return (tree) => {
+ const slugify = slugifyWithCounter();
+ visit(tree, "element", (node) => {
+ if (node.tagName === "h2" && !node.properties.id) {
+ node.properties.id = slugify(mdastToString(node));
+ }
+ });
+ };
+}
+
+function rehypeAddMDXExports(getExports) {
+ return (tree) => {
+ const exports = Object.entries(getExports(tree));
+
+ for (const [name, value] of exports) {
+ for (const node of tree.children) {
+ if (
+ node.type === "mdxjsEsm" &&
+ new RegExp(`export\\s+const\\s+${name}\\s*=`).test(
+ node.value,
+ )
+ ) {
+ return;
+ }
+ }
+
+ const exportStr = `export const ${name} = ${value}`;
+
+ tree.children.push({
+ type: "mdxjsEsm",
+ value: exportStr,
+ data: {
+ estree: acorn.parse(exportStr, {
+ sourceType: "module",
+ ecmaVersion: "latest",
+ }),
+ },
+ });
+ }
+ };
+}
+
+function getSections(node) {
+ const sections = [];
+
+ for (const child of node.children ?? []) {
+ if (child.type === "element" && child.tagName === "h2") {
+ sections.push(`{
+ title: ${JSON.stringify(mdastToString(child))},
+ id: ${JSON.stringify(child.properties.id)},
+ ...${child.properties.annotation}
+ }`);
+ } else if (child.children) {
+ sections.push(...getSections(child));
+ }
+ }
+
+ return sections;
+}
+
+export const rehypePlugins = [
+ mdxAnnotations.rehype,
+ rehypeParseCodeBlocks,
+ rehypeShiki,
+ rehypeSlugify,
+ [
+ rehypeAddMDXExports,
+ (tree) => ({
+ sections: `[${getSections(tree).join()}]`,
+ }),
+ ],
+];
diff --git a/mdx/remark.mjs b/mdx/remark.mjs
new file mode 100644
index 0000000..ea1a1e7
--- /dev/null
+++ b/mdx/remark.mjs
@@ -0,0 +1,4 @@
+import { mdxAnnotations } from "mdx-annotations";
+import remarkGfm from "remark-gfm";
+
+export const remarkPlugins = [mdxAnnotations.remark, remarkGfm];
diff --git a/mdx/search.mjs b/mdx/search.mjs
new file mode 100644
index 0000000..7dca834
--- /dev/null
+++ b/mdx/search.mjs
@@ -0,0 +1,141 @@
+import * as fs from "node:fs";
+import * as path from "node:path";
+import * as url from "node:url";
+import { slugifyWithCounter } from "@sindresorhus/slugify";
+import glob from "fast-glob";
+import { toString as mdastToString } from "mdast-util-to-string";
+import { remark } from "remark";
+import remarkMdx from "remark-mdx";
+import { createLoader } from "simple-functional-loader";
+import { filter } from "unist-util-filter";
+import { SKIP, visit } from "unist-util-visit";
+
+const __filename = url.fileURLToPath(import.meta.url);
+const processor = remark().use(remarkMdx).use(extractSections);
+const slugify = slugifyWithCounter();
+
+function isObjectExpression(node) {
+ return (
+ node.type === "mdxTextExpression" &&
+ node.data?.estree?.body?.[0]?.expression?.type === "ObjectExpression"
+ );
+}
+
+function excludeObjectExpressions(tree) {
+ return filter(tree, (node) => !isObjectExpression(node));
+}
+
+function extractSections() {
+ return (tree, { sections }) => {
+ slugify.reset();
+
+ visit(tree, (node) => {
+ if (node.type === "heading" || node.type === "paragraph") {
+ const content = mdastToString(excludeObjectExpressions(node));
+ if (node.type === "heading" && node.depth <= 2) {
+ const hash = node.depth === 1 ? null : slugify(content);
+ sections.push([content, hash, []]);
+ } else {
+ sections.at(-1)?.[2].push(content);
+ }
+ return SKIP;
+ }
+ });
+ };
+}
+
+export default function Search(nextConfig = {}) {
+ const cache = new Map();
+
+ return Object.assign({}, nextConfig, {
+ webpack(config, options) {
+ config.module.rules.push({
+ test: __filename,
+ use: [
+ createLoader(function () {
+ const appDir = path.resolve("./app");
+ this.addContextDependency(appDir);
+
+ const files = glob.sync("**/*.mdx", { cwd: appDir });
+ const data = files.map((file) => {
+ const url = `/${file.replace(/(^|\/)page\.mdx$/, "")}`;
+ const mdx = fs.readFileSync(
+ path.join(appDir, file),
+ "utf8",
+ );
+
+ let sections = [];
+
+ if (cache.get(file)?.[0] === mdx) {
+ sections = cache.get(file)[1];
+ } else {
+ const vfile = { value: mdx, sections };
+ processor.runSync(
+ processor.parse(vfile),
+ vfile,
+ );
+ cache.set(file, [mdx, sections]);
+ }
+
+ return { url, sections };
+ });
+
+ // When this file is imported within the application
+ // the following module is loaded:
+ return `
+ import FlexSearch from 'flexsearch'
+
+ let sectionIndex = new FlexSearch.Document({
+ tokenize: 'full',
+ document: {
+ id: 'url',
+ index: 'content',
+ store: ['title', 'pageTitle'],
+ },
+ context: {
+ resolution: 9,
+ depth: 2,
+ bidirectional: true
+ }
+ })
+
+ let data = ${JSON.stringify(data)}
+
+ for (let { url, sections } of data) {
+ for (let [title, hash, content] of sections) {
+ sectionIndex.add({
+ url: url + (hash ? ('#' + hash) : ''),
+ title,
+ content: [title, ...content].join('\\n'),
+ pageTitle: hash ? sections[0][0] : undefined,
+ })
+ }
+ }
+
+ export function search(query, options = {}) {
+ let result = sectionIndex.search(query, {
+ ...options,
+ enrich: true,
+ })
+ if (result.length === 0) {
+ return []
+ }
+ return result[0].result.map((item) => ({
+ url: item.id,
+ title: item.doc.title,
+ pageTitle: item.doc.pageTitle,
+ }))
+ }
+ `;
+ }),
+ ],
+ });
+
+ if (typeof nextConfig.webpack === "function") {
+ return nextConfig.webpack(config, options);
+ }
+
+ return config;
+ },
+ });
+}
diff --git a/next.config.mjs b/next.config.mjs
new file mode 100644
index 0000000..637edb1
--- /dev/null
+++ b/next.config.mjs
@@ -0,0 +1,21 @@
+import nextMDX from "@next/mdx";
+
+import { recmaPlugins } from "./mdx/recma.mjs";
+import { rehypePlugins } from "./mdx/rehype.mjs";
+import { remarkPlugins } from "./mdx/remark.mjs";
+import withSearch from "./mdx/search.mjs";
+
+const withMDX = nextMDX({
+ options: {
+ remarkPlugins,
+ rehypePlugins,
+ recmaPlugins,
+ },
+});
+
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ pageExtensions: ["js", "jsx", "ts", "tsx", "mdx"],
+};
+
+export default withSearch(withMDX(nextConfig));
diff --git a/package.json b/package.json
index 1437f94..3cbc88d 100644
--- a/package.json
+++ b/package.json
@@ -1,18 +1,60 @@
{
- "scripts": {
- "docs:dev": "vitepress dev",
- "docs:build": "vitepress build",
- "docs:preview": "vitepress preview"
- },
- "devDependencies": {
- "@biomejs/biome": "^1.8.3",
- "vitepress": "^1.3.1"
- },
- "trustedDependencies": ["@biomejs/biome", "esbuild", "vue-demi"],
- "dependencies": {
- "@tailwindcss/vite": "^4.0.0-alpha.17",
- "@vueuse/core": "^10.11.0",
- "iconify-icon": "^2.1.0",
- "tailwindcss": "^4.0.0-alpha.17"
- }
+ "name": "tailwindui-protocol",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint-next": "next lint",
+ "lint": "bunx @biomejs/biome check ."
+ },
+ "browserslist": "defaults, not ie <= 11",
+ "dependencies": {
+ "@algolia/autocomplete-core": "^1.7.3",
+ "@headlessui/react": "^2.0.1",
+ "@headlessui/tailwindcss": "^0.2.0",
+ "@mdx-js/loader": "^3.0.0",
+ "@mdx-js/react": "^3.0.0",
+ "@next/mdx": "^14.0.4",
+ "@sindresorhus/slugify": "^2.1.1",
+ "@tailwindcss/typography": "^0.5.10",
+ "@types/mdx": "^2.0.8",
+ "@types/node": "^20.10.8",
+ "@types/react": "^18.2.47",
+ "@types/react-dom": "^18.2.18",
+ "@types/react-highlight-words": "^0.16.4",
+ "acorn": "^8.8.1",
+ "autoprefixer": "^10.4.7",
+ "clsx": "^2.1.0",
+ "fast-glob": "^3.3.0",
+ "flexsearch": "^0.7.31",
+ "framer-motion": "^10.18.0",
+ "mdast-util-to-string": "^4.0.0",
+ "mdx-annotations": "^0.1.1",
+ "next": "^14.0.4",
+ "next-themes": "^0.2.1",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-highlight-words": "^0.20.0",
+ "remark": "^15.0.1",
+ "remark-gfm": "^4.0.0",
+ "remark-mdx": "^3.0.0",
+ "shiki": "^0.14.7",
+ "simple-functional-loader": "^1.2.1",
+ "tailwindcss": "^3.4.1",
+ "typescript": "^5.3.3",
+ "unist-util-filter": "^5.0.1",
+ "unist-util-visit": "^5.0.0",
+ "zustand": "^4.3.2"
+ },
+ "devDependencies": {
+ "@biomejs/biome": "^1.8.3",
+ "eslint": "^8.56.0",
+ "eslint-config-next": "^14.0.4",
+ "prettier": "^3.1.1",
+ "prettier-plugin-tailwindcss": "^0.5.11",
+ "sharp": "0.33.1"
+ },
+ "trustedDependencies": ["@biomejs/biome"]
}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..67cdf1a
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/prettier.config.js b/prettier.config.js
new file mode 100644
index 0000000..cc46424
--- /dev/null
+++ b/prettier.config.js
@@ -0,0 +1,6 @@
+/** @type {import('prettier').Options} */
+module.exports = {
+ singleQuote: true,
+ semi: false,
+ plugins: ["prettier-plugin-tailwindcss"],
+};
diff --git a/styles/tailwind.css b/styles/tailwind.css
new file mode 100644
index 0000000..6673210
--- /dev/null
+++ b/styles/tailwind.css
@@ -0,0 +1,21 @@
+@layer base {
+ :root {
+ --shiki-color-text: theme('colors.white');
+ --shiki-token-constant: theme('colors.emerald.300');
+ --shiki-token-string: theme('colors.emerald.300');
+ --shiki-token-comment: theme('colors.zinc.500');
+ --shiki-token-keyword: theme('colors.sky.300');
+ --shiki-token-parameter: theme('colors.pink.300');
+ --shiki-token-function: theme('colors.violet.300');
+ --shiki-token-string-expression: theme('colors.emerald.300');
+ --shiki-token-punctuation: theme('colors.zinc.200');
+ }
+
+ [inert] ::-webkit-scrollbar {
+ display: none;
+ }
+}
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/tailwind.config.ts b/tailwind.config.ts
new file mode 100644
index 0000000..4cca0b4
--- /dev/null
+++ b/tailwind.config.ts
@@ -0,0 +1,53 @@
+import headlessuiPlugin from "@headlessui/tailwindcss";
+import typographyPlugin from "@tailwindcss/typography";
+import type { Config } from "tailwindcss";
+
+import typographyStyles from "./typography";
+
+export default {
+ content: [
+ "./pages/**/*.{js,ts,jsx,tsx,mdx}",
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
+ ],
+ darkMode: "selector",
+ theme: {
+ fontSize: {
+ "2xs": ["0.75rem", { lineHeight: "1.25rem" }],
+ xs: ["0.8125rem", { lineHeight: "1.5rem" }],
+ sm: ["0.875rem", { lineHeight: "1.5rem" }],
+ base: ["1rem", { lineHeight: "1.75rem" }],
+ lg: ["1.125rem", { lineHeight: "1.75rem" }],
+ xl: ["1.25rem", { lineHeight: "1.75rem" }],
+ "2xl": ["1.5rem", { lineHeight: "2rem" }],
+ "3xl": ["1.875rem", { lineHeight: "2.25rem" }],
+ "4xl": ["2.25rem", { lineHeight: "2.5rem" }],
+ "5xl": ["3rem", { lineHeight: "1" }],
+ "6xl": ["3.75rem", { lineHeight: "1" }],
+ "7xl": ["4.5rem", { lineHeight: "1" }],
+ "8xl": ["6rem", { lineHeight: "1" }],
+ "9xl": ["8rem", { lineHeight: "1" }],
+ },
+ typography: typographyStyles,
+ extend: {
+ boxShadow: {
+ glow: "0 0 4px rgb(0 0 0 / 0.1)",
+ },
+ maxWidth: {
+ lg: "33rem",
+ "2xl": "40rem",
+ "3xl": "50rem",
+ "5xl": "66rem",
+ },
+ opacity: {
+ 1: "0.01",
+ // biome-ignore lint/style/useNamingConvention:
+ 2.5: "0.025",
+ // biome-ignore lint/style/useNamingConvention:
+ 7.5: "0.075",
+ 15: "0.15",
+ },
+ },
+ },
+ plugins: [typographyPlugin, headlessuiPlugin],
+} satisfies Config;
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..f62c03a
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "es6",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/types.d.ts b/types.d.ts
new file mode 100644
index 0000000..ecb8c35
--- /dev/null
+++ b/types.d.ts
@@ -0,0 +1,11 @@
+import type { SearchOptions } from "flexsearch";
+
+declare module "@/mdx/search.mjs" {
+ export type Result = {
+ url: string;
+ title: string;
+ pageTitle?: string;
+ };
+
+ export function search(query: string, options?: SearchOptions): Result[];
+}
diff --git a/typography.ts b/typography.ts
new file mode 100644
index 0000000..1d535bc
--- /dev/null
+++ b/typography.ts
@@ -0,0 +1,355 @@
+import type { PluginUtils } from "tailwindcss/types/config";
+
+export default function typographyStyles({ theme }: PluginUtils) {
+ return {
+ DEFAULT: {
+ css: {
+ "--tw-prose-body": theme("colors.zinc.700"),
+ "--tw-prose-headings": theme("colors.zinc.900"),
+ "--tw-prose-links": theme("colors.emerald.500"),
+ "--tw-prose-links-hover": theme("colors.emerald.600"),
+ "--tw-prose-links-underline": theme("colors.emerald.500 / 0.3"),
+ "--tw-prose-bold": theme("colors.zinc.900"),
+ "--tw-prose-counters": theme("colors.zinc.500"),
+ "--tw-prose-bullets": theme("colors.zinc.300"),
+ "--tw-prose-hr": theme("colors.zinc.900 / 0.05"),
+ "--tw-prose-quotes": theme("colors.zinc.900"),
+ "--tw-prose-quote-borders": theme("colors.zinc.200"),
+ "--tw-prose-captions": theme("colors.zinc.500"),
+ "--tw-prose-code": theme("colors.zinc.900"),
+ "--tw-prose-code-bg": theme("colors.zinc.100"),
+ "--tw-prose-code-ring": theme("colors.zinc.300"),
+ "--tw-prose-th-borders": theme("colors.zinc.300"),
+ "--tw-prose-td-borders": theme("colors.zinc.200"),
+
+ "--tw-prose-invert-body": theme("colors.zinc.400"),
+ "--tw-prose-invert-headings": theme("colors.white"),
+ "--tw-prose-invert-links": theme("colors.emerald.400"),
+ "--tw-prose-invert-links-hover": theme("colors.emerald.500"),
+ "--tw-prose-invert-links-underline": theme(
+ "colors.emerald.500 / 0.3",
+ ),
+ "--tw-prose-invert-bold": theme("colors.white"),
+ "--tw-prose-invert-counters": theme("colors.zinc.400"),
+ "--tw-prose-invert-bullets": theme("colors.zinc.600"),
+ "--tw-prose-invert-hr": theme("colors.white / 0.05"),
+ "--tw-prose-invert-quotes": theme("colors.zinc.100"),
+ "--tw-prose-invert-quote-borders": theme("colors.zinc.700"),
+ "--tw-prose-invert-captions": theme("colors.zinc.400"),
+ "--tw-prose-invert-code": theme("colors.white"),
+ "--tw-prose-invert-code-bg": theme("colors.zinc.700 / 0.15"),
+ "--tw-prose-invert-code-ring": theme("colors.white / 0.1"),
+ "--tw-prose-invert-th-borders": theme("colors.zinc.600"),
+ "--tw-prose-invert-td-borders": theme("colors.zinc.700"),
+
+ // Base
+ color: "var(--tw-prose-body)",
+ fontSize: theme("fontSize.sm")[0],
+ lineHeight: theme("lineHeight.7"),
+
+ // Text
+ p: {
+ marginTop: theme("spacing.6"),
+ marginBottom: theme("spacing.6"),
+ },
+ '[class~="lead"]': {
+ fontSize: theme("fontSize.base")[0],
+ ...theme("fontSize.base")[1],
+ },
+
+ // Lists
+ ol: {
+ listStyleType: "decimal",
+ marginTop: theme("spacing.5"),
+ marginBottom: theme("spacing.5"),
+ paddingLeft: "1.625rem",
+ },
+ 'ol[type="A"]': {
+ listStyleType: "upper-alpha",
+ },
+ 'ol[type="a"]': {
+ listStyleType: "lower-alpha",
+ },
+ 'ol[type="A" s]': {
+ listStyleType: "upper-alpha",
+ },
+ 'ol[type="a" s]': {
+ listStyleType: "lower-alpha",
+ },
+ 'ol[type="I"]': {
+ listStyleType: "upper-roman",
+ },
+ 'ol[type="i"]': {
+ listStyleType: "lower-roman",
+ },
+ 'ol[type="I" s]': {
+ listStyleType: "upper-roman",
+ },
+ 'ol[type="i" s]': {
+ listStyleType: "lower-roman",
+ },
+ 'ol[type="1"]': {
+ listStyleType: "decimal",
+ },
+ ul: {
+ listStyleType: "disc",
+ marginTop: theme("spacing.5"),
+ marginBottom: theme("spacing.5"),
+ paddingLeft: "1.625rem",
+ },
+ li: {
+ marginTop: theme("spacing.2"),
+ marginBottom: theme("spacing.2"),
+ },
+ ":is(ol, ul) > li": {
+ paddingLeft: theme("spacing[1.5]"),
+ },
+ "ol > li::marker": {
+ fontWeight: "400",
+ color: "var(--tw-prose-counters)",
+ },
+ "ul > li::marker": {
+ color: "var(--tw-prose-bullets)",
+ },
+ "> ul > li p": {
+ marginTop: theme("spacing.3"),
+ marginBottom: theme("spacing.3"),
+ },
+ "> ul > li > *:first-child": {
+ marginTop: theme("spacing.5"),
+ },
+ "> ul > li > *:last-child": {
+ marginBottom: theme("spacing.5"),
+ },
+ "> ol > li > *:first-child": {
+ marginTop: theme("spacing.5"),
+ },
+ "> ol > li > *:last-child": {
+ marginBottom: theme("spacing.5"),
+ },
+ "ul ul, ul ol, ol ul, ol ol": {
+ marginTop: theme("spacing.3"),
+ marginBottom: theme("spacing.3"),
+ },
+
+ // Horizontal rules
+ hr: {
+ borderColor: "var(--tw-prose-hr)",
+ borderTopWidth: 1,
+ marginTop: theme("spacing.16"),
+ marginBottom: theme("spacing.16"),
+ maxWidth: "none",
+ marginLeft: `calc(-1 * ${theme("spacing.4")})`,
+ marginRight: `calc(-1 * ${theme("spacing.4")})`,
+ "@screen sm": {
+ marginLeft: `calc(-1 * ${theme("spacing.6")})`,
+ marginRight: `calc(-1 * ${theme("spacing.6")})`,
+ },
+ "@screen lg": {
+ marginLeft: `calc(-1 * ${theme("spacing.8")})`,
+ marginRight: `calc(-1 * ${theme("spacing.8")})`,
+ },
+ },
+
+ // Quotes
+ blockquote: {
+ fontWeight: "500",
+ fontStyle: "italic",
+ color: "var(--tw-prose-quotes)",
+ borderLeftWidth: "0.25rem",
+ borderLeftColor: "var(--tw-prose-quote-borders)",
+ quotes: '"\\201C""\\201D""\\2018""\\2019"',
+ marginTop: theme("spacing.8"),
+ marginBottom: theme("spacing.8"),
+ paddingLeft: theme("spacing.5"),
+ },
+ "blockquote p:first-of-type::before": {
+ content: "open-quote",
+ },
+ "blockquote p:last-of-type::after": {
+ content: "close-quote",
+ },
+
+ // Headings
+ h1: {
+ color: "var(--tw-prose-headings)",
+ fontWeight: "700",
+ fontSize: theme("fontSize.2xl")[0],
+ ...theme("fontSize.2xl")[1],
+ marginBottom: theme("spacing.2"),
+ },
+ h2: {
+ color: "var(--tw-prose-headings)",
+ fontWeight: "600",
+ fontSize: theme("fontSize.lg")[0],
+ ...theme("fontSize.lg")[1],
+ marginTop: theme("spacing.16"),
+ marginBottom: theme("spacing.2"),
+ },
+ h3: {
+ color: "var(--tw-prose-headings)",
+ fontSize: theme("fontSize.base")[0],
+ ...theme("fontSize.base")[1],
+ fontWeight: "600",
+ marginTop: theme("spacing.10"),
+ marginBottom: theme("spacing.2"),
+ },
+
+ // Media
+ "img, video, figure": {
+ marginTop: theme("spacing.8"),
+ marginBottom: theme("spacing.8"),
+ },
+ "figure > *": {
+ marginTop: "0",
+ marginBottom: "0",
+ },
+ figcaption: {
+ color: "var(--tw-prose-captions)",
+ fontSize: theme("fontSize.xs")[0],
+ ...theme("fontSize.xs")[1],
+ marginTop: theme("spacing.2"),
+ },
+
+ // Tables
+ table: {
+ width: "100%",
+ tableLayout: "auto",
+ textAlign: "left",
+ marginTop: theme("spacing.8"),
+ marginBottom: theme("spacing.8"),
+ lineHeight: theme("lineHeight.6"),
+ },
+ thead: {
+ borderBottomWidth: "1px",
+ borderBottomColor: "var(--tw-prose-th-borders)",
+ },
+ "thead th": {
+ color: "var(--tw-prose-headings)",
+ fontWeight: "600",
+ verticalAlign: "bottom",
+ paddingRight: theme("spacing.2"),
+ paddingBottom: theme("spacing.2"),
+ paddingLeft: theme("spacing.2"),
+ },
+ "thead th:first-child": {
+ paddingLeft: "0",
+ },
+ "thead th:last-child": {
+ paddingRight: "0",
+ },
+ "tbody tr": {
+ borderBottomWidth: "1px",
+ borderBottomColor: "var(--tw-prose-td-borders)",
+ },
+ "tbody tr:last-child": {
+ borderBottomWidth: "0",
+ },
+ "tbody td": {
+ verticalAlign: "baseline",
+ },
+ tfoot: {
+ borderTopWidth: "1px",
+ borderTopColor: "var(--tw-prose-th-borders)",
+ },
+ "tfoot td": {
+ verticalAlign: "top",
+ },
+ ":is(tbody, tfoot) td": {
+ paddingTop: theme("spacing.2"),
+ paddingRight: theme("spacing.2"),
+ paddingBottom: theme("spacing.2"),
+ paddingLeft: theme("spacing.2"),
+ },
+ ":is(tbody, tfoot) td:first-child": {
+ paddingLeft: "0",
+ },
+ ":is(tbody, tfoot) td:last-child": {
+ paddingRight: "0",
+ },
+
+ // Inline elements
+ a: {
+ color: "var(--tw-prose-links)",
+ textDecoration: "underline transparent",
+ fontWeight: "500",
+ transitionProperty: "color, text-decoration-color",
+ transitionDuration: theme("transitionDuration.DEFAULT"),
+ transitionTimingFunction: theme(
+ "transitionTimingFunction.DEFAULT",
+ ),
+ "&:hover": {
+ color: "var(--tw-prose-links-hover)",
+ textDecorationColor: "var(--tw-prose-links-underline)",
+ },
+ },
+ ":is(h1, h2, h3) a": {
+ fontWeight: "inherit",
+ },
+ strong: {
+ color: "var(--tw-prose-bold)",
+ fontWeight: "600",
+ },
+ ":is(a, blockquote, thead th) strong": {
+ color: "inherit",
+ },
+ code: {
+ color: "var(--tw-prose-code)",
+ borderRadius: theme("borderRadius.lg"),
+ paddingTop: theme("padding.1"),
+ paddingRight: theme("padding[1.5]"),
+ paddingBottom: theme("padding.1"),
+ paddingLeft: theme("padding[1.5]"),
+ boxShadow: "inset 0 0 0 1px var(--tw-prose-code-ring)",
+ backgroundColor: "var(--tw-prose-code-bg)",
+ fontSize: theme("fontSize.2xs"),
+ },
+ ":is(a, h1, h2, h3, blockquote, thead th) code": {
+ color: "inherit",
+ },
+ "h2 code": {
+ fontSize: theme("fontSize.base")[0],
+ fontWeight: "inherit",
+ },
+ "h3 code": {
+ fontSize: theme("fontSize.sm")[0],
+ fontWeight: "inherit",
+ },
+
+ // Overrides
+ ":is(h1, h2, h3) + *": {
+ marginTop: "0",
+ },
+ "> :first-child": {
+ marginTop: "0 !important",
+ },
+ "> :last-child": {
+ marginBottom: "0 !important",
+ },
+ },
+ },
+ invert: {
+ css: {
+ "--tw-prose-body": "var(--tw-prose-invert-body)",
+ "--tw-prose-headings": "var(--tw-prose-invert-headings)",
+ "--tw-prose-links": "var(--tw-prose-invert-links)",
+ "--tw-prose-links-hover": "var(--tw-prose-invert-links-hover)",
+ "--tw-prose-links-underline":
+ "var(--tw-prose-invert-links-underline)",
+ "--tw-prose-bold": "var(--tw-prose-invert-bold)",
+ "--tw-prose-counters": "var(--tw-prose-invert-counters)",
+ "--tw-prose-bullets": "var(--tw-prose-invert-bullets)",
+ "--tw-prose-hr": "var(--tw-prose-invert-hr)",
+ "--tw-prose-quotes": "var(--tw-prose-invert-quotes)",
+ "--tw-prose-quote-borders":
+ "var(--tw-prose-invert-quote-borders)",
+ "--tw-prose-captions": "var(--tw-prose-invert-captions)",
+ "--tw-prose-code": "var(--tw-prose-invert-code)",
+ "--tw-prose-code-bg": "var(--tw-prose-invert-code-bg)",
+ "--tw-prose-code-ring": "var(--tw-prose-invert-code-ring)",
+ "--tw-prose-th-borders": "var(--tw-prose-invert-th-borders)",
+ "--tw-prose-td-borders": "var(--tw-prose-invert-td-borders)",
+ },
+ },
+ };
+}