mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 05:49:16 +01:00
docs(federation): 📝 Update SDK documentation
This commit is contained in:
parent
f79b0bc999
commit
45e5460975
67 changed files with 332 additions and 65 deletions
220
packages/sdk/README.md
Normal file
220
packages/sdk/README.md
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
<p align="center">
|
||||
<a href="https://versia.pub"><img src="https://cdn.versia.pub/branding/logo-dark.svg" alt="Versia Logo" height="110"></a>
|
||||
</p>
|
||||
|
||||
<center><h1><code>@versia/sdk</code></h1></center>
|
||||
|
||||
Federation types, validators and cryptography for Versia server implementations.
|
||||
|
||||
## Usage
|
||||
|
||||
## Entities
|
||||
|
||||
The `@versia/sdk/entities` module provides TypeScript classes for working with Versia entities. These classes provide type-safe access to entity properties and methods for serialization/deserialization.
|
||||
|
||||
```ts
|
||||
import { Note, User } from "@versia/sdk/entities";
|
||||
|
||||
const note = new Note({
|
||||
id: "00000000-0000-0000-0000-000000000000",
|
||||
type: "Note",
|
||||
});
|
||||
|
||||
// You can also parse from JSON, which will apply the schema validation
|
||||
const invalidJson = {
|
||||
id: "00000000-0000-0000-0000-000000000000",
|
||||
invalid: "property",
|
||||
};
|
||||
|
||||
// Will throw an error
|
||||
const invalidNote = await Note.fromJSON(invalidJson);
|
||||
|
||||
const validJson = {
|
||||
id: "00000000-0000-0000-0000-000000000000",
|
||||
type: "Note",
|
||||
};
|
||||
|
||||
const validNote = await Note.fromJSON(validJson);
|
||||
```
|
||||
|
||||
Some entities like `Note` have additional properties, like `content` or `attachments`, which are automatically calculated from the relevant properties.
|
||||
|
||||
```ts
|
||||
import { TextContentFormat, Note } from "@versia/sdk/entities";
|
||||
|
||||
const note = new Note({
|
||||
id: "00000000-0000-0000-0000-000000000000",
|
||||
type: "Note",
|
||||
content: {
|
||||
"text/plain": {
|
||||
content: "Hello, world!",
|
||||
remote: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const content = note.content;
|
||||
// Is equivalent to
|
||||
const content = new TextContentFormat(note.data.content);
|
||||
```
|
||||
|
||||
## Schemas
|
||||
|
||||
Additionally, the [**Zod**](https://zod.dev) schemas used for validation are available in the `@versia/sdk/schemas` module. You can use these to directly validate incoming data, without using the entity classes.
|
||||
|
||||
```ts
|
||||
import { NoteSchema, UserSchema } from "@versia/sdk/schemas";
|
||||
|
||||
const response = await fetch("https://example.com/notes/123");
|
||||
const json = await response.json();
|
||||
|
||||
const noteSchema = NoteSchema.parse(json);
|
||||
```
|
||||
|
||||
## Sorter
|
||||
|
||||
The `@versia/sdk/sorter` module provides a class for inbox request handling. It allows you to automatically sort and process incoming entities based on their type.
|
||||
|
||||
```ts
|
||||
import { EntitySorter } from "@versia/sdk";
|
||||
import { Note, User } from "@versia/sdk/entities";
|
||||
|
||||
app.post("/inbox", async (req, res) => {
|
||||
const json = await req.json();
|
||||
|
||||
const sorter = new EntitySorter(json);
|
||||
|
||||
await sorter
|
||||
.on(Note, (note) => {
|
||||
console.log(note);
|
||||
})
|
||||
.on(User, (user) => {
|
||||
console.log(user);
|
||||
})
|
||||
.sort();
|
||||
});
|
||||
```
|
||||
|
||||
## Cryptography
|
||||
|
||||
The `@versia/sdk/crypto` module provides functions for signing and verifying requests using the [**Ed25519**](https://en.wikipedia.org/wiki/EdDSA) algorithm.
|
||||
|
||||
```ts
|
||||
import { sign, verify } from "@versia/sdk/crypto";
|
||||
|
||||
const keys = await crypto.subtle.generateKey("Ed25519", true, [
|
||||
"sign",
|
||||
"verify",
|
||||
]);
|
||||
|
||||
// URI of the User that is signing the request
|
||||
const authorUrl = new URL("https://example.com");
|
||||
|
||||
const req = new Request("https://example.com/notes/123", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
id: "00000000-0000-0000-0000-000000000000",
|
||||
type: "Note",
|
||||
}),
|
||||
});
|
||||
|
||||
const signedReq = await sign(keys.privateKey, authorUrl, req);
|
||||
|
||||
const verified = await verify(keys.publicKey, signedReq);
|
||||
```
|
||||
|
||||
### Prerequisites
|
||||
|
||||
#### For Usage
|
||||
|
||||
See the [**Compatibility**](#compatibility) section for the supported environments. Any package manager can be used to install the packages.
|
||||
|
||||
#### For Development
|
||||
|
||||
- [**Bun**](https://bun.sh) version `1.1.8` or higher.
|
||||
- Either the [**Linux**](https://www.linux.org) or [**macOS**](https://www.apple.com/macos) operating systems. ([**Windows**](https://www.microsoft.com/windows) will work, but is not officially supported.)
|
||||
|
||||
### Compatibility
|
||||
|
||||
This library is built for JavaScript runtimes with the support for:
|
||||
|
||||
- [**ES Modules**](https://nodejs.org/api/esm.html)
|
||||
- [**ECMAScript 2020**](https://www.ecma-international.org/ecma-262/11.0/index.html)
|
||||
- (only required for cryptography) [**Ed25519**](https://en.wikipedia.org/wiki/EdDSA) cryptography in the [**WebCrypto API**](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API)
|
||||
|
||||
#### Runtimes
|
||||
|
||||
- **Node.js**: 14.0+ is the minimum (18.0+ for cryptography), but only Node.js 20.0+ (LTS) is officially supported.
|
||||
- **Deno**: Support is unknown. 1.0+ is expected to work.
|
||||
- **Bun**: Bun 1.1.8 is the minimum-supported version. As Bun is rapidly evolving, this may change. Previous versions may also work.
|
||||
|
||||
#### Browsers
|
||||
|
||||
Consequently, this library is compatible without any bundling in the following browser versions:
|
||||
|
||||
- **Chrome**: 80+
|
||||
- **Edge**: 80+
|
||||
- **Firefox**: 74+
|
||||
- **Safari**: 13.1+
|
||||
- **Opera**: 67+
|
||||
- **Internet Explorer**: None
|
||||
|
||||
Cryptography functions are supported in the following browsers:
|
||||
|
||||
- **Safari**: 17.0+
|
||||
- **Firefox**: 129.0+
|
||||
- **Chrome**: 113.0+ with `#enable-experimental-web-platform-features` enabled
|
||||
|
||||
If you are targeting older browsers, please don't, you are doing yourself a disservice.
|
||||
|
||||
Transpilation to non-ES Module environments is not officially supported, but should be simple with the use of a bundler like [**Parcel**](https://parceljs.org) or [**Rollup**](https://rollupjs.org).
|
||||
|
||||
### Installation
|
||||
|
||||
Package is distributed as a scoped package on the NPM registry and [JSR](https://jsr.io).
|
||||
|
||||
We strongly recommend using JSR over NPM for all your packages that are available on it.
|
||||
|
||||
```bash
|
||||
# NPM version
|
||||
deno add npm:@versia/sdk # For Deno
|
||||
npm install @versia/sdk # For NPM
|
||||
yarn add @versia/sdk # For Yarn
|
||||
pnpm add @versia/sdk # For PNPM
|
||||
bun add @versia/sdk # For Bun
|
||||
|
||||
# JSR version
|
||||
deno add @versia/sdk # For Deno
|
||||
npx jsr add @versia/sdk # For JSR
|
||||
yarn dlx jsr add @versia/sdk # For Yarn
|
||||
pnpm dlx jsr add @versia/sdk # For PNPM
|
||||
bunx jsr add @versia/sdk # For Bun
|
||||
```
|
||||
|
||||
#### From Source
|
||||
|
||||
If you want to install from source, you can clone [this repository](https://github.com/versia-pub/api) and run the following commands:
|
||||
|
||||
```bash
|
||||
bun install # Install dependencies
|
||||
|
||||
bun run build # Build the packages
|
||||
```
|
||||
|
||||
The built package will be in the `sdk/dist` folder.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
### Projects
|
||||
|
||||
- [**Bun**](https://bun.sh): Thanks to the Bun team for creating an amazing JavaScript runtime.
|
||||
- [**TypeScript**](https://www.typescriptlang.org): TypeScript is the backbone of this project.
|
||||
- [**Node.js**](https://nodejs.org): Node.js created the idea of JavaScript on the server.
|
||||
|
||||
### People
|
||||
|
||||
- [**April John**](https://github.com/cutestnekoaqua): Creator and maintainer of the Versia Server ActivityPub bridge.
|
||||
94
packages/sdk/crypto.ts
Normal file
94
packages/sdk/crypto.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
const stringToBase64Hash = async (str: string): Promise<string> => {
|
||||
const buffer = new TextEncoder().encode(str);
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
|
||||
const hashArray = new Uint8Array(hashBuffer);
|
||||
|
||||
return hashArray.toBase64();
|
||||
};
|
||||
|
||||
const base64ToArrayBuffer = (base64: string): ArrayBuffer =>
|
||||
Uint8Array.fromBase64(base64).buffer as ArrayBuffer;
|
||||
|
||||
/**
|
||||
* Signs a request using the Ed25519 algorithm, according to the [**Versia**](https://versia.pub/signatures) specification.
|
||||
*
|
||||
* @see https://versia.pub/signatures
|
||||
* @param privateKey - Private key of the User that is signing the request.
|
||||
* @param authorUrl - URL of the User that is signing the request.
|
||||
* @param req - Request to sign.
|
||||
* @param timestamp - (optional) Timestamp of the request.
|
||||
* @returns The signed request.
|
||||
*/
|
||||
export const sign = async (
|
||||
privateKey: CryptoKey,
|
||||
authorUrl: URL,
|
||||
req: Request,
|
||||
timestamp = new Date(),
|
||||
): Promise<Request> => {
|
||||
const body = await req.clone().text();
|
||||
const url = new URL(req.url);
|
||||
|
||||
const digest = stringToBase64Hash(body);
|
||||
const timestampSecs = Math.floor(timestamp.getTime() / 1000);
|
||||
|
||||
const signedString = `${req.method.toLowerCase()} ${encodeURI(
|
||||
url.pathname,
|
||||
)} ${timestampSecs} ${digest}`;
|
||||
|
||||
const signature = await crypto.subtle.sign(
|
||||
"Ed25519",
|
||||
privateKey,
|
||||
new TextEncoder().encode(signedString),
|
||||
);
|
||||
|
||||
const signatureBase64 = new Uint8Array(signature).toBase64();
|
||||
|
||||
const newReq = new Request(req, {
|
||||
headers: {
|
||||
...req.headers,
|
||||
"Versia-Signature": signatureBase64,
|
||||
"Versia-Signed-At": String(timestampSecs),
|
||||
"Versia-Signed-By": authorUrl.href,
|
||||
},
|
||||
});
|
||||
|
||||
return newReq;
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies a signed request using the Ed25519 algorithm, according to the [**Versia**](https://versia.pub/signatures) specification.
|
||||
*
|
||||
* @see https://versia.pub/signatures
|
||||
* @param publicKey - Public key of the User that is verifying the request.
|
||||
* @param req - Request to verify.
|
||||
* @returns Whether the request signature is valid or not.
|
||||
*/
|
||||
export const verify = async (
|
||||
publicKey: CryptoKey,
|
||||
req: Request,
|
||||
): Promise<boolean> => {
|
||||
const signature = req.headers.get("Versia-Signature");
|
||||
const signedAt = req.headers.get("Versia-Signed-At");
|
||||
const signedBy = req.headers.get("Versia-Signed-By");
|
||||
|
||||
if (!(signature && signedAt && signedBy)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const body = await req.clone().text();
|
||||
const url = new URL(req.url);
|
||||
|
||||
const digest = await stringToBase64Hash(body);
|
||||
|
||||
const expectedSignedString = `${req.method.toLowerCase()} ${encodeURI(
|
||||
url.pathname,
|
||||
)} ${signedAt} ${digest}`;
|
||||
|
||||
// Check if this matches the signature
|
||||
return crypto.subtle.verify(
|
||||
"Ed25519",
|
||||
publicKey,
|
||||
base64ToArrayBuffer(signature),
|
||||
new TextEncoder().encode(expectedSignedString),
|
||||
);
|
||||
};
|
||||
29
packages/sdk/entities/collection.ts
Normal file
29
packages/sdk/entities/collection.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import type { z } from "zod";
|
||||
import {
|
||||
CollectionSchema,
|
||||
URICollectionSchema,
|
||||
} from "../schemas/collection.ts";
|
||||
import type { JSONObject } from "../types.ts";
|
||||
import { Entity } from "./entity.ts";
|
||||
|
||||
export class Collection extends Entity {
|
||||
public constructor(public data: z.infer<typeof CollectionSchema>) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
public static fromJSON(json: JSONObject): Promise<Collection> {
|
||||
return CollectionSchema.parseAsync(json).then((u) => new Collection(u));
|
||||
}
|
||||
}
|
||||
|
||||
export class URICollection extends Entity {
|
||||
public constructor(public data: z.infer<typeof URICollectionSchema>) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
public static fromJSON(json: JSONObject): Promise<URICollection> {
|
||||
return URICollectionSchema.parseAsync(json).then(
|
||||
(u) => new URICollection(u),
|
||||
);
|
||||
}
|
||||
}
|
||||
82
packages/sdk/entities/contentformat.ts
Normal file
82
packages/sdk/entities/contentformat.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import type { z } from "zod";
|
||||
import {
|
||||
AudioContentFormatSchema,
|
||||
ContentFormatSchema,
|
||||
ImageContentFormatSchema,
|
||||
NonTextContentFormatSchema,
|
||||
TextContentFormatSchema,
|
||||
VideoContentFormatSchema,
|
||||
} from "../schemas/contentformat.ts";
|
||||
import type { JSONObject } from "../types.ts";
|
||||
|
||||
export class ContentFormat {
|
||||
public static fromJSON(data: JSONObject): Promise<ContentFormat> {
|
||||
return ContentFormatSchema.parseAsync(data).then(
|
||||
(d) => new ContentFormat(d),
|
||||
);
|
||||
}
|
||||
|
||||
public constructor(public data: z.infer<typeof ContentFormatSchema>) {}
|
||||
}
|
||||
|
||||
export class TextContentFormat extends ContentFormat {
|
||||
public static fromJSON(data: JSONObject): Promise<TextContentFormat> {
|
||||
return TextContentFormatSchema.parseAsync(data).then(
|
||||
(d) => new TextContentFormat(d),
|
||||
);
|
||||
}
|
||||
|
||||
public constructor(public data: z.infer<typeof TextContentFormatSchema>) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
export class NonTextContentFormat extends ContentFormat {
|
||||
public static fromJSON(data: JSONObject): Promise<NonTextContentFormat> {
|
||||
return NonTextContentFormatSchema.parseAsync(data).then(
|
||||
(d) => new NonTextContentFormat(d),
|
||||
);
|
||||
}
|
||||
|
||||
public constructor(
|
||||
public data: z.infer<typeof NonTextContentFormatSchema>,
|
||||
) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
export class ImageContentFormat extends ContentFormat {
|
||||
public static fromJSON(data: JSONObject): Promise<ImageContentFormat> {
|
||||
return ImageContentFormatSchema.parseAsync(data).then(
|
||||
(d) => new ImageContentFormat(d),
|
||||
);
|
||||
}
|
||||
|
||||
public constructor(public data: z.infer<typeof ImageContentFormatSchema>) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
export class VideoContentFormat extends ContentFormat {
|
||||
public static fromJSON(data: JSONObject): Promise<VideoContentFormat> {
|
||||
return VideoContentFormatSchema.parseAsync(data).then(
|
||||
(d) => new VideoContentFormat(d),
|
||||
);
|
||||
}
|
||||
|
||||
public constructor(public data: z.infer<typeof VideoContentFormatSchema>) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
|
||||
export class AudioContentFormat extends ContentFormat {
|
||||
public static fromJSON(data: JSONObject): Promise<AudioContentFormat> {
|
||||
return AudioContentFormatSchema.parseAsync(data).then(
|
||||
(d) => new AudioContentFormat(d),
|
||||
);
|
||||
}
|
||||
|
||||
public constructor(public data: z.infer<typeof AudioContentFormatSchema>) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
16
packages/sdk/entities/delete.ts
Normal file
16
packages/sdk/entities/delete.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { z } from "zod";
|
||||
import { DeleteSchema } from "../schemas/delete.ts";
|
||||
import type { JSONObject } from "../types.ts";
|
||||
import { Entity } from "./entity.ts";
|
||||
|
||||
export class Delete extends Entity {
|
||||
public static name = "Delete";
|
||||
|
||||
public constructor(public data: z.infer<typeof DeleteSchema>) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
public static fromJSON(json: JSONObject): Promise<Delete> {
|
||||
return DeleteSchema.parseAsync(json).then((u) => new Delete(u));
|
||||
}
|
||||
}
|
||||
17
packages/sdk/entities/entity.ts
Normal file
17
packages/sdk/entities/entity.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { EntitySchema } from "../schemas/entity.ts";
|
||||
import type { JSONObject } from "../types.ts";
|
||||
|
||||
export class Entity {
|
||||
public static name = "Entity";
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: This is a base class that is never instanciated directly
|
||||
public constructor(public data: any) {}
|
||||
|
||||
public static fromJSON(json: JSONObject): Promise<Entity> {
|
||||
return EntitySchema.parseAsync(json).then((u) => new Entity(u));
|
||||
}
|
||||
|
||||
public toJSON(): JSONObject {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
28
packages/sdk/entities/extensions/likes.ts
Normal file
28
packages/sdk/entities/extensions/likes.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import type { z } from "zod";
|
||||
import { DislikeSchema, LikeSchema } from "../../schemas/extensions/likes.ts";
|
||||
import type { JSONObject } from "../../types.ts";
|
||||
import { Entity } from "../entity.ts";
|
||||
|
||||
export class Like extends Entity {
|
||||
public static name = "pub.versia:likes/Like";
|
||||
|
||||
public constructor(public data: z.infer<typeof LikeSchema>) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
public static fromJSON(json: JSONObject): Promise<Like> {
|
||||
return LikeSchema.parseAsync(json).then((u) => new Like(u));
|
||||
}
|
||||
}
|
||||
|
||||
export class Dislike extends Entity {
|
||||
public static name = "pub.versia:likes/Dislike";
|
||||
|
||||
public constructor(public data: z.infer<typeof DislikeSchema>) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
public static fromJSON(json: JSONObject): Promise<Dislike> {
|
||||
return DislikeSchema.parseAsync(json).then((u) => new Dislike(u));
|
||||
}
|
||||
}
|
||||
16
packages/sdk/entities/extensions/polls.ts
Normal file
16
packages/sdk/entities/extensions/polls.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { z } from "zod";
|
||||
import { VoteSchema } from "../../schemas/extensions/polls.ts";
|
||||
import type { JSONObject } from "../../types.ts";
|
||||
import { Entity } from "../entity.ts";
|
||||
|
||||
export class Vote extends Entity {
|
||||
public static name = "pub.versia:polls/Vote";
|
||||
|
||||
public constructor(public data: z.infer<typeof VoteSchema>) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
public static fromJSON(json: JSONObject): Promise<Vote> {
|
||||
return VoteSchema.parseAsync(json).then((u) => new Vote(u));
|
||||
}
|
||||
}
|
||||
16
packages/sdk/entities/extensions/reactions.ts
Normal file
16
packages/sdk/entities/extensions/reactions.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { z } from "zod";
|
||||
import { ReactionSchema } from "../../schemas/extensions/reactions.ts";
|
||||
import type { JSONObject } from "../../types.ts";
|
||||
import { Entity } from "../entity.ts";
|
||||
|
||||
export class Reaction extends Entity {
|
||||
public static name = "pub.versia:reactions/Reaction";
|
||||
|
||||
public constructor(public data: z.infer<typeof ReactionSchema>) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
public static fromJSON(json: JSONObject): Promise<Reaction> {
|
||||
return ReactionSchema.parseAsync(json).then((u) => new Reaction(u));
|
||||
}
|
||||
}
|
||||
16
packages/sdk/entities/extensions/reports.ts
Normal file
16
packages/sdk/entities/extensions/reports.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { z } from "zod";
|
||||
import { ReportSchema } from "../../schemas/extensions/reports.ts";
|
||||
import type { JSONObject } from "../../types.ts";
|
||||
import { Entity } from "../entity.ts";
|
||||
|
||||
export class Report extends Entity {
|
||||
public static name = "pub.versia:reports/Report";
|
||||
|
||||
public constructor(public data: z.infer<typeof ReportSchema>) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
public static fromJSON(json: JSONObject): Promise<Report> {
|
||||
return ReportSchema.parseAsync(json).then((u) => new Report(u));
|
||||
}
|
||||
}
|
||||
16
packages/sdk/entities/extensions/share.ts
Normal file
16
packages/sdk/entities/extensions/share.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { z } from "zod";
|
||||
import { ShareSchema } from "../../schemas/extensions/share.ts";
|
||||
import type { JSONObject } from "../../types.ts";
|
||||
import { Entity } from "../entity.ts";
|
||||
|
||||
export class Share extends Entity {
|
||||
public static name = "pub.versia:share/Share";
|
||||
|
||||
public constructor(public data: z.infer<typeof ShareSchema>) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
public static fromJSON(json: JSONObject): Promise<Share> {
|
||||
return ShareSchema.parseAsync(json).then((u) => new Share(u));
|
||||
}
|
||||
}
|
||||
61
packages/sdk/entities/follow.ts
Normal file
61
packages/sdk/entities/follow.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import type { z } from "zod";
|
||||
import {
|
||||
FollowAcceptSchema,
|
||||
FollowRejectSchema,
|
||||
FollowSchema,
|
||||
UnfollowSchema,
|
||||
} from "../schemas/follow.ts";
|
||||
import type { JSONObject } from "../types.ts";
|
||||
import { Entity } from "./entity.ts";
|
||||
|
||||
export class Follow extends Entity {
|
||||
public static name = "Follow";
|
||||
|
||||
public constructor(public data: z.infer<typeof FollowSchema>) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
public static fromJSON(json: JSONObject): Promise<Follow> {
|
||||
return FollowSchema.parseAsync(json).then((u) => new Follow(u));
|
||||
}
|
||||
}
|
||||
|
||||
export class FollowAccept extends Entity {
|
||||
public static name = "FollowAccept";
|
||||
|
||||
public constructor(public data: z.infer<typeof FollowAcceptSchema>) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
public static fromJSON(json: JSONObject): Promise<FollowAccept> {
|
||||
return FollowAcceptSchema.parseAsync(json).then(
|
||||
(u) => new FollowAccept(u),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class FollowReject extends Entity {
|
||||
public static name = "FollowReject";
|
||||
|
||||
public constructor(public data: z.infer<typeof FollowRejectSchema>) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
public static fromJSON(json: JSONObject): Promise<FollowReject> {
|
||||
return FollowRejectSchema.parseAsync(json).then(
|
||||
(u) => new FollowReject(u),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class Unfollow extends Entity {
|
||||
public static name = "Unfollow";
|
||||
|
||||
public constructor(public data: z.infer<typeof UnfollowSchema>) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
public static fromJSON(json: JSONObject): Promise<Unfollow> {
|
||||
return UnfollowSchema.parseAsync(json).then((u) => new Unfollow(u));
|
||||
}
|
||||
}
|
||||
21
packages/sdk/entities/index.ts
Normal file
21
packages/sdk/entities/index.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// biome-ignore lint/performance/noBarrelFile: <explanation>
|
||||
export { User } from "./user.ts";
|
||||
export { Note } from "./note.ts";
|
||||
export { Entity } from "./entity.ts";
|
||||
export { Delete } from "./delete.ts";
|
||||
export { InstanceMetadata } from "./instancemetadata.ts";
|
||||
export {
|
||||
ImageContentFormat,
|
||||
AudioContentFormat,
|
||||
NonTextContentFormat,
|
||||
TextContentFormat,
|
||||
ContentFormat,
|
||||
VideoContentFormat,
|
||||
} from "./contentformat.ts";
|
||||
export { Follow, FollowAccept, FollowReject, Unfollow } from "./follow.ts";
|
||||
export { Collection, URICollection } from "./collection.ts";
|
||||
export { Like, Dislike } from "./extensions/likes.ts";
|
||||
export { Vote } from "./extensions/polls.ts";
|
||||
export { Reaction } from "./extensions/reactions.ts";
|
||||
export { Report } from "./extensions/reports.ts";
|
||||
export { Share } from "./extensions/share.ts";
|
||||
31
packages/sdk/entities/instancemetadata.ts
Normal file
31
packages/sdk/entities/instancemetadata.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { z } from "zod";
|
||||
import { InstanceMetadataSchema } from "../schemas/instance.ts";
|
||||
import type { JSONObject } from "../types.ts";
|
||||
import { ImageContentFormat } from "./contentformat.ts";
|
||||
import { Entity } from "./entity.ts";
|
||||
|
||||
export class InstanceMetadata extends Entity {
|
||||
public static name = "InstanceMetadata";
|
||||
|
||||
public constructor(public data: z.infer<typeof InstanceMetadataSchema>) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
public get logo(): ImageContentFormat | undefined {
|
||||
return this.data.logo
|
||||
? new ImageContentFormat(this.data.logo)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
public get banner(): ImageContentFormat | undefined {
|
||||
return this.data.banner
|
||||
? new ImageContentFormat(this.data.banner)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
public static fromJSON(json: JSONObject): Promise<InstanceMetadata> {
|
||||
return InstanceMetadataSchema.parseAsync(json).then(
|
||||
(u) => new InstanceMetadata(u),
|
||||
);
|
||||
}
|
||||
}
|
||||
29
packages/sdk/entities/note.ts
Normal file
29
packages/sdk/entities/note.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import type { z } from "zod";
|
||||
import { NoteSchema } from "../schemas/note.ts";
|
||||
import type { JSONObject } from "../types.ts";
|
||||
import { NonTextContentFormat, TextContentFormat } from "./contentformat.ts";
|
||||
import { Entity } from "./entity.ts";
|
||||
|
||||
export class Note extends Entity {
|
||||
public static name = "Note";
|
||||
|
||||
public constructor(public data: z.infer<typeof NoteSchema>) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
public static fromJSON(json: JSONObject): Promise<Note> {
|
||||
return NoteSchema.parseAsync(json).then((n) => new Note(n));
|
||||
}
|
||||
|
||||
public get attachments(): NonTextContentFormat[] {
|
||||
return (
|
||||
this.data.attachments?.map((a) => new NonTextContentFormat(a)) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
public get content(): TextContentFormat | undefined {
|
||||
return this.data.content
|
||||
? new TextContentFormat(this.data.content)
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
33
packages/sdk/entities/user.ts
Normal file
33
packages/sdk/entities/user.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import type { z } from "zod";
|
||||
import { UserSchema } from "../schemas/user.ts";
|
||||
import type { JSONObject } from "../types.ts";
|
||||
import { ImageContentFormat, TextContentFormat } from "./contentformat.ts";
|
||||
import { Entity } from "./entity.ts";
|
||||
|
||||
export class User extends Entity {
|
||||
public static name = "User";
|
||||
|
||||
public constructor(public data: z.infer<typeof UserSchema>) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
public static fromJSON(json: JSONObject): Promise<User> {
|
||||
return UserSchema.parseAsync(json).then((u) => new User(u));
|
||||
}
|
||||
|
||||
public get avatar(): ImageContentFormat | undefined {
|
||||
return this.data.avatar
|
||||
? new ImageContentFormat(this.data.avatar)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
public get header(): ImageContentFormat | undefined {
|
||||
return this.data.header
|
||||
? new ImageContentFormat(this.data.header)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
public get bio(): TextContentFormat | undefined {
|
||||
return this.data.bio ? new TextContentFormat(this.data.bio) : undefined;
|
||||
}
|
||||
}
|
||||
203
packages/sdk/http.ts
Normal file
203
packages/sdk/http.ts
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
import { sign } from "./crypto.ts";
|
||||
import { Collection, URICollection } from "./entities/collection.ts";
|
||||
import type { Entity } from "./entities/entity.ts";
|
||||
import { homepage, version } from "./package.json";
|
||||
import { WebFingerSchema } from "./schemas/webfinger.ts";
|
||||
|
||||
const DEFAULT_UA = `VersiaFederationClient/${version} (+${homepage})`;
|
||||
|
||||
/**
|
||||
* A class that handles fetching Versia entities
|
||||
*
|
||||
* @example
|
||||
* const requester = new FederationRequester(privateKey, authorUrl);
|
||||
*
|
||||
* const user = await requester.fetchEntity(
|
||||
* new URL("https://example.com/users/1"),
|
||||
* User,
|
||||
* );
|
||||
*
|
||||
* console.log(user); // => User { ... }
|
||||
*/
|
||||
export class FederationRequester {
|
||||
public constructor(
|
||||
private readonly privateKey: CryptoKey,
|
||||
private readonly authorUrl: URL,
|
||||
) {}
|
||||
|
||||
public async fetchEntity<T extends typeof Entity>(
|
||||
url: URL,
|
||||
expectedType: T,
|
||||
): Promise<InstanceType<T>> {
|
||||
const req = new Request(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"User-Agent": DEFAULT_UA,
|
||||
},
|
||||
});
|
||||
|
||||
const finalReq = await sign(this.privateKey, this.authorUrl, req);
|
||||
|
||||
const { ok, json, text, headers, status } = await fetch(finalReq);
|
||||
|
||||
if (!ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch entity from ${url.toString()}: got HTTP code ${status} with body "${await text()}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const contentType = headers.get("Content-Type");
|
||||
|
||||
if (!contentType?.includes("application/json")) {
|
||||
throw new Error(
|
||||
`Expected JSON response from ${url.toString()}, got "${contentType}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const jsonData = await json();
|
||||
const type = jsonData.type;
|
||||
|
||||
if (type && type !== expectedType.name) {
|
||||
throw new Error(
|
||||
`Expected entity type "${expectedType.name}", got "${type}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const entity = await expectedType.fromJSON(jsonData);
|
||||
|
||||
return entity as InstanceType<T>;
|
||||
}
|
||||
|
||||
public async postEntity(url: URL, entity: Entity): Promise<Response> {
|
||||
const req = new Request(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"User-Agent": DEFAULT_UA,
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
body: JSON.stringify(entity.toJSON()),
|
||||
});
|
||||
|
||||
const finalReq = await sign(this.privateKey, this.authorUrl, req);
|
||||
|
||||
return fetch(finalReq);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively go through a Collection of entities until reaching the end
|
||||
* @param url URL to reach the Collection
|
||||
* @param expectedType
|
||||
* @param options.limit Limit the number of entities to fetch
|
||||
*/
|
||||
public async resolveCollection<T extends typeof Entity>(
|
||||
url: URL,
|
||||
expectedType: T,
|
||||
options?: {
|
||||
limit?: number;
|
||||
},
|
||||
): Promise<InstanceType<T>[]> {
|
||||
const entities: InstanceType<T>[] = [];
|
||||
let nextUrl: URL | null = url;
|
||||
let limit = options?.limit ?? Number.POSITIVE_INFINITY;
|
||||
|
||||
while (nextUrl && limit > 0) {
|
||||
const collection: Collection = await this.fetchEntity(
|
||||
nextUrl,
|
||||
Collection,
|
||||
);
|
||||
|
||||
for (const entity of collection.data.items) {
|
||||
if (entity.type === expectedType.name) {
|
||||
entities.push(
|
||||
(await expectedType.fromJSON(
|
||||
entity,
|
||||
)) as InstanceType<T>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
nextUrl = collection.data.next;
|
||||
limit -= collection.data.items.length;
|
||||
}
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively go through a URICollection of entities until reaching the end
|
||||
* @param url URL to reach the Collection
|
||||
* @param options.limit Limit the number of entities to fetch
|
||||
*/
|
||||
public async resolveURICollection(
|
||||
url: URL,
|
||||
options?: {
|
||||
limit?: number;
|
||||
},
|
||||
): Promise<URL[]> {
|
||||
const entities: URL[] = [];
|
||||
let nextUrl: URL | null = url;
|
||||
let limit = options?.limit ?? Number.POSITIVE_INFINITY;
|
||||
|
||||
while (nextUrl && limit > 0) {
|
||||
const collection: URICollection = await this.fetchEntity(
|
||||
nextUrl,
|
||||
URICollection,
|
||||
);
|
||||
|
||||
entities.push(...collection.data.items);
|
||||
nextUrl = collection.data.next;
|
||||
limit -= collection.data.items.length;
|
||||
}
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to resolve a webfinger URL to a User
|
||||
* @returns {Promise<User | null>} The resolved User or null if not found
|
||||
*/
|
||||
public async resolveWebFinger(
|
||||
username: string,
|
||||
hostname: string,
|
||||
contentType = "application/json",
|
||||
serverUrl = `https://${hostname}`,
|
||||
): Promise<URL | null> {
|
||||
const { ok, json, text } = await fetch(
|
||||
new URL(
|
||||
`/.well-known/webfinger?${new URLSearchParams({
|
||||
resource: `acct:${username}@${hostname}`,
|
||||
})}`,
|
||||
serverUrl,
|
||||
),
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"User-Agent": DEFAULT_UA,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch webfinger from ${serverUrl}: got HTTP code ${ok} with body "${await text()}"`,
|
||||
);
|
||||
}
|
||||
|
||||
// Validate the response
|
||||
const data = await WebFingerSchema.parseAsync(await json());
|
||||
|
||||
// Get the first link with a rel of "self"
|
||||
const selfLink = data.links?.find(
|
||||
(link) => link.rel === "self" && link.type === contentType,
|
||||
);
|
||||
|
||||
if (!selfLink?.href) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new URL(selfLink.href);
|
||||
}
|
||||
}
|
||||
54
packages/sdk/inbox-processor.ts
Normal file
54
packages/sdk/inbox-processor.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import type { Entity } from "./entities/entity.ts";
|
||||
import type { JSONObject } from "./types.ts";
|
||||
|
||||
type EntitySorterHandlers = Map<
|
||||
typeof Entity,
|
||||
(entity: Entity) => MaybePromise<void>
|
||||
>;
|
||||
type MaybePromise<T> = T | Promise<T>;
|
||||
|
||||
/**
|
||||
* @example
|
||||
* const jsonData = { ... };
|
||||
* const processor = await new EntitySorter(jsonData)
|
||||
* .on(User, async (user) => {
|
||||
* // Do something with the user
|
||||
* })
|
||||
* .sort();
|
||||
*/
|
||||
export class EntitySorter {
|
||||
private handlers: EntitySorterHandlers = new Map();
|
||||
|
||||
public constructor(private jsonData: JSONObject) {}
|
||||
|
||||
public on<T extends typeof Entity>(
|
||||
entity: T,
|
||||
handler: (entity: InstanceType<T>) => MaybePromise<void>,
|
||||
): EntitySorter {
|
||||
this.handlers.set(
|
||||
entity,
|
||||
handler as (entity: Entity) => MaybePromise<void>,
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the entity based on the provided JSON data.
|
||||
* @param {() => MaybePromise<void>} defaultHandler - A default handler to call if no specific handler is found.
|
||||
* @throws {Error} If no handler is found for the entity type
|
||||
*/
|
||||
public async sort(
|
||||
defaultHandler?: () => MaybePromise<void>,
|
||||
): Promise<void> {
|
||||
const type = this.jsonData.type;
|
||||
const entity = this.handlers.keys().find((key) => key.name === type);
|
||||
|
||||
if (entity) {
|
||||
await this.handlers.get(entity)?.(
|
||||
await entity.fromJSON(this.jsonData),
|
||||
);
|
||||
} else {
|
||||
await defaultHandler?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
69
packages/sdk/package.json
Normal file
69
packages/sdk/package.json
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"name": "@versia/sdk",
|
||||
"displayName": "Versia SDK",
|
||||
"version": "0.0.1",
|
||||
"author": {
|
||||
"email": "jesse.wierzbinski@lysand.org",
|
||||
"name": "Jesse Wierzbinski (CPlusPatch)",
|
||||
"url": "https://cpluspatch.com"
|
||||
},
|
||||
"readme": "README.md",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/versia-pub/server.git",
|
||||
"directory": "packages/federation"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/versia-pub/server/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Jesse Wierzbinski",
|
||||
"email": "jesse.wierzbinski@lysand.org",
|
||||
"url": "https://cpluspatch.com"
|
||||
}
|
||||
],
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Jesse Wierzbinski",
|
||||
"email": "jesse.wierzbinski@lysand.org",
|
||||
"url": "https://cpluspatch.com"
|
||||
}
|
||||
],
|
||||
"description": "Versia Federation SDK",
|
||||
"categories": ["Other"],
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"bun": ">=1.2.5"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./inbox-processor.ts",
|
||||
"default": "./inbox-processor.ts"
|
||||
},
|
||||
"./http": {
|
||||
"import": "./http.ts",
|
||||
"default": "./http.ts"
|
||||
},
|
||||
"./crypto": {
|
||||
"import": "./crypto.ts",
|
||||
"default": "./crypto.ts"
|
||||
},
|
||||
"./entities": {
|
||||
"import": "./entities/index.ts",
|
||||
"default": "./entities/index.ts"
|
||||
},
|
||||
"./schemas": {
|
||||
"import": "./schemas/index.ts",
|
||||
"default": "./schemas/index.ts"
|
||||
}
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/lysand"
|
||||
},
|
||||
"homepage": "https://versia.pub",
|
||||
"keywords": ["versia", "typescript", "sdk"],
|
||||
"packageManager": "bun@1.2.5"
|
||||
}
|
||||
64
packages/sdk/regex.ts
Normal file
64
packages/sdk/regex.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import {
|
||||
charIn,
|
||||
charNotIn,
|
||||
createRegExp,
|
||||
digit,
|
||||
exactly,
|
||||
global,
|
||||
letter,
|
||||
not,
|
||||
oneOrMore,
|
||||
} from "magic-regexp";
|
||||
|
||||
export const semverRegex: RegExp = new RegExp(
|
||||
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/gm,
|
||||
);
|
||||
|
||||
/**
|
||||
* Regular expression for matching an extension_type
|
||||
* @example pub.versia:custom_emojis/Emoji
|
||||
*/
|
||||
export const extensionTypeRegex: RegExp = createRegExp(
|
||||
// org namespace, then colon, then alphanumeric/_/-, then extension name
|
||||
exactly(
|
||||
oneOrMore(exactly(letter.lowercase.or(digit).or(charIn("_-.")))),
|
||||
exactly(":"),
|
||||
oneOrMore(exactly(letter.lowercase.or(digit).or(charIn("_-")))),
|
||||
exactly("/"),
|
||||
oneOrMore(exactly(letter.or(digit).or(charIn("_-")))),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Regular expression for matching an extension
|
||||
* @example pub.versia:custom_emojis
|
||||
*/
|
||||
export const extensionRegex: RegExp = createRegExp(
|
||||
// org namespace, then colon, then alphanumeric/_/-, then extension name
|
||||
exactly(
|
||||
oneOrMore(exactly(letter.lowercase.or(digit).or(charIn("_-.")))),
|
||||
exactly(":"),
|
||||
oneOrMore(exactly(letter.lowercase.or(digit).or(charIn("_-")))),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Regular expression for matching emojis.
|
||||
*/
|
||||
export const emojiRegex: RegExp = createRegExp(
|
||||
exactly(
|
||||
exactly(not.letter.or(not.digit).or(charNotIn("_-"))).times(1),
|
||||
oneOrMore(letter.or(digit).or(charIn("_-"))),
|
||||
exactly(not.letter.or(not.digit).or(charNotIn("_-"))).times(1),
|
||||
),
|
||||
[global],
|
||||
);
|
||||
|
||||
// This will accept a lot of stuff that isn't an ISO string
|
||||
// but ISO validation is incredibly complex so fuck it
|
||||
export const isISOString = (val: string | Date): boolean => {
|
||||
const d = new Date(val);
|
||||
return !Number.isNaN(d.valueOf());
|
||||
};
|
||||
|
||||
export const ianaTimezoneRegex = /^(?:[A-Za-z]+(?:\/[A-Za-z_]+)+|UTC)$/;
|
||||
16
packages/sdk/schemas/collection.ts
Normal file
16
packages/sdk/schemas/collection.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { z } from "zod";
|
||||
import { url, u64 } from "./common.ts";
|
||||
|
||||
export const CollectionSchema = z.strictObject({
|
||||
author: url.nullable(),
|
||||
first: url,
|
||||
last: url,
|
||||
total: u64,
|
||||
next: url.nullable(),
|
||||
previous: url.nullable(),
|
||||
items: z.array(z.any()),
|
||||
});
|
||||
|
||||
export const URICollectionSchema = CollectionSchema.extend({
|
||||
items: z.array(url),
|
||||
});
|
||||
17
packages/sdk/schemas/common.ts
Normal file
17
packages/sdk/schemas/common.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const f64 = z
|
||||
.number()
|
||||
.nonnegative()
|
||||
.max(2 ** 64 - 1);
|
||||
|
||||
export const u64 = z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.max(2 ** 64 - 1);
|
||||
|
||||
export const url = z
|
||||
.string()
|
||||
.url()
|
||||
.transform((z) => new URL(z));
|
||||
117
packages/sdk/schemas/contentformat.ts
Normal file
117
packages/sdk/schemas/contentformat.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import { types } from "mime-types";
|
||||
import { z } from "zod";
|
||||
import { f64, u64 } from "./common.ts";
|
||||
|
||||
const hashSizes = {
|
||||
sha256: 64,
|
||||
sha512: 128,
|
||||
"sha3-256": 64,
|
||||
"sha3-512": 128,
|
||||
"blake2b-256": 64,
|
||||
"blake2b-512": 128,
|
||||
"blake3-256": 64,
|
||||
"blake3-512": 128,
|
||||
md5: 32,
|
||||
sha1: 40,
|
||||
sha224: 56,
|
||||
sha384: 96,
|
||||
"sha3-224": 56,
|
||||
"sha3-384": 96,
|
||||
"blake2s-256": 64,
|
||||
"blake2s-512": 128,
|
||||
"blake3-224": 56,
|
||||
"blake3-384": 96,
|
||||
};
|
||||
const allMimeTypes = Object.values(types) as [string, ...string[]];
|
||||
const textMimeTypes = Object.values(types).filter((v) =>
|
||||
v.startsWith("text/"),
|
||||
) as [string, ...string[]];
|
||||
const nonTextMimeTypes = Object.values(types).filter(
|
||||
(v) => !v.startsWith("text/"),
|
||||
) as [string, ...string[]];
|
||||
const imageMimeTypes = Object.values(types).filter((v) =>
|
||||
v.startsWith("image/"),
|
||||
) as [string, ...string[]];
|
||||
const videoMimeTypes = Object.values(types).filter((v) =>
|
||||
v.startsWith("video/"),
|
||||
) as [string, ...string[]];
|
||||
const audioMimeTypes = Object.values(types).filter((v) =>
|
||||
v.startsWith("audio/"),
|
||||
) as [string, ...string[]];
|
||||
|
||||
export const ContentFormatSchema = z.record(
|
||||
z.enum(allMimeTypes),
|
||||
z.strictObject({
|
||||
content: z.string().or(z.string().url()),
|
||||
remote: z.boolean(),
|
||||
description: z.string().nullish(),
|
||||
size: u64.nullish(),
|
||||
hash: z
|
||||
.strictObject(
|
||||
Object.fromEntries(
|
||||
Object.entries(hashSizes).map(([k, v]) => [
|
||||
k,
|
||||
z.string().length(v).nullish(),
|
||||
]),
|
||||
),
|
||||
)
|
||||
.nullish(),
|
||||
thumbhash: z.string().nullish(),
|
||||
width: u64.nullish(),
|
||||
height: u64.nullish(),
|
||||
duration: f64.nullish(),
|
||||
fps: u64.nullish(),
|
||||
}),
|
||||
);
|
||||
|
||||
export const TextContentFormatSchema = z.record(
|
||||
z.enum(textMimeTypes),
|
||||
ContentFormatSchema.valueSchema
|
||||
.pick({
|
||||
content: true,
|
||||
remote: true,
|
||||
})
|
||||
.extend({
|
||||
content: z.string(),
|
||||
remote: z.literal(false),
|
||||
}),
|
||||
);
|
||||
|
||||
export const NonTextContentFormatSchema = z.record(
|
||||
z.enum(nonTextMimeTypes),
|
||||
ContentFormatSchema.valueSchema
|
||||
.pick({
|
||||
content: true,
|
||||
remote: true,
|
||||
description: true,
|
||||
size: true,
|
||||
hash: true,
|
||||
thumbhash: true,
|
||||
width: true,
|
||||
height: true,
|
||||
})
|
||||
.extend({
|
||||
content: z.string().url(),
|
||||
remote: z.literal(true),
|
||||
}),
|
||||
);
|
||||
|
||||
export const ImageContentFormatSchema = z.record(
|
||||
z.enum(imageMimeTypes),
|
||||
NonTextContentFormatSchema.valueSchema,
|
||||
);
|
||||
|
||||
export const VideoContentFormatSchema = z.record(
|
||||
z.enum(videoMimeTypes),
|
||||
NonTextContentFormatSchema.valueSchema.extend({
|
||||
duration: ContentFormatSchema.valueSchema.shape.duration,
|
||||
fps: ContentFormatSchema.valueSchema.shape.fps,
|
||||
}),
|
||||
);
|
||||
|
||||
export const AudioContentFormatSchema = z.record(
|
||||
z.enum(audioMimeTypes),
|
||||
NonTextContentFormatSchema.valueSchema.extend({
|
||||
duration: ContentFormatSchema.valueSchema.shape.duration,
|
||||
}),
|
||||
);
|
||||
11
packages/sdk/schemas/delete.ts
Normal file
11
packages/sdk/schemas/delete.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "./common.ts";
|
||||
import { EntitySchema } from "./entity.ts";
|
||||
|
||||
export const DeleteSchema = EntitySchema.extend({
|
||||
uri: z.null().optional(),
|
||||
type: z.literal("Delete"),
|
||||
author: url.nullable(),
|
||||
deleted_type: z.string(),
|
||||
deleted: url,
|
||||
});
|
||||
23
packages/sdk/schemas/entity.ts
Normal file
23
packages/sdk/schemas/entity.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { z } from "zod";
|
||||
import { isISOString } from "../regex.ts";
|
||||
import { url } from "./common.ts";
|
||||
import { CustomEmojiExtensionSchema } from "./extensions/emojis.ts";
|
||||
|
||||
export const ExtensionPropertySchema = z
|
||||
.object({
|
||||
"pub.versia:custom_emojis":
|
||||
CustomEmojiExtensionSchema.optional().nullable(),
|
||||
})
|
||||
.catchall(z.any());
|
||||
|
||||
export const EntitySchema = z.strictObject({
|
||||
// biome-ignore lint/style/useNamingConvention:
|
||||
$schema: z.string().url().nullish(),
|
||||
id: z.string().max(512),
|
||||
created_at: z
|
||||
.string()
|
||||
.refine((v) => isISOString(v), "must be a valid ISO8601 datetime"),
|
||||
uri: url,
|
||||
type: z.string(),
|
||||
extensions: ExtensionPropertySchema.nullish(),
|
||||
});
|
||||
25
packages/sdk/schemas/extensions/emojis.ts
Normal file
25
packages/sdk/schemas/extensions/emojis.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* Custom emojis extension.
|
||||
* @module federation/schemas/extensions/custom_emojis
|
||||
* @see module:federation/schemas/base
|
||||
* @see https://versia.pub/extensions/custom-emojis
|
||||
*/
|
||||
import { z } from "zod";
|
||||
import { emojiRegex } from "../../regex.ts";
|
||||
import { ImageContentFormatSchema } from "../contentformat.ts";
|
||||
|
||||
export const CustomEmojiExtensionSchema = z.strictObject({
|
||||
emojis: z.array(
|
||||
z.strictObject({
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(256)
|
||||
.regex(
|
||||
emojiRegex,
|
||||
"Emoji name must be alphanumeric, underscores, or dashes, and surrounded by identifiers",
|
||||
),
|
||||
url: ImageContentFormatSchema,
|
||||
}),
|
||||
),
|
||||
});
|
||||
41
packages/sdk/schemas/extensions/groups.ts
Normal file
41
packages/sdk/schemas/extensions/groups.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "../common.ts";
|
||||
import { TextContentFormatSchema } from "../contentformat.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
|
||||
export const GroupSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:groups/Group"),
|
||||
name: TextContentFormatSchema.nullish(),
|
||||
description: TextContentFormatSchema.nullish(),
|
||||
open: z.boolean().nullish(),
|
||||
members: url,
|
||||
notes: url.nullish(),
|
||||
});
|
||||
|
||||
export const GroupSubscribeSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:groups/Subscribe"),
|
||||
uri: z.null().optional(),
|
||||
subscriber: url,
|
||||
group: url,
|
||||
});
|
||||
|
||||
export const GroupUnsubscribeSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:groups/Unsubscribe"),
|
||||
uri: z.null().optional(),
|
||||
subscriber: url,
|
||||
group: url,
|
||||
});
|
||||
|
||||
export const GroupSubscribeAcceptSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:groups/SubscribeAccept"),
|
||||
uri: z.null().optional(),
|
||||
subscriber: url,
|
||||
group: url,
|
||||
});
|
||||
|
||||
export const GroupSubscribeRejectSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:groups/SubscribeReject"),
|
||||
uri: z.null().optional(),
|
||||
subscriber: url,
|
||||
group: url,
|
||||
});
|
||||
15
packages/sdk/schemas/extensions/likes.ts
Normal file
15
packages/sdk/schemas/extensions/likes.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "../common.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
|
||||
export const LikeSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:likes/Like"),
|
||||
author: url,
|
||||
liked: url,
|
||||
});
|
||||
|
||||
export const DislikeSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:likes/Dislike"),
|
||||
author: url,
|
||||
disliked: url,
|
||||
});
|
||||
15
packages/sdk/schemas/extensions/migration.ts
Normal file
15
packages/sdk/schemas/extensions/migration.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "../common.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
|
||||
export const MigrationSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:migration/Migration"),
|
||||
uri: z.null().optional(),
|
||||
author: url,
|
||||
destination: url,
|
||||
});
|
||||
|
||||
export const MigrationExtensionSchema = z.strictObject({
|
||||
previous: url,
|
||||
new: url.nullish(),
|
||||
});
|
||||
22
packages/sdk/schemas/extensions/polls.ts
Normal file
22
packages/sdk/schemas/extensions/polls.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { z } from "zod";
|
||||
import { isISOString } from "../../regex.ts";
|
||||
import { url, u64 } from "../common.ts";
|
||||
import { TextContentFormatSchema } from "../contentformat.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
|
||||
export const VoteSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:polls/Vote"),
|
||||
author: url,
|
||||
poll: url,
|
||||
option: u64,
|
||||
});
|
||||
|
||||
export const PollExtensionSchema = z.strictObject({
|
||||
options: z.array(TextContentFormatSchema),
|
||||
votes: z.array(u64),
|
||||
multiple_choice: z.boolean(),
|
||||
expires_at: z
|
||||
.string()
|
||||
.refine((v) => isISOString(v), "must be a valid ISO8601 datetime")
|
||||
.nullish(),
|
||||
});
|
||||
10
packages/sdk/schemas/extensions/reactions.ts
Normal file
10
packages/sdk/schemas/extensions/reactions.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "../common.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
|
||||
export const ReactionSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:reactions/Reaction"),
|
||||
author: url,
|
||||
object: url,
|
||||
content: z.string().min(1).max(256),
|
||||
});
|
||||
15
packages/sdk/schemas/extensions/reports.ts
Normal file
15
packages/sdk/schemas/extensions/reports.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "../common.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
|
||||
export const ReportSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:reports/Report"),
|
||||
uri: z.null().optional(),
|
||||
author: url.nullish(),
|
||||
reported: z.array(url),
|
||||
tags: z.array(z.string()),
|
||||
comment: z
|
||||
.string()
|
||||
.max(2 ** 16)
|
||||
.nullish(),
|
||||
});
|
||||
9
packages/sdk/schemas/extensions/share.ts
Normal file
9
packages/sdk/schemas/extensions/share.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "../common.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
|
||||
export const ShareSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:share/Share"),
|
||||
author: url,
|
||||
shared: url,
|
||||
});
|
||||
46
packages/sdk/schemas/extensions/vanity.ts
Normal file
46
packages/sdk/schemas/extensions/vanity.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* Vanity extension schema.
|
||||
* @module federation/schemas/extensions/vanity
|
||||
* @see module:federation/schemas/base
|
||||
* @see https://versia.pub/extensions/vanity
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { ianaTimezoneRegex, isISOString } from "../../regex.ts";
|
||||
import { url } from "../common.ts";
|
||||
import {
|
||||
AudioContentFormatSchema,
|
||||
ImageContentFormatSchema,
|
||||
} from "../contentformat.ts";
|
||||
|
||||
export const VanityExtensionSchema = z.strictObject({
|
||||
avatar_overlays: z.array(ImageContentFormatSchema).nullish(),
|
||||
avatar_mask: ImageContentFormatSchema.nullish(),
|
||||
background: ImageContentFormatSchema.nullish(),
|
||||
audio: AudioContentFormatSchema.nullish(),
|
||||
pronouns: z.record(
|
||||
z.string(),
|
||||
z.array(
|
||||
z.union([
|
||||
z.strictObject({
|
||||
subject: z.string(),
|
||||
object: z.string(),
|
||||
dependent_possessive: z.string(),
|
||||
independent_possessive: z.string(),
|
||||
reflexive: z.string(),
|
||||
}),
|
||||
z.string(),
|
||||
]),
|
||||
),
|
||||
),
|
||||
birthday: z
|
||||
.string()
|
||||
.refine((v) => isISOString(v), "must be a valid ISO8601 datetime")
|
||||
.nullish(),
|
||||
location: z.string().nullish(),
|
||||
aliases: z.array(url).nullish(),
|
||||
timezone: z
|
||||
.string()
|
||||
.regex(ianaTimezoneRegex, "must be a valid IANA timezone")
|
||||
.nullish(),
|
||||
});
|
||||
31
packages/sdk/schemas/follow.ts
Normal file
31
packages/sdk/schemas/follow.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "./common.ts";
|
||||
import { EntitySchema } from "./entity.ts";
|
||||
|
||||
export const FollowSchema = EntitySchema.extend({
|
||||
type: z.literal("Follow"),
|
||||
uri: z.null().optional(),
|
||||
author: url,
|
||||
followee: url,
|
||||
});
|
||||
|
||||
export const FollowAcceptSchema = EntitySchema.extend({
|
||||
type: z.literal("FollowAccept"),
|
||||
uri: z.null().optional(),
|
||||
author: url,
|
||||
follower: url,
|
||||
});
|
||||
|
||||
export const FollowRejectSchema = EntitySchema.extend({
|
||||
type: z.literal("FollowReject"),
|
||||
uri: z.null().optional(),
|
||||
author: url,
|
||||
follower: url,
|
||||
});
|
||||
|
||||
export const UnfollowSchema = EntitySchema.extend({
|
||||
type: z.literal("Unfollow"),
|
||||
uri: z.null().optional(),
|
||||
author: url,
|
||||
followee: url,
|
||||
});
|
||||
27
packages/sdk/schemas/index.ts
Normal file
27
packages/sdk/schemas/index.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// biome-ignore lint/performance/noBarrelFile: <explanation>
|
||||
export { UserSchema } from "./user.ts";
|
||||
export { NoteSchema } from "./note.ts";
|
||||
export { EntitySchema } from "./entity.ts";
|
||||
export { DeleteSchema } from "./delete.ts";
|
||||
export { InstanceMetadataSchema } from "./instance.ts";
|
||||
export {
|
||||
ContentFormatSchema,
|
||||
ImageContentFormatSchema,
|
||||
AudioContentFormatSchema,
|
||||
NonTextContentFormatSchema,
|
||||
TextContentFormatSchema,
|
||||
VideoContentFormatSchema,
|
||||
} from "./contentformat.ts";
|
||||
export {
|
||||
FollowSchema,
|
||||
FollowAcceptSchema,
|
||||
FollowRejectSchema,
|
||||
UnfollowSchema,
|
||||
} from "./follow.ts";
|
||||
export { CollectionSchema, URICollectionSchema } from "./collection.ts";
|
||||
export { LikeSchema, DislikeSchema } from "./extensions/likes.ts";
|
||||
export { VoteSchema } from "./extensions/polls.ts";
|
||||
export { ReactionSchema } from "./extensions/reactions.ts";
|
||||
export { ReportSchema } from "./extensions/reports.ts";
|
||||
export { ShareSchema } from "./extensions/share.ts";
|
||||
export { WebFingerSchema } from "./webfinger.ts";
|
||||
41
packages/sdk/schemas/instance.ts
Normal file
41
packages/sdk/schemas/instance.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { z } from "zod";
|
||||
import { extensionRegex, semverRegex } from "../regex.ts";
|
||||
import { url } from "./common.ts";
|
||||
import { ImageContentFormatSchema } from "./contentformat.ts";
|
||||
import { EntitySchema } from "./entity.ts";
|
||||
|
||||
export const InstanceMetadataSchema = EntitySchema.extend({
|
||||
type: z.literal("InstanceMetadata"),
|
||||
id: z.null().optional(),
|
||||
uri: z.null().optional(),
|
||||
name: z.string().min(1),
|
||||
software: z.strictObject({
|
||||
name: z.string().min(1),
|
||||
version: z.string().min(1),
|
||||
}),
|
||||
compatibility: z.strictObject({
|
||||
versions: z.array(
|
||||
z.string().regex(semverRegex, "must be a valid SemVer version"),
|
||||
),
|
||||
extensions: z.array(
|
||||
z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(
|
||||
extensionRegex,
|
||||
"must be in the format 'namespaced_url:extension_name', e.g. 'pub.versia:reactions'",
|
||||
),
|
||||
),
|
||||
}),
|
||||
description: z.string().nullish(),
|
||||
host: z.string(),
|
||||
shared_inbox: url.nullish(),
|
||||
public_key: z.strictObject({
|
||||
key: z.string().min(1),
|
||||
algorithm: z.literal("ed25519"),
|
||||
}),
|
||||
moderators: url.nullish(),
|
||||
admins: url.nullish(),
|
||||
logo: ImageContentFormatSchema.nullish(),
|
||||
banner: ImageContentFormatSchema.nullish(),
|
||||
});
|
||||
67
packages/sdk/schemas/note.ts
Normal file
67
packages/sdk/schemas/note.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "./common.ts";
|
||||
import {
|
||||
NonTextContentFormatSchema,
|
||||
TextContentFormatSchema,
|
||||
} from "./contentformat.ts";
|
||||
import { EntitySchema } from "./entity.ts";
|
||||
import { PollExtensionSchema } from "./extensions/polls.ts";
|
||||
|
||||
export const NoteSchema = EntitySchema.extend({
|
||||
type: z.literal("Note"),
|
||||
attachments: z.array(NonTextContentFormatSchema).nullish(),
|
||||
author: url,
|
||||
category: z
|
||||
.enum([
|
||||
"microblog",
|
||||
"forum",
|
||||
"blog",
|
||||
"image",
|
||||
"video",
|
||||
"audio",
|
||||
"messaging",
|
||||
])
|
||||
.nullish(),
|
||||
content: TextContentFormatSchema.nullish(),
|
||||
collections: z
|
||||
.strictObject({
|
||||
replies: url,
|
||||
quotes: url,
|
||||
"pub.versia:reactions/Reactions": url.nullish(),
|
||||
"pub.versia:share/Shares": url.nullish(),
|
||||
"pub.versia:likes/Likes": url.nullish(),
|
||||
"pub.versia:likes/Dislikes": url.nullish(),
|
||||
})
|
||||
.catchall(url),
|
||||
device: z
|
||||
.strictObject({
|
||||
name: z.string(),
|
||||
version: z.string().nullish(),
|
||||
url: url.nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
group: url.or(z.enum(["public", "followers"])).nullish(),
|
||||
is_sensitive: z.boolean().nullish(),
|
||||
mentions: z.array(url).nullish(),
|
||||
previews: z
|
||||
.array(
|
||||
z.strictObject({
|
||||
link: url,
|
||||
title: z.string(),
|
||||
description: z.string().nullish(),
|
||||
image: url.nullish(),
|
||||
icon: url.nullish(),
|
||||
}),
|
||||
)
|
||||
.nullish(),
|
||||
quotes: url.nullish(),
|
||||
replies_to: url.nullish(),
|
||||
subject: z.string().nullish(),
|
||||
extensions: EntitySchema.shape.extensions
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.extend({
|
||||
"pub.versia:polls": PollExtensionSchema.nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
60
packages/sdk/schemas/user.ts
Normal file
60
packages/sdk/schemas/user.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "./common.ts";
|
||||
import {
|
||||
ImageContentFormatSchema,
|
||||
TextContentFormatSchema,
|
||||
} from "./contentformat.ts";
|
||||
import { EntitySchema } from "./entity.ts";
|
||||
import { MigrationExtensionSchema } from "./extensions/migration.ts";
|
||||
import { VanityExtensionSchema } from "./extensions/vanity.ts";
|
||||
|
||||
export const PublicKeyDataSchema = z.strictObject({
|
||||
key: z.string().min(1),
|
||||
actor: url,
|
||||
algorithm: z.literal("ed25519"),
|
||||
});
|
||||
|
||||
export const UserSchema = EntitySchema.extend({
|
||||
type: z.literal("User"),
|
||||
avatar: ImageContentFormatSchema.nullish(),
|
||||
bio: TextContentFormatSchema.nullish(),
|
||||
display_name: z.string().nullish(),
|
||||
fields: z
|
||||
.array(
|
||||
z.strictObject({
|
||||
key: TextContentFormatSchema,
|
||||
value: TextContentFormatSchema,
|
||||
}),
|
||||
)
|
||||
.nullish(),
|
||||
username: z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(
|
||||
/^[a-zA-Z0-9_-]+$/,
|
||||
"must be alphanumeric, and may contain _ or -",
|
||||
),
|
||||
header: ImageContentFormatSchema.nullish(),
|
||||
public_key: PublicKeyDataSchema,
|
||||
manually_approves_followers: z.boolean().nullish(),
|
||||
indexable: z.boolean().nullish(),
|
||||
inbox: url,
|
||||
collections: z
|
||||
.object({
|
||||
featured: url,
|
||||
followers: url,
|
||||
following: url,
|
||||
outbox: url,
|
||||
"pub.versia:likes/Likes": url.nullish(),
|
||||
"pub.versia:likes/Dislikes": url.nullish(),
|
||||
})
|
||||
.catchall(url),
|
||||
extensions: EntitySchema.shape.extensions
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.extend({
|
||||
"pub.versia:vanity": VanityExtensionSchema.nullish(),
|
||||
"pub.versia:migration": MigrationExtensionSchema.nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
19
packages/sdk/schemas/webfinger.ts
Normal file
19
packages/sdk/schemas/webfinger.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "./common.ts";
|
||||
|
||||
export const WebFingerSchema = z.object({
|
||||
subject: url,
|
||||
aliases: z.array(url).optional(),
|
||||
properties: z.record(url, z.string().or(z.null())).optional(),
|
||||
links: z
|
||||
.array(
|
||||
z.object({
|
||||
rel: z.string(),
|
||||
type: z.string().optional(),
|
||||
href: url.optional(),
|
||||
titles: z.record(z.string(), z.string()).optional(),
|
||||
properties: z.record(url, z.string().or(z.null())).optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
11
packages/sdk/types.ts
Normal file
11
packages/sdk/types.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
type JSONValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| JSONValue[]
|
||||
| { [key: string]: JSONValue };
|
||||
|
||||
export interface JSONObject {
|
||||
[k: string]: JSONValue;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue