mirror of
https://github.com/versia-pub/api.git
synced 2025-12-06 16:38:20 +01:00
Compare commits
No commits in common. "ae5c3bb281e126d62c5fb34ee922c121fc1507f2" and "bac34b3f39e627fafc350115aaf9dbd3f335c37d" have entirely different histories.
ae5c3bb281
...
bac34b3f39
|
|
@ -7,7 +7,7 @@ describe("SignatureValidator", () => {
|
||||||
let publicKey: CryptoKey;
|
let publicKey: CryptoKey;
|
||||||
let body: string;
|
let body: string;
|
||||||
let signature: string;
|
let signature: string;
|
||||||
let timestamp: Date;
|
let nonce: string;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const keys = await crypto.subtle.generateKey("Ed25519", true, [
|
const keys = await crypto.subtle.generateKey("Ed25519", true, [
|
||||||
|
|
@ -25,8 +25,8 @@ describe("SignatureValidator", () => {
|
||||||
"https://bob.org/users/6a18f2c3-120e-4949-bda4-2aa4c8264d51",
|
"https://bob.org/users/6a18f2c3-120e-4949-bda4-2aa4c8264d51",
|
||||||
).sign("GET", new URL("https://example.com"), body);
|
).sign("GET", new URL("https://example.com"), body);
|
||||||
|
|
||||||
signature = headers.get("Versia-Signature") ?? "";
|
signature = headers.get("X-Signature") ?? "";
|
||||||
timestamp = new Date(Number(headers.get("Versia-Signed-At")) * 1000);
|
nonce = headers.get("X-Nonce") ?? "";
|
||||||
});
|
});
|
||||||
|
|
||||||
test("fromStringKey", async () => {
|
test("fromStringKey", async () => {
|
||||||
|
|
@ -46,8 +46,8 @@ describe("SignatureValidator", () => {
|
||||||
const request = new Request("https://example.com", {
|
const request = new Request("https://example.com", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Versia-Signature": signature,
|
"X-Signature": signature,
|
||||||
"Versia-Signed-At": String(timestamp.getTime() / 1000),
|
"X-Nonce": nonce,
|
||||||
},
|
},
|
||||||
body: body,
|
body: body,
|
||||||
});
|
});
|
||||||
|
|
@ -59,8 +59,8 @@ describe("SignatureValidator", () => {
|
||||||
const request = new Request("https://example.com", {
|
const request = new Request("https://example.com", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Versia-Signature": "invalid",
|
"X-Signature": "invalid",
|
||||||
"Versia-Signed-At": String(timestamp.getTime() / 1000),
|
"X-Nonce": nonce,
|
||||||
},
|
},
|
||||||
body: body,
|
body: body,
|
||||||
});
|
});
|
||||||
|
|
@ -70,16 +70,16 @@ describe("SignatureValidator", () => {
|
||||||
expect(isValid).toBe(false);
|
expect(isValid).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw with missing timestamp", async () => {
|
test("should throw with missing nonce", async () => {
|
||||||
const request = new Request("https://example.com", {
|
const request = new Request("https://example.com", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Versia-Signature": signature,
|
"X-Signature": signature,
|
||||||
},
|
},
|
||||||
body: body,
|
body: body,
|
||||||
});
|
});
|
||||||
expect(() => validator.validate(request)).toThrow(
|
expect(() => validator.validate(request)).toThrow(
|
||||||
"Headers are missing in request: Versia-Signed-At",
|
"Headers are missing in request: X-Nonce",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -87,8 +87,8 @@ describe("SignatureValidator", () => {
|
||||||
const request = new Request("https://example.com", {
|
const request = new Request("https://example.com", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Versia-Signature": signature,
|
"X-Signature": signature,
|
||||||
"Versia-Signed-At": String(timestamp.getTime() / 1000),
|
"X-Nonce": nonce,
|
||||||
},
|
},
|
||||||
body: "different",
|
body: "different",
|
||||||
});
|
});
|
||||||
|
|
@ -101,8 +101,8 @@ describe("SignatureValidator", () => {
|
||||||
const request = new Request("https://example.com", {
|
const request = new Request("https://example.com", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Versia-Signature": "thisIsNotbase64OhNo$^ù",
|
"X-Signature": "thisIsNotbase64OhNo$^ù",
|
||||||
"Versia-Signed-At": String(timestamp.getTime() / 1000),
|
"X-Nonce": nonce,
|
||||||
},
|
},
|
||||||
body: body,
|
body: body,
|
||||||
});
|
});
|
||||||
|
|
@ -151,11 +151,11 @@ describe("SignatureConstructor", () => {
|
||||||
test("should correctly sign ", async () => {
|
test("should correctly sign ", async () => {
|
||||||
const url = new URL("https://example.com");
|
const url = new URL("https://example.com");
|
||||||
headers = (await ctor.sign("GET", url, body)).headers;
|
headers = (await ctor.sign("GET", url, body)).headers;
|
||||||
expect(headers.get("Versia-Signature")).toBeDefined();
|
expect(headers.get("X-Signature")).toBeDefined();
|
||||||
expect(headers.get("Versia-Signed-At")).toBeDefined();
|
expect(headers.get("X-Nonce")).toBeDefined();
|
||||||
|
|
||||||
expect(headers.get("Versia-Signed-At")?.length).toBeGreaterThan(10);
|
expect(headers.get("X-Nonce")?.length).toBeGreaterThan(10);
|
||||||
expect(headers.get("Versia-Signature")?.length).toBeGreaterThan(10);
|
expect(headers.get("X-Signature")?.length).toBeGreaterThan(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should correctly sign a Request", async () => {
|
test("should correctly sign a Request", async () => {
|
||||||
|
|
@ -167,8 +167,8 @@ describe("SignatureConstructor", () => {
|
||||||
const { request: newRequest } = await ctor.sign(request);
|
const { request: newRequest } = await ctor.sign(request);
|
||||||
|
|
||||||
headers = newRequest.headers;
|
headers = newRequest.headers;
|
||||||
expect(headers.get("Versia-Signature")).toBeDefined();
|
expect(headers.get("X-Signature")).toBeDefined();
|
||||||
expect(headers.get("Versia-Signed-At")).toBeDefined();
|
expect(headers.get("X-Nonce")).toBeDefined();
|
||||||
|
|
||||||
expect(await newRequest.text()).toBe(body);
|
expect(await newRequest.text()).toBe(body);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ const base64ToArrayBuffer = (base64: string) =>
|
||||||
const arrayBufferToBase64 = (arrayBuffer: ArrayBuffer) =>
|
const arrayBufferToBase64 = (arrayBuffer: ArrayBuffer) =>
|
||||||
btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
|
btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
|
||||||
|
|
||||||
|
const uint8ArrayToBase64 = (uint8Array: Uint8Array) =>
|
||||||
|
btoa(String.fromCharCode(...uint8Array));
|
||||||
|
|
||||||
const checkEvironmentSupport = () => {
|
const checkEvironmentSupport = () => {
|
||||||
// Check if WebCrypto is supported
|
// Check if WebCrypto is supported
|
||||||
if (!globalThis.crypto?.subtle) {
|
if (!globalThis.crypto?.subtle) {
|
||||||
|
|
@ -75,7 +78,7 @@ export class SignatureValidator {
|
||||||
/**
|
/**
|
||||||
* Validates the signature of a request.
|
* Validates the signature of a request.
|
||||||
* @param signature The signature string.
|
* @param signature The signature string.
|
||||||
* @param timestamp Signature timestamp.
|
* @param nonce Signature nonce.
|
||||||
* @param method The HTTP verb.
|
* @param method The HTTP verb.
|
||||||
* @param url The URL object.
|
* @param url The URL object.
|
||||||
* @param body The request body.
|
* @param body The request body.
|
||||||
|
|
@ -83,15 +86,15 @@ export class SignatureValidator {
|
||||||
* @throws TypeError if any required parameters are missing or empty.
|
* @throws TypeError if any required parameters are missing or empty.
|
||||||
* @example
|
* @example
|
||||||
* const signature = "k4QNt5Grl40KK8orIdiaq118Z+P5pa6vIeArq55wsvfL7wNy4cE3f2fhsGcpZql+PStm+x2ZjZIhudrAC/32Cg==";
|
* const signature = "k4QNt5Grl40KK8orIdiaq118Z+P5pa6vIeArq55wsvfL7wNy4cE3f2fhsGcpZql+PStm+x2ZjZIhudrAC/32Cg==";
|
||||||
* const date = new Date(1549312452000)
|
* const nonce = "bJzyhTNK2RXUCetKIpm0Fw==";
|
||||||
* const method = "GET";
|
* const method = "GET";
|
||||||
* const url = new URL("https://example.com/users/ff54ee40-2ce9-4d2e-86ac-3cd06a1e1480");
|
* const url = new URL("https://example.com/users/ff54ee40-2ce9-4d2e-86ac-3cd06a1e1480");
|
||||||
* const body = "{ ... }";
|
* const body = "{ ... }";
|
||||||
* const isValid = await validator.validate(signature, date, method, url, body);
|
* const isValid = await validator.validate(signature, nonce, method, url, body);
|
||||||
*/
|
*/
|
||||||
async validate(
|
async validate(
|
||||||
signature: string,
|
signature: string,
|
||||||
timestamp: Date,
|
nonce: string,
|
||||||
method: HttpVerb,
|
method: HttpVerb,
|
||||||
url: URL,
|
url: URL,
|
||||||
body: string,
|
body: string,
|
||||||
|
|
@ -99,28 +102,25 @@ export class SignatureValidator {
|
||||||
|
|
||||||
async validate(
|
async validate(
|
||||||
requestOrSignature: Request | string,
|
requestOrSignature: Request | string,
|
||||||
timestamp?: Date,
|
nonce?: string,
|
||||||
method?: HttpVerb,
|
method?: HttpVerb,
|
||||||
url?: URL,
|
url?: URL,
|
||||||
body?: string,
|
body?: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (requestOrSignature instanceof Request) {
|
if (requestOrSignature instanceof Request) {
|
||||||
const signature =
|
const signature = requestOrSignature.headers.get("X-Signature");
|
||||||
requestOrSignature.headers.get("Versia-Signature");
|
const nonce = requestOrSignature.headers.get("X-Nonce");
|
||||||
const timestampHeader =
|
|
||||||
requestOrSignature.headers.get("Versia-Signed-At");
|
|
||||||
const timestamp = new Date(Number(timestampHeader) * 1000);
|
|
||||||
const url = new URL(requestOrSignature.url);
|
const url = new URL(requestOrSignature.url);
|
||||||
const body = await requestOrSignature.text();
|
const body = await requestOrSignature.text();
|
||||||
const method = requestOrSignature.method as HttpVerb;
|
const method = requestOrSignature.method as HttpVerb;
|
||||||
|
|
||||||
const missingHeaders = [
|
const missingHeaders = [
|
||||||
!signature && "Versia-Signature",
|
!signature && "X-Signature",
|
||||||
!timestampHeader && "Versia-Signed-At",
|
!nonce && "X-Nonce",
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
// Check if all headers are present
|
// Check if all headers are present
|
||||||
if (!(signature && timestampHeader && method && url && body)) {
|
if (!(signature && nonce && method && url && body)) {
|
||||||
// Say which headers are missing
|
// Say which headers are missing
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
`Headers are missing in request: ${missingHeaders.join(
|
`Headers are missing in request: ${missingHeaders.join(
|
||||||
|
|
@ -129,12 +129,12 @@ export class SignatureValidator {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.validate(signature, timestamp, method, url, body);
|
return this.validate(signature, nonce, method, url, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(timestamp && method && url && body)) {
|
if (!(nonce && method && url && body)) {
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
"Missing or empty required parameters: timestamp, method, url or body",
|
"Missing or empty required parameters: nonce, method, url or body",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,7 +152,7 @@ export class SignatureValidator {
|
||||||
new TextEncoder().encode(body),
|
new TextEncoder().encode(body),
|
||||||
);
|
);
|
||||||
|
|
||||||
const expectedSignedString = `${method.toLowerCase()} ${encodeURIComponent(url.pathname)} ${timestamp.getTime() / 1000} ${arrayBufferToBase64(digest)}`;
|
const expectedSignedString = `${method.toLowerCase()} ${encodeURIComponent(url.pathname)} ${nonce} ${arrayBufferToBase64(digest)}`;
|
||||||
|
|
||||||
// Check if signed string is valid
|
// Check if signed string is valid
|
||||||
const isValid = await crypto.subtle.verify(
|
const isValid = await crypto.subtle.verify(
|
||||||
|
|
@ -232,7 +232,7 @@ export class SignatureConstructor {
|
||||||
* @param url The URL object.
|
* @param url The URL object.
|
||||||
* @param body The request body.
|
* @param body The request body.
|
||||||
* @param headers The request headers.
|
* @param headers The request headers.
|
||||||
* @param timestamp The signature timestamp (optional).
|
* @param nonce The signature nonce (optional).
|
||||||
* @returns A Promise that resolves to the signed headers, and the signed string.
|
* @returns A Promise that resolves to the signed headers, and the signed string.
|
||||||
* @throws TypeError if any required parameters are missing or empty.
|
* @throws TypeError if any required parameters are missing or empty.
|
||||||
* @example
|
* @example
|
||||||
|
|
@ -246,7 +246,7 @@ export class SignatureConstructor {
|
||||||
url: URL,
|
url: URL,
|
||||||
body?: string,
|
body?: string,
|
||||||
headers?: Headers,
|
headers?: Headers,
|
||||||
timestamp?: Date,
|
nonce?: string,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
headers: Headers;
|
headers: Headers;
|
||||||
signedString: string;
|
signedString: string;
|
||||||
|
|
@ -257,7 +257,7 @@ export class SignatureConstructor {
|
||||||
url?: URL,
|
url?: URL,
|
||||||
body?: string,
|
body?: string,
|
||||||
headers: Headers = new Headers(),
|
headers: Headers = new Headers(),
|
||||||
timestamp?: Date,
|
nonce?: string,
|
||||||
): Promise<
|
): Promise<
|
||||||
| {
|
| {
|
||||||
headers: Headers;
|
headers: Headers;
|
||||||
|
|
@ -270,23 +270,19 @@ export class SignatureConstructor {
|
||||||
> {
|
> {
|
||||||
if (requestOrMethod instanceof Request) {
|
if (requestOrMethod instanceof Request) {
|
||||||
const request = requestOrMethod.clone();
|
const request = requestOrMethod.clone();
|
||||||
const signedAt = requestOrMethod.headers.get("Versia-Signed-At");
|
|
||||||
|
|
||||||
const { headers, signedString } = await this.sign(
|
const { headers, signedString } = await this.sign(
|
||||||
requestOrMethod.method as HttpVerb,
|
requestOrMethod.method as HttpVerb,
|
||||||
new URL(requestOrMethod.url),
|
new URL(requestOrMethod.url),
|
||||||
await requestOrMethod.text(),
|
await requestOrMethod.text(),
|
||||||
requestOrMethod.headers,
|
requestOrMethod.headers,
|
||||||
signedAt ? new Date(Number(signedAt) * 1000) : undefined,
|
requestOrMethod.headers.get("X-Nonce") ?? undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
request.headers.set("X-Nonce", headers.get("X-Nonce") ?? "");
|
||||||
request.headers.set(
|
request.headers.set(
|
||||||
"Versia-Signed-At",
|
"X-Signature",
|
||||||
headers.get("Versia-Signed-At") ?? "",
|
headers.get("X-Signature") ?? "",
|
||||||
);
|
|
||||||
request.headers.set(
|
|
||||||
"Versia-Signature",
|
|
||||||
headers.get("Versia-Signature") ?? "",
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return { request, signedString };
|
return { request, signedString };
|
||||||
|
|
@ -298,7 +294,9 @@ export class SignatureConstructor {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalTimestamp = timestamp || new Date();
|
const finalNonce =
|
||||||
|
nonce ||
|
||||||
|
uint8ArrayToBase64(crypto.getRandomValues(new Uint8Array(16)));
|
||||||
|
|
||||||
const digest = await crypto.subtle.digest(
|
const digest = await crypto.subtle.digest(
|
||||||
"SHA-256",
|
"SHA-256",
|
||||||
|
|
@ -307,7 +305,7 @@ export class SignatureConstructor {
|
||||||
|
|
||||||
const signedString = `${requestOrMethod.toLowerCase()} ${encodeURIComponent(
|
const signedString = `${requestOrMethod.toLowerCase()} ${encodeURIComponent(
|
||||||
url.pathname,
|
url.pathname,
|
||||||
)} ${finalTimestamp.getTime() / 1000} ${arrayBufferToBase64(digest)}`;
|
)} ${finalNonce} ${arrayBufferToBase64(digest)}`;
|
||||||
|
|
||||||
const signature = await crypto.subtle.sign(
|
const signature = await crypto.subtle.sign(
|
||||||
"Ed25519",
|
"Ed25519",
|
||||||
|
|
@ -317,12 +315,9 @@ export class SignatureConstructor {
|
||||||
|
|
||||||
const signatureBase64 = arrayBufferToBase64(signature);
|
const signatureBase64 = arrayBufferToBase64(signature);
|
||||||
|
|
||||||
headers.set(
|
headers.set("X-Nonce", finalNonce);
|
||||||
"Versia-Signed-At",
|
headers.set("X-Signature", signatureBase64);
|
||||||
String(finalTimestamp.getTime() / 1000),
|
headers.set("X-Signed-By", this.authorUri.toString());
|
||||||
);
|
|
||||||
headers.set("Versia-Signature", signatureBase64);
|
|
||||||
headers.set("Versia-Signed-By", this.authorUri.toString());
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
headers,
|
headers,
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,7 @@ import type {
|
||||||
Follow,
|
Follow,
|
||||||
FollowAccept,
|
FollowAccept,
|
||||||
FollowReject,
|
FollowReject,
|
||||||
GroupExtensionSubscribe,
|
Group,
|
||||||
GroupExtensionSubscribeAccept,
|
|
||||||
GroupExtensionSubscribeReject,
|
|
||||||
GroupExtensionUnsubscribe,
|
|
||||||
InstanceMetadata,
|
InstanceMetadata,
|
||||||
LikeExtension,
|
LikeExtension,
|
||||||
Note,
|
Note,
|
||||||
|
|
@ -31,18 +28,7 @@ type ParserCallbacks<T> = {
|
||||||
"pub.versia:likes/Dislike": (dislike: DislikeExtension) => MaybePromise<T>;
|
"pub.versia:likes/Dislike": (dislike: DislikeExtension) => MaybePromise<T>;
|
||||||
delete: (undo: Delete) => MaybePromise<T>;
|
delete: (undo: Delete) => MaybePromise<T>;
|
||||||
instanceMetadata: (instanceMetadata: InstanceMetadata) => MaybePromise<T>;
|
instanceMetadata: (instanceMetadata: InstanceMetadata) => MaybePromise<T>;
|
||||||
"pub.versia:groups/Subscribe": (
|
group: (group: Group) => MaybePromise<T>;
|
||||||
groupSubscribe: GroupExtensionSubscribe,
|
|
||||||
) => MaybePromise<T>;
|
|
||||||
"pub.versia:groups/SubscribeAccept": (
|
|
||||||
groupSubscribeAccept: GroupExtensionSubscribeAccept,
|
|
||||||
) => MaybePromise<T>;
|
|
||||||
"pub.versia:groups/SubscribeReject": (
|
|
||||||
groupSubscribeReject: GroupExtensionSubscribeReject,
|
|
||||||
) => MaybePromise<T>;
|
|
||||||
"pub.versia:groups/Unsubscribe": (
|
|
||||||
groupUnsubscribe: GroupExtensionUnsubscribe,
|
|
||||||
) => MaybePromise<T>;
|
|
||||||
"pub.versia:reactions/Reaction": (
|
"pub.versia:reactions/Reaction": (
|
||||||
reaction: ReactionExtension,
|
reaction: ReactionExtension,
|
||||||
) => MaybePromise<T>;
|
) => MaybePromise<T>;
|
||||||
|
|
@ -196,52 +182,11 @@ export class RequestParserHandler {
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "pub.versia:groups/Subscribe": {
|
case "Group": {
|
||||||
const groupSubscribe = await this.validator.GroupSubscribe(
|
const group = await this.validator.Group(this.body);
|
||||||
this.body,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (callbacks["pub.versia:groups/Subscribe"]) {
|
if (callbacks.group) {
|
||||||
return await callbacks["pub.versia:groups/Subscribe"](
|
return await callbacks.group(group);
|
||||||
groupSubscribe,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "pub.versia:groups/SubscribeAccept": {
|
|
||||||
const groupSubscribeAccept =
|
|
||||||
await this.validator.GroupSubscribeAccept(this.body);
|
|
||||||
|
|
||||||
if (callbacks["pub.versia:groups/SubscribeAccept"]) {
|
|
||||||
return await callbacks["pub.versia:groups/SubscribeAccept"](
|
|
||||||
groupSubscribeAccept,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "pub.versia:groups/SubscribeReject": {
|
|
||||||
const groupSubscribeReject =
|
|
||||||
await this.validator.GroupSubscribeReject(this.body);
|
|
||||||
|
|
||||||
if (callbacks["pub.versia:groups/SubscribeReject"]) {
|
|
||||||
return await callbacks["pub.versia:groups/SubscribeReject"](
|
|
||||||
groupSubscribeReject,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "pub.versia:groups/Unsubscribe": {
|
|
||||||
const groupUnsubscribe = await this.validator.GroupUnsubscribe(
|
|
||||||
this.body,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (callbacks["pub.versia:groups/Unsubscribe"]) {
|
|
||||||
return await callbacks["pub.versia:groups/Unsubscribe"](
|
|
||||||
groupUnsubscribe,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://jsr.io/schema/config-file.v1.json",
|
"$schema": "https://jsr.io/schema/config-file.v1.json",
|
||||||
"name": "@versia/federation",
|
"name": "@versia/federation",
|
||||||
"version": "0.2.0",
|
"version": "0.1.4",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./index.ts",
|
".": "./index.ts",
|
||||||
"./types": "./types.ts",
|
"./types": "./types.ts",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@versia/federation",
|
"name": "@versia/federation",
|
||||||
"displayName": "Versia Federation",
|
"displayName": "Versia Federation",
|
||||||
"version": "0.2.0",
|
"version": "0.1.4",
|
||||||
"author": {
|
"author": {
|
||||||
"email": "jesse.wierzbinski@lysand.org",
|
"email": "jesse.wierzbinski@lysand.org",
|
||||||
"name": "Jesse Wierzbinski (CPlusPatch)",
|
"name": "Jesse Wierzbinski (CPlusPatch)",
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export {
|
||||||
FollowAcceptSchema as FollowAccept,
|
FollowAcceptSchema as FollowAccept,
|
||||||
FollowRejectSchema as FollowReject,
|
FollowRejectSchema as FollowReject,
|
||||||
FollowSchema as Follow,
|
FollowSchema as Follow,
|
||||||
URICollectionSchema as URICollection,
|
GroupSchema as Group,
|
||||||
InstanceMetadataSchema as InstanceMetadata,
|
InstanceMetadataSchema as InstanceMetadata,
|
||||||
NoteSchema as Note,
|
NoteSchema as Note,
|
||||||
UnfollowSchema as Unfollow,
|
UnfollowSchema as Unfollow,
|
||||||
|
|
@ -20,13 +20,6 @@ export {
|
||||||
export { ContentFormatSchema as ContentFormat } from "./schemas/content_format.ts";
|
export { ContentFormatSchema as ContentFormat } from "./schemas/content_format.ts";
|
||||||
export { ExtensionPropertySchema as EntityExtensionProperty } from "./schemas/extensions.ts";
|
export { ExtensionPropertySchema as EntityExtensionProperty } from "./schemas/extensions.ts";
|
||||||
export { CustomEmojiExtensionSchema as CustomEmojiExtension } from "./schemas/extensions/custom_emojis.ts";
|
export { CustomEmojiExtensionSchema as CustomEmojiExtension } from "./schemas/extensions/custom_emojis.ts";
|
||||||
export {
|
|
||||||
GroupSchema as GroupExtension,
|
|
||||||
GroupSubscribeSchema as GroupExtensionSubscribe,
|
|
||||||
GroupSubscribeAcceptSchema as GroupExtensionSubscribeAccept,
|
|
||||||
GroupSubscribeRejectSchema as GroupExtensionSubscribeReject,
|
|
||||||
GroupUnsubscribeSchema as GroupExtensionUnsubscribe,
|
|
||||||
} from "./schemas/extensions/groups.ts";
|
|
||||||
export {
|
export {
|
||||||
DislikeSchema as DislikeExtension,
|
DislikeSchema as DislikeExtension,
|
||||||
LikeSchema as LikeExtension,
|
LikeSchema as LikeExtension,
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ import { extensionRegex, isISOString, semverRegex } from "./regex.ts";
|
||||||
|
|
||||||
export const EntitySchema = z
|
export const EntitySchema = z
|
||||||
.object({
|
.object({
|
||||||
// biome-ignore lint/style/useNamingConvention:
|
|
||||||
$schema: z.string().url().optional().nullable(),
|
|
||||||
id: z.string().max(512),
|
id: z.string().max(512),
|
||||||
created_at: z
|
created_at: z
|
||||||
.string()
|
.string()
|
||||||
|
|
@ -39,17 +37,6 @@ export const NoteSchema = EntitySchema.extend({
|
||||||
.optional()
|
.optional()
|
||||||
.nullable(),
|
.nullable(),
|
||||||
content: TextOnlyContentFormatSchema.optional().nullable(),
|
content: TextOnlyContentFormatSchema.optional().nullable(),
|
||||||
collections: z.object({
|
|
||||||
replies: z.string().url(),
|
|
||||||
quotes: z.string().url(),
|
|
||||||
"pub.versia:reactions/Reactions": z
|
|
||||||
.string()
|
|
||||||
.url()
|
|
||||||
.optional()
|
|
||||||
.nullable(),
|
|
||||||
"pub.versia:likes/Likes": z.string().url().optional().nullable(),
|
|
||||||
"pub.versia:likes/Dislikes": z.string().url().optional().nullable(),
|
|
||||||
}),
|
|
||||||
device: z
|
device: z
|
||||||
.object({
|
.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
|
@ -85,6 +72,13 @@ export const NoteSchema = EntitySchema.extend({
|
||||||
replies_to: z.string().url().optional().nullable(),
|
replies_to: z.string().url().optional().nullable(),
|
||||||
subject: z.string().optional().nullable(),
|
subject: z.string().optional().nullable(),
|
||||||
extensions: ExtensionPropertySchema.extend({
|
extensions: ExtensionPropertySchema.extend({
|
||||||
|
"pub.versia:reactions": z
|
||||||
|
.object({
|
||||||
|
reactions: z.string().url(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
"pub.versia:polls": z
|
"pub.versia:polls": z
|
||||||
.object({
|
.object({
|
||||||
options: z.array(TextOnlyContentFormatSchema),
|
options: z.array(TextOnlyContentFormatSchema),
|
||||||
|
|
@ -125,10 +119,6 @@ export const CollectionSchema = z.object({
|
||||||
items: z.array(z.any()),
|
items: z.array(z.any()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const URICollectionSchema = CollectionSchema.extend({
|
|
||||||
items: z.array(z.string().url()),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const PublicKeyDataSchema = z
|
export const PublicKeyDataSchema = z
|
||||||
.object({
|
.object({
|
||||||
key: z.string().min(1),
|
key: z.string().min(1),
|
||||||
|
|
@ -157,8 +147,8 @@ export const UserSchema = EntitySchema.extend({
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
.regex(
|
.regex(
|
||||||
/^[a-zA-Z0-9_-]+$/,
|
/^[a-z0-9_-]+$/,
|
||||||
"must be alphanumeric, and may contain _ or -",
|
"must be lowercase, alphanumeric, and may contain _ or -",
|
||||||
),
|
),
|
||||||
header: ImageOnlyContentFormatSchema.optional().nullable(),
|
header: ImageOnlyContentFormatSchema.optional().nullable(),
|
||||||
public_key: PublicKeyDataSchema,
|
public_key: PublicKeyDataSchema,
|
||||||
|
|
@ -218,6 +208,14 @@ export const UnfollowSchema = EntitySchema.extend({
|
||||||
followee: z.string().url(),
|
followee: z.string().url(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const GroupSchema = EntitySchema.extend({
|
||||||
|
type: z.literal("Group"),
|
||||||
|
name: TextOnlyContentFormatSchema.optional().nullable(),
|
||||||
|
description: TextOnlyContentFormatSchema.optional().nullable(),
|
||||||
|
members: z.string().url(),
|
||||||
|
notes: z.string().url().optional().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
export const InstanceMetadataSchema = EntitySchema.extend({
|
export const InstanceMetadataSchema = EntitySchema.extend({
|
||||||
type: z.literal("InstanceMetadata"),
|
type: z.literal("InstanceMetadata"),
|
||||||
id: z.null().optional(),
|
id: z.null().optional(),
|
||||||
|
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
import { z } from "zod";
|
|
||||||
import { EntitySchema } from "../base.ts";
|
|
||||||
import { TextOnlyContentFormatSchema } from "../content_format.ts";
|
|
||||||
|
|
||||||
export const GroupSchema = EntitySchema.extend({
|
|
||||||
type: z.literal("pub.versia:groups/Group"),
|
|
||||||
name: TextOnlyContentFormatSchema.optional().nullable(),
|
|
||||||
description: TextOnlyContentFormatSchema.optional().nullable(),
|
|
||||||
open: z.boolean().optional().nullable(),
|
|
||||||
members: z.string().url(),
|
|
||||||
notes: z.string().url().optional().nullable(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const GroupSubscribeSchema = EntitySchema.extend({
|
|
||||||
type: z.literal("pub.versia:groups/Subscribe"),
|
|
||||||
uri: z.null().optional(),
|
|
||||||
subscriber: z.string().url(),
|
|
||||||
group: z.string().url(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const GroupUnsubscribeSchema = EntitySchema.extend({
|
|
||||||
type: z.literal("pub.versia:groups/Unsubscribe"),
|
|
||||||
uri: z.null().optional(),
|
|
||||||
subscriber: z.string().url(),
|
|
||||||
group: z.string().url(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const GroupSubscribeAcceptSchema = EntitySchema.extend({
|
|
||||||
type: z.literal("pub.versia:groups/SubscribeAccept"),
|
|
||||||
uri: z.null().optional(),
|
|
||||||
subscriber: z.string().url(),
|
|
||||||
group: z.string().url(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const GroupSubscribeRejectSchema = EntitySchema.extend({
|
|
||||||
type: z.literal("pub.versia:groups/SubscribeReject"),
|
|
||||||
uri: z.null().optional(),
|
|
||||||
subscriber: z.string().url(),
|
|
||||||
group: z.string().url(),
|
|
||||||
});
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
AudioOnlyContentFormatSchema,
|
AudioOnlyContentFormatSchema,
|
||||||
ImageOnlyContentFormatSchema,
|
ImageOnlyContentFormatSchema,
|
||||||
} from "../content_format.ts";
|
} from "../content_format.ts";
|
||||||
import { ianaTimezoneRegex, isISOString } from "../regex.ts";
|
import { isISOString } from "../regex.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Vanity extension entity
|
* @description Vanity extension entity
|
||||||
|
|
@ -103,10 +103,5 @@ export const VanityExtensionSchema = z
|
||||||
.nullable(),
|
.nullable(),
|
||||||
location: z.string().optional().nullable(),
|
location: z.string().optional().nullable(),
|
||||||
aliases: z.array(z.string().url()).optional().nullable(),
|
aliases: z.array(z.string().url()).optional().nullable(),
|
||||||
timezone: z
|
|
||||||
.string()
|
|
||||||
.regex(ianaTimezoneRegex, "must be a valid IANA timezone")
|
|
||||||
.optional()
|
|
||||||
.nullable(),
|
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
|
||||||
|
|
@ -66,5 +66,3 @@ export const isISOString = (val: string | Date) => {
|
||||||
const d = new Date(val);
|
const d = new Date(val);
|
||||||
return !Number.isNaN(d.valueOf());
|
return !Number.isNaN(d.valueOf());
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ianaTimezoneRegex = /^(?:[A-Za-z]+(?:\/[A-Za-z_]+)+|UTC)$/;
|
|
||||||
|
|
|
||||||
|
|
@ -11,22 +11,15 @@ import type {
|
||||||
FollowAcceptSchema,
|
FollowAcceptSchema,
|
||||||
FollowRejectSchema,
|
FollowRejectSchema,
|
||||||
FollowSchema,
|
FollowSchema,
|
||||||
|
GroupSchema,
|
||||||
InstanceMetadataSchema,
|
InstanceMetadataSchema,
|
||||||
NoteSchema,
|
NoteSchema,
|
||||||
URICollectionSchema,
|
|
||||||
UnfollowSchema,
|
UnfollowSchema,
|
||||||
UserSchema,
|
UserSchema,
|
||||||
} from "./schemas/base.ts";
|
} from "./schemas/base.ts";
|
||||||
import type { ContentFormatSchema } from "./schemas/content_format.ts";
|
import type { ContentFormatSchema } from "./schemas/content_format.ts";
|
||||||
import type { ExtensionPropertySchema } from "./schemas/extensions.ts";
|
import type { ExtensionPropertySchema } from "./schemas/extensions.ts";
|
||||||
import type { CustomEmojiExtensionSchema } from "./schemas/extensions/custom_emojis.ts";
|
import type { CustomEmojiExtensionSchema } from "./schemas/extensions/custom_emojis.ts";
|
||||||
import type {
|
|
||||||
GroupSchema,
|
|
||||||
GroupSubscribeAcceptSchema,
|
|
||||||
GroupSubscribeRejectSchema,
|
|
||||||
GroupSubscribeSchema,
|
|
||||||
GroupUnsubscribeSchema,
|
|
||||||
} from "./schemas/extensions/groups.ts";
|
|
||||||
import type { DislikeSchema, LikeSchema } from "./schemas/extensions/likes.ts";
|
import type { DislikeSchema, LikeSchema } from "./schemas/extensions/likes.ts";
|
||||||
import type { VoteSchema } from "./schemas/extensions/polls.ts";
|
import type { VoteSchema } from "./schemas/extensions/polls.ts";
|
||||||
import type { ReactionSchema } from "./schemas/extensions/reactions.ts";
|
import type { ReactionSchema } from "./schemas/extensions/reactions.ts";
|
||||||
|
|
@ -40,7 +33,6 @@ type InferType<T extends AnyZod> = z.infer<T>;
|
||||||
|
|
||||||
export type Note = InferType<typeof NoteSchema>;
|
export type Note = InferType<typeof NoteSchema>;
|
||||||
export type Collection = InferType<typeof CollectionSchema>;
|
export type Collection = InferType<typeof CollectionSchema>;
|
||||||
export type URICollection = InferType<typeof URICollectionSchema>;
|
|
||||||
export type EntityExtensionProperty = InferType<typeof ExtensionPropertySchema>;
|
export type EntityExtensionProperty = InferType<typeof ExtensionPropertySchema>;
|
||||||
export type VanityExtension = InferType<typeof VanityExtensionSchema>;
|
export type VanityExtension = InferType<typeof VanityExtensionSchema>;
|
||||||
export type User = InferType<typeof UserSchema>;
|
export type User = InferType<typeof UserSchema>;
|
||||||
|
|
@ -51,19 +43,9 @@ export type ContentFormat = InferType<typeof ContentFormatSchema>;
|
||||||
export type CustomEmojiExtension = InferType<typeof CustomEmojiExtensionSchema>;
|
export type CustomEmojiExtension = InferType<typeof CustomEmojiExtensionSchema>;
|
||||||
export type Entity = InferType<typeof EntitySchema>;
|
export type Entity = InferType<typeof EntitySchema>;
|
||||||
export type Delete = InferType<typeof DeleteSchema>;
|
export type Delete = InferType<typeof DeleteSchema>;
|
||||||
|
export type Group = InferType<typeof GroupSchema>;
|
||||||
export type InstanceMetadata = InferType<typeof InstanceMetadataSchema>;
|
export type InstanceMetadata = InferType<typeof InstanceMetadataSchema>;
|
||||||
export type Unfollow = InferType<typeof UnfollowSchema>;
|
export type Unfollow = InferType<typeof UnfollowSchema>;
|
||||||
export type GroupExtension = InferType<typeof GroupSchema>;
|
|
||||||
export type GroupExtensionSubscribe = InferType<typeof GroupSubscribeSchema>;
|
|
||||||
export type GroupExtensionSubscribeAccept = InferType<
|
|
||||||
typeof GroupSubscribeAcceptSchema
|
|
||||||
>;
|
|
||||||
export type GroupExtensionSubscribeReject = InferType<
|
|
||||||
typeof GroupSubscribeRejectSchema
|
|
||||||
>;
|
|
||||||
export type GroupExtensionUnsubscribe = InferType<
|
|
||||||
typeof GroupUnsubscribeSchema
|
|
||||||
>;
|
|
||||||
export type LikeExtension = InferType<typeof LikeSchema>;
|
export type LikeExtension = InferType<typeof LikeSchema>;
|
||||||
export type DislikeExtension = InferType<typeof DislikeSchema>;
|
export type DislikeExtension = InferType<typeof DislikeSchema>;
|
||||||
export type PollVoteExtension = InferType<typeof VoteSchema>;
|
export type PollVoteExtension = InferType<typeof VoteSchema>;
|
||||||
|
|
|
||||||
|
|
@ -7,22 +7,15 @@ import {
|
||||||
FollowAcceptSchema,
|
FollowAcceptSchema,
|
||||||
FollowRejectSchema,
|
FollowRejectSchema,
|
||||||
FollowSchema,
|
FollowSchema,
|
||||||
|
GroupSchema,
|
||||||
InstanceMetadataSchema,
|
InstanceMetadataSchema,
|
||||||
NoteSchema,
|
NoteSchema,
|
||||||
URICollectionSchema,
|
|
||||||
UnfollowSchema,
|
UnfollowSchema,
|
||||||
UserSchema,
|
UserSchema,
|
||||||
} from "./schemas/base.ts";
|
} from "./schemas/base.ts";
|
||||||
import { ContentFormatSchema } from "./schemas/content_format.ts";
|
import { ContentFormatSchema } from "./schemas/content_format.ts";
|
||||||
import { ExtensionPropertySchema } from "./schemas/extensions.ts";
|
import { ExtensionPropertySchema } from "./schemas/extensions.ts";
|
||||||
import { CustomEmojiExtensionSchema } from "./schemas/extensions/custom_emojis.ts";
|
import { CustomEmojiExtensionSchema } from "./schemas/extensions/custom_emojis.ts";
|
||||||
import {
|
|
||||||
GroupSchema,
|
|
||||||
GroupSubscribeAcceptSchema,
|
|
||||||
GroupSubscribeRejectSchema,
|
|
||||||
GroupSubscribeSchema,
|
|
||||||
GroupUnsubscribeSchema,
|
|
||||||
} from "./schemas/extensions/groups.ts";
|
|
||||||
import { DislikeSchema, LikeSchema } from "./schemas/extensions/likes.ts";
|
import { DislikeSchema, LikeSchema } from "./schemas/extensions/likes.ts";
|
||||||
import { VoteSchema } from "./schemas/extensions/polls.ts";
|
import { VoteSchema } from "./schemas/extensions/polls.ts";
|
||||||
import { ReactionSchema } from "./schemas/extensions/reactions.ts";
|
import { ReactionSchema } from "./schemas/extensions/reactions.ts";
|
||||||
|
|
@ -39,22 +32,18 @@ import type {
|
||||||
Follow,
|
Follow,
|
||||||
FollowAccept,
|
FollowAccept,
|
||||||
FollowReject,
|
FollowReject,
|
||||||
GroupExtension,
|
Group,
|
||||||
GroupExtensionSubscribe,
|
|
||||||
GroupExtensionSubscribeAccept,
|
|
||||||
GroupExtensionSubscribeReject,
|
|
||||||
GroupExtensionUnsubscribe,
|
|
||||||
InstanceMetadata,
|
InstanceMetadata,
|
||||||
LikeExtension,
|
LikeExtension,
|
||||||
Note,
|
Note,
|
||||||
PollVoteExtension,
|
PollVoteExtension,
|
||||||
ReactionExtension,
|
ReactionExtension,
|
||||||
ShareExtension,
|
ShareExtension,
|
||||||
URICollection,
|
|
||||||
Unfollow,
|
Unfollow,
|
||||||
User,
|
User,
|
||||||
VanityExtension,
|
VanityExtension,
|
||||||
} from "./types.ts";
|
} from "./types.ts";
|
||||||
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: Used only as a base type
|
// biome-ignore lint/suspicious/noExplicitAny: Used only as a base type
|
||||||
type AnyZod = z.ZodType<any, any, any>;
|
type AnyZod = z.ZodType<any, any, any>;
|
||||||
|
|
||||||
|
|
@ -121,15 +110,6 @@ export class EntityValidator {
|
||||||
return this.validate(CollectionSchema, data);
|
return this.validate(CollectionSchema, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a URICollection entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public URICollection(data: unknown): Promise<URICollection> {
|
|
||||||
return this.validate(URICollectionSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates a VanityExtension entity.
|
* Validates a VanityExtension entity.
|
||||||
* @param data - The data to validate
|
* @param data - The data to validate
|
||||||
|
|
@ -227,50 +207,10 @@ export class EntityValidator {
|
||||||
* @param data - The data to validate
|
* @param data - The data to validate
|
||||||
* @returns A promise that resolves to the validated data.
|
* @returns A promise that resolves to the validated data.
|
||||||
*/
|
*/
|
||||||
public Group(data: unknown): Promise<GroupExtension> {
|
public Group(data: unknown): Promise<Group> {
|
||||||
return this.validate(GroupSchema, data);
|
return this.validate(GroupSchema, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a GroupSubscribe entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public GroupSubscribe(data: unknown): Promise<GroupExtensionSubscribe> {
|
|
||||||
return this.validate(GroupSubscribeSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a GroupSubscribeAccept entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public GroupSubscribeAccept(
|
|
||||||
data: unknown,
|
|
||||||
): Promise<GroupExtensionSubscribeAccept> {
|
|
||||||
return this.validate(GroupSubscribeAcceptSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a GroupSubscribeReject entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public GroupSubscribeReject(
|
|
||||||
data: unknown,
|
|
||||||
): Promise<GroupExtensionSubscribeReject> {
|
|
||||||
return this.validate(GroupSubscribeRejectSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a GroupUnsubscribe entity.
|
|
||||||
* @param data - The data to validate
|
|
||||||
* @returns A promise that resolves to the validated data.
|
|
||||||
*/
|
|
||||||
public GroupUnsubscribe(data: unknown): Promise<GroupExtensionUnsubscribe> {
|
|
||||||
return this.validate(GroupUnsubscribeSchema, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates an InstanceMetadata entity.
|
* Validates an InstanceMetadata entity.
|
||||||
* @param data - The data to validate
|
* @param data - The data to validate
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue