feat(api): Add initial Push Notifications support

This commit is contained in:
Jesse Wierzbinski 2025-01-02 01:29:33 +01:00
parent acd2bcb469
commit d096ab830c
No known key found for this signature in database
21 changed files with 3301 additions and 3 deletions

View file

@ -8,6 +8,7 @@ Versia Server `0.8.0` is fully backwards compatible with `0.7.0`.
- Outbound federation, inbox processing and data fetching are now handled by a queue system (like most federated software).
- Added an administration UI for managing the queue.
- Added [Push Notifications](https://docs.joinmastodon.org/methods/push) support.
- Upgraded Bun to `1.1.42`.
- Implemented support for the [**Instance Messaging Extension**](https://versia.pub/extensions/instance-messaging)
- Implement [**Shared Inboxes**](https://versia.pub/federation#inboxes) support.

View file

@ -0,0 +1,31 @@
import { auth } from "@/api";
import { createRoute, z } from "@hono/zod-openapi";
import { RolePermissions } from "~/drizzle/schema";
export const route = createRoute({
method: "delete",
path: "/api/v1/push/subscription",
summary: "Remove current subscription",
description: "Removes the current Web Push API subscription.",
externalDocs: {
url: "https://docs.joinmastodon.org/methods/push/#delete",
},
middleware: [
auth({
auth: true,
permissions: [RolePermissions.UsePushNotifications],
scopes: ["push"],
}),
] as const,
responses: {
200: {
description:
"PushSubscription successfully deleted or did not exist previously.",
content: {
"application/json": {
schema: z.object({}),
},
},
},
},
});

View file

@ -0,0 +1,23 @@
import { apiRoute } from "@/api";
import { PushSubscription } from "@versia/kit/db";
import { ApiError } from "~/classes/errors/api-error";
import { route } from "./index.delete.schema";
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { token } = context.get("auth");
const ps = await PushSubscription.fromToken(token);
if (!ps) {
throw new ApiError(
404,
"No push subscription associated with this access token",
);
}
await ps.delete();
return context.json({}, 200);
}),
);

View file

@ -0,0 +1,85 @@
import { auth } from "@/api";
import { createRoute, z } from "@hono/zod-openapi";
import { PushSubscription } from "@versia/kit/db";
import { RolePermissions } from "~/drizzle/schema";
export const WebPushSubscriptionInput = z
.object({
subscription: z.object({
endpoint: z.string().url().openapi({
example: "https://yourdomain.example/listener",
description: "Where push alerts will be sent to.",
}),
keys: z
.object({
p256dh: z.string().base64().openapi({
description:
"User agent public key. Base64 encoded string of a public key from a ECDH keypair using the prime256v1 curve.",
example:
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoKCJeHCy69ywHcb3dAR/T8Sud5ljSFHJkuiR6it1ycqAjGTe5F1oZ0ef5QiMX/zdQ+d4jSKiO7RztIz+o/eGuQ==",
}),
auth: z.string().base64().length(24).openapi({
description:
"Auth secret. Base64 encoded string of 16 bytes of random data.",
example: "u67u09PXZW4ncK9l9mAXkA==",
}),
})
.strict(),
}),
data: z
.object({
policy: z
.enum(["all", "followed", "follower", "none"])
.default("all")
.openapi({
description:
"Specify whether to receive push notifications from all, followed, follower, or none users.",
}),
alerts: PushSubscription.schema.shape.alerts,
})
.strict()
.default({
policy: "all",
alerts: {
mention: false,
favourite: false,
reblog: false,
follow: false,
poll: false,
follow_request: false,
status: false,
update: false,
"admin.sign_up": false,
"admin.report": false,
},
}),
})
.strict();
export const route = createRoute({
method: "get",
path: "/api/v1/push/subscription",
summary: "Get current subscription",
description:
"View the PushSubscription currently associated with this access token.",
externalDocs: {
url: "https://docs.joinmastodon.org/methods/push/#get",
},
middleware: [
auth({
auth: true,
permissions: [RolePermissions.UsePushNotifications],
scopes: ["push"],
}),
] as const,
responses: {
200: {
description: "WebPushSubscription",
content: {
"application/json": {
schema: PushSubscription.schema,
},
},
},
},
});

View file

@ -0,0 +1,21 @@
import { apiRoute } from "@/api";
import { PushSubscription } from "@versia/kit/db";
import { ApiError } from "~/classes/errors/api-error";
import { route } from "./index.get.schema";
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { token } = context.get("auth");
const ps = await PushSubscription.fromToken(token);
if (!ps) {
throw new ApiError(
404,
"No push subscription associated with this access token",
);
}
return context.json(ps.toApi(), 200);
}),
);

View file

@ -0,0 +1,44 @@
import { auth, jsonOrForm } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { PushSubscription } from "@versia/kit/db";
import { RolePermissions } from "~/drizzle/schema";
import { WebPushSubscriptionInput } from "./index.get.schema";
export const route = createRoute({
method: "post",
path: "/api/v1/push/subscription",
summary: "Subscribe to push notifications",
description:
"Add a Web Push API subscription to receive notifications. Each access token can have one push subscription. If you create a new subscription, the old subscription is deleted.",
externalDocs: {
url: "https://docs.joinmastodon.org/methods/push/#create",
},
middleware: [
auth({
auth: true,
permissions: [RolePermissions.UsePushNotifications],
scopes: ["push"],
}),
jsonOrForm(),
] as const,
request: {
body: {
content: {
"application/json": {
schema: WebPushSubscriptionInput,
},
},
},
},
responses: {
200: {
description:
"A new PushSubscription has been generated, which will send the requested alerts to your endpoint.",
content: {
"application/json": {
schema: PushSubscription.schema,
},
},
},
},
});

View file

@ -0,0 +1,45 @@
import { apiRoute } from "@/api";
import { PushSubscription } from "@versia/kit/db";
import { ApiError } from "~/classes/errors/api-error";
import { RolePermissions } from "~/drizzle/schema";
import { route } from "./index.post.schema";
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { user, token } = context.get("auth");
const { subscription, data } = context.req.valid("json");
if (
data.alerts["admin.report"] &&
!user.hasPermission(RolePermissions.ManageReports)
) {
throw new ApiError(
403,
`You do not have the '${RolePermissions.ManageReports}' permission to receive report alerts`,
);
}
if (
data.alerts["admin.sign_up"] &&
!user.hasPermission(RolePermissions.ManageAccounts)
) {
throw new ApiError(
403,
`You do not have the '${RolePermissions.ManageAccounts}' permission to receive sign-up alerts`,
);
}
await PushSubscription.clearAllOfToken(token);
const ps = await PushSubscription.insert({
alerts: data.alerts,
policy: data.policy,
endpoint: subscription.endpoint,
publicKey: subscription.keys.p256dh,
authSecret: subscription.keys.auth,
tokenId: token.id,
});
return context.json(ps.toApi(), 200);
}),
);

View file

@ -0,0 +1,43 @@
import { auth, jsonOrForm } from "@/api";
import { createRoute } from "@hono/zod-openapi";
import { PushSubscription } from "@versia/kit/db";
import { RolePermissions } from "~/drizzle/schema";
import { WebPushSubscriptionInput } from "./index.get.schema";
export const route = createRoute({
method: "put",
path: "/api/v1/push/subscription",
summary: "Change types of notifications",
description:
"Updates the current push subscription. Only the data part can be updated. To change fundamentals, a new subscription must be created instead.",
externalDocs: {
url: "https://docs.joinmastodon.org/methods/push/#update",
},
middleware: [
auth({
auth: true,
permissions: [RolePermissions.UsePushNotifications],
scopes: ["push"],
}),
jsonOrForm(),
] as const,
request: {
body: {
content: {
"application/json": {
schema: WebPushSubscriptionInput.shape.data,
},
},
},
},
responses: {
200: {
description: "The WebPushSubscription has been updated.",
content: {
"application/json": {
schema: PushSubscription.schema,
},
},
},
},
});

View file

@ -0,0 +1,51 @@
import { apiRoute } from "@/api";
import { PushSubscription } from "@versia/kit/db";
import { ApiError } from "~/classes/errors/api-error";
import { RolePermissions } from "~/drizzle/schema";
import { route } from "./index.put.schema";
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { user, token } = context.get("auth");
const { alerts, policy } = context.req.valid("json");
const ps = await PushSubscription.fromToken(token);
if (!ps) {
throw new ApiError(
404,
"No push subscription associated with this access token",
);
}
if (
alerts["admin.report"] &&
!user.hasPermission(RolePermissions.ManageReports)
) {
throw new ApiError(
403,
`You do not have the '${RolePermissions.ManageReports}' permission to receive report alerts`,
);
}
if (
alerts["admin.sign_up"] &&
!user.hasPermission(RolePermissions.ManageAccounts)
) {
throw new ApiError(
403,
`You do not have the '${RolePermissions.ManageAccounts}' permission to receive sign-up alerts`,
);
}
await ps.update({
policy,
alerts: {
...ps.data.alerts,
...alerts,
},
});
return context.json(ps.toApi(), 200);
}),
);

View file

@ -0,0 +1,304 @@
import { afterAll, beforeEach, describe, expect, test } from "bun:test";
import { PushSubscription } from "@versia/kit/db";
import { fakeRequest, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(2);
afterAll(async () => {
await deleteUsers();
});
beforeEach(async () => {
await PushSubscription.clearAllOfToken(tokens[0]);
await PushSubscription.clearAllOfToken(tokens[1]);
});
describe("/api/v1/push/subscriptions", () => {
test("should create a push subscription", async () => {
const res = await fakeRequest("/api/v1/push/subscription", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
data: {
alerts: {
update: true,
},
policy: "all",
},
subscription: {
endpoint: "https://example.com",
keys: {
p256dh: "test",
auth: "testthatis24charactersha",
},
},
}),
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body).toMatchObject({
endpoint: "https://example.com",
alerts: {
update: true,
},
});
});
test("should retrieve the same push subscription", async () => {
await PushSubscription.insert({
endpoint: "https://example.com",
alerts: {
update: true,
"admin.report": false,
"admin.sign_up": false,
favourite: false,
follow: false,
follow_request: false,
mention: false,
poll: false,
reblog: false,
status: false,
},
policy: "all",
authSecret: "test",
publicKey: "test",
tokenId: tokens[1].id,
});
const res = await fakeRequest("/api/v1/push/subscription", {
headers: {
Authorization: `Bearer ${tokens[1].data.accessToken}`,
},
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body).toMatchObject({
endpoint: "https://example.com",
alerts: {
update: true,
},
});
});
test("should update a push subscription", async () => {
await PushSubscription.insert({
endpoint: "https://example.com",
alerts: {
update: true,
"admin.report": false,
"admin.sign_up": false,
favourite: false,
follow: false,
follow_request: false,
mention: false,
poll: false,
reblog: false,
status: false,
},
policy: "all",
authSecret: "test",
publicKey: "test",
tokenId: tokens[0].id,
});
const res = await fakeRequest("/api/v1/push/subscription", {
method: "PUT",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
alerts: {
update: false,
favourite: true,
},
policy: "follower",
}),
});
expect(res.status).toBe(200);
expect(await res.json()).toMatchObject({
alerts: {
update: false,
favourite: true,
},
});
});
describe("permissions", () => {
test("should not allow watching admin reports without permissions", async () => {
const res = await fakeRequest("/api/v1/push/subscription", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
data: {
alerts: {
"admin.report": true,
},
policy: "all",
},
subscription: {
endpoint: "https://example.com",
keys: {
p256dh: "test",
auth: "testthatis24charactersha",
},
},
}),
});
expect(res.status).toBe(403);
expect(await res.json()).toMatchObject({
error: expect.stringContaining("permission"),
});
});
test("should allow watching admin reports with permissions", async () => {
await users[0].update({
isAdmin: true,
});
const res = await fakeRequest("/api/v1/push/subscription", {
method: "POST",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
data: {
alerts: {
"admin.report": true,
},
policy: "all",
},
subscription: {
endpoint: "https://example.com",
keys: {
p256dh: "test",
auth: "testthatis24charactersha",
},
},
}),
});
expect(res.status).toBe(200);
await users[0].update({
isAdmin: false,
});
});
test("should not allow editing to add admin reports without permissions", async () => {
await PushSubscription.insert({
endpoint: "https://example.com",
alerts: {
update: true,
"admin.report": false,
"admin.sign_up": false,
favourite: false,
follow: false,
follow_request: false,
mention: false,
poll: false,
reblog: false,
status: false,
},
policy: "all",
authSecret: "test",
publicKey: "test",
tokenId: tokens[0].id,
});
const res = await fakeRequest("/api/v1/push/subscription", {
method: "PUT",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
alerts: {
"admin.report": true,
},
policy: "all",
}),
});
expect(res.status).toBe(403);
expect(await res.json()).toMatchObject({
error: expect.stringContaining("permission"),
});
});
test("should allow editing to add admin reports with permissions", async () => {
await users[0].update({
isAdmin: true,
});
await PushSubscription.insert({
endpoint: "https://example.com",
alerts: {
update: true,
"admin.report": false,
"admin.sign_up": false,
favourite: false,
follow: false,
follow_request: false,
mention: false,
poll: false,
reblog: false,
status: false,
},
policy: "all",
authSecret: "test",
publicKey: "test",
tokenId: tokens[0].id,
});
const res = await fakeRequest("/api/v1/push/subscription", {
method: "PUT",
headers: {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
alerts: {
"admin.report": true,
},
policy: "all",
}),
});
expect(res.status).toBe(200);
expect(await res.json()).toMatchObject({
alerts: {
update: true,
"admin.report": true,
"admin.sign_up": false,
favourite: false,
follow: false,
follow_request: false,
mention: false,
poll: false,
reblog: false,
status: false,
},
});
await users[0].update({
isAdmin: false,
});
});
});
});

2
app.ts
View file

@ -110,7 +110,7 @@ export const appFactory = async (): Promise<OpenAPIHono<HonoEnv>> => {
const route: ApiRouteExports = await import(path);
if (!route.default) {
throw new Error(`Route ${path} does not have the correct exports.`);
continue;
}
route.default(app);

BIN
bun.lockb

Binary file not shown.

View file

@ -0,0 +1,256 @@
import { z } from "@hono/zod-openapi";
import type {
Alerts,
PushSubscription as ApiPushSubscription,
} from "@versia/client/types";
import { type Token, db } from "@versia/kit/db";
import { PushSubscriptions } from "@versia/kit/tables";
import {
type InferInsertModel,
type InferSelectModel,
type SQL,
desc,
eq,
inArray,
} from "drizzle-orm";
import { BaseInterface } from "./base.ts";
type PushSubscriptionType = InferSelectModel<typeof PushSubscriptions>;
export class PushSubscription extends BaseInterface<
typeof PushSubscriptions,
PushSubscriptionType
> {
public static schema = z.object({
id: z.string().uuid().openapi({
example: "24eb1891-accc-43b4-b213-478e37d525b4",
description: "The ID of the Web Push subscription in the database.",
}),
endpoint: z.string().url().openapi({
example: "https://yourdomain.example/listener",
description: "Where push alerts will be sent to.",
}),
alerts: z
.object({
mention: z.boolean().optional().openapi({
example: true,
description: "Receive mention notifications?",
}),
favourite: z.boolean().optional().openapi({
example: true,
description: "Receive favourite notifications?",
}),
reblog: z.boolean().optional().openapi({
example: true,
description: "Receive reblog notifications?",
}),
follow: z.boolean().optional().openapi({
example: true,
description: "Receive follow notifications?",
}),
poll: z.boolean().optional().openapi({
example: false,
description: "Receive poll notifications?",
}),
follow_request: z.boolean().optional().openapi({
example: false,
description: "Receive follow request notifications?",
}),
status: z.boolean().optional().openapi({
example: false,
description:
"Receive new subscribed account notifications?",
}),
update: z.boolean().optional().openapi({
example: false,
description: "Receive status edited notifications?",
}),
"admin.sign_up": z.boolean().optional().openapi({
example: false,
description:
"Receive new user signup notifications? Must have a role with the appropriate permissions.",
}),
"admin.report": z.boolean().optional().openapi({
example: false,
description:
"Receive new report notifications? Must have a role with the appropriate permissions.",
}),
})
.default({})
.openapi({
example: {
mention: true,
favourite: true,
reblog: true,
follow: true,
poll: false,
follow_request: false,
status: false,
update: false,
"admin.sign_up": false,
"admin.report": false,
},
description:
"Which alerts should be delivered to the endpoint.",
}),
server_key: z.string().openapi({
example:
"BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
description: "The streaming servers VAPID key.",
}),
});
public static $type: PushSubscriptionType;
public async reload(): Promise<void> {
const reloaded = await PushSubscription.fromId(this.data.id);
if (!reloaded) {
throw new Error("Failed to reload subscription");
}
this.data = reloaded.data;
}
public static async fromId(
id: string | null,
): Promise<PushSubscription | null> {
if (!id) {
return null;
}
return await PushSubscription.fromSql(eq(PushSubscriptions.id, id));
}
public static async fromIds(ids: string[]): Promise<PushSubscription[]> {
return await PushSubscription.manyFromSql(
inArray(PushSubscriptions.id, ids),
);
}
public static async fromToken(
token: Token,
): Promise<PushSubscription | null> {
return await PushSubscription.fromSql(
eq(PushSubscriptions.tokenId, token.id),
);
}
public static async fromSql(
sql: SQL<unknown> | undefined,
orderBy: SQL<unknown> | undefined = desc(PushSubscriptions.id),
): Promise<PushSubscription | null> {
const found = await db.query.PushSubscriptions.findFirst({
where: sql,
orderBy,
});
if (!found) {
return null;
}
return new PushSubscription(found);
}
public static async manyFromSql(
sql: SQL<unknown> | undefined,
orderBy: SQL<unknown> | undefined = desc(PushSubscriptions.id),
limit?: number,
offset?: number,
extra?: Parameters<typeof db.query.PushSubscriptions.findMany>[0],
): Promise<PushSubscription[]> {
const found = await db.query.PushSubscriptions.findMany({
where: sql,
orderBy,
limit,
offset,
with: extra?.with,
});
return found.map((s) => new PushSubscription(s));
}
public async update(
newSubscription: Partial<PushSubscriptionType>,
): Promise<PushSubscriptionType> {
await db
.update(PushSubscriptions)
.set(newSubscription)
.where(eq(PushSubscriptions.id, this.id));
const updated = await PushSubscription.fromId(this.data.id);
if (!updated) {
throw new Error("Failed to update subscription");
}
this.data = updated.data;
return updated.data;
}
public save(): Promise<PushSubscriptionType> {
return this.update(this.data);
}
public static async clearAllOfToken(token: Token): Promise<void> {
await db
.delete(PushSubscriptions)
.where(eq(PushSubscriptions.tokenId, token.id));
}
public async delete(ids?: string[]): Promise<void> {
if (Array.isArray(ids)) {
await db
.delete(PushSubscriptions)
.where(inArray(PushSubscriptions.id, ids));
} else {
await db
.delete(PushSubscriptions)
.where(eq(PushSubscriptions.id, this.id));
}
}
public static async insert(
data: InferInsertModel<typeof PushSubscriptions>,
): Promise<PushSubscription> {
const inserted = (
await db.insert(PushSubscriptions).values(data).returning()
)[0];
const subscription = await PushSubscription.fromId(inserted.id);
if (!subscription) {
throw new Error("Failed to insert subscription");
}
return subscription;
}
public get id(): string {
return this.data.id;
}
public getAlerts(): Alerts {
return {
mention: this.data.alerts.mention ?? false,
favourite: this.data.alerts.favourite ?? false,
reblog: this.data.alerts.reblog ?? false,
follow: this.data.alerts.follow ?? false,
poll: this.data.alerts.poll ?? false,
follow_request: this.data.alerts.follow_request ?? false,
status: this.data.alerts.status ?? false,
update: this.data.alerts.update ?? false,
"admin.sign_up": this.data.alerts["admin.sign_up"] ?? false,
"admin.report": this.data.alerts["admin.report"] ?? false,
};
}
public toApi(): ApiPushSubscription {
return {
id: this.data.id,
alerts: this.getAlerts(),
endpoint: this.data.endpoint,
// FIXME: Add real key
server_key: "",
};
}
}

View file

@ -61,6 +61,7 @@ ManageFollows: "follows",
ManageOwnFollows: "owner:follow",
ManageOwnApps: "owner:app",
Search: "search",
UsePushNotifications: "push_notifications",
ViewPublicTimelines: "public_timelines",
ViewPrimateTimelines: "private_timelines",
IgnoreRateLimits: "ignore_rate_limits",

View file

@ -0,0 +1,14 @@
CREATE TABLE "PushSubscriptions" (
"id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL,
"endpoint" text NOT NULL,
"public_key" text NOT NULL,
"auth_secret" text NOT NULL,
"alerts" jsonb NOT NULL,
"policy" text NOT NULL,
"created_at" timestamp(3) DEFAULT now() NOT NULL,
"updated_at" timestamp(3) DEFAULT now() NOT NULL,
"tokenId" uuid NOT NULL,
CONSTRAINT "PushSubscriptions_tokenId_unique" UNIQUE("tokenId")
);
--> statement-breakpoint
ALTER TABLE "PushSubscriptions" ADD CONSTRAINT "PushSubscriptions_tokenId_Tokens_id_fk" FOREIGN KEY ("tokenId") REFERENCES "public"."Tokens"("id") ON DELETE cascade ON UPDATE cascade;

File diff suppressed because it is too large Load diff

View file

@ -281,6 +281,13 @@
"when": 1734555117380,
"tag": "0039_special_serpent_society",
"breakpoints": true
},
{
"idx": 40,
"version": "7",
"when": 1735776034097,
"tag": "0040_good_nocturne",
"breakpoints": true
}
]
}

View file

@ -67,6 +67,49 @@ export const Emojis = pgTable("Emojis", {
category: text("category"),
});
export const PushSubscriptions = pgTable("PushSubscriptions", {
id: id(),
endpoint: text("endpoint").notNull(),
publicKey: text("public_key").notNull(),
authSecret: text("auth_secret").notNull(),
alerts: jsonb("alerts").notNull().$type<
Partial<{
mention: boolean;
favourite: boolean;
reblog: boolean;
follow: boolean;
poll: boolean;
follow_request: boolean;
status: boolean;
update: boolean;
"admin.sign_up": boolean;
"admin.report": boolean;
}>
>(),
policy: text("policy")
.notNull()
.$type<"all" | "followed" | "follower" | "none">(),
createdAt: createdAt(),
updatedAt: updatedAt(),
tokenId: uuid("tokenId")
.references(() => Tokens.id, {
onDelete: "cascade",
onUpdate: "cascade",
})
.notNull()
.unique(),
});
export const PushSubscriptionsRelations = relations(
PushSubscriptions,
({ one }) => ({
token: one(Tokens, {
fields: [PushSubscriptions.tokenId],
references: [Tokens.id],
}),
}),
);
export const Reactions = pgTable("Reaction", {
id: id(),
uri: uri(),
@ -548,6 +591,7 @@ export enum RolePermissions {
ManageOwnFollows = "owner:follow",
ManageOwnApps = "owner:app",
Search = "search",
UsePushNotifications = "push_notifications",
ViewPublicTimelines = "public_timelines",
ViewPrivateTimelines = "private_timelines",
IgnoreRateLimits = "ignore_rate_limits",
@ -583,6 +627,7 @@ export const DEFAULT_ROLES = [
RolePermissions.ManageOwnFollows,
RolePermissions.ManageOwnApps,
RolePermissions.Search,
RolePermissions.UsePushNotifications,
RolePermissions.ViewPublicTimelines,
RolePermissions.ViewPrivateTimelines,
RolePermissions.OAuth,

View file

@ -121,7 +121,7 @@
"@oclif/core": "^4.2.0",
"@sentry/bun": "^8.47.0",
"@tufjs/canonical-json": "^2.0.0",
"@versia/client": "^0.1.4",
"@versia/client": "^0.1.5",
"@versia/federation": "^0.1.4",
"@versia/kit": "workspace:*",
"altcha-lib": "^1.2.0",

View file

@ -13,3 +13,4 @@ export { Like } from "~/classes/database/like.ts";
export { Token } from "~/classes/database/token.ts";
export { Notification } from "~/classes/database/notification.ts";
export { Reaction } from "~/classes/database/reaction.ts";
export { PushSubscription } from "~/classes/database/pushsubscription.ts";

View file

@ -246,7 +246,10 @@ export const checkRouteNeedsChallenge = async (
type HonoEnvWithAuth = HonoEnv & {
Variables: {
auth: AuthData & { user: NonNullable<AuthData["user"]> };
auth: AuthData & {
user: NonNullable<AuthData["user"]>;
token: NonNullable<AuthData["token"]>;
};
};
};