diff --git a/Dockerfile b/Dockerfile index fce0976aa94ed8b13b4be1f04639fbfc61661a48..1e91bef6ccdf7ec5cd5fac83b9b721a5ff490a4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ -FROM node:20-alpine AS base +FROM node:23-alpine AS base +RUN apk add --no-cache openssl -FROM base as dev_dep +FROM base AS dev_dep RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app WORKDIR /home/node/app @@ -12,7 +13,7 @@ COPY --chown=node:node frontend/package*.json ./frontend/ USER node RUN npm install --include=dev -FROM base as dep +FROM base AS dep RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app WORKDIR /home/node/app @@ -28,7 +29,7 @@ RUN npm install --omit=dev # === BUILDER === # -FROM base as build +FROM base AS build RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app WORKDIR /home/node/app @@ -49,7 +50,7 @@ RUN npm -w backend run build # === RUNNER === # -FROM base as run +FROM base AS run WORKDIR /home/node/app COPY --from=dep /home/node/app/ ./ COPY package*.json docker-start.sh ./ @@ -72,9 +73,9 @@ RUN npx -w backend prisma generate # set runtime env variables -ENV PORT 3000 -ENV NODE_ENV production -ENV SERVE_FRONTEND /home/node/app/frontend +ENV PORT=3000 +ENV NODE_ENV=production +ENV SERVE_FRONTEND=/home/node/app/frontend EXPOSE 3000 ENTRYPOINT [ "/bin/sh" ] diff --git a/backend/package.json b/backend/package.json index 34847565d816e63f7af2541ecd74c15f02ebec1c..d3311ff594e7909caaa07692ac25946b564cafd8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,14 +6,21 @@ "private": true, "type": "module", "dependencies": { + "@fedify/express": "^0.2.0", + "@fedify/fedify": "^1.5.1", + "@fedify/redis": "^0.4.0", + "@js-temporal/polyfill": "^0.5.1", + "@logtape/logtape": "^0.9.1", "@prisma/client": "^5.13.0", "@tsconfig/recommended": "^1.0.6", "body-parser": "^1.20.2", "cookie-parser": "^1.4.6", "cors": "^2.8.5", + "cron": "^4.3.0", "express": "^4.19.2", "express-session": "^1.18.0", - "oidc-provider": "^8.4.6", + "ioredis": "^5.6.1", + "oidc-provider": "^8.8.1", "openid-client": "^5.6.5" }, "devDependencies": { diff --git a/backend/prisma/migrations/20250427050825_internal_federation/migration.sql b/backend/prisma/migrations/20250427050825_internal_federation/migration.sql new file mode 100644 index 0000000000000000000000000000000000000000..3070ef52ab66e663a388ed57759e42ea457c6a4b --- /dev/null +++ b/backend/prisma/migrations/20250427050825_internal_federation/migration.sql @@ -0,0 +1,25 @@ +/* + Warnings: + + - You are about to drop the column `mode` on the `AuthSession` table. All the data in the column will be lost. + - A unique constraint covering the columns `[objectId]` on the table `AuthSession` will be added. If there are existing duplicate values, this will fail. + - The required column `objectId` was added to the `AuthSession` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required. + +*/ +-- AlterTable +ALTER TABLE "AuthSession" DROP COLUMN "mode", +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "expiresAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "objectId" TEXT NOT NULL; + +-- CreateTable +CREATE TABLE "FediverseKeyPair" ( + "id" TEXT NOT NULL, + "keyType" TEXT NOT NULL, + "value" TEXT NOT NULL, + + CONSTRAINT "FediverseKeyPair_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "AuthSession_objectId_key" ON "AuthSession"("objectId"); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 44fcf290f3e85ff8cdea6d17d83f8433a90b60f8..042de33ab1c16a84d26de6850d6ca7b0b6d93827 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -28,7 +28,16 @@ model OidcModel { model AuthSession { id String @id @default(uuid()) + objectId String @unique @default(uuid()) // fediverse object id one_time_code String - mode String // RECV_CODE | SEND_CODE -- is the service receiving or sending user_sub String + + createdAt DateTime @default(now()) + expiresAt DateTime @default(now()) +} + +model FediverseKeyPair { + id String @id @default(uuid()) + keyType String + value String } diff --git a/backend/src/controllers/AuthSession.ts b/backend/src/controllers/AuthSession.ts new file mode 100644 index 0000000000000000000000000000000000000000..b7400c770b8141bfaff480f1c6bf31aba400a4cb --- /dev/null +++ b/backend/src/controllers/AuthSession.ts @@ -0,0 +1,86 @@ +import { AuthSession as DBAuthSession } from "@prisma/client"; +import { prisma } from "../lib/prisma.js"; + +export class AuthSession { + static generateCode(): string { + return "." + .repeat(5) + .split("") + .map(() => Math.floor(Math.random() * 10)) + .join(""); + } + static async create(handle: `${string}@${string}`): Promise { + // day in seconds + const expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24); + + const session = await prisma.authSession.create({ + data: { + one_time_code: this.generateCode(), + user_sub: handle, + expiresAt, + }, + }); + + return new AuthSession(session); + } + static async getExpired() { + const sessions = await prisma.authSession.findMany({ + where: { + expiresAt: { + lte: new Date(), + }, + }, + }); + return sessions.map((d) => new AuthSession(d)); + } + static async getActive( + handle: `${string}@${string}` + ): Promise { + const session = await prisma.authSession.findFirst({ + where: { + user_sub: handle, + expiresAt: { + gt: new Date(), + }, + }, + }); + + if (!session) return null; + + return new AuthSession(session); + } + + private _id: string; + private _user_sub: `${string}@${string}`; + private _createdAt: Date; + private _expiresAt: Date; + private _code: string; + + private constructor(session: DBAuthSession) { + this._id = session.id; + this._user_sub = session.user_sub as any; + this._createdAt = session.createdAt; + this._expiresAt = session.expiresAt; + this._code = session.one_time_code; + } + + get id() { + return this._id; + } + + get user_sub() { + return this._user_sub; + } + + get createdAt() { + return this._createdAt; + } + + get expiresAt() { + return this._expiresAt; + } + + get code() { + return this._code; + } +} diff --git a/backend/src/index.ts b/backend/src/index.ts index cb6f6fc036879dda1fb5b47702c19e9684542b25..aeb45473e82e25a62cf4d6f1ef7a2fef2c9e6b0f 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,6 +1,8 @@ -import { setupAllProviders } from "./lib/delivery/index.js"; +import { configure, getConsoleSink } from "@logtape/logtape"; import { app as Express } from "./lib/express.js"; import { oidc } from "./lib/oidc.js"; +import { FederationWorker } from "./lib/apub/worker.js"; +import { Jobs } from "./jobs/jobs.js"; if (typeof process.env.SESSION_SECRET !== "string") { throw new Error("SESSION_SECRET is not defined"); @@ -40,22 +42,25 @@ if (process.env.NODE_ENV === "production") { } } -setupAllProviders().then((providers) => { - let errors: string[] = []; - - for (const prov of providers) { - if (prov.status === "rejected") { - errors.push(prov.reason || "unknown"); - } - } - - if (errors.length === 0) { - console.log("setup all deliver providers"); - } else { - console.error("provider setup failed", errors); - } +await configure({ + sinks: { console: getConsoleSink() }, + loggers: [ + { + category: "fedify", + sinks: ["console"], + lowestLevel: "debug", + }, + ], + filters: {}, }); -Express.listen(process.env.PORT, () => { - console.log("Listening on :" + process.env.PORT); -}); +if (process.env.NODE_TYPE === "worker") { + Jobs.start(); + FederationWorker.create().then(() => { + console.log("FederationWorker started"); + }); +} else { + Express.listen(process.env.PORT, () => { + console.log("Listening on :" + process.env.PORT); + }); +} diff --git a/backend/src/jobs/jobs.ts b/backend/src/jobs/jobs.ts new file mode 100644 index 0000000000000000000000000000000000000000..999e9c8314f2f2122c0fedd69e4d6927d3abfe70 --- /dev/null +++ b/backend/src/jobs/jobs.ts @@ -0,0 +1,30 @@ +import { CronJob } from "cron"; +import { AuthSession } from "../controllers/AuthSession.js"; +import { APub } from "../lib/apub/utils.js"; +import { prisma } from "../lib/prisma.js"; + +export class Jobs { + private static instance: Jobs; + + static start() { + this.instance = new Jobs(); + + CronJob.from({ + name: "Destroy expired tokens", + cronTime: "*/5 * * * *", + onTick: this.instance.handleExpiredTokens.bind(this), + start: true, + }); + } + + async handleExpiredTokens() { + const expired = await AuthSession.getExpired(); + console.debug(`Deleting ${expired.length} expired tokens`); + + for (const session of expired) { + console.debug(`Deleting ${session.id}`); + await APub.deleteDM(session); + await prisma.authSession.delete({ where: { id: session.id } }); + } + } +} diff --git a/backend/src/lib/api.ts b/backend/src/lib/api.ts index 2f6be2a584fc682837ff9e78304e28593780acc7..060de9af48b7a410e68e920bd8019a2eb4951fd7 100644 --- a/backend/src/lib/api.ts +++ b/backend/src/lib/api.ts @@ -8,12 +8,12 @@ import { makeClientPublic, } from "./utils.js"; import { getNodeInfo } from "./nodeinfo.js"; -import { getProviderFor } from "./delivery/index.js"; import { prisma } from "./prisma.js"; -import { ReceiveCodeProvider } from "./delivery/receive.js"; import { IProfile, getUserMeta } from "./instance/userMeta.js"; import { IInstance, getInstanceMeta } from "./instance/instanceMeta.js"; import { ShadowAPI, UserLoginError } from "./shadow.js"; +import { APub } from "./apub/utils.js"; +import { AuthSession } from "../controllers/AuthSession.js"; const app = express.Router(); @@ -95,12 +95,10 @@ app.post("/login/step/instance", async (req, res) => { } const nodeinfo = await getNodeInfo(domain); - const deliveryProvider = getProviderFor(nodeinfo.software.name); req.session.login = { prompt: "USERNAME", // change this if oidc is available instance: domain, - method: deliveryProvider ? "SEND_CODE" : "RECV_CODE", attempt: 0, }; @@ -118,9 +116,7 @@ app.post("/login/step/username", async (req, res) => { return res.status(400).json({ success: false, error: "wrong_step" }); } - const { method, instance } = req.session.login; - const nodeinfo = await getNodeInfo(instance); - const deliveryProvider = getProviderFor(nodeinfo.software.name); + const { instance } = req.session.login; let username: string; if (typeof req.body.username !== "string") { @@ -133,102 +129,58 @@ app.post("/login/step/username", async (req, res) => { req.session.login.username = username; // this is the prompt for the user - // method is how the service is acting - // therefore if the server is sending a code, the user needs to enter the code - req.session.login.prompt = - method === "SEND_CODE" ? "ENTER_CODE" : "SEND_CODE"; + req.session.login.prompt = "ENTER_CODE"; - switch (method) { - case "SEND_CODE": { - // send code to user + // TODO: prevent spam sending codes to someone - const code = "." - .repeat(5) - .split("") - .map(() => Math.floor(Math.random() * 10)) - .join(""); + const existing = await AuthSession.getActive(`${username}@${instance}`); - // TODO: prevent spam sending codes to someone - - const session = await prisma.authSession.create({ + if (existing) { + // if there's an active session, don't create another one + req.session.login.session_id = existing.id; + req.session.save(() => { + res.send({ + success: true, data: { - one_time_code: code, - mode: "SEND_CODE", - user_sub: [username, instance].join("@"), + session_id: session.id, + account: APub.accountHandle, }, }); - req.session.login.session_id = session.id; - - try { - await deliveryProvider!.send([username, instance], "code: " + code); - - req.session.save(() => { - res.send({ - success: true, - step: "CODE_SENT", - data: { - session_id: session.id, - account: - deliveryProvider!.service_account.username + - "@" + - deliveryProvider!.service_account.host, - }, - }); - }); - } catch (e) { - console.error( - "Error while delivering to " + [username, instance].join("@"), - e - ); - - await prisma.authSession.delete({ where: { id: session.id } }); - req.session.login.session_id = undefined; - - req.session.save(() => { - res.send({ - success: false, - error: - "Error while sending: " + - ((e as any)?.message || "unknown error"), - }); - }); - } + }); + return; + } - break; - } - case "RECV_CODE": { - // tell user to send code to service + const session = await AuthSession.create(`${username}@${instance}`); + req.session.login.session_id = session.id; - const code = "." - .repeat(5) - .split("") - .map(() => Math.floor(Math.random() * 10)) - .join(""); + try { + await APub.sendDM(session); - const session = await prisma.authSession.create({ + req.session.save(() => { + res.send({ + success: true, data: { - one_time_code: code, - mode: "RECV_CODE", - user_sub: [username, instance].join("@"), + session_id: session.id, + account: APub.accountHandle, }, }); - req.session.login.session_id = session.id; - - req.session.save(() => { - res.send({ - success: true, - step: "SEND_CODE", - data: { - session_id: session.id, - message_to_send: - ReceiveCodeProvider.fullMention() + - " " + - ReceiveCodeProvider.getMessageTemplate(code), - }, - }); + }); + } catch (e) { + console.error( + "Error while delivering to " + [username, instance].join("@"), + e + ); + + await prisma.authSession.delete({ where: { id: session.id } }); + req.session.login.session_id = undefined; + + req.session.save(() => { + res.send({ + success: false, + error: + "Error while sending: " + ((e as any)?.message || "unknown error"), }); - break; - } + }); } }); @@ -275,58 +227,29 @@ app.post("/login/step/verify", async (req, res) => { return; } - switch (session.mode) { - case "SEND_CODE": { - // code has been sent, we're expecting the user to give a code - let code: string; - - if (typeof req.body.code !== "string") { - return res - .status(400) - .json({ success: false, error: "code is not a string" }); - } - code = req.body.code; + // code has been sent, we're expecting the user to give a code + let code: string; - if (session.one_time_code !== code) { - req.session.login.attempt++; - req.session.save(() => { - res.status(400).json({ success: false, error: "code_invalid" }); - }); - return; - } + if (typeof req.body.code !== "string") { + return res + .status(400) + .json({ success: false, error: "code is not a string" }); + } + code = req.body.code; - req.session.user = { sub: session.user_sub }; - req.session.login = undefined; - req.session.save(() => { - res.json({ success: true }); - }); - break; - } - case "RECV_CODE": { - // the user has notified us that they've sent the post, now to check it being delivered - - ReceiveCodeProvider.checkUser( - [username!, instance], - session.one_time_code - ).then((data) => { - if (data.success) { - req.session.user = { sub: session.user_sub }; - req.session.login = undefined; - req.session.save(() => { - res.json({ success: true }); - }); - } else { - if (req.session.login) { - req.session.login.attempt++; - } - req.session.save(() => { - res.status(400).json({ success: false, error: data.error }); - }); - } - }); - break; - } + if (session.one_time_code !== code) { + req.session.login.attempt++; + req.session.save(() => { + res.status(400).json({ success: false, error: "code_invalid" }); + }); + return; } + + req.session.user = { sub: session.user_sub }; + req.session.login = undefined; + req.session.save(() => { + res.json({ success: true }); + }); }); /**** diff --git a/backend/src/lib/apub/federation.ts b/backend/src/lib/apub/federation.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac9c47a2b8a7958418c486cda714ee8a6a884d38 --- /dev/null +++ b/backend/src/lib/apub/federation.ts @@ -0,0 +1,191 @@ +import { + ActivityTransformer, + ChatMessage, + createFederation, + Endpoints, + exportJwk, + Follow, + generateCryptoKeyPair, + getDefaultActivityTransformers, + importJwk, + isActor, + lookupObject, + Note, + Reject, + Service, +} from "@fedify/fedify"; +import { RedisKvStore, RedisMessageQueue } from "@fedify/redis"; +import { Redis } from "ioredis"; +import { FedifyTransformers } from "./transformers.js"; +import { prisma } from "../prisma.js"; +import { APub } from "./utils.js"; + +// @auth@some.domain +export const USER_IDENTIFIER = "auth"; + +// CryptoKeyPair is from the Web Crypto API +type CryptoKeyPair = Awaited>; + +export const federation = createFederation({ + kv: new RedisKvStore(new Redis(process.env.REDIS_URI)), + queue: new RedisMessageQueue(() => new Redis(process.env.REDIS_URI)), + activityTransformers: FedifyTransformers, + manuallyStartQueue: true, + origin: process.env.OIDC_ISSUER, + userAgent: { + software: "FediverseAuth/" + (process.env.VERSION || "dev"), + url: process.env.OIDC_ISSUER, + }, + + allowPrivateAddress: process.env.NODE_ENV === "development", +}); + +federation + .setActorDispatcher("/x/users/{identifier}", async (ctx, identifier) => { + if (identifier !== USER_IDENTIFIER) return null; + + return new Service({ + id: ctx.getActorUri(USER_IDENTIFIER), + name: process.env.SERVICE_NAME, + summary: "//TODO", + manuallyApprovesFollowers: true, + preferredUsername: USER_IDENTIFIER, + url: ctx.getActorUri(USER_IDENTIFIER), + inbox: ctx.getInboxUri(USER_IDENTIFIER), + outbox: ctx.getOutboxUri(USER_IDENTIFIER), + endpoints: new Endpoints({ + sharedInbox: ctx.getInboxUri(), + }), + publicKeys: (await ctx.getActorKeyPairs(USER_IDENTIFIER)).map( + (keyPair) => keyPair.cryptographicKey + ), + }); + }) + .setKeyPairsDispatcher(async (ctx, identifier) => { + if (identifier !== USER_IDENTIFIER) return []; + + const result: CryptoKeyPair[] = []; + + const rsaPair = await prisma.fediverseKeyPair.findFirst({ + where: { + keyType: "RSASSA-PKCS1-v1_5", + }, + }); + if (rsaPair) { + const { privateKey, publicKey } = JSON.parse(rsaPair.value); + result.push({ + privateKey: await importJwk(privateKey, "private"), + publicKey: await importJwk(publicKey, "public"), + }); + } else { + const { privateKey, publicKey } = await generateCryptoKeyPair( + "RSASSA-PKCS1-v1_5" + ); + await prisma.fediverseKeyPair.create({ + data: { + keyType: "RSASSA-PKCS1-v1_5", + value: JSON.stringify({ + privateKey: await exportJwk(privateKey), + publicKey: await exportJwk(publicKey), + }), + }, + }); + result.push({ + privateKey, + publicKey, + }); + } + + // TODO: add ed25519 back to response as the library complains if it isn't returned in some circumstances + // Lemmy will reject user resolves if the public_key field isn't exactly one key response + // @see https://github.com/LemmyNet/lemmy/blob/ed5a3831aa3ac10c9e0de6b70a7df282c94fcdb3/crates/apub/src/protocol/objects/person.rs#L33 + + return result; + }); + +federation + .setInboxListeners("/x/users/{identifier}/inbox", "/inbox") + .on(Follow, async (ctx, follow) => { + if (follow.id == null || follow.actorId == null || follow.objectId == null) + return; + + const parsed = ctx.parseUri(follow.objectId); + if (parsed?.type !== "actor" || parsed.identifier !== USER_IDENTIFIER) + return; + + const follower = await follow.getActor(ctx); + if (follower == null) return; + + // reject incoming follow requests + await ctx.sendActivity( + { identifier: parsed.identifier }, + follower, + new Reject({ + actor: follow.objectId, + object: follow, + }) + ); + }); + +federation.setOutboxDispatcher( + "/x/users/{identifier}/outbox", + (ctx, identifier) => { + return { + items: [], + }; + } +); + +federation.setObjectDispatcher( + ChatMessage, + "/x/object/chatmessage/{id}", + async (ctx, { id }) => { + const authSession = await prisma.authSession.findFirst({ + where: { + objectId: id, + }, + }); + + if (!authSession) return null; + + const recipient = await lookupObject( + authSession.user_sub, + APub.options(ctx) + ); + const apub = new APub(ctx); + + if (!isActor(recipient)) return null; + + return apub.build("ChatMessage", { + ...authSession, + target: recipient!, + }); + } +); + +federation.setObjectDispatcher( + Note, + "/x/object/note/{id}", + async (ctx, { id }) => { + const authSession = await prisma.authSession.findFirst({ + where: { + objectId: id, + }, + }); + + if (!authSession) return null; + + const recipient = await lookupObject( + authSession.user_sub, + APub.options(ctx) + ); + const apub = new APub(ctx); + + if (!isActor(recipient)) return null; + + return apub.build("Note", { + ...authSession, + target: recipient!, + }); + } +); diff --git a/backend/src/lib/apub/transformers.ts b/backend/src/lib/apub/transformers.ts new file mode 100644 index 0000000000000000000000000000000000000000..fa4c121a656de4cf07d35101694f37f41a6ebef2 --- /dev/null +++ b/backend/src/lib/apub/transformers.ts @@ -0,0 +1,19 @@ +import { + ActivityTransformer, + getDefaultActivityTransformers, +} from "@fedify/fedify"; + +const toDehydrator: ActivityTransformer = (activity, ctx) => { + // dehydrate {to} to just IDs because lemmy rejects the message otherwise + // @see https://github.com/LemmyNet/lemmy/blob/ed5a3831aa3ac10c9e0de6b70a7df282c94fcdb3/crates/apub/src/protocol/activities/create_or_update/private_message.rs#L15 + if (activity.toIds.length < 1) return activity; + + return activity.clone({ + tos: activity.toIds, + }); +}; + +export const FedifyTransformers: readonly ActivityTransformer[] = [ + ...getDefaultActivityTransformers(), + toDehydrator, +]; diff --git a/backend/src/lib/apub/utils.ts b/backend/src/lib/apub/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..d0b192a6cea17f3ddd748ef6323250e54f80e120 --- /dev/null +++ b/backend/src/lib/apub/utils.ts @@ -0,0 +1,230 @@ +import { + Actor, + ChatMessage, + Context, + Create, + Delete, + isActor, + lookupObject, + LookupObjectOptions, + Mention, + Note, + Tombstone, +} from "@fedify/fedify"; +import { federation, USER_IDENTIFIER } from "./federation.js"; +import { Temporal } from "@js-temporal/polyfill"; +import { AuthSession } from "../../controllers/AuthSession.js"; + +type BuildObjectOpts = { + id: string; + one_time_code: string; + createdAt: Date; + target: Actor; +}; + +export class APub { + private ctx: Context; + + constructor(ctx: Context) { + this.ctx = ctx; + } + + static options: (ctx: Context) => LookupObjectOptions = (ctx) => ({ + ...ctx, + allowPrivateAddress: process.env.NODE_ENV === "development", + }); + + static get accountHandle() { + return USER_IDENTIFIER + "@" + new URL(process.env.OIDC_ISSUER).host; + } + + static async sendDM(session: AuthSession) { + const ctx = federation.createContext(new URL("/", process.env.OIDC_ISSUER)); + const recipient = await lookupObject(session.user_sub, this.options(ctx)); + + if (!isActor(recipient)) throw new Error("Not an actor"); + + const apub = new APub(ctx); + const opts: BuildObjectOpts = { + id: session.id, + one_time_code: session.code, + createdAt: session.createdAt, + target: recipient, + }; + + try { + await apub.sendChatMessage( + session.id, + recipient, + apub.build("ChatMessage", opts) + ); + } catch (e) { + if (process.env.NODE_ENV === "development") { + // fediverse services may return non 2xx status codes, causing an error + // we can silently try another object if thats the case + // we log it here for development usage, but ignore otherwise + console.error(`Failed to send ChatMessage to ${session.user_sub}`, e); + } + } + + try { + await apub.sendNote(session.id, recipient, apub.build("Note", opts)); + } catch (e) { + if (process.env.NODE_ENV === "development") { + // fediverse services may return non 2xx status codes, causing an error + // we can silently try another object if thats the case + // we log it here for development usage, but ignore otherwise + console.error(`Failed to send Note to ${session.user_sub}`, e); + } + } + } + + static async deleteDM(session: AuthSession) { + const ctx = federation.createContext(new URL("/", process.env.OIDC_ISSUER)); + const recipient = await lookupObject(session.user_sub, this.options(ctx)); + + if (!isActor(recipient)) throw new Error("Not an actor"); + + const apub = new APub(ctx); + + try { + await apub.deleteChatMessage(session.id, recipient); + } catch (e) { + if (process.env.NODE_ENV === "development") { + // fediverse services may return non 2xx status codes, causing an error + // we can silently try another object if thats the case + // we log it here for development usage, but ignore otherwise + console.error(`Failed to delete ChatMessage to ${session.user_sub}`, e); + } + } + + try { + await apub.deleteNote(session.id, recipient); + } catch (e) { + if (process.env.NODE_ENV === "development") { + // fediverse services may return non 2xx status codes, causing an error + // we can silently try another object if thats the case + // we log it here for development usage, but ignore otherwise + console.error(`Failed to delete Note to ${session.user_sub}`, e); + } + } + } + + /** + * Send a ChatMessage message targeted at Actor + * + * Not many fediverse software supports this, but Lemmy <0.19 uses this exclusively for DMs + */ + private async sendChatMessage( + id: string, + target: Actor, + content: ChatMessage + ) { + const sender = this.ctx.getActorUri(USER_IDENTIFIER); + + await this.ctx.sendActivity( + { identifier: USER_IDENTIFIER }, + target, + new Create({ + id: new URL("#create", this.ctx.getObjectUri(ChatMessage, { id })), + actor: sender, + to: target, + object: content, + }), + { fanout: "skip" } + ); + } + + private async deleteChatMessage(id: string, target: Actor) { + const sender = this.ctx.getActorUri(USER_IDENTIFIER); + + await this.ctx.sendActivity( + { identifier: USER_IDENTIFIER }, + target, + new Delete({ + id: new URL("#delete", this.ctx.getObjectUri(ChatMessage, { id })), + object: this.ctx.getObjectUri(ChatMessage, { id }), + actor: sender, + to: target, + }) + ); + } + + /** + * Send a private Note message targeted at Actor + * + * Most fediverse software supports this + */ + private async sendNote(id: string, target: Actor, content: Note) { + const sender = this.ctx.getActorUri(USER_IDENTIFIER); + + await this.ctx.sendActivity( + { identifier: USER_IDENTIFIER }, + target, + new Create({ + id: new URL("#create", this.ctx.getObjectUri(Note, { id })), + actor: sender, + to: target, + object: content, + }), + { fanout: "skip" } + ); + } + + private async deleteNote(id: string, target: Actor) { + const sender = this.ctx.getActorUri(USER_IDENTIFIER); + + await this.ctx.sendActivity( + { identifier: USER_IDENTIFIER }, + target, + new Delete({ + id: new URL("#delete", this.ctx.getObjectUri(Note, { id })), + object: new Tombstone({ + id: this.ctx.getObjectUri(Note, { id }), + }), + actor: sender, + to: target, + }) + ); + } + + build(type: "ChatMessage", opts: BuildObjectOpts): ChatMessage; + build(type: "Note", opts: BuildObjectOpts): Note; + build(type: "ChatMessage" | "Note", opts: BuildObjectOpts): unknown { + if (!type) throw new Error(); + + const { id, one_time_code, target, createdAt } = opts; + const sender = this.ctx.getActorUri(USER_IDENTIFIER); + + const content = { + content: `Code: ${one_time_code} + +Do not share this code. This code is used to identify you.`, + }; + + switch (type) { + case "ChatMessage": + return new ChatMessage({ + id: this.ctx.getObjectUri(ChatMessage, { id }), + attribution: sender, + to: target.id, + published: Temporal.Instant.from(createdAt.toISOString()), + ...content, + }); + case "Note": + return new Note({ + id: this.ctx.getObjectUri(Note, { id }), + attribution: sender, + to: target.id, + published: Temporal.Instant.from(createdAt.toISOString()), + tags: [ + new Mention({ + href: target.id, + name: target.id!.toString(), + }), + ], + ...content, + }); + } + } +} diff --git a/backend/src/lib/apub/worker.ts b/backend/src/lib/apub/worker.ts new file mode 100644 index 0000000000000000000000000000000000000000..d1b31b8bad61167af58db9e17b983543f50eab18 --- /dev/null +++ b/backend/src/lib/apub/worker.ts @@ -0,0 +1,11 @@ +import { federation } from "./federation.js"; + +export class FederationWorker { + static async create() { + const controller = new AbortController(); + process.once("SIGINT", () => controller.abort()); + await federation.startQueue(undefined, { + signal: controller.signal, + }); + } +} diff --git a/backend/src/lib/delivery/DeliveryProvider.ts b/backend/src/lib/delivery/DeliveryProvider.ts deleted file mode 100644 index 9abb0473587eb448562bdeaece2ec28474e6d87c..0000000000000000000000000000000000000000 --- a/backend/src/lib/delivery/DeliveryProvider.ts +++ /dev/null @@ -1,37 +0,0 @@ -interface IServiceAccount { - username: string; - host: string; -} - -/** - * Delivery method for messages - * - * Why is this needed? - * - This is needed because some softwares don't understand DMs from other software - * - * @template T Additonal service_account params - */ -export abstract class DeliveryProvider { - service_account: Partial = {}; - - /** - * Verify credentials and/or tokens - */ - abstract setup(): Promise; - - /** - * Does this provider support this software name - * @param software_name - */ - abstract isThisFor(software_name: string): boolean; - - /** - * Send message to user - * @param user - * @param content - */ - abstract send( - user: [username: string, instance: string], - content: string - ): Promise; -} diff --git a/backend/src/lib/delivery/index.ts b/backend/src/lib/delivery/index.ts deleted file mode 100644 index bc40be869a59670d4497fec9295d0d641fb7ea82..0000000000000000000000000000000000000000 --- a/backend/src/lib/delivery/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { DeliveryProvider } from "./DeliveryProvider.js"; -import { LemmyCompatible } from "./lemmy.js"; -import { MastodonCompatible } from "./mastodon.js"; -import { ReceiveCodeProvider } from "./receive.js"; - -const allProviders: DeliveryProvider[] = [LemmyCompatible, MastodonCompatible]; - -export const getProviderFor = (software_name: string) => - allProviders.find((p) => p.isThisFor(software_name)); - -export const setupAllProviders = () => - Promise.allSettled([ - ...allProviders.map((p) => p.setup()), - ReceiveCodeProvider.setup(), - ]); diff --git a/backend/src/lib/delivery/lemmy.ts b/backend/src/lib/delivery/lemmy.ts deleted file mode 100644 index 89ec3f13f0bdb978be9ede73c2ecf67de410f463..0000000000000000000000000000000000000000 --- a/backend/src/lib/delivery/lemmy.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { DeliveryProvider } from "./DeliveryProvider.js"; - -/** - * Lemmy compatible DM - */ -class LemmyCompatible_ extends DeliveryProvider<{ token: string }> { - async setup(): Promise { - const { LEMMY_HOST, LEMMY_USER, LEMMY_PASS, LEMMY_TOKEN } = process.env; - - if (!LEMMY_HOST) { - throw new Error("Missing LEMMY_HOST cannot enable Lemmy polyfill"); - } - this.service_account.host = LEMMY_HOST; - - if (LEMMY_TOKEN) { - const lemmyapi = new LemmyAPI(LEMMY_HOST, LEMMY_TOKEN); - const validToken = await lemmyapi.verifyToken(); - - if (!validToken) { - if (!LEMMY_USER || !LEMMY_PASS) { - throw new Error( - "LEMMY_TOKEN is invalid and LEMMY_USER & LEMMY_PASS are not specified" - ); - } - } else { - this.service_account.username = validToken.name; - this.service_account.token = LEMMY_TOKEN; - return true; - } - } - - if (!LEMMY_USER || !LEMMY_PASS) { - throw new Error( - "LEMMY_TOKEN is is not set and LEMMY_USER & LEMMY_PASS are not specified" - ); - } - - const auth = await LemmyAPI.login(LEMMY_HOST, LEMMY_USER, LEMMY_PASS); - if (!auth.jwt) { - throw new Error("LEMMY_HOST, LEMMY_USER or LEMMY_PASS are incorrect"); - } - - console.log("[LEMMY AUTH] Success! Save this token as LEMMY_TOKEN in env!"); - console.log("[LEMMY AUTH] TOKEN: " + auth.jwt); - - this.service_account.username = LEMMY_USER; - this.service_account.token = auth.jwt; - } - - isThisFor(software_name: string): boolean { - return software_name === "lemmy"; - } - - async send( - user: [username: string, instance: string], - content: string - ): Promise { - const api = new LemmyAPI( - this.service_account.host!, - this.service_account.token! - ); - - await api.sendPM(user[0] + "@" + user[1], content); - } -} - -interface ILemmyUser { - name: string; -} - -interface ILemmyAuth { - jwt?: string; - /** - * Registration application not approved - */ - registration_created: boolean; - /** - * Email not verified? - */ - verify_email_sent: boolean; -} - -/** - * Lemmy v0.19 API wrapper - */ -class LemmyAPI { - private host: string; - private token: string; - - static async login( - host: string, - username: string, - password: string, - totp?: string - ): Promise { - const req = await fetch(`https://${host}/api/v3/user/login`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - username_or_email: username, - password, - totp_2fa_token: totp, - }), - }); - - let data: any; - try { - data = await req.json(); - } catch (e) { - throw new Error("Invalid JSON"); - } - - if (req.status === 404 || req.status === 400) { - if ( - data?.error === "missing_totp_token" || - data?.error === "incorrect_totp token" - ) - throw new Error("totp"); - throw new Error("invalid_login"); - } - - if (req.status !== 200) throw new Error("status code " + req.status); - - return data; - } - - constructor(host: string, token: string) { - this.host = host; - this.token = token; - } - - private fetch( - endpoint: string, - method: string = "GET", - body: { [k: string]: any } = {} - ): Promise { - let url = `https://${this.host}${endpoint}`; - let request: any = { - method, - headers: { - Authorization: "Bearer " + this.token, - }, - }; - - if (method === "GET") { - let params = new URLSearchParams(); - for (let [key, value] of Object.entries(body)) { - params.set(key, value + ""); - } - url += "?" + params.toString(); - } else { - request.headers["Content-Type"] = "application/json"; - request.body = JSON.stringify(body); - } - - // TODO: error handling - return fetch(url, request).then((a) => a.json() as T); - } - - async verifyToken(): Promise { - const req = await this.fetch("/api/v3/site"); - - if (!req.my_user || !req.my_user?.local_user_view?.person?.name) - return false; - - return { - name: req.my_user.local_user_view.person.name, - }; - } - - /** - * localparts can have any capitalization - * homeservers have to be fully lowercase otherwise lemmy freaks out - */ - normalizeUsername(username: string): string { - let [localpart, homeserver] = username.split("@").map((l) => l.trim()); - homeserver = homeserver.toLowerCase(); - - return localpart + "@" + homeserver; - } - - /** - * Gets actor id from homeserver so the bot can pm them - * - * @param username Username - */ - async getActorID(username: string): Promise { - username = this.normalizeUsername(username); - - const actor = await this.fetch("/api/v3/user", "GET", { username }); - - if (actor.error || !actor?.person_view?.person?.id) { - console.info("Can't find account", this.host, username, actor); - throw new Error("unknown_account"); - } - - return actor?.person_view?.person?.id; - } - - /** - * Send a Lemmy PM to a user via the homeserver the bot is on - * - * @param username username - * @param content content - */ - async sendPM(username: string, content: string) { - console.log("attempting ", username); - const actorId = await this.getActorID(username); - - const pm = await this.fetch("/api/v3/private_message", "POST", { - content, - recipient_id: actorId, - }); - - if (pm.error) { - console.error("create pm", { username, content }, pm); - throw new Error("Failed to send direct message"); - } - } -} - -export const LemmyCompatible = new LemmyCompatible_(); diff --git a/backend/src/lib/delivery/mastodon.ts b/backend/src/lib/delivery/mastodon.ts deleted file mode 100644 index 638cd91d4ac56c77308e263134a376fbd0fc52cc..0000000000000000000000000000000000000000 --- a/backend/src/lib/delivery/mastodon.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { DeliveryProvider } from "./DeliveryProvider.js"; - -/** - * Mastodon compatible DM - */ -class MastodonCompatible_ extends DeliveryProvider<{ token: string }> { - async setup() { - const { MASTODON_HOST, MASTODON_USER, MASTODON_TOKEN } = process.env; - - if (!MASTODON_HOST) { - throw new Error("Missing MASTODON_HOST cannot enable Mastodon polyfill"); - } - this.service_account.host = MASTODON_HOST; - - if (!MASTODON_TOKEN) { - throw new Error( - "Missing MASTODON_TOKEN cannot enable Mastodon polyfill (you can get this from Settings -> Development -> -> Access Token" - ); - } - - const userCheckReq = await fetch( - `https://${MASTODON_HOST}/api/v1/accounts/verify_credentials`, - { - headers: { - Authorization: "Bearer " + MASTODON_TOKEN, - }, - } - ); - const userCheck: any = await userCheckReq.json(); - - if (userCheckReq.status !== 200) { - throw new Error( - "MASTODON_TOKEN is invalid (server responded with " + - userCheckReq.status + - ") " + - userCheck - ); - } - - // the return type from the API is version tagged & status code checked, assume username property is there - this.service_account.username = userCheck.username; - this.service_account.token = MASTODON_TOKEN; - } - - isThisFor(software_name: string): boolean { - // TODO: add other softwares that work w/ mastodon DMs - const compatible = ["mastodon", "pixelfed"]; - return compatible.indexOf(software_name) > -1; - } - - async send( - user: [username: string, instance: string], - content: string - ): Promise { - const form = new URLSearchParams(); - form.set("status", `${content}\n\n@${user[0]}@${user[1]}`); - form.set("visibility", "direct"); - - const api = await fetch( - `https://${this.service_account.host}/api/v1/statuses`, - { - method: "POST", - headers: { - Authorization: "Bearer " + this.service_account.token, - }, - body: form, - } - ); - const apiResponse = await api.json(); - - if (api.status !== 200) { - console.error( - "Failed to send Mastodon DM", - { user, content }, - apiResponse - ); - throw new Error("Failed to send Mastodon DM"); - } - } -} - -export const MastodonCompatible = new MastodonCompatible_(); diff --git a/backend/src/lib/delivery/receive.ts b/backend/src/lib/delivery/receive.ts deleted file mode 100644 index eeff8be2a9720106f320526d3b7246527d61c626..0000000000000000000000000000000000000000 --- a/backend/src/lib/delivery/receive.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Fallback for sending messages to a bot - */ -class ReceiveCodeProvider_ { - service_account: { - username: string; - host: string; - token: string; - } = {} as any; - - async setup() { - const { MASTODON_HOST, MASTODON_USER, MASTODON_TOKEN } = process.env; - - if (!MASTODON_USER) { - throw new Error("Missing MASTODON_USER cannot enable receive provider"); - } - this.service_account.username = MASTODON_USER; - - if (!MASTODON_HOST) { - throw new Error("Missing MASTODON_HOST cannot enable receive provider"); - } - this.service_account.host = MASTODON_HOST; - - if (!MASTODON_TOKEN) { - throw new Error( - "Missing MASTODON_TOKEN cannot enable receive provider (you can get this from Settings -> Development -> -> Access Token" - ); - } - - const userCheckReq = await fetch( - `https://${MASTODON_HOST}/api/v1/accounts/verify_credentials`, - { - headers: { - Authorization: "Bearer " + MASTODON_TOKEN, - }, - } - ); - const userCheck = await userCheckReq.json(); - - if (userCheckReq.status !== 200) { - throw new Error( - "MASTODON_TOKEN is invalid (server responded with " + - userCheckReq.status + - ") " + - userCheck - ); - } - - this.service_account.token = MASTODON_TOKEN; - } - - getMessageTemplate(code: string) { - return `authorization code: ${code}`; - } - - fullMention() { - return `@${this.service_account.username}@${this.service_account.host}`; - } - - async checkUser( - user: [username: string, instance: string], - code: string - ): Promise< - | { - success: true; - } - | { success: false; error: string } - > { - let accreq_data = new URLSearchParams(); - accreq_data.set("acct", user[0] + "@" + user[1]); - const accreq = await fetch( - `https://${this.service_account.host}/api/v1/accounts/lookup?` + - accreq_data - ); - const accres = (await accreq.json()) as { error: string } | { id: string }; - - if (accreq.status !== 200 || "error" in accres) { - console.error("Failed to query webfinger -> Mastodon ID", user, accres); - return { success: false, error: "failed_to_find_user" }; - } - - // local account ID, this will be used to filter notifications - const account_id = accres.id; - - let notifreq_data = new URLSearchParams(); - notifreq_data.set("types", "mention"); - notifreq_data.set("account_id", account_id); - const notifreq = await fetch( - `https://${this.service_account.host}/api/v1/notifications?` + - notifreq_data, - { - headers: { - Authorization: "Bearer " + this.service_account.token, - }, - } - ); - const notifres = (await notifreq.json()) as - | { status: { content: string } }[] - | { error: string }; - - if (notifreq.status !== 200 || "error" in notifres) { - console.error("Failed to get mentions", notifres); - return { success: false, error: "internal_error" }; - } - - const mostRecentMention = notifres?.[0]?.status; - if (!mostRecentMention) { - return { success: false, error: "no_mentions" }; - } - - if ( - mostRecentMention.content?.indexOf(this.getMessageTemplate(code)) === -1 - ) { - console.log(mostRecentMention.content); - return { success: false, error: "message_content_invalid" }; - } - - return { success: true }; - } -} - -export const ReceiveCodeProvider = new ReceiveCodeProvider_(); diff --git a/backend/src/lib/express.ts b/backend/src/lib/express.ts index 5bf32de00e38fb6a21308a202fe6be8fba6d2fd9..49c3be45b772bf0f1432936afedc221c7e2f075c 100644 --- a/backend/src/lib/express.ts +++ b/backend/src/lib/express.ts @@ -9,6 +9,8 @@ import { errors as OIDC_Errors } from "oidc-provider"; import "../types/session-types.js"; import { APIRouter } from "./api.js"; +import { integrateFederation } from "@fedify/express"; +import { federation } from "./apub/federation.js"; export const app = express(); @@ -23,6 +25,11 @@ if (process.env.NODE_ENV === "development") { ); } +app.use("/api/oidc", oidc.callback()); +app.get("/logout", (req, res) => { + res.redirect("/api/oidc/logout"); +}); + app.use(bodyParser.json()); app.use( @@ -152,4 +159,5 @@ if (process.env.SERVE_FRONTEND) { app.use("/api/v1", APIRouter); -app.use(oidc.callback()); +app.use(integrateFederation(federation, (req) => {})); +// app.use(oidc.callback()); diff --git a/backend/src/lib/oidc.ts b/backend/src/lib/oidc.ts index 2238de744b20a32738c40b15664f9fa4e2b88da9..dc4482e85510ead06f6af637919b00ae1140fc9b 100644 --- a/backend/src/lib/oidc.ts +++ b/backend/src/lib/oidc.ts @@ -99,18 +99,18 @@ export const oidc = new Provider(process.env.OIDC_ISSUER!, { }, }, routes: { - authorization: "/api/oidc/auth", - backchannel_authentication: "/api/oidc/backchannel", - code_verification: "api/oidc/device", - device_authorization: "/api/oidc/device/auth", + authorization: "/auth", + backchannel_authentication: "/backchannel", + code_verification: "/device", + device_authorization: "/device/auth", end_session: "/logout", - introspection: "/api/oidc/token/introspection", - jwks: "/api/oidc/jwks", - pushed_authorization_request: "/api/oidc/request", - registration: "/api/oidc/registration", - revocation: "/api/oidc/token/revoke", - token: "/api/oidc/token", - userinfo: "/api/oidc/me", + introspection: "/token/introspection", + jwks: "/jwks", + pushed_authorization_request: "/request", + registration: "/registration", + revocation: "/token/revoke", + token: "/token", + userinfo: "/me", }, }); diff --git a/backend/src/types/env.d.ts b/backend/src/types/env.d.ts index 0d64b51554e7d458d3b01baae6f5e9a761773f9a..1980a7f6c3b7643d12fe0f264ea41738f5528314 100644 --- a/backend/src/types/env.d.ts +++ b/backend/src/types/env.d.ts @@ -3,10 +3,14 @@ declare global { interface ProcessEnv { NODE_ENV: "development" | "production"; DATABASE_URL: string; + REDIS_URI: string; SESSION_SECRET: string; PORT: string; OIDC_ISSUER: string; + SERVICE_NAME: string; + VERSION?: string; + NODE_TYPE?: "worker"; LEMMY_HOST?: string; LEMMY_USER?: string; diff --git a/backend/src/types/session-types.ts b/backend/src/types/session-types.ts index 3e3796dd9d689648e174496b02e5e528b62694c2..41f875f854d33aa47f7961a74c8ec902dc34a525 100644 --- a/backend/src/types/session-types.ts +++ b/backend/src/types/session-types.ts @@ -10,9 +10,8 @@ declare module "express-session" { * - SEND_CODE: user needs to make a post * - OIDC: open oidc [todo] */ - prompt: "USERNAME" | "ENTER_CODE" | "SEND_CODE" | "OIDC"; + prompt: "USERNAME" | "ENTER_CODE" | "OIDC"; instance: string; - method: "SEND_CODE" | "RECV_CODE"; // what delivery to attempt username?: string; session_id?: string; attempt: number; diff --git a/docker-compose.yml b/docker-compose.yml index 31e8fd2840b32a985529b3f1e16e81b1d936b1bb..0aabb47decb38d6e975757e3aef6d3b75950f55b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,7 @@ services: - "3000:3000" environment: - DATABASE_URL=postgres://postgres@postgres:5432/fediauth + - REDIS_URI=redis://redis - OIDC_JWK_KEYS_FILE=/run/secrets/oidc_jwk_keys - OIDC_COOKIE_KEYS_FILE=/run/secrets/oidc_cookie_keys env_file: @@ -16,19 +17,49 @@ services: depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy + + worker: + image: sc07/fediverse-auth + build: . + environment: + - NODE_TYPE=worker + - DATABASE_URL=postgres://postgres@postgres:5432/fediauth + - REDIS_URI=redis://redis + - OIDC_JWK_KEYS_FILE=/run/secrets/oidc_jwk_keys + - OIDC_COOKIE_KEYS_FILE=/run/secrets/oidc_cookie_keys + env_file: + - .env.local + secrets: + - oidc_jwk_keys + - oidc_cookie_keys + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy postgres: restart: always image: postgres:14-alpine healthcheck: - test: ['CMD', 'pg_isready', '-U', 'postgres'] + test: ["CMD", "pg_isready", "-U", "postgres"] volumes: - ./data/postgres:/var/lib/postgresql/data environment: - - 'POSTGRES_HOST_AUTH_METHOD=trust' + - "POSTGRES_HOST_AUTH_METHOD=trust" + + redis: + restart: always + image: redis:7-alpine + healthcheck: + test: ["CMD", "redis-cli", "ping"] + volumes: + - ./data/redis:/data secrets: oidc_jwk_keys: file: ./secrets/jwks.json oidc_cookie_keys: - file: ./secrets/cookies.json \ No newline at end of file + file: ./secrets/cookies.json diff --git a/frontend/src/Login/Login.tsx b/frontend/src/Login/Login.tsx index 9209798725de876be91836814d9246af323cc120..64abf4025eea1c4fbe5e014dc26786f84acd7e82 100644 --- a/frontend/src/Login/Login.tsx +++ b/frontend/src/Login/Login.tsx @@ -10,9 +10,6 @@ import { DialogActions, DialogContent, DialogTitle, - IconButton, - InputAdornment, - OutlinedInput, Stack, Step, StepContent, @@ -23,15 +20,12 @@ import { } from "@mui/material"; import { useEffect, useRef, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; -import ContentCopyIcon from "@mui/icons-material/ContentCopy"; -import CheckIcon from "@mui/icons-material/Check"; import { api } from "../lib/utils"; enum LoginState { INSTANCE, USERNAME, CODE_INPUT, - CODE_SEND, } export const LoginPage = () => { @@ -80,20 +74,6 @@ export const LoginPage = () => { ]); }; - const copyMessageToClipboard = () => { - navigator.clipboard - .writeText(meta.message_to_send) - .then(() => { - setMeta((m: any) => ({ ...m, copied: true })); - setTimeout(() => { - setMeta((m: any) => ({ ...m, copied: false })); - }, 2000); - }) - .catch(() => { - alert("Unable to copy to clipboard"); - }); - }; - useEffect(() => { switch (state) { case LoginState.INSTANCE: @@ -122,14 +102,7 @@ export const LoginPage = () => { if (data.success) { if (data.data) setMeta(data.data); - switch (data.step) { - case "CODE_SENT": - setState(LoginState.CODE_INPUT); - break; - case "SEND_CODE": - setState(LoginState.CODE_SEND); - break; - } + setState(LoginState.CODE_INPUT); } else { handleError(`[${status}] ` + (data.error || "unknown")); } @@ -214,26 +187,6 @@ export const LoginPage = () => { .finally(() => setLoading(false)); break; } - case LoginState.CODE_SEND: - setLoading(true); - - api("/api/v1/login/step/verify", "POST") - .then(({ status, data }) => { - if (data.success) { - // NOTE: possible issue; redirecting unprotected - if (searchParams.has("return")) { - // do not use navigate() to bypass history navigation - // we need the backend to preprocess the request - window.location.href = searchParams.get("return")!; - } else { - navigate("/"); - } - } else { - handleError(`[${status}] ` + (data.error || "unknown")); - } - }) - .finally(() => setLoading(false)); - break; } }; @@ -329,32 +282,6 @@ export const LoginPage = () => { /> )} - {state === LoginState.CODE_SEND && ( - <> - - I don't know how to send you a message! -
- You will need to make a post using your account -
- - { - copyMessageToClipboard(); - }} - edge="end" - > - {meta.copied ? : } - - - } - /> - - )} diff --git a/package-lock.json b/package-lock.json index 1c7142cbf9ee9661de87238e0f6ef3d789f54d4a..d4b3802073f9bf5a4a25fd50a3675ffd90c8f3da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,14 +18,21 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@fedify/express": "^0.2.0", + "@fedify/fedify": "^1.5.1", + "@fedify/redis": "^0.4.0", + "@js-temporal/polyfill": "^0.5.1", + "@logtape/logtape": "^0.9.1", "@prisma/client": "^5.13.0", "@tsconfig/recommended": "^1.0.6", "body-parser": "^1.20.2", "cookie-parser": "^1.4.6", "cors": "^2.8.5", + "cron": "^4.3.0", "express": "^4.19.2", "express-session": "^1.18.0", - "oidc-provider": "^8.4.6", + "ioredis": "^5.6.1", + "oidc-provider": "^8.8.1", "openid-client": "^5.6.5" }, "devDependencies": { @@ -41,6 +48,14 @@ "typescript": "^5.4.5" } }, + "backend/node_modules/@logtape/logtape": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@logtape/logtape/-/logtape-0.9.1.tgz", + "integrity": "sha512-L/MNHzc0BWpbb+PIAC9yAdqHDW1ZQrBSmOep/gbp/9DMeFcLvshlxJPH4+isjwueWt01b6jPWjT1PMbtiUPZlg==", + "funding": [ + "https://github.com/sponsors/dahlia" + ] + }, "frontend": { "name": "@fediverse-auth/frontend", "version": "0.0.0", @@ -212,6 +227,60 @@ "node": ">=6.9.0" } }, + "node_modules/@deno/shim-crypto": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@deno/shim-crypto/-/shim-crypto-0.3.1.tgz", + "integrity": "sha512-ed4pNnfur6UbASEgF34gVxR9p7Mc3qF+Ygbmjiil8ws5IhNFhPDFy5vE5hQAUA9JmVsSxXPcVLM5Rf8LOZqQ5Q==" + }, + "node_modules/@deno/shim-deno": { + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@deno/shim-deno/-/shim-deno-0.18.2.tgz", + "integrity": "sha512-oQ0CVmOio63wlhwQF75zA4ioolPvOwAoK0yuzcS5bDC1JUvH3y1GS8xPh8EOpcoDQRU4FTG8OQfxhpR+c6DrzA==", + "dependencies": { + "@deno/shim-deno-test": "^0.5.0", + "which": "^4.0.0" + } + }, + "node_modules/@deno/shim-deno-test": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@deno/shim-deno-test/-/shim-deno-test-0.5.0.tgz", + "integrity": "sha512-4nMhecpGlPi0cSzT67L+Tm+GOJqvuk8gqHBziqcUQOarnuIax1z96/gJHCSIz2Z0zhxE6Rzwb3IZXPtFh51j+w==" + }, + "node_modules/@deno/shim-deno/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "engines": { + "node": ">=16" + } + }, + "node_modules/@deno/shim-deno/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@digitalbazaar/http-client": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@digitalbazaar/http-client/-/http-client-3.4.1.tgz", + "integrity": "sha512-Ahk1N+s7urkgj7WvvUND5f8GiWEPfUw0D41hdElaqLgu8wZScI8gdI0q+qWw5N1d35x7GCRH2uk9mi+Uzo9M3g==", + "dependencies": { + "ky": "^0.33.3", + "ky-universal": "^0.11.0", + "undici": "^5.21.2" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", @@ -412,6 +481,124 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@fedify/express": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@fedify/express/-/express-0.2.0.tgz", + "integrity": "sha512-DLQ9RBm1gwzYfyj7KOeOfQRTYtcxB7DCU7ZvWuJQTDWVrtLAVowRk2jEBjs7ep15Z8ZHzDlbsNV6myws7RjPXQ==", + "funding": [ + "https://github.com/sponsors/dahlia", + "https://toss.me/hongminhee" + ], + "peerDependencies": { + "@fedify/fedify": ">=0.12.0, <2" + } + }, + "node_modules/@fedify/fedify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@fedify/fedify/-/fedify-1.5.1.tgz", + "integrity": "sha512-I+QFHSnshhPUmklkQxU/A4v9pLY9c/BFQKUmYC5cCV7UuYROxoEALfUeE0LjI7EsnRBMFRx3V6Aq2MKTePXJkg==", + "funding": [ + "https://opencollective.com/fedify", + "https://github.com/sponsors/dahlia" + ], + "dependencies": { + "@deno/shim-crypto": "~0.3.1", + "@deno/shim-deno": "~0.18.0", + "@hugoalh/http-header-link": "^1.0.2", + "@js-temporal/polyfill": "^0.5.0", + "@logtape/logtape": "^0.8.2", + "@multiformats/base-x": "^4.0.1", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@phensley/language-tag": "^1.9.0", + "asn1js": "^3.0.5", + "json-canon": "^1.0.1", + "jsonld": "^8.3.2", + "multicodec": "^3.2.1", + "pkijs": "^3.2.4", + "uri-template-router": "^0.0.17", + "url-template": "^3.1.1", + "urlpattern-polyfill": "~10.0.0" + }, + "engines": { + "bun": ">=1.1.0", + "deno": ">=2.0.0", + "node": ">=20.0.0" + } + }, + "node_modules/@fedify/redis": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@fedify/redis/-/redis-0.4.0.tgz", + "integrity": "sha512-dhP/x1jELEpgYTOKZomdqug3K52sCP6QcWihJdI2h+wSCFvYIAPfPnx7ojB/lRm9+jgfGHyDRh1Y2pE/rCCWoA==", + "funding": [ + "https://opencollective.com/fedify", + "https://github.com/sponsors/dahlia" + ], + "dependencies": { + "@deno/shim-deno": "~0.18.0", + "@fedify/fedify": "1.5.0", + "@js-temporal/polyfill": "^0.5.0", + "@logtape/logtape": "^0.9.0", + "ioredis": "^5.4.1" + } + }, + "node_modules/@fedify/redis/node_modules/@fedify/fedify": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@fedify/fedify/-/fedify-1.5.0.tgz", + "integrity": "sha512-gGD8+mwkLsavBoj/qyfhMD8Tnv9+hid59NrQ6ZrD5Zn5rWvq4LleU09GF78OqPWdSLY03ihxb6+EArcsb5VZCA==", + "funding": [ + "https://opencollective.com/fedify", + "https://github.com/sponsors/dahlia" + ], + "dependencies": { + "@deno/shim-crypto": "~0.3.1", + "@deno/shim-deno": "~0.18.0", + "@hugoalh/http-header-link": "^1.0.2", + "@js-temporal/polyfill": "^0.5.0", + "@logtape/logtape": "^0.8.2", + "@multiformats/base-x": "^4.0.1", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@phensley/language-tag": "^1.9.0", + "asn1js": "^3.0.5", + "json-canon": "^1.0.1", + "jsonld": "^8.3.2", + "multicodec": "^3.2.1", + "pkijs": "^3.2.4", + "uri-template-router": "^0.0.17", + "url-template": "^3.1.1", + "urlpattern-polyfill": "~10.0.0" + }, + "engines": { + "bun": ">=1.1.0", + "deno": ">=2.0.0", + "node": ">=20.0.0" + } + }, + "node_modules/@fedify/redis/node_modules/@fedify/fedify/node_modules/@logtape/logtape": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@logtape/logtape/-/logtape-0.8.2.tgz", + "integrity": "sha512-KikaMHi64p0BHYrYOE2Lom4dOE3R8PGT+21QJ5Ql/SWy0CNOp69dkAlG9RXzENQ6PAMWtiU+4kelJYNvfUvHOQ==", + "funding": [ + "https://github.com/sponsors/dahlia" + ] + }, + "node_modules/@fedify/redis/node_modules/@logtape/logtape": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@logtape/logtape/-/logtape-0.9.1.tgz", + "integrity": "sha512-L/MNHzc0BWpbb+PIAC9yAdqHDW1ZQrBSmOep/gbp/9DMeFcLvshlxJPH4+isjwueWt01b6jPWjT1PMbtiUPZlg==", + "funding": [ + "https://github.com/sponsors/dahlia" + ] + }, "node_modules/@fediverse-auth/backend": { "resolved": "backend", "link": true @@ -459,6 +646,25 @@ "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.13.tgz", "integrity": "sha512-j61DHjsdUCKMXSdNLTOxcG701FWnF0jcqNNQi2iPCDxU8seN/sMxeh62dC++UiagCWq9ghTypX+Pcy7kX+QOeQ==" }, + "node_modules/@hugoalh/http-header-link": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@hugoalh/http-header-link/-/http-header-link-1.0.3.tgz", + "integrity": "sha512-x4jzzKSzZQY115H/GxUWaAHzT5eqLXt99uSKY7+0O/h3XrV248+CkZA7cA274QahXzWkGQYYug/AF6QUkTnLEw==", + "dependencies": { + "@hugoalh/is-string-singleline": "^1.0.4" + }, + "engines": { + "node": ">=16.13.0" + } + }, + "node_modules/@hugoalh/is-string-singleline": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@hugoalh/is-string-singleline/-/is-string-singleline-1.0.4.tgz", + "integrity": "sha512-yXIwBXb97DEw1zENgEv8OjVsgXx7kcrXzoFVa4VM9y1H29SaOwD1C0OursnZGqCEw0K2IrmY/CMcoRdehG+Hkg==", + "engines": { + "node": ">=16.13.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "dev": true, @@ -489,6 +695,22 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, + "node_modules/@js-temporal/polyfill": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@js-temporal/polyfill/-/polyfill-0.5.1.tgz", + "integrity": "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==", + "dependencies": { + "jsbi": "^4.3.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@koa/cors": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@koa/cors/-/cors-5.0.0.tgz", @@ -501,24 +723,30 @@ } }, "node_modules/@koa/router": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@koa/router/-/router-12.0.1.tgz", - "integrity": "sha512-ribfPYfHb+Uw3b27Eiw6NPqjhIhTpVFzEWLwyc/1Xp+DCdwRRyIlAUODX+9bPARF6aQtUu1+/PHzdNvRzcs/+Q==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-13.1.0.tgz", + "integrity": "sha512-mNVu1nvkpSd8Q8gMebGbCkDWJ51ODetrFvLKYusej+V0ByD4btqHYnPIzTBLXnQMVUlm/oxVwqmWBY3zQfZilw==", "dependencies": { - "debug": "^4.3.4", "http-errors": "^2.0.0", "koa-compose": "^4.1.0", - "methods": "^1.1.2", - "path-to-regexp": "^6.2.1" + "path-to-regexp": "^6.3.0" }, "engines": { - "node": ">= 12" + "node": ">= 18" } }, "node_modules/@koa/router/node_modules/path-to-regexp": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", - "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==" + }, + "node_modules/@logtape/logtape": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@logtape/logtape/-/logtape-0.8.2.tgz", + "integrity": "sha512-KikaMHi64p0BHYrYOE2Lom4dOE3R8PGT+21QJ5Ql/SWy0CNOp69dkAlG9RXzENQ6PAMWtiU+4kelJYNvfUvHOQ==", + "funding": [ + "https://github.com/sponsors/dahlia" + ] }, "node_modules/@mui/base": { "version": "5.0.0-beta.40", @@ -805,6 +1033,22 @@ } } }, + "node_modules/@multiformats/base-x": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@multiformats/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==" + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "dev": true, @@ -837,6 +1081,30 @@ "node": ">= 8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.32.0.tgz", + "integrity": "sha512-s0OpmpQFSfMrmedAn9Lhg4KWJELHCU6uU9dtIJ28N8UGhf9Y55im5X8fEzwhwDwiSqN+ZPSNrDJF7ivf/AuRPQ==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@phensley/language-tag": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@phensley/language-tag/-/language-tag-1.12.0.tgz", + "integrity": "sha512-d6P4riI4XFlscmx1dOSzDv2P7rKRp9HI8u9Kx3hXRYvsXVtarwsJzDMyHNaItzcMhdoH8YWErUa0OUVFVW1dbw==", + "dependencies": { + "tslib": "^2.8.1" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -1202,6 +1470,11 @@ "@types/koa": "*" } }, + "node_modules/@types/luxon": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz", + "integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -1518,6 +1791,17 @@ "vite": "^4 || ^5" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -1604,6 +1888,19 @@ "node": ">=8" } }, + "node_modules/asn1js": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz", + "integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", @@ -1687,6 +1984,14 @@ "node": ">= 0.8" } }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/cache-content-type": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", @@ -1742,6 +2047,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "license": "MIT", @@ -1749,6 +2081,11 @@ "node": ">=6" } }, + "node_modules/canonicalize": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-1.0.8.tgz", + "integrity": "sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A==" + }, "node_modules/chalk": { "version": "4.1.2", "dev": true, @@ -1772,6 +2109,14 @@ "node": ">=6" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -1903,6 +2248,18 @@ "node": ">=10" } }, + "node_modules/cron": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/cron/-/cron-4.3.0.tgz", + "integrity": "sha512-ciiYNLfSlF9MrDqnbMdRWFiA6oizSF7kA1osPP9lRzNu0Uu+AWog1UKy7SkckiDY2irrNjeO6qLyKnXC8oxmrw==", + "dependencies": { + "@types/luxon": "~3.6.0", + "luxon": "~3.6.0" + }, + "engines": { + "node": ">=18.x" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "dev": true, @@ -1920,11 +2277,20 @@ "version": "3.1.3", "license": "MIT" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { - "version": "4.3.4", - "license": "MIT", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1999,6 +2365,14 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2059,6 +2433,19 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2081,12 +2468,9 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "engines": { "node": ">= 0.4" } @@ -2099,6 +2483,17 @@ "node": ">= 0.4" } }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.20.2", "dev": true, @@ -2305,9 +2700,9 @@ } }, "node_modules/eta": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eta/-/eta-3.4.0.tgz", - "integrity": "sha512-tCsc7WXTjrTx4ZjYLplcqrI3o4mYJ+Z6YspeuGL8tbt/hHoMchwBwtKfwM09svEY86iRapY93vUqQttcNuIO5Q==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-3.5.0.tgz", + "integrity": "sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug==", "engines": { "node": ">=6.0.0" }, @@ -2323,6 +2718,14 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "4.19.2", "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", @@ -2462,6 +2865,28 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "dev": true, @@ -2560,6 +2985,17 @@ "node": ">= 14.17" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2604,15 +3040,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2621,6 +3062,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -2708,11 +3161,11 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2766,21 +3219,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -2971,6 +3413,29 @@ "version": "2.0.4", "license": "ISC" }, + "node_modules/ioredis": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", + "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -3004,11 +3469,14 @@ } }, "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3044,15 +3512,32 @@ "node": ">=8" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/isexe": { "version": "2.0.0", "dev": true, "license": "ISC" }, "node_modules/jose": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.4.tgz", - "integrity": "sha512-6ScbIk2WWCeXkmzF6bRPmEuaqy1m8SbsRFMa/FLrSCkGIhj8OLVG/IH+XHVmNMx/KUo8cVWEE6oKR4dJ+S0Rkg==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", "funding": { "url": "https://github.com/sponsors/panva" } @@ -3072,10 +3557,15 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbi": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.2.tgz", + "integrity": "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==" + }, "node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "bin": { "jsesc": "bin/jsesc" }, @@ -3087,6 +3577,11 @@ "version": "3.0.1", "license": "MIT" }, + "node_modules/json-canon": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-canon/-/json-canon-1.0.1.tgz", + "integrity": "sha512-PQcj4PFOTAQxE8PgoQ4KrM0DcKWZd7S3ELOON8rmysl9I8JuFMgxu1H9v+oZsTPjjkpeS3IHPwLjr7d+gKygnw==" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -3102,6 +3597,20 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonld": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-8.3.3.tgz", + "integrity": "sha512-9YcilrF+dLfg9NTEof/mJLMtbdX1RJ8dbWtJgE00cMOIohb1lIyJl710vFiTaiHTl6ZYODJuBd32xFvUhmv3kg==", + "dependencies": { + "@digitalbazaar/http-client": "^3.4.1", + "canonicalize": "^1.0.1", + "lru-cache": "^6.0.0", + "rdf-canonize": "^3.4.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/keygrip": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", @@ -3121,9 +3630,9 @@ } }, "node_modules/koa": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.3.tgz", - "integrity": "sha512-j/8tY9j5t+GVMLeioLaxweJiKUayFhlGqNTzf2ZGwL0ZCQijd2RLHK0SLW5Tsko8YyyqCZC2cojIb0/s62qTAg==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.1.tgz", + "integrity": "sha512-umfX9d3iuSxTQP4pnzLOz0HKnPg0FaUUIKcye2lOiz3KPu1Y3M3xlz76dISdFPQs37P9eJz1wUpcTS6KDPn9fA==", "dependencies": { "accepts": "^1.3.5", "cache-content-type": "^1.0.0", @@ -3201,6 +3710,41 @@ "node": ">= 0.6" } }, + "node_modules/ky": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/ky/-/ky-0.33.3.tgz", + "integrity": "sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, + "node_modules/ky-universal": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/ky-universal/-/ky-universal-0.11.0.tgz", + "integrity": "sha512-65KyweaWvk+uKKkCrfAf+xqN2/epw1IJDtlyCPxYffFCMR8u1sp2U65NtWpnozYfZxQ6IUzIlvUcw+hQ82U2Xw==", + "dependencies": { + "abort-controller": "^3.0.0", + "node-fetch": "^3.2.10" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky-universal?sponsor=1" + }, + "peerDependencies": { + "ky": ">=0.31.4", + "web-streams-polyfill": ">=3.2.1" + }, + "peerDependenciesMeta": { + "web-streams-polyfill": { + "optional": true + } + } + }, "node_modules/levn": { "version": "0.4.1", "dev": true, @@ -3248,6 +3792,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "dev": true, @@ -3284,6 +3838,14 @@ "node": ">=10" } }, + "node_modules/luxon": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", + "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/match-sorter": { "version": "6.3.4", "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz", @@ -3293,6 +3855,14 @@ "remove-accents": "0.5.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -3387,8 +3957,24 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "license": "MIT" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/multicodec": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/multicodec/-/multicodec-3.2.1.tgz", + "integrity": "sha512-+expTPftro8VAW8kfvcuNNNBgb9gPeNYV9dn+z1kJRWF2vih+/S79f2RVeIwmrJBUJ6NT9IUPWnZDQvegEh5pw==", + "deprecated": "This module has been superseded by the multiformats module", + "dependencies": { + "uint8arrays": "^3.0.0", + "varint": "^6.0.0" + } + }, + "node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" }, "node_modules/nanoid": { "version": "3.3.7", @@ -3420,6 +4006,42 @@ "node": ">= 0.6" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/normalize-url": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", @@ -3464,32 +4086,43 @@ } }, "node_modules/oidc-provider": { - "version": "8.4.6", - "resolved": "https://registry.npmjs.org/oidc-provider/-/oidc-provider-8.4.6.tgz", - "integrity": "sha512-liuHBXRaIjer6nPGWagrl5UjPhIZqahqLVPoYlc2WXsRR7XddwNCBUl1ks5r3Q3uCUfMdQTv1VsjmlaObdff8w==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/oidc-provider/-/oidc-provider-8.8.1.tgz", + "integrity": "sha512-qVChpayTwojUREJxLkFofUSK8kiSRIdzPrVSsoGibqRHl/YO60ege94OZS8vh7zaK+zxcG/Gu8UMaYB5ulohCQ==", "dependencies": { "@koa/cors": "^5.0.0", - "@koa/router": "^12.0.1", - "debug": "^4.3.4", - "eta": "^3.4.0", + "@koa/router": "^13.1.0", + "debug": "^4.4.0", + "eta": "^3.5.0", "got": "^13.0.0", - "jose": "^5.2.4", - "jsesc": "^3.0.2", - "koa": "^2.15.3", - "nanoid": "^5.0.7", + "jose": "^5.9.6", + "jsesc": "^3.1.0", + "koa": "^2.15.4", + "nanoid": "^5.0.9", "object-hash": "^3.0.0", "oidc-token-hash": "^5.0.3", "quick-lru": "^7.0.0", - "raw-body": "^2.5.2" + "raw-body": "^3.0.0" }, "funding": { "url": "https://github.com/sponsors/panva" } }, + "node_modules/oidc-provider/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/oidc-provider/node_modules/nanoid": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", - "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", + "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", "funding": [ { "type": "github", @@ -3503,6 +4136,20 @@ "node": "^18 || >=20" } }, + "node_modules/oidc-provider/node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/oidc-token-hash": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", @@ -3716,6 +4363,22 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkijs": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.2.5.tgz", + "integrity": "sha512-WX0la7n7CbnguuaIQoT4Fc0IJckPDOUldzOwlZ0nwpOcySS+Six/tXBdc0RX17J5o1To0SAr3xDJjDLsOfDFQA==", + "dependencies": { + "@noble/hashes": "^1.4.0", + "asn1js": "^3.0.5", + "bytestreamjs": "^2.0.0", + "pvtsutils": "^1.3.2", + "pvutils": "^1.1.3", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/postcss": { "version": "8.4.38", "dev": true, @@ -3802,6 +4465,22 @@ "node": ">=6" } }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -3876,6 +4555,17 @@ "node": ">= 0.8" } }, + "node_modules/rdf-canonize": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-3.4.0.tgz", + "integrity": "sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA==", + "dependencies": { + "setimmediate": "^1.0.5" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/react": { "version": "18.3.1", "license": "MIT", @@ -3947,6 +4637,25 @@ "react-dom": ">=16.6.0" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -4106,6 +4815,22 @@ } ] }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -4168,11 +4893,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -4203,6 +4923,11 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -4276,6 +5001,11 @@ "node": ">=0.10.0" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -4376,6 +5106,11 @@ "typescript": ">=4.2.0" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "node_modules/tsscmp": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", @@ -4461,6 +5196,25 @@ "node": ">= 0.8" } }, + "node_modules/uint8arrays": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.1.tgz", + "integrity": "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==", + "dependencies": { + "multiformats": "^9.4.2" + } + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -4483,6 +5237,24 @@ "punycode": "^2.1.0" } }, + "node_modules/uri-template-router": { + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/uri-template-router/-/uri-template-router-0.0.17.tgz", + "integrity": "sha512-5h2I/eSN+XFRAFaSR72KTFWg5rf8GB6Ur5+yWHjtwEqmn6cfZqoWsoWTh6NhxW8pIlFq144G2J23OCg3CeAaSg==" + }, + "node_modules/url-template": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-3.1.1.tgz", + "integrity": "sha512-4oszoaEKE/mQOtAmdMWqIRHmkxWkUZMnXFnjQ5i01CuRSK3uluxcH1MRVVVWmhlnzT1SCDfKxxficm2G37qzCA==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -4491,6 +5263,11 @@ "node": ">= 0.4.0" } }, + "node_modules/varint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -4553,6 +5330,14 @@ } } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, "node_modules/which": { "version": "2.0.2", "dev": true,