mirror of
https://github.com/versia-pub/docs.git
synced 2025-12-06 06:18:19 +01:00
More docs porting
This commit is contained in:
parent
39f95bad3e
commit
309ef312b1
141
docs/cryptography/signing.md
Normal file
141
docs/cryptography/signing.md
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
# Cryptography
|
||||||
|
|
||||||
|
Lysand uses cryptography to ensure that objects are not tampered with during transit. This is done by signing objects with a private key, and verifying the signature with a public key.
|
||||||
|
|
||||||
|
> **Note**: The author of the object is the actor that created the object, indicated by the `author` property on the object body. The server that is sending the object is the server that is sending the object to another server.
|
||||||
|
|
||||||
|
All HTTP requests **MUST** be sent over HTTPS. Servers **MUST** not accept HTTP requests, unless it is for development purposes.
|
||||||
|
|
||||||
|
HTTP requests **MUST** be signed with the public key of the author of the object. This is done by adding a `Signature` header to the request.
|
||||||
|
|
||||||
|
The `Signature` header **MUST** be formatted as follows:
|
||||||
|
```
|
||||||
|
Signature: keyId="https://example.com/users/uuid",algorithm="ed25519",headers="(request-target) host date digest",signature="base64_signature"
|
||||||
|
```
|
||||||
|
|
||||||
|
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 **MUST** be calculated as follows:
|
||||||
|
|
||||||
|
1. Create a string that contains the following:
|
||||||
|
```
|
||||||
|
(request-target): post /users/uuid/inbox
|
||||||
|
host: example.com
|
||||||
|
date: Fri, 01 Jan 2021 00:00:00 GMT
|
||||||
|
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):
|
||||||
|
```
|
||||||
|
Fri, 01 Jan 2021 00:00:00 GMT
|
||||||
|
```
|
||||||
|
|
||||||
|
The `host` field **MUST** be the domain 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.
|
||||||
|
|
||||||
|
Here is an example of signing a request using TypeScript and the WebCrypto API:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const privateKey = await crypto.subtle.importKey(
|
||||||
|
"pkcs8",
|
||||||
|
atob("base64_private_key"),
|
||||||
|
"Ed25519",
|
||||||
|
false,
|
||||||
|
["sign"]
|
||||||
|
);
|
||||||
|
|
||||||
|
const digest = await crypto.subtle.digest(
|
||||||
|
"SHA-256",
|
||||||
|
new TextEncoder().encode("request_body")
|
||||||
|
);
|
||||||
|
|
||||||
|
const signature = await crypto.subtle.sign(
|
||||||
|
"Ed25519",
|
||||||
|
privateKey,
|
||||||
|
new TextEncoder().encode(
|
||||||
|
"(request-target): post /users/uuid/inbox\n" +
|
||||||
|
"host: example.com\n" +
|
||||||
|
"date: Fri, 01 Jan 2021 00:00:00 GMT\n" +
|
||||||
|
"digest: SHA-256=" + btoa(digest)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const signatureBase64 = base64Encode(signature);
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note**: 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://example.com/users/uuid/inbox", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Date": "Fri, 01 Jan 2021 00:00:00 GMT",
|
||||||
|
"Origin": "https://example.com",
|
||||||
|
"Signature": `keyId="https://example.com/users/uuid",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Example of validation on the server side:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// request is a Request object containing the previous request
|
||||||
|
// public_key is the user's public key in raw base64 format
|
||||||
|
|
||||||
|
const signatureHeader = request.headers.get("Signature");
|
||||||
|
|
||||||
|
const signature = signatureHeader.split("signature=")[1].replace(/"/g, "");
|
||||||
|
|
||||||
|
const origin = request.headers.get("Origin");
|
||||||
|
|
||||||
|
const date = request.headers.get("Date");
|
||||||
|
|
||||||
|
const digest = await crypto.subtle.digest(
|
||||||
|
"SHA-256",
|
||||||
|
new TextEncoder().encode(await request.text())
|
||||||
|
);
|
||||||
|
|
||||||
|
const expectedSignedString = `(request-target): ${request.method.toLowerCase()} ${request.url}\n` +
|
||||||
|
`host: ${request.url}\n` +
|
||||||
|
`date: ${date}\n` +
|
||||||
|
`digest: SHA-256=${btoa(digest)}`;
|
||||||
|
|
||||||
|
// Check if signed string is valid
|
||||||
|
const isValid = await crypto.subtle.verify(
|
||||||
|
"Ed25519",
|
||||||
|
publicKey,
|
||||||
|
new TextEncoder().encode(signature),
|
||||||
|
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.
|
||||||
188
docs/federation/endpoints.md
Normal file
188
docs/federation/endpoints.md
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
# Federation
|
||||||
|
|
||||||
|
The Lysand protocol is only useful when it is federated. This section describes how federation works in Lysand.
|
||||||
|
|
||||||
|
Federation in Lysand is based on the HTTP protocol. Servers communicate with each other by sending HTTP requests to one another.
|
||||||
|
objects
|
||||||
|
These requests are usually `POST` requests containing a JSON object in the body. This JSON object **MUST BE** a valid Lysand object.
|
||||||
|
|
||||||
|
Servers that receive invalid Lysand objects **SHOULD** discard this object as invalid.
|
||||||
|
|
||||||
|
## User Actor Endpoints
|
||||||
|
|
||||||
|
A server is trying to get the profile of a user on another server. User discovery is done as specified in [User Discovery](/federation/user-discovery).
|
||||||
|
|
||||||
|
Once the requesting server has discovered the endpoints of the server, it can send a `GET` request to the user's endpoint to discover the user's actor.
|
||||||
|
|
||||||
|
In the above example, to discover user information, the requesting server **MUST** send a `GET` request to the endpoint `https://example.com/users/uuid` with the headers `Accept: application/json`. To sign the request, the server may use either the [Server Actor](/federation/server-actor) or a requesting user's actor as appropriate.
|
||||||
|
|
||||||
|
The server **MUST** respond with a `200 OK` response code, and a JSON object in the body. This JSON object **MUST** be a valid User object.
|
||||||
|
|
||||||
|
> **Note**: Servers are not required to implement the functionality between certain endpoints such as dislikes or featured notes, but they **MUST** at least return an empty collection for these endpoints. Servers may also discard any objects they do not handle, but should return a success response code.
|
||||||
|
|
||||||
|
## User Inbox
|
||||||
|
|
||||||
|
Once the requesting server has discovered the endpoints of the server, it can send a `POST` request to the `inbox` endpoint to send an object to the user. This is similar to how objects are sent in ActivityPub.
|
||||||
|
|
||||||
|
Typically, the inbox can be located on the same URL as the user's actor, but this is not required. 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** be 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 `200 OK` response code if no error occurred.
|
||||||
|
|
||||||
|
## User Outbox
|
||||||
|
|
||||||
|
Users in Lysand have an outbox, which is a list of objects that the user has posted. This is similar 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 object containing 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 do not want to display this list publically, you can make the followers endpoint 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 object containing Actors. This collection may be empty.
|
||||||
|
|
||||||
|
## User Following
|
||||||
|
|
||||||
|
Users in Lysand have a list of following, which is a list of users that the user follows. This is similar to the following list in ActivityPub.
|
||||||
|
|
||||||
|
> **Note:** If you do not want to display this list publically, you can make the following endpoint 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 object containing 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 are important. This is similar to the featured publications list in ActivityPub.
|
||||||
|
|
||||||
|
> **Note:** If you do not want to display this list publically, you can make the featured publications endpoint 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 object containing 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 do not want to display this list publically, you can make the likes endpoint 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 object containing 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 do not want to display this list publically, you can make the dislikes endpoint 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 object containing 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 trying to discover the server.
|
||||||
|
|
||||||
|
To discover 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` 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
11
docs/federation/server-actor.md
Normal file
11
docs/federation/server-actor.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# 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.
|
||||||
75
docs/federation/user-discovery.md
Normal file
75
docs/federation/user-discovery.md
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
# 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
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
||||||
|
<Link rel="lrdd" type="application/xrd+xml" template="https://example.com/.well-known/webfinger?resource={uri}" />
|
||||||
|
</XRD>
|
||||||
|
```
|
||||||
|
|
||||||
|
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:uuid@example.com`)
|
||||||
|
|
||||||
|
Breaking down this URI, we get the following:
|
||||||
|
|
||||||
|
- `acct`: The protocol of the URI. This is always `acct` for Lysand.
|
||||||
|
- `uuid`: The UUID 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 a UUID instead of a username. Users should typically not use the `id` of an actor to identify it, but instead its `username`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
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:uuid@example.com` (replace `uuid` with the user's ID)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
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:uuid@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.`.
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
### 1.6.2. Actors
|
||||||
|
|
||||||
|
Actors are the main users of the Lysand protocol. They are JSON objects that represent a user. They are similar to ActivityPub's `Actor` objects.
|
||||||
|
|
||||||
|
Actors **MUST** be referred to by their `id` internally and across the protocol. The `username` property MUST be treated as a changeable display name, and **MUST NOT** be used to identify the actor.
|
||||||
|
|
||||||
|
Here is an example 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": [
|
||||||
|
{
|
||||||
|
"content": "https://cdn.test.com/avatars/ab5081cf-b11f-408f-92c2-7c246f290593.png",
|
||||||
|
"content_type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "https://cdn.test.com/avatars/ab5081cf-b11f-408f-92c2-7c246f290593.webp",
|
||||||
|
"content_type": "image/webp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"content": "https://cdn.test.com/banners/ab5081cf-b11f-408f-92c2-7c246f290593.png",
|
||||||
|
"content_type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "https://cdn.test.com/banners/ab5081cf-b11f-408f-92c2-7c246f290593.webp",
|
||||||
|
"content_type": "image/webp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexable": true,
|
||||||
|
"public_key": {
|
||||||
|
"public_key": "...",
|
||||||
|
"actor": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7"
|
||||||
|
},
|
||||||
|
"bio": [
|
||||||
|
{
|
||||||
|
"content": "My name is Gordon Ramsay, I'm a silly quirky little pony that LOVES to roleplay in the bedroom!",
|
||||||
|
"content_type": "text/plain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "My name is <b>Gordon Ramsay</b>, I'm a silly quirky little pony that <i>LOVES</i> to roleplay in the bedroom!",
|
||||||
|
"content_type": "text/html"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"key": [
|
||||||
|
{
|
||||||
|
"content": "Where I live",
|
||||||
|
"content_type": "text/plain"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"content": "Portland, Oregon",
|
||||||
|
"content_type": "text/plain"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As there is only one type of `Actor`, the `User`, please see [`User`](/objects/user) for more information.
|
||||||
264
docs/objects/user.md
Normal file
264
docs/objects/user.md
Normal file
|
|
@ -0,0 +1,264 @@
|
||||||
|
# User
|
||||||
|
|
||||||
|
Users are Actors that represent a user on the server. They are the only 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": [
|
||||||
|
{
|
||||||
|
"content": "https://cdn.test.com/avatars/ab5081cf-b11f-408f-92c2-7c246f290593.png",
|
||||||
|
"content_type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "https://cdn.test.com/avatars/ab5081cf-b11f-408f-92c2-7c246f290593.webp",
|
||||||
|
"content_type": "image/webp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"content": "https://cdn.test.com/banners/ab5081cf-b11f-408f-92c2-7c246f290593.png",
|
||||||
|
"content_type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "https://cdn.test.com/banners/ab5081cf-b11f-408f-92c2-7c246f290593.webp",
|
||||||
|
"content_type": "image/webp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexable": true,
|
||||||
|
"public_key": {
|
||||||
|
"public_key": "...",
|
||||||
|
"actor": "https://test.com/users/02e1e3b2-cb1f-4e4a-b82e-98866bee5de7"
|
||||||
|
},
|
||||||
|
"bio": [
|
||||||
|
{
|
||||||
|
"content": "My name is Gordon Ramsay, I'm a silly quirky little pony that LOVES to roleplay in the bedroom!",
|
||||||
|
"content_type": "text/plain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "My name is <b>Gordon Ramsay</b>, I'm a silly quirky little pony that <i>LOVES</i> to roleplay in the bedroom!",
|
||||||
|
"content_type": "text/html"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"key": [
|
||||||
|
{
|
||||||
|
"content": "Where I live",
|
||||||
|
"content_type": "text/plain"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"content": "Portland, Oregon",
|
||||||
|
"content_type": "text/plain"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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
|
||||||
|
|
||||||
|
### Type
|
||||||
|
|
||||||
|
The `type` of a `User` is always `User`.
|
||||||
|
|
||||||
|
### ID
|
||||||
|
|
||||||
|
The `id` field on an Actor is a UUID that represents the unique identifier of the actor as a string. It is used to identify the actor, and **MUST** be unique across all actors of the server.
|
||||||
|
|
||||||
|
### Public Key
|
||||||
|
|
||||||
|
The `public_key` field on an Actor is an [`ActorPublicKeyData`](/cryptography/keys) object. It is used to verify that the actor is who they say they are. The key **MUST** be encoded in base64.
|
||||||
|
|
||||||
|
All actors **MUST** have a `public_key` field. All servers **SHOULD** verify that the actor is who they say they are 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.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
The `display_name` field on an Actor is a string that represents the display name of the actor. It is used to display the actor's name to the user.
|
||||||
|
|
||||||
|
The `display_name` field is not required on all actors. 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
|
||||||
|
|
||||||
|
Display names **MUST** be treated as changeable, and **MUST NOT** be used to identify the actor.
|
||||||
|
|
||||||
|
It is recommended that servers limit the length of the display name from 1 to 50 characters, but it is up to the server to decide how long the display name can be. The protocol does not have an upper limit for the length of the display name.
|
||||||
|
|
||||||
|
### Username
|
||||||
|
|
||||||
|
The `username` field on an Actor is a string that represents the username of the actor. It is used to loosely identify the actor, and **MUST** be unique across all actors of a server.
|
||||||
|
|
||||||
|
The `username` field is required on all actors.
|
||||||
|
|
||||||
|
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 changeable by the user.
|
||||||
|
|
||||||
|
The `username` field **MUST** be a string that contains only alphanumeric characters, underscores, and dashes. It **MUST NOT** contain any spaces or other special characters.
|
||||||
|
|
||||||
|
It **MUST** match this regex: `/^[a-zA-Z0-9_-]+$/`
|
||||||
|
|
||||||
|
It is recommended that servers limit the length of the username from 1 to 20 characters, but it is up to the server to decide how long the username can be. The protocol does not have an upper limit for the length of the username.
|
||||||
|
|
||||||
|
### Indexable
|
||||||
|
|
||||||
|
The `indexable` field on an Actor is a boolean that represents whether or not the actor should be indexed by search engines. This field is required and must be included.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Avatar
|
||||||
|
|
||||||
|
The `avatar` field on an Actor is an array of `ContentFormat` objects.
|
||||||
|
|
||||||
|
The `avatar` field is not required on actors. 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`.
|
||||||
|
|
||||||
|
Lysand heavily recommends 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.
|
||||||
|
|
||||||
|
> **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.
|
||||||
|
|
||||||
|
### Header
|
||||||
|
|
||||||
|
The `header` field on an Actor is an array that an array of`ContentFormat` objects. It is meant to serve as a banner for users.
|
||||||
|
|
||||||
|
The `header` field is not required on all actors. 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`. The header 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 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.
|
||||||
|
|
||||||
|
> **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.
|
||||||
|
|
||||||
|
### Bio
|
||||||
|
|
||||||
|
The `bio` field on an Actor is an array of `ContentFormat` objects.
|
||||||
|
|
||||||
|
The `bio` field is not required on all actors. If it is not provided, it is assumed that the actor does not have a bio.
|
||||||
|
|
||||||
|
The `bio` field is used to display a short description of the actor to the user. It is recommended that servers limit the length of the bio from 500 to a couple thousand characters, but it is up to the server to decide how long the bio can be. The protocol does not have an upper limit for the length of the bio.
|
||||||
|
|
||||||
|
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": [
|
||||||
|
{
|
||||||
|
"content": "This is my bio!",
|
||||||
|
"content_type": "text/plain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "This is my <b>bio</b>!",
|
||||||
|
"content_type": "text/html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **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.
|
||||||
|
|
||||||
|
> **Note**: 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
|
||||||
|
|
||||||
|
The `fields` field on an Actor is an array that contains a list of `Field` objects. It is used to display custom fields to the user, such as additional metadata.
|
||||||
|
|
||||||
|
The `fields` field is not required on all actors. If it is 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": [
|
||||||
|
{
|
||||||
|
"content": "Where I live",
|
||||||
|
"content_type": "text/plain"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"content": "Portland, Oregon",
|
||||||
|
"content_type": "text/plain"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Fields are formatted as follows:
|
||||||
|
```ts
|
||||||
|
interface Field {
|
||||||
|
key: ContentFormat[];
|
||||||
|
value: ContentFormat[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Both `key` and `value` should be presented to the user as a couple.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Please see [Featured Publications](/federation/endpoints) for more information.
|
||||||
|
|
||||||
|
### Followers
|
||||||
|
|
||||||
|
Please see [User Followers](/federation/endpoints) for more information.
|
||||||
|
|
||||||
|
### Following
|
||||||
|
|
||||||
|
Please see [User Following](/federation/endpoints) for more information.
|
||||||
|
|
||||||
|
### Likes
|
||||||
|
|
||||||
|
Please see [User Likes](/federation/endpoints) for more information.
|
||||||
|
|
||||||
|
### Dislikes
|
||||||
|
|
||||||
|
Please see [User Dislikes](/federation/endpoints) for more information.
|
||||||
|
|
||||||
|
### Inbox
|
||||||
|
|
||||||
|
The `inbox` field on an Actor is a string that represents the URI of the actor's inbox. It is used to identify the actor's inbox for federation.
|
||||||
|
|
||||||
|
Please see [Inbox](/federation/endpoints) for more information.
|
||||||
|
|
||||||
|
### Outbox
|
||||||
|
|
||||||
|
The `outbox` field on an Actor is a string that represents the URI of the actor's outbox. It is used to identify the actor's outbox for federation.
|
||||||
|
|
||||||
|
Please see [Outbox](/federation/endpoints) for more information.
|
||||||
Loading…
Reference in a new issue