mirror of
https://github.com/versia-pub/docs.git
synced 2025-12-06 22:38:19 +01:00
Merge branch 'main' into feat/lysand3
This commit is contained in:
commit
9e0969f332
26
Dockerfile
26
Dockerfile
|
|
@ -1,11 +1,24 @@
|
||||||
FROM oven/bun:alpine
|
FROM oven/bun:alpine as base
|
||||||
|
|
||||||
|
# Install dependencies into temp directory
|
||||||
|
# This will cache them and speed up future builds
|
||||||
|
FROM base AS install
|
||||||
|
RUN mkdir -p /temp/dev
|
||||||
|
COPY package.json bun.lockb /temp/dev/
|
||||||
|
RUN cd /temp/dev && bun install --frozen-lockfile
|
||||||
|
|
||||||
|
# Install with --production (exclude devDependencies)
|
||||||
|
RUN mkdir -p /temp/prod
|
||||||
|
COPY package.json bun.lockb /temp/prod/
|
||||||
|
RUN cd /temp/prod && bun install --frozen-lockfile --production
|
||||||
|
|
||||||
|
FROM base AS builder
|
||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
RUN cd /app && bun install
|
||||||
|
RUN cd /app && bun docs:build
|
||||||
|
|
||||||
RUN cd ./app && bun install
|
FROM base AS final
|
||||||
RUN cd ./app && bun docs:build
|
|
||||||
|
|
||||||
FROM oven/bun:alpine
|
|
||||||
|
|
||||||
COPY --from=builder /app/.vitepress/dist/ /app
|
COPY --from=builder /app/.vitepress/dist/ /app
|
||||||
|
|
||||||
|
|
@ -15,3 +28,6 @@ LABEL org.opencontainers.image.vendor "Lysand.org"
|
||||||
LABEL org.opencontainers.image.licenses "MIT"
|
LABEL org.opencontainers.image.licenses "MIT"
|
||||||
LABEL org.opencontainers.image.title "Lysand Docs"
|
LABEL org.opencontainers.image.title "Lysand Docs"
|
||||||
LABEL org.opencontainers.image.description "Documentation for Lysand"
|
LABEL org.opencontainers.image.description "Documentation for Lysand"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
CMD ["bun", "docs:serve"]
|
||||||
|
|
@ -28,7 +28,7 @@ The signature is calculated as follows:
|
||||||
```
|
```
|
||||||
(request-target): post /users/uuid/inbox
|
(request-target): post /users/uuid/inbox
|
||||||
host: example.com
|
host: example.com
|
||||||
date: Fri, 01 Jan 2021 00:00:00 GMT
|
date: 2024-04-10T01:27:24.880Z
|
||||||
digest: SHA-256=base64_digest
|
digest: SHA-256=base64_digest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -40,48 +40,41 @@ The `digest` field **MUST** be the SHA-256 digest of the request body, base64-en
|
||||||
|
|
||||||
The `date` field **MUST** be the date and time that the request was sent, formatted as follows (ISO 8601):
|
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
|
2024-04-10T01:27:24.880Z
|
||||||
```
|
```
|
||||||
|
|
||||||
The `host` field **MUST** be the domain of the server that is receiving the request.
|
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:
|
The `request-target` field **MUST** be the request target of the request, formatted as follows:
|
||||||
```
|
```
|
||||||
post /users/uuid/inbox
|
post /users/uuid/inbox
|
||||||
```
|
```
|
||||||
|
|
||||||
Where `/users/uuid/inbox` is the path of the request.
|
Where `/users/uuid/inbox` is the path of the request (this will depend on implementations).
|
||||||
|
|
||||||
Here is an example of signing a request using TypeScript and the WebCrypto API:
|
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
|
```typescript
|
||||||
/**
|
|
||||||
* Convert a string into an ArrayBuffer
|
|
||||||
* from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
|
|
||||||
*/
|
|
||||||
const str2ab = (str: string) => {
|
|
||||||
const buf = new ArrayBuffer(str.length);
|
|
||||||
const bufView = new Uint8Array(buf);
|
|
||||||
for (let i = 0, strLen = str.length; i < strLen; i++) {
|
|
||||||
bufView[i] = str.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return buf;
|
|
||||||
};
|
|
||||||
|
|
||||||
const privateKey = await crypto.subtle.importKey(
|
const privateKey = await crypto.subtle.importKey(
|
||||||
"pkcs8",
|
"pkcs8",
|
||||||
str2ab(atob("base64_private_key")),
|
Uint8Array.from(atob(status_author_private_key), (c) =>
|
||||||
|
c.charCodeAt(0),
|
||||||
|
),
|
||||||
"Ed25519",
|
"Ed25519",
|
||||||
false,
|
false,
|
||||||
["sign"]
|
["sign"],
|
||||||
);
|
);
|
||||||
|
|
||||||
const digest = await crypto.subtle.digest(
|
const digest = await crypto.subtle.digest(
|
||||||
"SHA-256",
|
"SHA-256",
|
||||||
new TextEncoder().encode("request_body")
|
new TextEncoder().encode(full_lysand_object_as_string),
|
||||||
);
|
);
|
||||||
|
|
||||||
const userInbox = new URL("...");
|
const userInbox = new URL(
|
||||||
|
"https://receiver.com/users/22a56612-9909-48ca-84af-548b28db6fd5/inbox"
|
||||||
|
);
|
||||||
|
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
|
|
||||||
|
|
@ -91,15 +84,15 @@ const signature = await crypto.subtle.sign(
|
||||||
new TextEncoder().encode(
|
new TextEncoder().encode(
|
||||||
`(request-target): post ${userInbox.pathname}\n` +
|
`(request-target): post ${userInbox.pathname}\n` +
|
||||||
`host: ${userInbox.host}\n` +
|
`host: ${userInbox.host}\n` +
|
||||||
`date: ${date.toUTCString()}\n` +
|
`date: ${date.toISOString()}\n` +
|
||||||
`digest: SHA-256=${btoa(
|
`digest: SHA-256=${btoa(
|
||||||
String.fromCharCode(...new Uint8Array(digest))
|
String.fromCharCode(...new Uint8Array(digest)),
|
||||||
)}\n`
|
)}\n`,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const signatureBase64 = btoa(
|
const signatureBase64 = btoa(
|
||||||
String.fromCharCode(...new Uint8Array(signature))
|
String.fromCharCode(...new Uint8Array(signature)),
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -108,50 +101,79 @@ const signatureBase64 = btoa(
|
||||||
|
|
||||||
The request can then be sent with the `Signature`, `Origin` and `Date` headers as follows:
|
The request can then be sent with the `Signature`, `Origin` and `Date` headers as follows:
|
||||||
```ts
|
```ts
|
||||||
await fetch("https://example.com/users/uuid/inbox", {
|
await fetch("https://receiver.com/users/22a56612-9909-48ca-84af-548b28db6fd5/inbox", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Date: date.toUTCString(),
|
Date: date.toISOString(),
|
||||||
Origin: "https://example.com",
|
Origin: "example.com",
|
||||||
Signature: `keyId="${...}",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`,
|
Signature: `keyId="https://example.com/users/caf18716-800d-4c88-843d-4947ab39ca0f",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: full_lysand_object_as_string,
|
||||||
// ...
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
Example of validation on the server side:
|
Example of validation on the server side:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// request is a Request object containing the previous request
|
// req is a Request object
|
||||||
// public_key is the user's public key in raw base64 format
|
const signatureHeader = req.headers.get("Signature");
|
||||||
|
const origin = req.headers.get("Origin");
|
||||||
|
const date = req.headers.get("Date");
|
||||||
|
|
||||||
const signatureHeader = request.headers.get("Signature");
|
if (!signatureHeader) {
|
||||||
|
return errorResponse("Missing Signature header", 400);
|
||||||
|
}
|
||||||
|
|
||||||
const signature = signatureHeader.split("signature=")[1].replace(/"/g, "");
|
if (!origin) {
|
||||||
|
return errorResponse("Missing Origin header", 400);
|
||||||
|
}
|
||||||
|
|
||||||
const origin = request.headers.get("Origin");
|
if (!date) {
|
||||||
|
return errorResponse("Missing Date header", 400);
|
||||||
|
}
|
||||||
|
|
||||||
const date = request.headers.get("Date");
|
const signature = signatureHeader
|
||||||
|
.split("signature=")[1]
|
||||||
|
.replace(/"/g, "");
|
||||||
|
|
||||||
const digest = await crypto.subtle.digest(
|
const digest = await crypto.subtle.digest(
|
||||||
"SHA-256",
|
"SHA-256",
|
||||||
new TextEncoder().encode(await request.text())
|
new TextEncoder().encode(JSON.stringify(body)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const expectedSignedString = `(request-target): ${request.method.toLowerCase()} ${request.url}\n` +
|
const keyId = signatureHeader
|
||||||
`host: ${request.url}\n` +
|
.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` +
|
`date: ${date}\n` +
|
||||||
`digest: SHA-256=${btoa(digest)}`;
|
`digest: SHA-256=${btoa(
|
||||||
|
String.fromCharCode(...new Uint8Array(digest)),
|
||||||
|
)}\n`;
|
||||||
|
|
||||||
// Check if signed string is valid
|
// Check if signed string is valid
|
||||||
const isValid = await crypto.subtle.verify(
|
const isValid = await crypto.subtle.verify(
|
||||||
"Ed25519",
|
"Ed25519",
|
||||||
publicKey,
|
public_key,
|
||||||
new TextEncoder().encode(signature),
|
Uint8Array.from(atob(signature), (c) => c.charCodeAt(0)),
|
||||||
new TextEncoder().encode(expectedSignedString)
|
new TextEncoder().encode(expectedSignedString),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ curl -X POST -H "Content-Type: application/json" -H "Accept: application/json" -
|
||||||
}' https://example.com/users/uuid/inbox
|
}' https://example.com/users/uuid/inbox
|
||||||
```
|
```
|
||||||
|
|
||||||
The server **MUST** respond with a `200 OK` response code if the operation was successful.
|
The server **MUST** respond with a `201 CREATED` response code if the operation was successful.
|
||||||
|
|
||||||
## User Outbox
|
## User Outbox
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,15 @@ The document **MUST** contain the following information, as specified by the Web
|
||||||
|
|
||||||
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 `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`)
|
The `resource` field **MUST** be the URI of the user that the server is trying to discover (in the format `acct:identifier@example.com`)
|
||||||
|
|
||||||
Breaking down this URI, we get the following:
|
Breaking down this URI, we get the following:
|
||||||
|
|
||||||
- `acct`: The protocol of the URI. This is always `acct` for Lysand.
|
- `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.
|
- `identifier`: Either the UUID or the username of the user that the server is trying to discover.
|
||||||
- `example.com`: The domain of the server that the user is on. This is usually the domain of the server. This can also be a subdomain of the server, such as `lysand.example.com`.
|
- `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 will typically not use the `id` of an actor to identify it, but instead its `username`: servers **MUST** only use the `id` to identify actors.
|
This format is reminiscent of the `acct` format used by ActivityPub, but with either a UUID or a username instead of just an username. Users will typically not use the `id` of an actor to identify it, but instead its `username`: servers **MUST** only use the `id` to identify actors.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ The requesting server **MUST** send the following headers with the request:
|
||||||
|
|
||||||
The requestinng server **MUST** send the following query parameters with the request:
|
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)
|
- `resource`: The URI of the user that the server is trying to discover (in the format `acct:identifier@example.com` (replace `identifier` with the user's ID or username)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ The server **MUST** respond with a `200 OK` response code, and a JSON object in
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
"subject": "acct:uuid@example.com",
|
"subject": "acct:identifier@example.com",
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
"rel": "self",
|
"rel": "self",
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,13 @@ interface Entity {
|
||||||
created_at: string;
|
created_at: string;
|
||||||
uri: string;
|
uri: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
extensions?: {
|
||||||
|
"org.lysand:custom_emojis"?: {
|
||||||
|
emojis: Emoji[];
|
||||||
};
|
};
|
||||||
|
[key: string]: object | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The `Entity` type is the base type for all entities in the Lysand protocol. It includes the `id`, `created_at`, `uri`, and `type` attributes.
|
The `Entity` type is the base type for all entities in the Lysand protocol. It includes the `id`, `created_at`, `uri`, and `type` attributes.
|
||||||
|
|
|
||||||
|
|
@ -41,3 +41,12 @@ This approach helps prevent potential misuse of the protocol to determine if a u
|
||||||
| author | String | Yes |
|
| author | String | Yes |
|
||||||
|
|
||||||
URI of the [Actor](./actors) who initiated the action.
|
URI of the [Actor](./actors) who initiated the action.
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Action extends Entity {
|
||||||
|
type: "Like" | "Dislike" | "Follow" | "FollowAccept" | "FollowReject" | "Announce" | "Undo";
|
||||||
|
author: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
@ -35,9 +35,8 @@ URI of the object being disliked. Must be of type [Note](./note)
|
||||||
## Types
|
## Types
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface Dislike extends Entity {
|
interface Dislike extends Action {
|
||||||
type: "Dislike";
|
type: "Dislike";
|
||||||
author: string;
|
|
||||||
object: string;
|
object: string;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -35,9 +35,8 @@ URI of the [User](./user) who tried to follow the author
|
||||||
## Types
|
## Types
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface FollowAccept extends Entity {
|
interface FollowAccept extends Action {
|
||||||
type: "FollowAccept";
|
type: "FollowAccept";
|
||||||
author: string;
|
|
||||||
follower: string;
|
follower: string;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -35,9 +35,8 @@ URI of the [User](./user) who tried to follow the author.
|
||||||
## Types
|
## Types
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface FollowReject extends Entity {
|
interface FollowReject extends Action {
|
||||||
type: "FollowReject";
|
type: "FollowReject";
|
||||||
author: string;
|
|
||||||
follower: string;
|
follower: string;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,8 @@ URI of the [User](./user) who is being follow requested.
|
||||||
## Types
|
## Types
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface Follow extends Entity {
|
interface Follow extends Action {
|
||||||
type: "Follow";
|
type: "Follow";
|
||||||
author: string;
|
|
||||||
followee: string;
|
followee: string;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,8 @@ URI of the object being liked. Must be of type [Note](./note)
|
||||||
## Types
|
## Types
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface Like extends Entity {
|
interface Like extends Action {
|
||||||
type: "Like";
|
type: "Like";
|
||||||
author: string;
|
|
||||||
object: string;
|
object: string;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ Servers **MUST** respect the visibility of the publication and **MUST NOT** show
|
||||||
## Types
|
## Types
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface Publication {
|
interface Publication extends Entity {
|
||||||
type: "Note" | "Patch";
|
type: "Note" | "Patch";
|
||||||
author: string;
|
author: string;
|
||||||
content?: ContentFormat;
|
content?: ContentFormat;
|
||||||
|
|
@ -223,6 +223,19 @@ interface Publication {
|
||||||
subject?: string;
|
subject?: string;
|
||||||
is_sensitive?: boolean;
|
is_sensitive?: boolean;
|
||||||
visibility: Visibility;
|
visibility: Visibility;
|
||||||
|
extensions?: Entity["extensions"] & {
|
||||||
|
"org.lysand:reactions"?: {
|
||||||
|
reactions: string;
|
||||||
|
};
|
||||||
|
"org.lysand:polls"?: {
|
||||||
|
poll: {
|
||||||
|
options: ContentFormat[];
|
||||||
|
votes: number[];
|
||||||
|
multiple_choice?: boolean;
|
||||||
|
expires_at: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -152,3 +152,23 @@ Clients should display the most modern format that they support, such as WebP, A
|
||||||
| supported_extensions | Array of String | Yes, can be empty array (`[]`) |
|
| supported_extensions | Array of String | Yes, can be empty array (`[]`) |
|
||||||
|
|
||||||
List of extension names that the server supports, in namespaced format (`"org.lysand:reactions"`).
|
List of extension names that the server supports, in namespaced format (`"org.lysand:reactions"`).
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ServerMetadata {
|
||||||
|
type: "ServerMetadata";
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
description?: string;
|
||||||
|
website?: string;
|
||||||
|
moderators?: string[];
|
||||||
|
admins?: string[];
|
||||||
|
logo?: ContentFormat;
|
||||||
|
banner?: ContentFormat;
|
||||||
|
supported_extensions: string[];
|
||||||
|
extensions?: {
|
||||||
|
[key: string]: object | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
@ -41,3 +41,13 @@ URI of the [Actor](./actors) who initiated the action.
|
||||||
| object | String | Yes |
|
| object | String | Yes |
|
||||||
|
|
||||||
URI of the object being undone. The object **MUST** be an [Action](./actions) or a [Note](./note). To undo [Patch](./patch) objects, use a subsequent [Patch](./patch) or delete the original [Note](./note).
|
URI of the object being undone. The object **MUST** be an [Action](./actions) or a [Note](./note). To undo [Patch](./patch) objects, use a subsequent [Patch](./patch) or delete the original [Note](./note).
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Undo extends Entity {
|
||||||
|
type: "Undo";
|
||||||
|
author: string;
|
||||||
|
object: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
@ -332,6 +332,9 @@ interface User extends Entity {
|
||||||
dislikes: string;
|
dislikes: string;
|
||||||
inbox: string;
|
inbox: string;
|
||||||
outbox: string;
|
outbox: string;
|
||||||
|
extensions?: Entity["extensions"] & {
|
||||||
|
"org.lysand:vanity"?: VanityExtension;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ All JSON objects disseminated during federation **MUST** be handled as follows:
|
||||||
|
|
||||||
## Requests and Responses
|
## Requests and Responses
|
||||||
|
|
||||||
All HTTP requests **MUST** be transmitted over HTTPS. Servers **MUST NOT** accept HTTP requests, unless for development purposes (e.g., if a server is operating on localhost or another local network).
|
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.
|
Servers should support HTTP/2 and HTTP/3 for enhanced performance and security. Servers **MUST** support HTTP/1.1 at a minimum.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,6 @@
|
||||||
"docs:preview": "vitepress preview"
|
"docs:preview": "vitepress preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vitepress": "1.0.0-rc.45"
|
"vitepress": "^1.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue