Merge branch 'feat/lysand3'

This commit is contained in:
Jesse Wierzbinski 2024-05-01 17:44:43 -10:00
commit 94e5ac67f7
No known key found for this signature in database
26 changed files with 1036 additions and 428 deletions

View file

@ -1,108 +1,164 @@
import { defineConfig } from 'vitepress'
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vitepress";
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "Lysand Documentation",
description: "Documentation for Lysand, a new federated protocol",
srcDir: 'docs',
vite: {
plugins: [tailwindcss()],
},
vue: {
template: {
compilerOptions: {
isCustomElement: (tag) => tag === "iconify-icon",
},
},
},
srcDir: "docs",
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: 'Home', link: '/' },
{ text: 'Specification', link: '/spec' },
{ text: "Home", link: "/" },
{ text: "Specification", link: "/spec" },
{ text: "Objects", link: "/objects" },
{ text: "Security", link: "/security/api" },
{ text: "Extensions", link: "/extensions" },
],
sidebar: [
{
text: 'Spec Details',
text: "Specification",
items: [
{ text: 'Spec', link: '/spec' },
]
{ text: "Spec", link: "/spec" },
{ text: "Objects", link: "/objects" },
],
},
{
text: "Structures",
items: [
{ text: "Content Format", link: '/structures/content-format' },
{ text: "Custom Emoji", link: '/structures/custom-emoji' },
{ text: "Collection", link: '/structures/collection' },
]
{
text: "Content Format",
link: "/structures/content-format",
},
{ text: "Custom Emoji", link: "/structures/custom-emoji" },
{ text: "Collection", link: "/structures/collection" },
],
},
{
text: "Cryptography",
text: "Groups",
items: [{ text: "Groups", link: "/groups" }],
},
{
text: "Security",
items: [
{ text: "Keys", link: "/cryptography/keys" },
{ text: "Signing", link: "/cryptography/signing" },
]
{ text: "API", link: "/security/api" },
{ text: "Keys", link: "/security/keys" },
{ text: "Signing", link: "/security/signing" },
],
},
{
text: "Objects",
link: "/objects",
items: [
{
text: "Publications", link: "/objects/publications", items: [
text: "Publications",
link: "/objects/publications",
items: [
{ text: "Note", link: "/objects/note" },
{ text: "Patch", link: "/objects/patch" },
]
],
},
{
text: "Actors", link: "/objects/actors", items: [
{ text: "User", link: "/objects/user" },
]
text: "Actors",
link: "/objects/actors",
items: [{ text: "User", link: "/objects/user" }],
},
{
text: "Actions", link: "/objects/actions", items: [
text: "Actions",
link: "/objects/actions",
items: [
{ text: "Like", link: "/objects/like" },
{ text: "Dislike", link: "/objects/dislike" },
{ text: "Follow", link: "/objects/follow" },
{ text: "FollowAccept", link: "/objects/follow-accept" },
{ text: "FollowReject", link: "/objects/follow-reject" },
{
text: "FollowAccept",
link: "/objects/follow-accept",
},
{
text: "FollowReject",
link: "/objects/follow-reject",
},
{ text: "Announce", link: "/objects/announce" },
{ text: "Undo", link: "/objects/undo" },
]
],
},
{ text: "Server Metadata", link: "/objects/server-metadata" }
]
{
text: "Server Metadata",
link: "/objects/server-metadata",
},
],
},
{
text: "Federation",
items: [
{ text: "Endpoints", link: "/federation/endpoints" },
{ text: "User Discovery", link: "/federation/user-discovery" },
{
text: "User Discovery",
link: "/federation/user-discovery",
},
{ text: "Server Actors", link: "/federation/server-actor" },
]
],
},
{
text: "Extensions",
link: "/extensions",
items: [
{ text: "Custom Emojis", link: "/extensions/custom-emojis" },
{
text: "Custom Emojis",
link: "/extensions/custom-emojis",
},
{
text: "Microblogging",
link: "/extensions/microblogging",
},
{ text: "Reactions", link: "/extensions/reactions" },
{ text: "Polls", link: "/extensions/polls" },
{ text: "Is Cat", link: "/extensions/is-cat" },
{ text: "Server Endorsements", link: "/extensions/server-endorsement" },
{
text: "Server Endorsements",
link: "/extensions/server-endorsement",
},
{ text: "Events", link: "/extensions/events" },
{ text: "Reports", link: "/extensions/reports" },
{ text: "Vanity", link: "/extensions/vanity" },
]
}
{
text: "Interactivity",
link: "/extensions/interactivity",
},
],
},
],
footer: {
message: "Released under the MIT License.",
copyright: "Copyright © 2023-present Gaspard Wierzbinski",
},
socialLinks: [
{ icon: 'github', link: 'https://github.com/lysand-org/' }
{ icon: "github", link: "https://github.com/lysand-org/" },
],
search: {
provider: "local",
},
editLink: {
pattern: "https://github.com/lysand-org/docs/edit/main/docs/:path"
pattern: "https://github.com/lysand-org/docs/edit/main/docs/:path",
},
logo: "/logo.png",
externalLinkIcon: true,
logo: "https://cdn.lysand.org/logo.webp",
},
lastUpdated: true,
cleanUrls: true,
titleTemplate: ":title · Lysand 2.0 Docs",
head: [
['link', { rel: 'icon', href: '/favicon.png', type: 'image/png' }]
],
lang: 'en-US',
})
titleTemplate: ":title · Lysand Docs",
head: [["link", { rel: "icon", href: "/favicon.png", type: "image/png" }]],
lang: "en-US",
});

View file

@ -0,0 +1,61 @@
@import "tailwindcss";
@theme {
}
:root {
/* --vp-home-hero-image-background-image: linear-gradient(
to top right,
rgb(249, 168, 212),
rgb(216, 180, 254),
rgb(129, 140, 248)
);
--vp-home-hero-image-filter: brightness(0.8) saturate(1.2); */
--vp-home-hero-name-color: rgb(249, 168, 212);
--vp-c-brand-1: rgb(249, 168, 212);
--lysand-gradient: linear-gradient(
to right,
rgb(249, 168, 212),
rgb(216, 180, 254),
rgb(129, 140, 248)
);
--vp-color-primary: rgb(249, 168, 212);
--vp-color-secondary: rgb(216, 180, 254);
--vp-button-brand-bg: transparent;
--vp-c-bg-soft: rgb(250, 250, 250);
}
.dark {
--vp-c-bg: rgb(24, 24, 24);
--vp-c-bg-soft: rgb(32, 32, 32);
}
.VPFeature {
border-radius: 0.3rem !important;
transition: all 0.2s ease-in-out !important;
}
.VPFeature:hover {
transform: scale(1.02);
border-color: var(--vp-color-primary);
}
.VPButton.medium {
border-radius: 0.3rem !important;
transition: all 0.2s ease-in-out !important;
}
.VPButton.medium:hover {
transform: scale(1.02);
}
.VPButton.brand {
background: var(--lysand-gradient);
border: none !important;
}
@media (min-width: 960px) {
.image-container {
width: 50% !important;
margin-right: 0.5rem !important;
}
}

View file

@ -0,0 +1,4 @@
import DefaultTheme from "vitepress/theme";
import "./custom.css";
export default DefaultTheme;

20
biome.json Normal file
View file

@ -0,0 +1,20 @@
{
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
"organizeImports": {
"enabled": true,
"ignore": ["node_modules", "dist", "cache"]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
},
"ignore": ["node_modules", "dist", "cache"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 4,
"ignore": ["node_modules", "dist", "cache"]
}
}

BIN
bun.lockb

Binary file not shown.

78
components/Features.vue Normal file
View file

@ -0,0 +1,78 @@
<template>
<div class="mt-12">
<div class="max-w-3xl">
<h1>Made by developers</h1>
<p>
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 <a
href="https://github.com/lysand-org/lysand">Git repository</a> to see how you can contribute.
</p>
</div>
<div
class="!mt-8 grid items-start gap-x-6 gap-y-6 sm:mt-16 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 lg:gap-x-8">
<div v-for="feature in features" :key="feature.name"
class="flex flex-row h-32 p-5 items-center gap-x-4 bg-[var(--vp-c-bg-soft)] shadow rounded duration-200 hover:ring-2 hover:scale-[101%] ring-[var(--vp-color-primary)]">
<div class="aspect-square flex items-center justify-center overflow-hidden rounded shrink-0 h-full">
<iconify-icon :icon="feature.icon" class="text-[var(--vp-color-primary)] text-5xl" />
</div>
<div class="text-pretty">
<h3 class="!text-base font-medium !mt-0">{{ feature.name }}</h3>
<p class="!mt-1 !mb-0 !text-sm">{{ feature.description }}</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import "iconify-icon";
const features = [
{
name: "JSON-based APIs",
description: "Simple JSON objects are used to represent all data.",
icon: "bx:bx-code-alt",
},
{
name: "MIT Licensed",
description:
"Lysand is licensed under the MIT License, which allows you to use it for any purpose.",
icon: "bx:bx-shield",
},
{
name: "Built-in namespaced extensions",
description:
"Extensions for common use cases are built-in, such as custom emojis and reactions",
icon: "bx:bx-extension",
},
{
name: "Easy to implement",
description:
"Lysand is designed to be easy to implement in any language.",
icon: "bx:bx-code-block",
},
{
name: "Secure by default",
description:
"All requests are signed using advanced cryptographic algorithms.",
icon: "bx:bx-shield-alt",
},
{
name: "No Mastodon Situation",
description:
"Standardization is heavy and designed to break vendor lock-in.",
icon: "bx:bx-code-curly",
},
{
name: "In-Depth Security Docs",
description:
"Docs provide lots of information on how to program a secure server.",
icon: "bx:bx-shield-x",
},
{
name: "TypeScript Types",
description: "TypeScript types are provided for all objects.",
icon: "bx:bx-code",
},
];
</script>

92
components/Team.vue Normal file
View file

@ -0,0 +1,92 @@
<template>
<div class="mt-20">
<div class="max-w-3xl">
<h1>Thank you!</h1>
<p>
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.
</p>
</div>
<ul role="list"
class="!mt-10 grid max-w-2xl grid-cols-1 gap-x-8 gap-y-16 sm:grid-cols-2 lg:max-w-none lg:grid-cols-3 !list-none !pl-0">
<li v-for="person in people" :key="person.name"
class="bg-[var(--vp-c-bg-soft)] shadow rounded duration-200 !m-0 hover:ring-2 hover:scale-[101%] ring-[var(--vp-color-primary)] p-4">
<img class="aspect-[3/2] w-full rounded object-cover ring-1 ring-white/5" :src="person.imageUrl"
:alt="`${person.name}'s avatar'`" />
<h3 class="mt-6">{{ person.name }}</h3>
<p class="!mt-3">
<span v-for="role in person.roles"
class="text-sm mr-2 last:mr-0 rounded bg-pink-700 text-pink-100 px-2 py-1">{{
role }}</span>
</p>
<ul role="list" class="!mt-6 !flex !gap-6 !list-none !pl-0 flex-wrap">
<li v-for="social in person.socials" :key="social.name" class="!m-0">
<a :href="social.url" class="text-[var(--vp-color-primary)]" target="_blank" rel="noreferrer">
<iconify-icon :icon="social.icon" class="text-2xl" />
</a>
</li>
</ul>
</li>
</ul>
</div>
</template>
<script setup>
const people = [
{
name: "CPlusPatch",
roles: ["Lead Developer", "UI Designer"],
imageUrl: "https://avatars.githubusercontent.com/u/42910258?v=4",
socials: [
{
name: "Website",
icon: "bx:link",
url: "https://cpluspatch.com",
},
{
name: "GitHub",
icon: "bxl:github",
url: "https://github.com/cpluspatch",
},
{
name: "Fediverse",
icon: "bxl:mastodon",
url: "https://mk.cpluspatch.com/@jessew",
},
{
name: "Lysand",
icon: "bx:server",
url: "https://social.lysand.org/@jessew",
},
{
name: "Matrix",
icon: "simple-icons:matrix",
url: "https://matrix.to/#/@jesse:cpluspatch.dev",
},
{
name: "Signal",
icon: "simple-icons:signal",
url: "https://signal.me/#eu/mdX6iV0ayndNmJst43sNtlw3eFXgHSm7if4Y/mwYT1+qFDzl1PFAeroW+RpHGaRu",
},
{
name: "Email",
icon: "bx:bxs-envelope",
url: "mailto:contact@cpluspatch.com",
},
],
},
{
name: "April",
roles: ["ActivityPub Bridge Developer"],
imageUrl: "https://avatars.githubusercontent.com/u/30842467?v=4",
socials: [
{
name: "GitHub",
icon: "bxl:github",
url: "https://github.com/cutestnekoaqua",
},
],
},
];
</script>

View file

@ -1,195 +0,0 @@
# Cryptography in Lysand
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.
All HTTP requests **MUST** be sent over HTTPS for security reasons. Servers **MUST NOT** accept HTTP requests, unless it is for development purposes.
HTTP requests **MUST** be signed with the public key of the object's author. This is done by adding a `Signature` header to the request.
The `Signature` header is too be formatted as follows:
```
Signature: keyId="https://example.com/users/uuid",algorithm="ed25519",headers="(request-target) host date digest",signature="base64_signature"
```
Here, the `keyId` field **MUST** be the URI of the user that is sending the request.
The `algorithm` field **MUST** be `ed25519`.
The `headers` field **MUST** be `(request-target) host date digest`.
The `signature` field **MUST** be the base64-encoded signature of the request.
The signature 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 /users/uuid/inbox
host: example.com
date: 2024-04-10T01:27:24.880Z
digest: SHA-256=base64_digest
```
1. Sign the string with the user's private key.
2. Base64-encode the signature.
The `digest` field **MUST** be the SHA-256 digest of the request body, base64-encoded.
The `date` field **MUST** be the date and time that the request was sent, formatted as follows (ISO 8601):
```
2024-04-10T01:27:24.880Z
```
The `host` field **MUST** be the host of the server that is receiving the request.
The `request-target` field **MUST** be the request target of the request, formatted as follows:
```
post /users/uuid/inbox
```
Where `/users/uuid/inbox` is the path of the request (this will depend on implementations).
Let's imagine a user at `example.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 (replace `status_author_private_key`, `full_lysand_object_as_string` and sample text appropriate):
```typescript
const privateKey = await crypto.subtle.importKey(
"pkcs8",
Uint8Array.from(atob(status_author_private_key), (c) =>
c.charCodeAt(0),
),
"Ed25519",
false,
["sign"],
);
const digest = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(full_lysand_object_as_string),
);
const userInbox = new URL(
"https://receiver.com/users/22a56612-9909-48ca-84af-548b28db6fd5/inbox"
);
const date = new Date();
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=${btoa(
String.fromCharCode(...new Uint8Array(digest)),
)}\n`,
),
);
const signatureBase64 = btoa(
String.fromCharCode(...new Uint8Array(signature)),
);
```
> [!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: "example.com",
Signature: `keyId="https://example.com/users/caf18716-800d-4c88-843d-4947ab39ca0f",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`,
},
body: full_lysand_object_as_string,
});
```
Example of validation on the 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",
Uint8Array.from(atob(sender.publicKey), (c) => c.charCodeAt(0)),
"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=${btoa(
String.fromCharCode(...new Uint8Array(digest)),
)}\n`;
// Check if signed string is valid
const isValid = await crypto.subtle.verify(
"Ed25519",
public_key,
Uint8Array.from(atob(signature), (c) => c.charCodeAt(0)),
new TextEncoder().encode(expectedSignedString),
);
if (!isValid) {
throw new Error("Invalid signature");
}
```
Signature is **REQUIRED** on **ALL** outbound 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 server actor, the [Server Actor](/federation/server-actor) **MUST** be used in the `author` field.
## Security Considerations
When implementing cryptography in Lysand, it is important to consider the following security considerations:
- **Key Management**: Ensure that private keys are stored securely and are not exposed to unauthorized parties.
- **Key Export**: Do not export private keys to untrusted environments, but allow users to export their private keys to secure locations.
- **Key Import**: Allow users to import private keys when creating new account, but ensure that the keys are not exposed to unauthorized parties.
Most implementations should not roll their own cryptography, but instead use well-established libraries such as the WebCrypto API. (See the [WebCrypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) documentation for more information). Libraries written in unsafe languages, such as C, or that are a frequent source of security issues (e.g., OpenSSL) should be avoided.

View file

@ -0,0 +1,10 @@
# 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.
![Discord Buttons in action](/assets/discord-buttons.webp)
...

View file

@ -0,0 +1,66 @@
# 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/):
![Bottom graphics of a Mastodon post, including a "boosting" icon with numbers next to it](/assets/boosting.png)
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;
}
```

View file

@ -48,7 +48,13 @@ Here is an example object:
},
"birthday": "1998-04-12",
"location": "+40.6894-074.0447/",
"activitypub": "@erikuden@mastodon.de"
"activitypub": [
"@erikuden@mastodon.de"
],
"aliases": [
"https://burger.social/accounts/349ee237-c672-41c1-aadc-677e185f795a",
"https://social.lysand.org/users/f565ef02-035d-4974-ba5e-f62a8558331d"
]
}
}
}
@ -131,12 +137,20 @@ Clients might choose to display a map of the user's location.
| Name | Type | Required |
| :--------- | :----- | :------- |
| activitypub | String | No |
| activitypub | Array of String | No |
The user's ActivityPub profile. This should be a string in the format `@username@domain`.
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
@ -150,7 +164,8 @@ interface VanityExtension {
};
birthday?: string;
location?: string;
activitypub?: string;
activitypub?: string[];
aliases?: string[];
}
```

67
docs/groups.md Normal file
View file

@ -0,0 +1,67 @@
# 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 <strong>Woozy</strong> 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.

View file

@ -7,7 +7,7 @@ hero:
text: "Federation, simpler"
tagline: A simple to implement and complete federation protocol
image:
src: /logo.png
src: https://cdn.lysand.org/logo.webp
alt: Lysand Logo
actions:
- theme: brand
@ -16,34 +16,13 @@ hero:
- theme: alt
text: Lysand Server
link: https://github.com/lysand-org/lysand
features:
- title: JSON-based APIs
details: Simple JSON objects are used to represent all data
- title: Built-in namespaced extensions
details: Extensions for common use cases are built-in, such as custom emojis and reactions
- title: Easy to implement
details: The protocol is simple to implement, and can be used with any language
- title: Secure by default
details: All requests are signed with the latest cryptography algorithms
- title: No vendor-specific implementations
details: Everything is heavily standardized to ensure compatibility
- title: TypeScript types
details: TypeScript types are provided for every object in the protocol
---
---
<Features />
> [!INFO]
> The latest version of Lysand is **2.0**, released on **March 19th 2024** by [**CPlusPatch**](https://cpluspatch.dev).
>
> Lysand 2.0 features **more standardization**, **simpler object structures**, and **documentation rewrite**.
>
> [See the full Git diff here](https://github.com/lysand-org/docs/compare/158ec6e...f11d51c)
<Team />
<style>
:root {
--vp-home-hero-image-background-image: linear-gradient(to top right, rgb(249, 168, 212), rgb(216, 180, 254), rgb(129, 140, 248));
--vp-home-hero-image-filter: blur(168px);
}
</style>
<script setup lang="ts">
import Features from "../components/Features.vue"
import Team from "../components/Team.vue"
</script>

View file

@ -29,21 +29,7 @@ URIs must adhere to the rules defined [here](spec).
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** be one of the following values:
- `Note`
- `Patch`
- `Actor`
- `Like`
- `Dislike`
- `Follow`
- `FollowAccept`
- `FollowReject`
- `Announce`
- `Undo`
- `ServerMetadata`
- `Extension`
Other values are not permitted in this current version of the protocol.
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

View file

@ -1,43 +1 @@
# 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.
Here's an example of an `Announce` action:
```json5
{
"type": "Announce",
"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 announced. Must be of type [Note](./note)
## Types
```typescript
interface Announce extends Action {
type: "Announce";
object: string;
}
```
This page has been moved to the [Microblogging Extension](../extensions/microblogging#announce).

View file

@ -12,13 +12,28 @@ Here is an example publication:
"created_at": "2021-01-01T00:00:00.000Z",
"content": {
"text/plain": {
"content": "Hello, world!"
"content": "Hello, world! I own this website: https://google.com"
},
"text/html": {
"content": "Hello, <b>world</b>!"
"content": "Hello, <b>world</b>! I own this website! <a href=\"https://google.com\">https://google.com</a>"
}
},
"visibility": "public",
"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": {
@ -85,9 +100,55 @@ An example value for the `content` field would be:
>
> 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 `<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ">https://google.com</a>`
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.
Lysand recommends that clients display the richest content format that they support, such as HTML or more exotic formats such as MFM.
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
@ -216,6 +277,10 @@ 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;
@ -247,3 +312,33 @@ enum Visibility {
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
/*
* microblog -> Twitter, Mastodon-style
* forum -> Reddit-style
* blog -> Wordpress, WriteFreely-style
* image -> Instagram-style
* video -> YouTube-style
* audio -> SoundCloud, Spotify-style
*/
type CategoryType = "microblog" | "forum" | "blog" | "image" | "video" | "audio"
```

View file

@ -77,13 +77,13 @@ The `type` of a `User` is invariably `User`.
| Name | Type | Required |
| :--------- | :----------------------------------------- | :------- |
| public_key | [ActorPublicKeyData](../cryptography/keys) | Yes |
| 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](/cryptography/signing) page.
For more information on cryptographic signing, please see the [Signing](/security/signing) page.
Example of encoding the key in TypeScript:
```ts

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

116
docs/security/api.md Normal file
View file

@ -0,0 +1,116 @@
# 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.

View file

@ -6,7 +6,7 @@ All public keys in Lysand **MUST** be encoded using the [ed25519](https://ed2551
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 Kyber algorithms, once they are incorporated into the Web Crypto API.
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:
@ -17,10 +17,14 @@ const keyPair = await crypto.subtle.generateKey(
["sign", "verify"]
);
// Encode both to base64
const publicKey = btoa(String.fromCharCode(...new Uint8Array(await crypto.subtle.exportKey("spki", keyPair.publicKey))));
// 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 privateKey = btoa(String.fromCharCode(...new Uint8Array(await crypto.subtle.exportKey("pkcs8", keyPair.privateKey))));
const publicKey = Buffer.from(
await crypto.subtle.exportKey("spki", keyPair.publicKey),
).toString("base64");
// Store the public and private key somewhere in your user data
```

184
docs/security/signing.md Normal file
View file

@ -0,0 +1,184 @@
# 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.

View file

@ -1,7 +1,12 @@
# Introduction
> [!NOTE]
> You are looking at the documentation for `Lysand 2.0`, released in March 2024 after revision of the original `Lysand 1.0` specification published in September 2023.
> You are looking at the documentation for `Lysand 3.0-beta1`, released in May 2024
> Small changes may still be made before the final release.
>
> Previous versions:
> - `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.
@ -17,43 +22,30 @@ While Lysand draws parallels with popular protocols like ActivityPub and Activit
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
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`. If the server supports cryptography, a `Signature` header as defined in [/signatures](the signatures spec) **MUST** also be present.
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.
## 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** contain the `id` of the object in the URI. They **should not** contain mutable data, such as the actor's `username`.
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. URI normalization is defined in [RFC 3986 Section 6](https://datatracker.ietf.org/doc/html/rfc3986#section-6). Servers **MUST** reject any requests with non-normalized URIs.
# Definitions
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**.
# Universal Guidelines
While some servers may choose to relax these rules for incoming content, provided it doesn't induce errors or edge cases, these guidelines are crucial for outgoing content.
## 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](cryptography/keys) and [Signing](cryptography/signing) spec). Signatures are encoded using request/response headers, not within the JSON object itself.
## Requests and Responses
All Hypertext Transfer Protocol requests MUST be transmitted using the Hypertext Transfer Protocol Secure Extension. Servers MUST NOT accept requests 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.
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
@ -73,3 +65,16 @@ All responses **MUST** include at least the following headers:
- `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.

View file

@ -5,6 +5,13 @@
"docs:preview": "vitepress preview"
},
"devDependencies": {
"@biomejs/biome": "^1.7.1",
"vitepress": "^1.1.0"
},
"trustedDependencies": ["@biomejs/biome"],
"dependencies": {
"@tailwindcss/vite": "^4.0.0-alpha.14",
"iconify-icon": "^2.1.0",
"tailwindcss": "^4.0.0-alpha.14"
}
}