From 7779910949ebc6faf870e4dd6cfbb1c455338841 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 2 Nov 2025 23:09:01 +0900 Subject: [PATCH 01/11] =?UTF-8?q?tRPC=20=EC=B4=88=EA=B8=B0=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +++- pnpm-lock.yaml | 36 ++++++++++++++++++++++++---- src/routes/trpc/[...trpc]/+server.ts | 15 ++++++++++++ src/trpc/client.ts | 23 ++++++++++++++++++ src/trpc/init.server.ts | 9 +++++++ src/trpc/router.server.ts | 13 ++++++++++ svelte.config.js | 9 +++---- 7 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 src/routes/trpc/[...trpc]/+server.ts create mode 100644 src/trpc/client.ts create mode 100644 src/trpc/init.server.ts create mode 100644 src/trpc/router.server.ts diff --git a/package.json b/package.json index 5c7d0f3..4a8493b 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,9 @@ "@sveltejs/adapter-node": "^5.4.0", "@sveltejs/kit": "^2.48.4", "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@trpc/client": "^11.7.1", "@types/file-saver": "^2.0.7", - "@types/ms": "^2.1.0", + "@types/ms": "^0.7.34", "@types/node-schedule": "^2.1.8", "@types/pg": "^8.15.6", "autoprefixer": "^10.4.21", @@ -53,6 +54,7 @@ }, "dependencies": { "@fastify/busboy": "^3.2.0", + "@trpc/server": "^11.7.1", "argon2": "^0.44.0", "kysely": "^0.28.8", "ms": "^2.1.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da0ae97..50affd7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@fastify/busboy': specifier: ^3.2.0 version: 3.2.0 + '@trpc/server': + specifier: ^11.7.1 + version: 11.7.1(typescript@5.9.3) argon2: specifier: ^0.44.0 version: 0.44.0 @@ -48,12 +51,15 @@ importers: '@sveltejs/vite-plugin-svelte': specifier: ^6.2.1 version: 6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0)) + '@trpc/client': + specifier: ^11.7.1 + version: 11.7.1(@trpc/server@11.7.1(typescript@5.9.3))(typescript@5.9.3) '@types/file-saver': specifier: ^2.0.7 version: 2.0.7 '@types/ms': - specifier: ^2.1.0 - version: 2.1.0 + specifier: ^0.7.34 + version: 0.7.34 '@types/node-schedule': specifier: ^2.1.8 version: 2.1.8 @@ -612,6 +618,17 @@ packages: svelte: ^5.0.0 vite: ^6.3.0 || ^7.0.0 + '@trpc/client@11.7.1': + resolution: {integrity: sha512-uOnAjElKI892/U6aQMcBHYs3x7mme3Cvv1F87ytBL56rBvs7+DyK7r43zgaXKf13+GtPEI6ex5xjVUfyDW8XcQ==} + peerDependencies: + '@trpc/server': 11.7.1 + typescript: '>=5.7.2' + + '@trpc/server@11.7.1': + resolution: {integrity: sha512-N3U8LNLIP4g9C7LJ/sLkjuPHwqlvE3bnspzC4DEFVdvx2+usbn70P80E3wj5cjOTLhmhRiwJCSXhlB+MHfGeCw==} + peerDependencies: + typescript: '>=5.7.2' + '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -624,8 +641,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/ms@2.1.0': - resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/ms@0.7.34': + resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} '@types/node-schedule@2.1.8': resolution: {integrity: sha512-k00g6Yj/oUg/CDC+MeLHUzu0+OFxWbIqrFfDiLi6OPKxTujvpv29mHGM8GtKr7B+9Vv92FcK/8mRqi1DK5f3hA==} @@ -2450,6 +2467,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@trpc/client@11.7.1(@trpc/server@11.7.1(typescript@5.9.3))(typescript@5.9.3)': + dependencies: + '@trpc/server': 11.7.1(typescript@5.9.3) + typescript: 5.9.3 + + '@trpc/server@11.7.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@types/cookie@0.6.0': {} '@types/estree@1.0.8': {} @@ -2458,7 +2484,7 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/ms@2.1.0': {} + '@types/ms@0.7.34': {} '@types/node-schedule@2.1.8': dependencies: diff --git a/src/routes/trpc/[...trpc]/+server.ts b/src/routes/trpc/[...trpc]/+server.ts new file mode 100644 index 0000000..052ee09 --- /dev/null +++ b/src/routes/trpc/[...trpc]/+server.ts @@ -0,0 +1,15 @@ +import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; +import { createContext } from "$trpc/init.server"; +import { appRouter } from "$trpc/router.server"; +import type { RequestHandler } from "./$types"; + +const trpcHandler: RequestHandler = (event) => + fetchRequestHandler({ + endpoint: "/trpc", + req: event.request, + router: appRouter, + createContext: () => createContext(event), + }); + +export const GET = trpcHandler; +export const POST = trpcHandler; diff --git a/src/trpc/client.ts b/src/trpc/client.ts new file mode 100644 index 0000000..0596c63 --- /dev/null +++ b/src/trpc/client.ts @@ -0,0 +1,23 @@ +import { createTRPCClient, httpBatchLink } from "@trpc/client"; +import { browser } from "$app/environment"; +import type { AppRouter } from "./router.server"; + +const createClient = (fetch: typeof globalThis.fetch) => + createTRPCClient({ + links: [ + httpBatchLink({ + url: "/trpc", + fetch, + }), + ], + }); + +let browserClient: ReturnType; + +export const trpc = (fetch = globalThis.fetch) => { + const client = browserClient ?? createClient(fetch); + if (browser) { + browserClient ??= client; + } + return client; +}; diff --git a/src/trpc/init.server.ts b/src/trpc/init.server.ts new file mode 100644 index 0000000..a6af870 --- /dev/null +++ b/src/trpc/init.server.ts @@ -0,0 +1,9 @@ +import type { RequestEvent } from "@sveltejs/kit"; +import { initTRPC } from "@trpc/server"; + +export const createContext = (event: RequestEvent) => event; + +const t = initTRPC.context>>().create(); + +export const router = t.router; +export const publicProcedure = t.procedure; diff --git a/src/trpc/router.server.ts b/src/trpc/router.server.ts new file mode 100644 index 0000000..35aff92 --- /dev/null +++ b/src/trpc/router.server.ts @@ -0,0 +1,13 @@ +import type { RequestEvent } from "@sveltejs/kit"; +import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; +import { createContext, router } from "./init.server"; + +export const appRouter = router({ + // TODO +}); + +export const createCaller = (event: RequestEvent) => appRouter.createCaller(createContext(event)); + +export type AppRouter = typeof appRouter; +export type RouterInputs = inferRouterInputs; +export type RouterOutputs = inferRouterOutputs; diff --git a/svelte.config.js b/svelte.config.js index bbef2bb..4ffc844 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -3,15 +3,12 @@ import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; /** @type {import('@sveltejs/kit').Config} */ const config = { - // Consult https://svelte.dev/docs/kit/integrations - // for more information about preprocessors preprocess: vitePreprocess(), - kit: { - // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://svelte.dev/docs/kit/adapters for more information about adapters. adapter: adapter(), + alias: { + $trpc: "./src/trpc", + }, }, }; From 640e12d2c398711e1118fb466fcd355028fcb15b Mon Sep 17 00:00:00 2001 From: static Date: Thu, 25 Dec 2025 16:50:41 +0900 Subject: [PATCH 02/11] =?UTF-8?q?tRPC=20Authorization=20=EB=AF=B8=EB=93=A4?= =?UTF-8?q?=EC=9B=A8=EC=96=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/middlewares/authenticate.ts | 2 +- src/lib/server/modules/auth.ts | 89 +++++++++++++--------- src/trpc/init.server.ts | 20 ++++- src/trpc/middlewares/authorize.ts | 36 +++++++++ 4 files changed, 109 insertions(+), 38 deletions(-) create mode 100644 src/trpc/middlewares/authorize.ts diff --git a/src/lib/server/middlewares/authenticate.ts b/src/lib/server/middlewares/authenticate.ts index 8585bce..ad8c585 100644 --- a/src/lib/server/middlewares/authenticate.ts +++ b/src/lib/server/middlewares/authenticate.ts @@ -4,7 +4,7 @@ import { authenticate, AuthenticationError } from "$lib/server/modules/auth"; export const authenticateMiddleware: Handle = async ({ event, resolve }) => { const { pathname, search } = event.url; - if (pathname === "/api/auth/login") { + if (pathname === "/api/auth/login" || pathname.startsWith("/trpc")) { return await resolve(event); } diff --git a/src/lib/server/modules/auth.ts b/src/lib/server/modules/auth.ts index d25033d..6ae3865 100644 --- a/src/lib/server/modules/auth.ts +++ b/src/lib/server/modules/auth.ts @@ -11,10 +11,17 @@ interface Session { clientId?: number; } -interface ClientSession extends Session { +export interface ClientSession extends Session { clientId: number; } +export type SessionPermission = + | "any" + | "notClient" + | "anyClient" + | "pendingClient" + | "activeClient"; + export class AuthenticationError extends Error { constructor( public status: 400 | 401, @@ -25,6 +32,16 @@ export class AuthenticationError extends Error { } } +export class AuthorizationError extends Error { + constructor( + public status: 403 | 500, + message: string, + ) { + super(message); + this.name = "AuthorizationError"; + } +} + export const startSession = async (userId: number, ip: string, userAgent: string) => { const { sessionId, sessionIdSigned } = await issueSessionId(32, env.session.secret); await createSession(userId, sessionId, ip, userAgent); @@ -52,34 +69,12 @@ export const authenticate = async (sessionIdSigned: string, ip: string, userAgen } }; -export async function authorize(locals: App.Locals, requiredPermission: "any"): Promise; - -export async function authorize( +export const authorizeInternal = async ( locals: App.Locals, - requiredPermission: "notClient", -): Promise; - -export async function authorize( - locals: App.Locals, - requiredPermission: "anyClient", -): Promise; - -export async function authorize( - locals: App.Locals, - requiredPermission: "pendingClient", -): Promise; - -export async function authorize( - locals: App.Locals, - requiredPermission: "activeClient", -): Promise; - -export async function authorize( - locals: App.Locals, - requiredPermission: "any" | "notClient" | "anyClient" | "pendingClient" | "activeClient", -): Promise { + requiredPermission: SessionPermission, +): Promise => { if (!locals.session) { - error(500, "Unauthenticated"); + throw new AuthorizationError(500, "Unauthenticated"); } const { id: sessionId, userId, clientId } = locals.session; @@ -89,39 +84,63 @@ export async function authorize( break; case "notClient": if (clientId) { - error(403, "Forbidden"); + throw new AuthorizationError(403, "Forbidden"); } break; case "anyClient": if (!clientId) { - error(403, "Forbidden"); + throw new AuthorizationError(403, "Forbidden"); } break; case "pendingClient": { if (!clientId) { - error(403, "Forbidden"); + throw new AuthorizationError(403, "Forbidden"); } const userClient = await getUserClient(userId, clientId); if (!userClient) { - error(500, "Invalid session id"); + throw new AuthorizationError(500, "Invalid session id"); } else if (userClient.state !== "pending") { - error(403, "Forbidden"); + throw new AuthorizationError(403, "Forbidden"); } break; } case "activeClient": { if (!clientId) { - error(403, "Forbidden"); + throw new AuthorizationError(403, "Forbidden"); } const userClient = await getUserClient(userId, clientId); if (!userClient) { - error(500, "Invalid session id"); + throw new AuthorizationError(500, "Invalid session id"); } else if (userClient.state !== "active") { - error(403, "Forbidden"); + throw new AuthorizationError(403, "Forbidden"); } break; } } return { sessionId, userId, clientId }; +}; + +export async function authorize( + locals: App.Locals, + requiredPermission: "any" | "notClient", +): Promise; + +export async function authorize( + locals: App.Locals, + requiredPermission: "anyClient" | "pendingClient" | "activeClient", +): Promise; + +export async function authorize( + locals: App.Locals, + requiredPermission: SessionPermission, +): Promise { + try { + return await authorizeInternal(locals, requiredPermission); + } catch (e) { + if (e instanceof AuthorizationError) { + error(e.status, e.message); + } + throw e; + } } diff --git a/src/trpc/init.server.ts b/src/trpc/init.server.ts index a6af870..15a35fa 100644 --- a/src/trpc/init.server.ts +++ b/src/trpc/init.server.ts @@ -1,9 +1,25 @@ import type { RequestEvent } from "@sveltejs/kit"; -import { initTRPC } from "@trpc/server"; +import { initTRPC, TRPCError } from "@trpc/server"; +import { authorizeMiddleware, authorizeClientMiddleware } from "./middlewares/authorize"; export const createContext = (event: RequestEvent) => event; -const t = initTRPC.context>>().create(); +export const t = initTRPC.context>>().create(); export const router = t.router; export const publicProcedure = t.procedure; + +const authedProcedure = publicProcedure.use(async ({ ctx, next }) => { + if (!ctx.locals.session) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + return next(); +}); + +export const roleProcedure = { + any: authedProcedure.use(authorizeMiddleware("any")), + notClient: authedProcedure.use(authorizeMiddleware("notClient")), + anyClient: authedProcedure.use(authorizeClientMiddleware("anyClient")), + pendingClient: authedProcedure.use(authorizeClientMiddleware("pendingClient")), + activeClient: authedProcedure.use(authorizeClientMiddleware("activeClient")), +}; diff --git a/src/trpc/middlewares/authorize.ts b/src/trpc/middlewares/authorize.ts new file mode 100644 index 0000000..53413b3 --- /dev/null +++ b/src/trpc/middlewares/authorize.ts @@ -0,0 +1,36 @@ +import { TRPCError } from "@trpc/server"; +import { + AuthorizationError, + authorizeInternal, + type ClientSession, + type SessionPermission, +} from "$lib/server/modules/auth"; +import { t } from "../init.server"; + +const authorize = async (locals: App.Locals, requiredPermission: SessionPermission) => { + try { + return await authorizeInternal(locals, requiredPermission); + } catch (e) { + if (e instanceof AuthorizationError) { + throw new TRPCError({ + code: e.status === 403 ? "FORBIDDEN" : "INTERNAL_SERVER_ERROR", + message: e.message, + }); + } + throw e; + } +}; + +export const authorizeMiddleware = (requiredPermission: "any" | "notClient") => + t.middleware(async ({ ctx, next }) => { + const session = await authorize(ctx.locals, requiredPermission); + return next({ ctx: { session } }); + }); + +export const authorizeClientMiddleware = ( + requiredPermission: "anyClient" | "pendingClient" | "activeClient", +) => + t.middleware(async ({ ctx, next }) => { + const session = (await authorize(ctx.locals, requiredPermission)) as ClientSession; + return next({ ctx: { session } }); + }); From aa4a1a74eab20fc962369ed8bf82b38ea515ab39 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 25 Dec 2025 18:59:41 +0900 Subject: [PATCH 03/11] =?UTF-8?q?/api/client=20=EC=95=84=EB=9E=98=EC=9D=98?= =?UTF-8?q?=20Endpoint=EB=93=A4=EC=9D=84=20tRPC=EB=A1=9C=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/db/client.ts | 16 --- src/lib/server/db/index.ts | 10 ++ src/lib/server/db/mek.ts | 13 -- src/lib/server/modules/mek.ts | 6 - src/lib/server/schemas/client.ts | 36 ------ src/lib/server/schemas/index.ts | 1 - src/lib/server/services/client.ts | 116 ------------------ src/lib/services/key.ts | 35 +++--- src/routes/api/client/list/+server.ts | 11 -- src/routes/api/client/register/+server.ts | 20 --- .../api/client/register/verify/+server.ts | 16 --- src/routes/api/client/status/+server.ts | 17 --- src/trpc/client.ts | 2 +- src/trpc/router.server.ts | 3 +- src/trpc/routers/client.ts | 96 +++++++++++++++ src/trpc/routers/index.ts | 1 + 16 files changed, 128 insertions(+), 271 deletions(-) create mode 100644 src/lib/server/db/index.ts delete mode 100644 src/lib/server/schemas/client.ts delete mode 100644 src/lib/server/services/client.ts delete mode 100644 src/routes/api/client/list/+server.ts delete mode 100644 src/routes/api/client/register/+server.ts delete mode 100644 src/routes/api/client/register/verify/+server.ts delete mode 100644 src/routes/api/client/status/+server.ts create mode 100644 src/trpc/routers/client.ts create mode 100644 src/trpc/routers/index.ts diff --git a/src/lib/server/db/client.ts b/src/lib/server/db/client.ts index 373357a..272df61 100644 --- a/src/lib/server/db/client.ts +++ b/src/lib/server/db/client.ts @@ -98,22 +98,6 @@ export const createUserClient = async (userId: number, clientId: number) => { } }; -export const getAllUserClients = async (userId: number) => { - const userClients = await db - .selectFrom("user_client") - .selectAll() - .where("user_id", "=", userId) - .execute(); - return userClients.map( - ({ user_id, client_id, state }) => - ({ - userId: user_id, - clientId: client_id, - state, - }) satisfies UserClient, - ); -}; - export const getUserClient = async (userId: number, clientId: number) => { const userClient = await db .selectFrom("user_client") diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts new file mode 100644 index 0000000..5c21deb --- /dev/null +++ b/src/lib/server/db/index.ts @@ -0,0 +1,10 @@ +export * as CategoryRepo from "./category"; +export * as ClientRepo from "./client"; +export * as FileRepo from "./file"; +export * as HskRepo from "./hsk"; +export * as MediaRepo from "./media"; +export * as MekRepo from "./mek"; +export * as SessionRepo from "./session"; +export * as UserRepo from "./user"; + +export * from "./error"; diff --git a/src/lib/server/db/mek.ts b/src/lib/server/db/mek.ts index d6eecb0..65d00f8 100644 --- a/src/lib/server/db/mek.ts +++ b/src/lib/server/db/mek.ts @@ -60,19 +60,6 @@ export const registerInitialMek = async ( }); }; -export const getInitialMek = async (userId: number) => { - const mek = await db - .selectFrom("master_encryption_key") - .selectAll() - .where("user_id", "=", userId) - .where("version", "=", 1) - .limit(1) - .executeTakeFirst(); - return mek - ? ({ userId: mek.user_id, version: mek.version, state: mek.state } satisfies Mek) - : null; -}; - export const getAllValidClientMeks = async (userId: number, clientId: number) => { const clientMeks = await db .selectFrom("client_master_encryption_key") diff --git a/src/lib/server/modules/mek.ts b/src/lib/server/modules/mek.ts index 1605d75..23623f8 100644 --- a/src/lib/server/modules/mek.ts +++ b/src/lib/server/modules/mek.ts @@ -1,13 +1,7 @@ import { error } from "@sveltejs/kit"; import { getUserClientWithDetails } from "$lib/server/db/client"; -import { getInitialMek } from "$lib/server/db/mek"; import { verifySignature } from "$lib/server/modules/crypto"; -export const isInitialMekNeeded = async (userId: number) => { - const initialMek = await getInitialMek(userId); - return !initialMek; -}; - export const verifyClientEncMekSig = async ( userId: number, clientId: number, diff --git a/src/lib/server/schemas/client.ts b/src/lib/server/schemas/client.ts deleted file mode 100644 index 08a76b7..0000000 --- a/src/lib/server/schemas/client.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { z } from "zod"; - -export const clientListResponse = z.object({ - clients: z.array( - z.object({ - id: z.number().int().positive(), - state: z.enum(["pending", "active"]), - }), - ), -}); -export type ClientListResponse = z.output; - -export const clientRegisterRequest = z.object({ - encPubKey: z.string().base64().nonempty(), - sigPubKey: z.string().base64().nonempty(), -}); -export type ClientRegisterRequest = z.input; - -export const clientRegisterResponse = z.object({ - id: z.number().int().positive(), - challenge: z.string().base64().nonempty(), -}); -export type ClientRegisterResponse = z.output; - -export const clientRegisterVerifyRequest = z.object({ - id: z.number().int().positive(), - answerSig: z.string().base64().nonempty(), -}); -export type ClientRegisterVerifyRequest = z.input; - -export const clientStatusResponse = z.object({ - id: z.number().int().positive(), - state: z.enum(["pending", "active"]), - isInitialMekNeeded: z.boolean(), -}); -export type ClientStatusResponse = z.output; diff --git a/src/lib/server/schemas/index.ts b/src/lib/server/schemas/index.ts index 1fed0d0..b2d4fa5 100644 --- a/src/lib/server/schemas/index.ts +++ b/src/lib/server/schemas/index.ts @@ -1,6 +1,5 @@ export * from "./auth"; export * from "./category"; -export * from "./client"; export * from "./directory"; export * from "./file"; export * from "./hsk"; diff --git a/src/lib/server/services/client.ts b/src/lib/server/services/client.ts deleted file mode 100644 index 811e58c..0000000 --- a/src/lib/server/services/client.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { error } from "@sveltejs/kit"; -import { - createClient, - getClient, - getClientByPubKeys, - createUserClient, - getAllUserClients, - getUserClient, - setUserClientStateToPending, - registerUserClientChallenge, - consumeUserClientChallenge, -} from "$lib/server/db/client"; -import { IntegrityError } from "$lib/server/db/error"; -import { verifyPubKey, verifySignature, generateChallenge } from "$lib/server/modules/crypto"; -import { isInitialMekNeeded } from "$lib/server/modules/mek"; -import env from "$lib/server/loadenv"; - -export const getUserClientList = async (userId: number) => { - const userClients = await getAllUserClients(userId); - return { - userClients: userClients.map(({ clientId, state }) => ({ - id: clientId, - state: state as "pending" | "active", - })), - }; -}; - -const expiresAt = () => new Date(Date.now() + env.challenge.userClientExp); - -const createUserClientChallenge = async ( - ip: string, - userId: number, - clientId: number, - encPubKey: string, -) => { - const { answer, challenge } = await generateChallenge(32, encPubKey); - const { id } = await registerUserClientChallenge( - userId, - clientId, - answer.toString("base64"), - ip, - expiresAt(), - ); - return { id, challenge: challenge.toString("base64") }; -}; - -export const registerUserClient = async ( - userId: number, - ip: string, - encPubKey: string, - sigPubKey: string, -) => { - const client = await getClientByPubKeys(encPubKey, sigPubKey); - if (client) { - try { - await createUserClient(userId, client.id); - return await createUserClientChallenge(ip, userId, client.id, encPubKey); - } catch (e) { - if (e instanceof IntegrityError && e.message === "User client already exists") { - error(409, "Client already registered"); - } - throw e; - } - } else { - if (encPubKey === sigPubKey) { - error(400, "Same public keys"); - } else if (!verifyPubKey(encPubKey) || !verifyPubKey(sigPubKey)) { - error(400, "Invalid public key(s)"); - } - - try { - const { id: clientId } = await createClient(encPubKey, sigPubKey, userId); - return await createUserClientChallenge(ip, userId, clientId, encPubKey); - } catch (e) { - if (e instanceof IntegrityError && e.message === "Public key(s) already registered") { - error(409, "Public key(s) already used"); - } - throw e; - } - } -}; - -export const verifyUserClient = async ( - userId: number, - ip: string, - challengeId: number, - answerSig: string, -) => { - const challenge = await consumeUserClientChallenge(challengeId, userId, ip); - if (!challenge) { - error(403, "Invalid challenge answer"); - } - - const client = await getClient(challenge.clientId); - if (!client) { - error(500, "Invalid challenge answer"); - } else if ( - !verifySignature(Buffer.from(challenge.answer, "base64"), answerSig, client.sigPubKey) - ) { - error(403, "Invalid challenge answer signature"); - } - - await setUserClientStateToPending(userId, client.id); -}; - -export const getUserClientStatus = async (userId: number, clientId: number) => { - const userClient = await getUserClient(userId, clientId); - if (!userClient) { - error(500, "Invalid session id"); - } - - return { - state: userClient.state as "pending" | "active", - isInitialMekNeeded: await isInitialMekNeeded(userId), - }; -}; diff --git a/src/lib/services/key.ts b/src/lib/services/key.ts index cecd241..349197b 100644 --- a/src/lib/services/key.ts +++ b/src/lib/services/key.ts @@ -10,15 +10,13 @@ import { verifyMasterKeyWrapped, } from "$lib/modules/crypto"; import type { - ClientRegisterRequest, - ClientRegisterResponse, - ClientRegisterVerifyRequest, InitialHmacSecretRegisterRequest, MasterKeyListResponse, InitialMasterKeyRegisterRequest, } from "$lib/server/schemas"; import { requestSessionUpgrade } from "$lib/services/auth"; import { masterKeyStore, type ClientKeys } from "$lib/stores"; +import { useTRPC } from "$trpc/client"; export const requestClientRegistration = async ( encryptKeyBase64: string, @@ -26,21 +24,24 @@ export const requestClientRegistration = async ( verifyKeyBase64: string, signKey: CryptoKey, ) => { - let res = await callPostApi("/api/client/register", { - encPubKey: encryptKeyBase64, - sigPubKey: verifyKeyBase64, - }); - if (!res.ok) return false; + const trpc = useTRPC(); - const { id, challenge }: ClientRegisterResponse = await res.json(); - const answer = await decryptChallenge(challenge, decryptKey); - const answerSig = await signMessageRSA(answer, signKey); - - res = await callPostApi("/api/client/register/verify", { - id, - answerSig: encodeToBase64(answerSig), - }); - return res.ok; + try { + const { id, challenge } = await trpc.client.register.mutate({ + encPubKey: encryptKeyBase64, + sigPubKey: verifyKeyBase64, + }); + const answer = await decryptChallenge(challenge, decryptKey); + const answerSig = await signMessageRSA(answer, signKey); + await trpc.client.verify.mutate({ + id, + answerSig: encodeToBase64(answerSig), + }); + return true; + } catch { + // TODO: Error Handling + return false; + } }; export const requestClientRegistrationAndSessionUpgrade = async ( diff --git a/src/routes/api/client/list/+server.ts b/src/routes/api/client/list/+server.ts deleted file mode 100644 index 78193ee..0000000 --- a/src/routes/api/client/list/+server.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { json } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { clientListResponse, type ClientListResponse } from "$lib/server/schemas"; -import { getUserClientList } from "$lib/server/services/client"; -import type { RequestHandler } from "./$types"; - -export const GET: RequestHandler = async ({ locals }) => { - const { userId } = await authorize(locals, "anyClient"); - const { userClients } = await getUserClientList(userId); - return json(clientListResponse.parse({ clients: userClients } satisfies ClientListResponse)); -}; diff --git a/src/routes/api/client/register/+server.ts b/src/routes/api/client/register/+server.ts deleted file mode 100644 index 5ac2a53..0000000 --- a/src/routes/api/client/register/+server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { error, json } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { - clientRegisterRequest, - clientRegisterResponse, - type ClientRegisterResponse, -} from "$lib/server/schemas"; -import { registerUserClient } from "$lib/server/services/client"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, request }) => { - const { userId } = await authorize(locals, "notClient"); - - const zodRes = clientRegisterRequest.safeParse(await request.json()); - if (!zodRes.success) error(400, "Invalid request body"); - const { encPubKey, sigPubKey } = zodRes.data; - - const { id, challenge } = await registerUserClient(userId, locals.ip, encPubKey, sigPubKey); - return json(clientRegisterResponse.parse({ id, challenge } satisfies ClientRegisterResponse)); -}; diff --git a/src/routes/api/client/register/verify/+server.ts b/src/routes/api/client/register/verify/+server.ts deleted file mode 100644 index 5ac9396..0000000 --- a/src/routes/api/client/register/verify/+server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { error, text } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { clientRegisterVerifyRequest } from "$lib/server/schemas"; -import { verifyUserClient } from "$lib/server/services/client"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, request }) => { - const { userId } = await authorize(locals, "notClient"); - - const zodRes = clientRegisterVerifyRequest.safeParse(await request.json()); - if (!zodRes.success) error(400, "Invalid request body"); - const { id, answerSig } = zodRes.data; - - await verifyUserClient(userId, locals.ip, id, answerSig); - return text("Client verified", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/routes/api/client/status/+server.ts b/src/routes/api/client/status/+server.ts deleted file mode 100644 index a7ecc82..0000000 --- a/src/routes/api/client/status/+server.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { json } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { clientStatusResponse, type ClientStatusResponse } from "$lib/server/schemas"; -import { getUserClientStatus } from "$lib/server/services/client"; -import type { RequestHandler } from "./$types"; - -export const GET: RequestHandler = async ({ locals }) => { - const { userId, clientId } = await authorize(locals, "anyClient"); - const { state, isInitialMekNeeded } = await getUserClientStatus(userId, clientId); - return json( - clientStatusResponse.parse({ - id: clientId, - state, - isInitialMekNeeded, - } satisfies ClientStatusResponse), - ); -}; diff --git a/src/trpc/client.ts b/src/trpc/client.ts index 0596c63..433a743 100644 --- a/src/trpc/client.ts +++ b/src/trpc/client.ts @@ -14,7 +14,7 @@ const createClient = (fetch: typeof globalThis.fetch) => let browserClient: ReturnType; -export const trpc = (fetch = globalThis.fetch) => { +export const useTRPC = (fetch = globalThis.fetch) => { const client = browserClient ?? createClient(fetch); if (browser) { browserClient ??= client; diff --git a/src/trpc/router.server.ts b/src/trpc/router.server.ts index 35aff92..fdaee15 100644 --- a/src/trpc/router.server.ts +++ b/src/trpc/router.server.ts @@ -1,9 +1,10 @@ import type { RequestEvent } from "@sveltejs/kit"; import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; import { createContext, router } from "./init.server"; +import { clientRouter } from "./routers"; export const appRouter = router({ - // TODO + client: clientRouter, }); export const createCaller = (event: RequestEvent) => appRouter.createCaller(createContext(event)); diff --git a/src/trpc/routers/client.ts b/src/trpc/routers/client.ts new file mode 100644 index 0000000..8add385 --- /dev/null +++ b/src/trpc/routers/client.ts @@ -0,0 +1,96 @@ +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { ClientRepo, IntegrityError } from "$lib/server/db"; +import { verifyPubKey, verifySignature, generateChallenge } from "$lib/server/modules/crypto"; +import env from "$lib/server/loadenv"; +import { router, roleProcedure } from "../init.server"; + +const createUserClientChallenge = async ( + ip: string, + userId: number, + clientId: number, + encPubKey: string, +) => { + const { answer, challenge } = await generateChallenge(32, encPubKey); + const { id } = await ClientRepo.registerUserClientChallenge( + userId, + clientId, + answer.toString("base64"), + ip, + new Date(Date.now() + env.challenge.userClientExp), + ); + return { id, challenge: challenge.toString("base64") }; +}; + +const clientRouter = router({ + register: roleProcedure["notClient"] + .input( + z.object({ + encPubKey: z.string().base64().nonempty(), + sigPubKey: z.string().base64().nonempty(), + }), + ) + .mutation(async ({ ctx, input }) => { + const { userId } = ctx.session; + const { encPubKey, sigPubKey } = input; + const client = await ClientRepo.getClientByPubKeys(encPubKey, sigPubKey); + if (client) { + try { + await ClientRepo.createUserClient(userId, client.id); + return await createUserClientChallenge(ctx.locals.ip, userId, client.id, encPubKey); + } catch (e) { + if (e instanceof IntegrityError && e.message === "User client already exists") { + throw new TRPCError({ code: "CONFLICT", message: "Client already registered" }); + } + throw e; + } + } else { + if (encPubKey === sigPubKey) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Same public keys" }); + } else if (!verifyPubKey(encPubKey) || !verifyPubKey(sigPubKey)) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid public key(s)" }); + } + + try { + const { id: clientId } = await ClientRepo.createClient(encPubKey, sigPubKey, userId); + return await createUserClientChallenge(ctx.locals.ip, userId, clientId, encPubKey); + } catch (e) { + if (e instanceof IntegrityError && e.message === "Public key(s) already registered") { + throw new TRPCError({ code: "CONFLICT", message: "Public key(s) already used" }); + } + throw e; + } + } + }), + + verify: roleProcedure["notClient"] + .input( + z.object({ + id: z.number().int().positive(), + answerSig: z.string().base64().nonempty(), + }), + ) + .mutation(async ({ ctx, input }) => { + const challenge = await ClientRepo.consumeUserClientChallenge( + input.id, + ctx.session.userId, + ctx.locals.ip, + ); + if (!challenge) { + throw new TRPCError({ code: "FORBIDDEN", message: "Invalid challenge answer" }); + } + + const client = await ClientRepo.getClient(challenge.clientId); + if (!client) { + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid challenge answer" }); + } else if ( + !verifySignature(Buffer.from(challenge.answer, "base64"), input.answerSig, client.sigPubKey) + ) { + throw new TRPCError({ code: "FORBIDDEN", message: "Invalid challenge answer signature" }); + } + + await ClientRepo.setUserClientStateToPending(ctx.session.userId, client.id); + }), +}); + +export default clientRouter; diff --git a/src/trpc/routers/index.ts b/src/trpc/routers/index.ts new file mode 100644 index 0000000..6f13a73 --- /dev/null +++ b/src/trpc/routers/index.ts @@ -0,0 +1 @@ +export { default as clientRouter } from "./client"; From 208252f6b2ad1655f0c65a7f1eeeda3a711ae7d5 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 25 Dec 2025 20:00:15 +0900 Subject: [PATCH 04/11] =?UTF-8?q?/api/hsk,=20/api/mek,=20/api/user=20?= =?UTF-8?q?=EC=95=84=EB=9E=98=EC=9D=98=20Endpoint=EB=93=A4=EC=9D=84=20tRPC?= =?UTF-8?q?=EB=A1=9C=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/modules/mek.ts | 19 ------ src/lib/server/schemas/hsk.ts | 19 ------ src/lib/server/schemas/index.ts | 3 - src/lib/server/schemas/mek.ts | 19 ------ src/lib/server/schemas/user.ts | 12 ---- src/lib/server/services/hsk.ts | 31 --------- src/lib/server/services/mek.ts | 38 ----------- src/lib/server/services/user.ts | 15 ----- src/lib/services/key.ts | 56 +++++++++++------ .../(main)/directory/[[id]]/service.svelte.ts | 16 +++-- src/routes/(main)/menu/+page.ts | 14 ++--- src/routes/api/hsk/list/+server.ts | 20 ------ .../api/hsk/register/initial/+server.ts | 16 ----- src/routes/api/mek/list/+server.ts | 20 ------ .../api/mek/register/initial/+server.ts | 16 ----- src/routes/api/user/+server.ts | 11 ---- src/routes/api/user/changeNickname/+server.ts | 16 ----- src/trpc/router.server.ts | 5 +- src/trpc/routers/hsk.ts | 41 ++++++++++++ src/trpc/routers/index.ts | 3 + src/trpc/routers/mek.ts | 63 +++++++++++++++++++ src/trpc/routers/user.ts | 27 ++++++++ 22 files changed, 192 insertions(+), 288 deletions(-) delete mode 100644 src/lib/server/modules/mek.ts delete mode 100644 src/lib/server/schemas/hsk.ts delete mode 100644 src/lib/server/schemas/mek.ts delete mode 100644 src/lib/server/schemas/user.ts delete mode 100644 src/lib/server/services/hsk.ts delete mode 100644 src/lib/server/services/mek.ts delete mode 100644 src/lib/server/services/user.ts delete mode 100644 src/routes/api/hsk/list/+server.ts delete mode 100644 src/routes/api/hsk/register/initial/+server.ts delete mode 100644 src/routes/api/mek/list/+server.ts delete mode 100644 src/routes/api/mek/register/initial/+server.ts delete mode 100644 src/routes/api/user/+server.ts delete mode 100644 src/routes/api/user/changeNickname/+server.ts create mode 100644 src/trpc/routers/hsk.ts create mode 100644 src/trpc/routers/mek.ts create mode 100644 src/trpc/routers/user.ts diff --git a/src/lib/server/modules/mek.ts b/src/lib/server/modules/mek.ts deleted file mode 100644 index 23623f8..0000000 --- a/src/lib/server/modules/mek.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { error } from "@sveltejs/kit"; -import { getUserClientWithDetails } from "$lib/server/db/client"; -import { verifySignature } from "$lib/server/modules/crypto"; - -export const verifyClientEncMekSig = async ( - userId: number, - clientId: number, - version: number, - encMek: string, - encMekSig: string, -) => { - const userClient = await getUserClientWithDetails(userId, clientId); - if (!userClient) { - error(500, "Invalid session id"); - } - - const data = JSON.stringify({ version, key: encMek }); - return verifySignature(Buffer.from(data), encMekSig, userClient.sigPubKey); -}; diff --git a/src/lib/server/schemas/hsk.ts b/src/lib/server/schemas/hsk.ts deleted file mode 100644 index 6f6b428..0000000 --- a/src/lib/server/schemas/hsk.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { z } from "zod"; - -export const hmacSecretListResponse = z.object({ - hsks: z.array( - z.object({ - version: z.number().int().positive(), - state: z.enum(["active"]), - mekVersion: z.number().int().positive(), - hsk: z.string().base64().nonempty(), - }), - ), -}); -export type HmacSecretListResponse = z.output; - -export const initialHmacSecretRegisterRequest = z.object({ - mekVersion: z.number().int().positive(), - hsk: z.string().base64().nonempty(), -}); -export type InitialHmacSecretRegisterRequest = z.input; diff --git a/src/lib/server/schemas/index.ts b/src/lib/server/schemas/index.ts index b2d4fa5..d9ddce7 100644 --- a/src/lib/server/schemas/index.ts +++ b/src/lib/server/schemas/index.ts @@ -2,6 +2,3 @@ export * from "./auth"; export * from "./category"; export * from "./directory"; export * from "./file"; -export * from "./hsk"; -export * from "./mek"; -export * from "./user"; diff --git a/src/lib/server/schemas/mek.ts b/src/lib/server/schemas/mek.ts deleted file mode 100644 index 3d6f468..0000000 --- a/src/lib/server/schemas/mek.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { z } from "zod"; - -export const masterKeyListResponse = z.object({ - meks: z.array( - z.object({ - version: z.number().int().positive(), - state: z.enum(["active", "retired"]), - mek: z.string().base64().nonempty(), - mekSig: z.string().base64().nonempty(), - }), - ), -}); -export type MasterKeyListResponse = z.output; - -export const initialMasterKeyRegisterRequest = z.object({ - mek: z.string().base64().nonempty(), - mekSig: z.string().base64().nonempty(), -}); -export type InitialMasterKeyRegisterRequest = z.input; diff --git a/src/lib/server/schemas/user.ts b/src/lib/server/schemas/user.ts deleted file mode 100644 index 9aec819..0000000 --- a/src/lib/server/schemas/user.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { z } from "zod"; - -export const userInfoResponse = z.object({ - email: z.string().email(), - nickname: z.string().nonempty(), -}); -export type UserInfoResponse = z.output; - -export const nicknameChangeRequest = z.object({ - newNickname: z.string().trim().min(2).max(8), -}); -export type NicknameChangeRequest = z.input; diff --git a/src/lib/server/services/hsk.ts b/src/lib/server/services/hsk.ts deleted file mode 100644 index c381c51..0000000 --- a/src/lib/server/services/hsk.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { error } from "@sveltejs/kit"; -import { IntegrityError } from "$lib/server/db/error"; -import { registerInitialHsk, getAllValidHsks } from "$lib/server/db/hsk"; - -export const getHskList = async (userId: number) => { - const hsks = await getAllValidHsks(userId); - return { - encHsks: hsks.map(({ version, state, mekVersion, encHsk }) => ({ - version, - state, - mekVersion, - encHsk, - })), - }; -}; - -export const registerInitialActiveHsk = async ( - userId: number, - createdBy: number, - mekVersion: number, - encHsk: string, -) => { - try { - await registerInitialHsk(userId, createdBy, mekVersion, encHsk); - } catch (e) { - if (e instanceof IntegrityError && e.message === "HSK already registered") { - error(409, "Initial HSK already registered"); - } - throw e; - } -}; diff --git a/src/lib/server/services/mek.ts b/src/lib/server/services/mek.ts deleted file mode 100644 index 097906a..0000000 --- a/src/lib/server/services/mek.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { error } from "@sveltejs/kit"; -import { setUserClientStateToActive } from "$lib/server/db/client"; -import { IntegrityError } from "$lib/server/db/error"; -import { registerInitialMek, getAllValidClientMeks } from "$lib/server/db/mek"; -import { verifyClientEncMekSig } from "$lib/server/modules/mek"; - -export const getClientMekList = async (userId: number, clientId: number) => { - const clientMeks = await getAllValidClientMeks(userId, clientId); - return { - encMeks: clientMeks.map(({ version, state, encMek, encMekSig }) => ({ - version, - state, - encMek, - encMekSig, - })), - }; -}; - -export const registerInitialActiveMek = async ( - userId: number, - createdBy: number, - encMek: string, - encMekSig: string, -) => { - if (!(await verifyClientEncMekSig(userId, createdBy, 1, encMek, encMekSig))) { - error(400, "Invalid signature"); - } - - try { - await registerInitialMek(userId, createdBy, encMek, encMekSig); - await setUserClientStateToActive(userId, createdBy); - } catch (e) { - if (e instanceof IntegrityError && e.message === "MEK already registered") { - error(409, "Initial MEK already registered"); - } - throw e; - } -}; diff --git a/src/lib/server/services/user.ts b/src/lib/server/services/user.ts deleted file mode 100644 index 5c06458..0000000 --- a/src/lib/server/services/user.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { error } from "@sveltejs/kit"; -import { getUser, setUserNickname } from "$lib/server/db/user"; - -export const getUserInformation = async (userId: number) => { - const user = await getUser(userId); - if (!user) { - error(500, "Invalid session id"); - } - - return { email: user.email, nickname: user.nickname }; -}; - -export const changeNickname = async (userId: number, nickname: string) => { - await setUserNickname(userId, nickname); -}; diff --git a/src/lib/services/key.ts b/src/lib/services/key.ts index 349197b..6149829 100644 --- a/src/lib/services/key.ts +++ b/src/lib/services/key.ts @@ -1,4 +1,4 @@ -import { callGetApi, callPostApi } from "$lib/hooks"; +import { TRPCClientError } from "@trpc/client"; import { storeMasterKeys } from "$lib/indexedDB"; import { encodeToBase64, @@ -9,11 +9,6 @@ import { signMasterKeyWrapped, verifyMasterKeyWrapped, } from "$lib/modules/crypto"; -import type { - InitialHmacSecretRegisterRequest, - MasterKeyListResponse, - InitialMasterKeyRegisterRequest, -} from "$lib/server/schemas"; import { requestSessionUpgrade } from "$lib/services/auth"; import { masterKeyStore, type ClientKeys } from "$lib/stores"; import { useTRPC } from "$trpc/client"; @@ -74,10 +69,16 @@ export const requestClientRegistrationAndSessionUpgrade = async ( }; export const requestMasterKeyDownload = async (decryptKey: CryptoKey, verifyKey: CryptoKey) => { - const res = await callGetApi("/api/mek/list"); - if (!res.ok) return false; + const trpc = useTRPC(); + + let masterKeysWrapped; + try { + masterKeysWrapped = await trpc.mek.list.query(); + } catch { + // TODO: Error Handling + return false; + } - const { meks: masterKeysWrapped }: MasterKeyListResponse = await res.json(); const masterKeys = await Promise.all( masterKeysWrapped.map( async ({ version, state, mek: masterKeyWrapped, mekSig: masterKeyWrappedSig }) => { @@ -109,17 +110,32 @@ export const requestInitialMasterKeyAndHmacSecretRegistration = async ( hmacSecretWrapped: string, signKey: CryptoKey, ) => { - let res = await callPostApi("/api/mek/register/initial", { - mek: masterKeyWrapped, - mekSig: await signMasterKeyWrapped(masterKeyWrapped, 1, signKey), - }); - if (!res.ok) { - return res.status === 403 || res.status === 409; + const trpc = useTRPC(); + + try { + await trpc.mek.registerInitial.mutate({ + mek: masterKeyWrapped, + mekSig: await signMasterKeyWrapped(masterKeyWrapped, 1, signKey), + }); + } catch (e) { + if ( + e instanceof TRPCClientError && + (e.data?.code === "FORBIDDEN" || e.data?.code === "CONFLICT") + ) { + return true; + } + // TODO: Error Handling + return false; } - res = await callPostApi("/api/hsk/register/initial", { - mekVersion: 1, - hsk: hmacSecretWrapped, - }); - return res.ok; + try { + await trpc.hsk.registerInitial.mutate({ + mekVersion: 1, + hsk: hmacSecretWrapped, + }); + return true; + } catch { + // TODO: Error Handling + return false; + } }; diff --git a/src/routes/(main)/directory/[[id]]/service.svelte.ts b/src/routes/(main)/directory/[[id]]/service.svelte.ts index ba5fc4a..40394f7 100644 --- a/src/routes/(main)/directory/[[id]]/service.svelte.ts +++ b/src/routes/(main)/directory/[[id]]/service.svelte.ts @@ -1,5 +1,5 @@ import { getContext, setContext } from "svelte"; -import { callGetApi, callPostApi } from "$lib/hooks"; +import { callPostApi } from "$lib/hooks"; import { storeHmacSecrets } from "$lib/indexedDB"; import { generateDataKey, wrapDataKey, unwrapHmacSecret, encryptString } from "$lib/modules/crypto"; import { @@ -13,10 +13,10 @@ import type { DirectoryRenameRequest, DirectoryCreateRequest, FileRenameRequest, - HmacSecretListResponse, DirectoryDeleteResponse, } from "$lib/server/schemas"; import { hmacSecretStore, type MasterKey, type HmacSecret } from "$lib/stores"; +import { useTRPC } from "$trpc/client"; export interface SelectedEntry { type: "directory" | "file"; @@ -40,10 +40,16 @@ export const useContext = () => { export const requestHmacSecretDownload = async (masterKey: CryptoKey) => { // TODO: MEK rotation - const res = await callGetApi("/api/hsk/list"); - if (!res.ok) return false; + const trpc = useTRPC(); + + let hmacSecretsWrapped; + try { + hmacSecretsWrapped = await trpc.hsk.list.query(); + } catch { + // TODO: Error Handling + return false; + } - const { hsks: hmacSecretsWrapped }: HmacSecretListResponse = await res.json(); const hmacSecrets = await Promise.all( hmacSecretsWrapped.map(async ({ version, state, hsk: hmacSecretWrapped }) => { const { hmacSecret } = await unwrapHmacSecret(hmacSecretWrapped, masterKey); diff --git a/src/routes/(main)/menu/+page.ts b/src/routes/(main)/menu/+page.ts index 30a265a..b1582e7 100644 --- a/src/routes/(main)/menu/+page.ts +++ b/src/routes/(main)/menu/+page.ts @@ -1,14 +1,14 @@ import { error } from "@sveltejs/kit"; -import { callGetApi } from "$lib/hooks"; -import type { UserInfoResponse } from "$lib/server/schemas"; +import { useTRPC } from "$trpc/client"; import type { PageLoad } from "./$types"; export const load: PageLoad = async ({ fetch }) => { - const res = await callGetApi("/api/user", fetch); - if (!res.ok) { + const trpc = useTRPC(fetch); + + try { + const { nickname } = await trpc.user.info.query(); + return { nickname }; + } catch { error(500, "Internal server error"); } - - const { nickname }: UserInfoResponse = await res.json(); - return { nickname }; }; diff --git a/src/routes/api/hsk/list/+server.ts b/src/routes/api/hsk/list/+server.ts deleted file mode 100644 index 50957a3..0000000 --- a/src/routes/api/hsk/list/+server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { json } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { hmacSecretListResponse, type HmacSecretListResponse } from "$lib/server/schemas"; -import { getHskList } from "$lib/server/services/hsk"; -import type { RequestHandler } from "./$types"; - -export const GET: RequestHandler = async ({ locals }) => { - const { userId } = await authorize(locals, "activeClient"); - const { encHsks } = await getHskList(userId); - return json( - hmacSecretListResponse.parse({ - hsks: encHsks.map(({ version, state, mekVersion, encHsk }) => ({ - version, - state, - mekVersion, - hsk: encHsk, - })), - } satisfies HmacSecretListResponse), - ); -}; diff --git a/src/routes/api/hsk/register/initial/+server.ts b/src/routes/api/hsk/register/initial/+server.ts deleted file mode 100644 index 8b32952..0000000 --- a/src/routes/api/hsk/register/initial/+server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { error, text } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { initialHmacSecretRegisterRequest } from "$lib/server/schemas"; -import { registerInitialActiveHsk } from "$lib/server/services/hsk"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, request }) => { - const { userId, clientId } = await authorize(locals, "activeClient"); - - const zodRes = initialHmacSecretRegisterRequest.safeParse(await request.json()); - if (!zodRes.success) error(400, "Invalid request body"); - const { mekVersion, hsk } = zodRes.data; - - await registerInitialActiveHsk(userId, clientId, mekVersion, hsk); - return text("HSK registered", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/routes/api/mek/list/+server.ts b/src/routes/api/mek/list/+server.ts deleted file mode 100644 index b3df9fe..0000000 --- a/src/routes/api/mek/list/+server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { json } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { masterKeyListResponse, type MasterKeyListResponse } from "$lib/server/schemas"; -import { getClientMekList } from "$lib/server/services/mek"; -import type { RequestHandler } from "./$types"; - -export const GET: RequestHandler = async ({ locals }) => { - const { userId, clientId } = await authorize(locals, "activeClient"); - const { encMeks } = await getClientMekList(userId, clientId); - return json( - masterKeyListResponse.parse({ - meks: encMeks.map(({ version, state, encMek, encMekSig }) => ({ - version, - state, - mek: encMek, - mekSig: encMekSig, - })), - } satisfies MasterKeyListResponse), - ); -}; diff --git a/src/routes/api/mek/register/initial/+server.ts b/src/routes/api/mek/register/initial/+server.ts deleted file mode 100644 index bb761e2..0000000 --- a/src/routes/api/mek/register/initial/+server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { error, text } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { initialMasterKeyRegisterRequest } from "$lib/server/schemas"; -import { registerInitialActiveMek } from "$lib/server/services/mek"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, request }) => { - const { userId, clientId } = await authorize(locals, "pendingClient"); - - const zodRes = initialMasterKeyRegisterRequest.safeParse(await request.json()); - if (!zodRes.success) error(400, "Invalid request body"); - const { mek, mekSig } = zodRes.data; - - await registerInitialActiveMek(userId, clientId, mek, mekSig); - return text("MEK registered", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/routes/api/user/+server.ts b/src/routes/api/user/+server.ts deleted file mode 100644 index b10b13e..0000000 --- a/src/routes/api/user/+server.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { json } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { userInfoResponse, type UserInfoResponse } from "$lib/server/schemas"; -import { getUserInformation } from "$lib/server/services/user"; -import type { RequestHandler } from "./$types"; - -export const GET: RequestHandler = async ({ locals }) => { - const { userId } = await authorize(locals, "any"); - const { email, nickname } = await getUserInformation(userId); - return json(userInfoResponse.parse({ email, nickname } satisfies UserInfoResponse)); -}; diff --git a/src/routes/api/user/changeNickname/+server.ts b/src/routes/api/user/changeNickname/+server.ts deleted file mode 100644 index ad651ac..0000000 --- a/src/routes/api/user/changeNickname/+server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { error, text } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { nicknameChangeRequest } from "$lib/server/schemas"; -import { changeNickname } from "$lib/server/services/user"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, request }) => { - const { userId } = await authorize(locals, "any"); - - const zodRes = nicknameChangeRequest.safeParse(await request.json()); - if (!zodRes.success) error(400, "Invalid request body"); - const { newNickname } = zodRes.data; - - await changeNickname(userId, newNickname); - return text("Nickname changed", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/trpc/router.server.ts b/src/trpc/router.server.ts index fdaee15..3c44bea 100644 --- a/src/trpc/router.server.ts +++ b/src/trpc/router.server.ts @@ -1,10 +1,13 @@ import type { RequestEvent } from "@sveltejs/kit"; import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; import { createContext, router } from "./init.server"; -import { clientRouter } from "./routers"; +import { clientRouter, hskRouter, mekRouter, userRouter } from "./routers"; export const appRouter = router({ client: clientRouter, + hsk: hskRouter, + mek: mekRouter, + user: userRouter, }); export const createCaller = (event: RequestEvent) => appRouter.createCaller(createContext(event)); diff --git a/src/trpc/routers/hsk.ts b/src/trpc/routers/hsk.ts new file mode 100644 index 0000000..eed9d25 --- /dev/null +++ b/src/trpc/routers/hsk.ts @@ -0,0 +1,41 @@ +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { HskRepo, IntegrityError } from "$lib/server/db"; +import { router, roleProcedure } from "../init.server"; + +const hskRouter = router({ + list: roleProcedure["activeClient"].query(async ({ ctx }) => { + const hsks = await HskRepo.getAllValidHsks(ctx.session.userId); + return hsks.map(({ version, state, mekVersion, encHsk }) => ({ + version, + state, + mekVersion, + hsk: encHsk, + })); + }), + + registerInitial: roleProcedure["activeClient"] + .input( + z.object({ + mekVersion: z.number().int().positive(), + hsk: z.string().base64().nonempty(), + }), + ) + .mutation(async ({ ctx, input }) => { + try { + await HskRepo.registerInitialHsk( + ctx.session.userId, + ctx.session.clientId, + input.mekVersion, + input.hsk, + ); + } catch (e) { + if (e instanceof IntegrityError && e.message === "HSK already registered") { + throw new TRPCError({ code: "CONFLICT", message: "Initial HSK already registered" }); + } + throw e; + } + }), +}); + +export default hskRouter; diff --git a/src/trpc/routers/index.ts b/src/trpc/routers/index.ts index 6f13a73..26ac7b2 100644 --- a/src/trpc/routers/index.ts +++ b/src/trpc/routers/index.ts @@ -1 +1,4 @@ export { default as clientRouter } from "./client"; +export { default as hskRouter } from "./hsk"; +export { default as mekRouter } from "./mek"; +export { default as userRouter } from "./user"; diff --git a/src/trpc/routers/mek.ts b/src/trpc/routers/mek.ts new file mode 100644 index 0000000..fa264a5 --- /dev/null +++ b/src/trpc/routers/mek.ts @@ -0,0 +1,63 @@ +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { ClientRepo, MekRepo, IntegrityError } from "$lib/server/db"; +import { verifySignature } from "$lib/server/modules/crypto"; +import { router, roleProcedure } from "../init.server"; + +const verifyClientEncMekSig = async ( + userId: number, + clientId: number, + version: number, + encMek: string, + encMekSig: string, +) => { + const userClient = await ClientRepo.getUserClientWithDetails(userId, clientId); + if (!userClient) { + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid session id" }); + } + + const data = JSON.stringify({ version, key: encMek }); + return verifySignature(Buffer.from(data), encMekSig, userClient.sigPubKey); +}; + +const mekRouter = router({ + list: roleProcedure["activeClient"].query(async ({ ctx }) => { + const clientMeks = await MekRepo.getAllValidClientMeks( + ctx.session.userId, + ctx.session.clientId, + ); + return clientMeks.map(({ version, state, encMek, encMekSig }) => ({ + version, + state, + mek: encMek, + mekSig: encMekSig, + })); + }), + + registerInitial: roleProcedure["pendingClient"] + .input( + z.object({ + mek: z.string().base64().nonempty(), + mekSig: z.string().base64().nonempty(), + }), + ) + .mutation(async ({ ctx, input }) => { + const { userId, clientId } = ctx.session; + const { mek, mekSig } = input; + if (!(await verifyClientEncMekSig(userId, clientId, 1, mek, mekSig))) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid signature" }); + } + + try { + await MekRepo.registerInitialMek(userId, clientId, mek, mekSig); + await ClientRepo.setUserClientStateToActive(userId, clientId); + } catch (e) { + if (e instanceof IntegrityError && e.message === "MEK already registered") { + throw new TRPCError({ code: "CONFLICT", message: "Initial MEK already registered" }); + } + throw e; + } + }), +}); + +export default mekRouter; diff --git a/src/trpc/routers/user.ts b/src/trpc/routers/user.ts new file mode 100644 index 0000000..37b2460 --- /dev/null +++ b/src/trpc/routers/user.ts @@ -0,0 +1,27 @@ +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { UserRepo } from "$lib/server/db"; +import { router, roleProcedure } from "../init.server"; + +const userRouter = router({ + info: roleProcedure.any.query(async ({ ctx }) => { + const user = await UserRepo.getUser(ctx.session.userId); + if (!user) { + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid session id" }); + } + + return { email: user.email, nickname: user.nickname }; + }), + + changeNickname: roleProcedure.any + .input( + z.object({ + newNickname: z.string().trim().min(2).max(8), + }), + ) + .mutation(async ({ ctx, input }) => { + await UserRepo.setUserNickname(ctx.session.userId, input.newNickname); + }), +}); + +export default userRouter; From a08ddf2c09d24d65fb07d6509cda6473a22d9371 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 25 Dec 2025 20:22:58 +0900 Subject: [PATCH 05/11] =?UTF-8?q?tRPC=20Endpoint=EB=A5=BC=20/api/trpc?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/middlewares/authenticate.ts | 4 ++-- src/routes/{trpc/[...trpc] => api/trpc/[trpc]}/+server.ts | 2 +- src/trpc/client.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/routes/{trpc/[...trpc] => api/trpc/[trpc]}/+server.ts (94%) diff --git a/src/lib/server/middlewares/authenticate.ts b/src/lib/server/middlewares/authenticate.ts index ad8c585..cc635b4 100644 --- a/src/lib/server/middlewares/authenticate.ts +++ b/src/lib/server/middlewares/authenticate.ts @@ -4,7 +4,7 @@ import { authenticate, AuthenticationError } from "$lib/server/modules/auth"; export const authenticateMiddleware: Handle = async ({ event, resolve }) => { const { pathname, search } = event.url; - if (pathname === "/api/auth/login" || pathname.startsWith("/trpc")) { + if (pathname === "/api/auth/login") { return await resolve(event); } @@ -24,7 +24,7 @@ export const authenticateMiddleware: Handle = async ({ event, resolve }) => { }); } catch (e) { if (e instanceof AuthenticationError) { - if (pathname === "/auth/login") { + if (pathname === "/auth/login" || pathname.startsWith("/api/trpc")) { return await resolve(event); } else if (pathname.startsWith("/api")) { error(e.status, e.message); diff --git a/src/routes/trpc/[...trpc]/+server.ts b/src/routes/api/trpc/[trpc]/+server.ts similarity index 94% rename from src/routes/trpc/[...trpc]/+server.ts rename to src/routes/api/trpc/[trpc]/+server.ts index 052ee09..ec1320a 100644 --- a/src/routes/trpc/[...trpc]/+server.ts +++ b/src/routes/api/trpc/[trpc]/+server.ts @@ -5,7 +5,7 @@ import type { RequestHandler } from "./$types"; const trpcHandler: RequestHandler = (event) => fetchRequestHandler({ - endpoint: "/trpc", + endpoint: "/api/trpc", req: event.request, router: appRouter, createContext: () => createContext(event), diff --git a/src/trpc/client.ts b/src/trpc/client.ts index 433a743..dbf4e80 100644 --- a/src/trpc/client.ts +++ b/src/trpc/client.ts @@ -6,7 +6,7 @@ const createClient = (fetch: typeof globalThis.fetch) => createTRPCClient({ links: [ httpBatchLink({ - url: "/trpc", + url: "/api/trpc", fetch, }), ], From 6d95059450ac554da54c55235e18a371c017a1bd Mon Sep 17 00:00:00 2001 From: static Date: Thu, 25 Dec 2025 22:45:55 +0900 Subject: [PATCH 06/11] =?UTF-8?q?/api/category,=20/api/directory,=20/api/f?= =?UTF-8?q?ile=20=EC=95=84=EB=9E=98=EC=9D=98=20=EB=8C=80=EB=B6=80=EB=B6=84?= =?UTF-8?q?=EC=9D=98=20Endpoint=EB=93=A4=EC=9D=84=20tRPC=EB=A1=9C=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 25 +++ src/lib/modules/file/upload.ts | 10 +- src/lib/modules/filesystem.ts | 74 +++---- src/lib/server/db/user.ts | 4 - src/lib/server/modules/filesystem.ts | 7 + src/lib/server/schemas/category.ts | 52 ----- src/lib/server/schemas/directory.ts | 38 ---- src/lib/server/schemas/file.ts | 55 ----- src/lib/server/services/category.ts | 133 ------------ src/lib/server/services/directory.ts | 96 --------- src/lib/server/services/file.ts | 125 ++--------- src/lib/services/category.ts | 41 ++-- src/lib/services/file.ts | 33 +-- src/routes/(fullscreen)/file/[id]/service.ts | 16 +- .../(fullscreen)/settings/thumbnail/+page.ts | 14 +- .../(main)/category/[[id]]/service.svelte.ts | 33 ++- .../(main)/directory/[[id]]/service.svelte.ts | 93 +++++---- src/routes/(main)/menu/+page.ts | 2 +- src/routes/api/category/[id]/+server.ts | 33 --- .../api/category/[id]/delete/+server.ts | 20 -- .../api/category/[id]/file/add/+server.ts | 25 --- .../api/category/[id]/file/list/+server.ts | 36 ---- .../api/category/[id]/file/remove/+server.ts | 25 --- .../api/category/[id]/rename/+server.ts | 25 --- src/routes/api/category/create/+server.ts | 23 --- src/routes/api/directory/[id]/+server.ts | 34 --- .../api/directory/[id]/delete/+server.ts | 23 --- .../api/directory/[id]/rename/+server.ts | 25 --- src/routes/api/directory/create/+server.ts | 23 --- src/routes/api/file/[id]/+server.ts | 48 ----- src/routes/api/file/[id]/delete/+server.ts | 20 -- src/routes/api/file/[id]/rename/+server.ts | 25 --- src/routes/api/file/[id]/thumbnail/+server.ts | 26 --- src/routes/api/file/list/+server.ts | 11 - src/routes/api/file/scanDuplicates/+server.ts | 20 -- .../api/file/scanMissingThumbnails/+server.ts | 16 -- src/trpc/client.ts | 2 + src/trpc/init.server.ts | 6 +- src/trpc/router.server.ts | 13 +- src/trpc/routers/category.ts | 194 ++++++++++++++++++ src/trpc/routers/directory.ts | 125 +++++++++++ src/trpc/routers/file.ts | 122 +++++++++++ src/trpc/routers/index.ts | 3 + src/trpc/routers/user.ts | 13 +- 45 files changed, 691 insertions(+), 1097 deletions(-) create mode 100644 src/lib/server/modules/filesystem.ts delete mode 100644 src/lib/server/services/category.ts delete mode 100644 src/lib/server/services/directory.ts delete mode 100644 src/routes/api/category/[id]/+server.ts delete mode 100644 src/routes/api/category/[id]/delete/+server.ts delete mode 100644 src/routes/api/category/[id]/file/add/+server.ts delete mode 100644 src/routes/api/category/[id]/file/list/+server.ts delete mode 100644 src/routes/api/category/[id]/file/remove/+server.ts delete mode 100644 src/routes/api/category/[id]/rename/+server.ts delete mode 100644 src/routes/api/category/create/+server.ts delete mode 100644 src/routes/api/directory/[id]/+server.ts delete mode 100644 src/routes/api/directory/[id]/delete/+server.ts delete mode 100644 src/routes/api/directory/[id]/rename/+server.ts delete mode 100644 src/routes/api/directory/create/+server.ts delete mode 100644 src/routes/api/file/[id]/+server.ts delete mode 100644 src/routes/api/file/[id]/delete/+server.ts delete mode 100644 src/routes/api/file/[id]/rename/+server.ts delete mode 100644 src/routes/api/file/[id]/thumbnail/+server.ts delete mode 100644 src/routes/api/file/list/+server.ts delete mode 100644 src/routes/api/file/scanDuplicates/+server.ts delete mode 100644 src/routes/api/file/scanMissingThumbnails/+server.ts create mode 100644 src/trpc/routers/category.ts create mode 100644 src/trpc/routers/directory.ts create mode 100644 src/trpc/routers/file.ts diff --git a/package.json b/package.json index 4a8493b..b1425b0 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "ms": "^2.1.3", "node-schedule": "^2.1.1", "pg": "^8.16.3", + "superjson": "^2.2.6", "uuid": "^13.0.0", "zod": "^3.25.76" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50affd7..b428b0f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: pg: specifier: ^8.16.3 version: 8.16.3 + superjson: + specifier: ^2.2.6 + version: 2.2.6 uuid: specifier: ^13.0.0 version: 13.0.0 @@ -889,6 +892,10 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} @@ -1259,6 +1266,10 @@ packages: is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1854,6 +1865,10 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2767,6 +2782,10 @@ snapshots: cookie@0.6.0: {} + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + cron-parser@4.9.0: dependencies: luxon: 3.7.2 @@ -3153,6 +3172,8 @@ snapshots: dependencies: '@types/estree': 1.0.8 + is-what@5.5.0: {} + isexe@2.0.0: {} jackspeak@3.4.3: @@ -3641,6 +3662,10 @@ snapshots: pirates: 4.0.7 ts-interface-checker: 0.1.13 + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 diff --git a/src/lib/modules/file/upload.ts b/src/lib/modules/file/upload.ts index b5b00a1..7c48503 100644 --- a/src/lib/modules/file/upload.ts +++ b/src/lib/modules/file/upload.ts @@ -13,8 +13,6 @@ import { } from "$lib/modules/crypto"; import { generateThumbnail } from "$lib/modules/thumbnail"; import type { - DuplicateFileScanRequest, - DuplicateFileScanResponse, FileThumbnailUploadRequest, FileUploadRequest, FileUploadResponse, @@ -25,18 +23,18 @@ import { type HmacSecret, type FileUploadStatus, } from "$lib/stores"; +import { useTRPC } from "$trpc/client"; const requestDuplicateFileScan = limitFunction( async (file: File, hmacSecret: HmacSecret, onDuplicate: () => Promise) => { + const trpc = useTRPC(); const fileBuffer = await file.arrayBuffer(); const fileSigned = encodeToBase64(await signMessageHmac(fileBuffer, hmacSecret.secret)); - const res = await axios.post("/api/file/scanDuplicates", { + const files = await trpc.file.listByHash.query({ hskVersion: hmacSecret.version, contentHmac: fileSigned, - } satisfies DuplicateFileScanRequest); - const { files }: DuplicateFileScanResponse = res.data; - + }); if (files.length === 0 || (await onDuplicate())) { return { fileBuffer, fileSigned }; } else { diff --git a/src/lib/modules/filesystem.ts b/src/lib/modules/filesystem.ts index c160534..8b9b203 100644 --- a/src/lib/modules/filesystem.ts +++ b/src/lib/modules/filesystem.ts @@ -1,5 +1,5 @@ +import { TRPCClientError } from "@trpc/client"; import { get, writable, type Writable } from "svelte/store"; -import { callGetApi } from "$lib/hooks"; import { getDirectoryInfos as getDirectoryInfosFromIndexedDB, getDirectoryInfo as getDirectoryInfoFromIndexedDB, @@ -18,12 +18,7 @@ import { type CategoryId, } from "$lib/indexedDB"; import { unwrapDataKey, decryptString } from "$lib/modules/crypto"; -import type { - CategoryInfoResponse, - CategoryFileListResponse, - DirectoryInfoResponse, - FileInfoResponse, -} from "$lib/server/schemas"; +import { useTRPC } from "$trpc/client"; export type DirectoryInfo = | { @@ -106,20 +101,20 @@ const fetchDirectoryInfoFromServer = async ( info: Writable, masterKey: CryptoKey, ) => { - const res = await callGetApi(`/api/directory/${id}`); - if (res.status === 404) { - info.set(null); - await deleteDirectoryInfo(id as number); - return; - } else if (!res.ok) { + const trpc = useTRPC(); + let data; + try { + data = await trpc.directory.get.query({ id }); + } catch (e) { + if (e instanceof TRPCClientError && e.data?.code === "NOT_FOUND") { + info.set(null); + await deleteDirectoryInfo(id as number); + return; + } throw new Error("Failed to fetch directory information"); } - const { - metadata, - subDirectories: subDirectoryIds, - files: fileIds, - }: DirectoryInfoResponse = await res.json(); + const { metadata, subDirectories: subDirectoryIds, files: fileIds } = data; if (id === "root") { info.set({ id, subDirectoryIds, fileIds }); @@ -179,16 +174,18 @@ const fetchFileInfoFromServer = async ( info: Writable, masterKey: CryptoKey, ) => { - const res = await callGetApi(`/api/file/${id}`); - if (res.status === 404) { - info.set(null); - await deleteFileInfo(id); - return; - } else if (!res.ok) { + const trpc = useTRPC(); + let metadata; + try { + metadata = await trpc.file.get.query({ id }); + } catch (e) { + if (e instanceof TRPCClientError && e.data?.code === "NOT_FOUND") { + info.set(null); + await deleteFileInfo(id); + return; + } throw new Error("Failed to fetch file information"); } - - const metadata: FileInfoResponse = await res.json(); const { dataKey } = await unwrapDataKey(metadata.dek, masterKey); const name = await decryptString(metadata.name, metadata.nameIv, dataKey); @@ -273,16 +270,20 @@ const fetchCategoryInfoFromServer = async ( info: Writable, masterKey: CryptoKey, ) => { - let res = await callGetApi(`/api/category/${id}`); - if (res.status === 404) { - info.set(null); - await deleteCategoryInfo(id as number); - return; - } else if (!res.ok) { + const trpc = useTRPC(); + let data; + try { + data = await trpc.category.get.query({ id }); + } catch (e) { + if (e instanceof TRPCClientError && e.data?.code === "NOT_FOUND") { + info.set(null); + await deleteCategoryInfo(id as number); + return; + } throw new Error("Failed to fetch category information"); } - const { metadata, subCategories }: CategoryInfoResponse = await res.json(); + const { metadata, subCategories } = data; if (id === "root") { info.set({ id, subCategoryIds: subCategories }); @@ -290,12 +291,13 @@ const fetchCategoryInfoFromServer = async ( const { dataKey } = await unwrapDataKey(metadata!.dek, masterKey); const name = await decryptString(metadata!.name, metadata!.nameIv, dataKey); - res = await callGetApi(`/api/category/${id}/file/list?recurse=true`); - if (!res.ok) { + let files; + try { + files = await trpc.category.files.query({ id, recurse: true }); + } catch { throw new Error("Failed to fetch category files"); } - const { files }: CategoryFileListResponse = await res.json(); const filesMapped = files.map(({ file, isRecursive }) => ({ id: file, isRecursive })); let isFileRecursive: boolean | undefined = undefined; diff --git a/src/lib/server/db/user.ts b/src/lib/server/db/user.ts index 3964a94..f718d4f 100644 --- a/src/lib/server/db/user.ts +++ b/src/lib/server/db/user.ts @@ -27,10 +27,6 @@ export const getUserByEmail = async (email: string) => { return user ? (user satisfies User) : null; }; -export const setUserNickname = async (userId: number, nickname: string) => { - await db.updateTable("user").set({ nickname }).where("id", "=", userId).execute(); -}; - export const setUserPassword = async (userId: number, password: string) => { await db.updateTable("user").set({ password }).where("id", "=", userId).execute(); }; diff --git a/src/lib/server/modules/filesystem.ts b/src/lib/server/modules/filesystem.ts new file mode 100644 index 0000000..65cb9ec --- /dev/null +++ b/src/lib/server/modules/filesystem.ts @@ -0,0 +1,7 @@ +import { unlink } from "fs/promises"; + +export const safeUnlink = async (path: string | null | undefined) => { + if (path) { + await unlink(path).catch(console.error); + } +}; diff --git a/src/lib/server/schemas/category.ts b/src/lib/server/schemas/category.ts index 55ae413..408af7b 100644 --- a/src/lib/server/schemas/category.ts +++ b/src/lib/server/schemas/category.ts @@ -1,55 +1,3 @@ import { z } from "zod"; export const categoryIdSchema = z.union([z.literal("root"), z.number().int().positive()]); - -export const categoryInfoResponse = z.object({ - metadata: z - .object({ - parent: categoryIdSchema, - mekVersion: z.number().int().positive(), - dek: z.string().base64().nonempty(), - dekVersion: z.string().datetime(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), - }) - .optional(), - subCategories: z.number().int().positive().array(), -}); -export type CategoryInfoResponse = z.output; - -export const categoryFileAddRequest = z.object({ - file: z.number().int().positive(), -}); -export type CategoryFileAddRequest = z.input; - -export const categoryFileListResponse = z.object({ - files: z.array( - z.object({ - file: z.number().int().positive(), - isRecursive: z.boolean(), - }), - ), -}); -export type CategoryFileListResponse = z.output; - -export const categoryFileRemoveRequest = z.object({ - file: z.number().int().positive(), -}); -export type CategoryFileRemoveRequest = z.input; - -export const categoryRenameRequest = z.object({ - dekVersion: z.string().datetime(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), -}); -export type CategoryRenameRequest = z.input; - -export const categoryCreateRequest = z.object({ - parent: categoryIdSchema, - mekVersion: z.number().int().positive(), - dek: z.string().base64().nonempty(), - dekVersion: z.string().datetime(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), -}); -export type CategoryCreateRequest = z.input; diff --git a/src/lib/server/schemas/directory.ts b/src/lib/server/schemas/directory.ts index ffd13bc..107c3ee 100644 --- a/src/lib/server/schemas/directory.ts +++ b/src/lib/server/schemas/directory.ts @@ -1,41 +1,3 @@ import { z } from "zod"; export const directoryIdSchema = z.union([z.literal("root"), z.number().int().positive()]); - -export const directoryInfoResponse = z.object({ - metadata: z - .object({ - parent: directoryIdSchema, - mekVersion: z.number().int().positive(), - dek: z.string().base64().nonempty(), - dekVersion: z.string().datetime(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), - }) - .optional(), - subDirectories: z.number().int().positive().array(), - files: z.number().int().positive().array(), -}); -export type DirectoryInfoResponse = z.output; - -export const directoryDeleteResponse = z.object({ - deletedFiles: z.number().int().positive().array(), -}); -export type DirectoryDeleteResponse = z.output; - -export const directoryRenameRequest = z.object({ - dekVersion: z.string().datetime(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), -}); -export type DirectoryRenameRequest = z.input; - -export const directoryCreateRequest = z.object({ - parent: directoryIdSchema, - mekVersion: z.number().int().positive(), - dek: z.string().base64().nonempty(), - dekVersion: z.string().datetime(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), -}); -export type DirectoryCreateRequest = z.input; diff --git a/src/lib/server/schemas/file.ts b/src/lib/server/schemas/file.ts index 8b9cfe9..5177bbd 100644 --- a/src/lib/server/schemas/file.ts +++ b/src/lib/server/schemas/file.ts @@ -2,67 +2,12 @@ import mime from "mime"; import { z } from "zod"; import { directoryIdSchema } from "./directory"; -export const fileInfoResponse = z.object({ - parent: directoryIdSchema, - mekVersion: z.number().int().positive(), - dek: z.string().base64().nonempty(), - dekVersion: z.string().datetime(), - contentType: z - .string() - .trim() - .nonempty() - .refine((value) => mime.getExtension(value) !== null), // MIME type - contentIv: z.string().base64().nonempty(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), - createdAt: z.string().base64().nonempty().optional(), - createdAtIv: z.string().base64().nonempty().optional(), - lastModifiedAt: z.string().base64().nonempty(), - lastModifiedAtIv: z.string().base64().nonempty(), - categories: z.number().int().positive().array(), -}); -export type FileInfoResponse = z.output; - -export const fileRenameRequest = z.object({ - dekVersion: z.string().datetime(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), -}); -export type FileRenameRequest = z.input; - -export const fileThumbnailInfoResponse = z.object({ - updatedAt: z.string().datetime(), - contentIv: z.string().base64().nonempty(), -}); -export type FileThumbnailInfoResponse = z.output; - export const fileThumbnailUploadRequest = z.object({ dekVersion: z.string().datetime(), contentIv: z.string().base64().nonempty(), }); export type FileThumbnailUploadRequest = z.input; -export const fileListResponse = z.object({ - files: z.number().int().positive().array(), -}); -export type FileListResponse = z.output; - -export const duplicateFileScanRequest = z.object({ - hskVersion: z.number().int().positive(), - contentHmac: z.string().base64().nonempty(), -}); -export type DuplicateFileScanRequest = z.input; - -export const duplicateFileScanResponse = z.object({ - files: z.number().int().positive().array(), -}); -export type DuplicateFileScanResponse = z.output; - -export const missingThumbnailFileScanResponse = z.object({ - files: z.number().int().positive().array(), -}); -export type MissingThumbnailFileScanResponse = z.output; - export const fileUploadRequest = z.object({ parent: directoryIdSchema, mekVersion: z.number().int().positive(), diff --git a/src/lib/server/services/category.ts b/src/lib/server/services/category.ts deleted file mode 100644 index cb3db7a..0000000 --- a/src/lib/server/services/category.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { error } from "@sveltejs/kit"; -import { - registerCategory, - getAllCategoriesByParent, - getCategory, - setCategoryEncName, - unregisterCategory, - type CategoryId, - type NewCategory, -} from "$lib/server/db/category"; -import { IntegrityError } from "$lib/server/db/error"; -import { - getAllFilesByCategory, - getFile, - addFileToCategory, - removeFileFromCategory, -} from "$lib/server/db/file"; -import type { Ciphertext } from "$lib/server/db/schema"; - -export const getCategoryInformation = async (userId: number, categoryId: CategoryId) => { - const category = categoryId !== "root" ? await getCategory(userId, categoryId) : undefined; - if (category === null) { - error(404, "Invalid category id"); - } - - const categories = await getAllCategoriesByParent(userId, categoryId); - return { - metadata: category && { - parentId: category.parentId ?? ("root" as const), - mekVersion: category.mekVersion, - encDek: category.encDek, - dekVersion: category.dekVersion, - encName: category.encName, - }, - categories: categories.map(({ id }) => id), - }; -}; - -export const deleteCategory = async (userId: number, categoryId: number) => { - try { - await unregisterCategory(userId, categoryId); - } catch (e) { - if (e instanceof IntegrityError && e.message === "Category not found") { - error(404, "Invalid category id"); - } - throw e; - } -}; - -export const addCategoryFile = async (userId: number, categoryId: number, fileId: number) => { - const category = await getCategory(userId, categoryId); - const file = await getFile(userId, fileId); - if (!category) { - error(404, "Invalid category id"); - } else if (!file) { - error(404, "Invalid file id"); - } - - try { - await addFileToCategory(fileId, categoryId); - } catch (e) { - if (e instanceof IntegrityError && e.message === "File already added to category") { - error(400, "File already added"); - } - throw e; - } -}; - -export const getCategoryFiles = async (userId: number, categoryId: number, recurse: boolean) => { - const category = await getCategory(userId, categoryId); - if (!category) { - error(404, "Invalid category id"); - } - - const files = await getAllFilesByCategory(userId, categoryId, recurse); - return { files }; -}; - -export const removeCategoryFile = async (userId: number, categoryId: number, fileId: number) => { - const category = await getCategory(userId, categoryId); - const file = await getFile(userId, fileId); - if (!category) { - error(404, "Invalid category id"); - } else if (!file) { - error(404, "Invalid file id"); - } - - try { - await removeFileFromCategory(fileId, categoryId); - } catch (e) { - if (e instanceof IntegrityError && e.message === "File not found in category") { - error(400, "File not added"); - } - throw e; - } -}; - -export const renameCategory = async ( - userId: number, - categoryId: number, - dekVersion: Date, - newEncName: Ciphertext, -) => { - try { - await setCategoryEncName(userId, categoryId, dekVersion, newEncName); - } catch (e) { - if (e instanceof IntegrityError) { - if (e.message === "Category not found") { - error(404, "Invalid category id"); - } else if (e.message === "Invalid DEK version") { - error(400, "Invalid DEK version"); - } - } - throw e; - } -}; - -export const createCategory = async (params: NewCategory) => { - const oneMinuteAgo = new Date(Date.now() - 60 * 1000); - const oneMinuteLater = new Date(Date.now() + 60 * 1000); - if (params.dekVersion <= oneMinuteAgo || params.dekVersion >= oneMinuteLater) { - error(400, "Invalid DEK version"); - } - - try { - await registerCategory(params); - } catch (e) { - if (e instanceof IntegrityError && e.message === "Inactive MEK version") { - error(400, "Inactive MEK version"); - } - throw e; - } -}; diff --git a/src/lib/server/services/directory.ts b/src/lib/server/services/directory.ts deleted file mode 100644 index fdab587..0000000 --- a/src/lib/server/services/directory.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { error } from "@sveltejs/kit"; -import { unlink } from "fs/promises"; -import { IntegrityError } from "$lib/server/db/error"; -import { - registerDirectory, - getAllDirectoriesByParent, - getDirectory, - setDirectoryEncName, - unregisterDirectory, - getAllFilesByParent, - type DirectoryId, - type NewDirectory, -} from "$lib/server/db/file"; -import type { Ciphertext } from "$lib/server/db/schema"; - -export const getDirectoryInformation = async (userId: number, directoryId: DirectoryId) => { - const directory = directoryId !== "root" ? await getDirectory(userId, directoryId) : undefined; - if (directory === null) { - error(404, "Invalid directory id"); - } - - const directories = await getAllDirectoriesByParent(userId, directoryId); - const files = await getAllFilesByParent(userId, directoryId); - return { - metadata: directory && { - parentId: directory.parentId ?? ("root" as const), - mekVersion: directory.mekVersion, - encDek: directory.encDek, - dekVersion: directory.dekVersion, - encName: directory.encName, - }, - directories: directories.map(({ id }) => id), - files: files.map(({ id }) => id), - }; -}; - -const safeUnlink = async (path: string | null) => { - if (path) { - await unlink(path).catch(console.error); - } -}; - -export const deleteDirectory = async (userId: number, directoryId: number) => { - try { - const files = await unregisterDirectory(userId, directoryId); - return { - files: files.map(({ id, path, thumbnailPath }) => { - safeUnlink(path); // Intended - safeUnlink(thumbnailPath); // Intended - return id; - }), - }; - } catch (e) { - if (e instanceof IntegrityError && e.message === "Directory not found") { - error(404, "Invalid directory id"); - } - throw e; - } -}; - -export const renameDirectory = async ( - userId: number, - directoryId: number, - dekVersion: Date, - newEncName: Ciphertext, -) => { - try { - await setDirectoryEncName(userId, directoryId, dekVersion, newEncName); - } catch (e) { - if (e instanceof IntegrityError) { - if (e.message === "Directory not found") { - error(404, "Invalid directory id"); - } else if (e.message === "Invalid DEK version") { - error(400, "Invalid DEK version"); - } - } - throw e; - } -}; - -export const createDirectory = async (params: NewDirectory) => { - const oneMinuteAgo = new Date(Date.now() - 60 * 1000); - const oneMinuteLater = new Date(Date.now() + 60 * 1000); - if (params.dekVersion <= oneMinuteAgo || params.dekVersion >= oneMinuteLater) { - error(400, "Invalid DEK version"); - } - - try { - await registerDirectory(params); - } catch (e) { - if (e instanceof IntegrityError && e.message === "Inactive MEK version") { - error(400, "Invalid MEK version"); - } - throw e; - } -}; diff --git a/src/lib/server/services/file.ts b/src/lib/server/services/file.ts index ab98dbf..9032ffb 100644 --- a/src/lib/server/services/file.ts +++ b/src/lib/server/services/file.ts @@ -1,72 +1,17 @@ import { error } from "@sveltejs/kit"; import { createHash } from "crypto"; import { createReadStream, createWriteStream } from "fs"; -import { mkdir, stat, unlink } from "fs/promises"; +import { mkdir, stat } from "fs/promises"; import { dirname } from "path"; import { Readable } from "stream"; import { pipeline } from "stream/promises"; import { v4 as uuidv4 } from "uuid"; -import { IntegrityError } from "$lib/server/db/error"; -import { - registerFile, - getAllFileIds, - getAllFileIdsByContentHmac, - getFile, - setFileEncName, - unregisterFile, - getAllFileCategories, - type NewFile, -} from "$lib/server/db/file"; -import { - updateFileThumbnail, - getFileThumbnail, - getMissingFileThumbnails, -} from "$lib/server/db/media"; -import type { Ciphertext } from "$lib/server/db/schema"; +import { FileRepo, MediaRepo, IntegrityError } from "$lib/server/db"; import env from "$lib/server/loadenv"; - -export const getFileInformation = async (userId: number, fileId: number) => { - const file = await getFile(userId, fileId); - if (!file) { - error(404, "Invalid file id"); - } - - const categories = await getAllFileCategories(fileId); - return { - parentId: file.parentId ?? ("root" as const), - mekVersion: file.mekVersion, - encDek: file.encDek, - dekVersion: file.dekVersion, - contentType: file.contentType, - encContentIv: file.encContentIv, - encName: file.encName, - encCreatedAt: file.encCreatedAt, - encLastModifiedAt: file.encLastModifiedAt, - categories: categories.map(({ id }) => id), - }; -}; - -const safeUnlink = async (path: string | null) => { - if (path) { - await unlink(path).catch(console.error); - } -}; - -export const deleteFile = async (userId: number, fileId: number) => { - try { - const { path, thumbnailPath } = await unregisterFile(userId, fileId); - safeUnlink(path); // Intended - safeUnlink(thumbnailPath); // Intended - } catch (e) { - if (e instanceof IntegrityError && e.message === "File not found") { - error(404, "Invalid file id"); - } - throw e; - } -}; +import { safeUnlink } from "$lib/server/modules/filesystem"; export const getFileStream = async (userId: number, fileId: number) => { - const file = await getFile(userId, fileId); + const file = await FileRepo.getFile(userId, fileId); if (!file) { error(404, "Invalid file id"); } @@ -78,37 +23,8 @@ export const getFileStream = async (userId: number, fileId: number) => { }; }; -export const renameFile = async ( - userId: number, - fileId: number, - dekVersion: Date, - newEncName: Ciphertext, -) => { - try { - await setFileEncName(userId, fileId, dekVersion, newEncName); - } catch (e) { - if (e instanceof IntegrityError) { - if (e.message === "File not found") { - error(404, "Invalid file id"); - } else if (e.message === "Invalid DEK version") { - error(400, "Invalid DEK version"); - } - } - throw e; - } -}; - -export const getFileThumbnailInformation = async (userId: number, fileId: number) => { - const thumbnail = await getFileThumbnail(userId, fileId); - if (!thumbnail) { - error(404, "File or its thumbnail not found"); - } - - return { updatedAt: thumbnail.updatedAt, encContentIv: thumbnail.encContentIv }; -}; - export const getFileThumbnailStream = async (userId: number, fileId: number) => { - const thumbnail = await getFileThumbnail(userId, fileId); + const thumbnail = await MediaRepo.getFileThumbnail(userId, fileId); if (!thumbnail) { error(404, "File or its thumbnail not found"); } @@ -133,7 +49,13 @@ export const uploadFileThumbnail = async ( try { await pipeline(encContentStream, createWriteStream(path, { flags: "wx", mode: 0o600 })); - const oldPath = await updateFileThumbnail(userId, fileId, dekVersion, path, encContentIv); + const oldPath = await MediaRepo.updateFileThumbnail( + userId, + fileId, + dekVersion, + path, + encContentIv, + ); safeUnlink(oldPath); // Intended } catch (e) { await safeUnlink(path); @@ -149,27 +71,8 @@ export const uploadFileThumbnail = async ( } }; -export const getFileList = async (userId: number) => { - const fileIds = await getAllFileIds(userId); - return { files: fileIds }; -}; - -export const scanDuplicateFiles = async ( - userId: number, - hskVersion: number, - contentHmac: string, -) => { - const fileIds = await getAllFileIdsByContentHmac(userId, hskVersion, contentHmac); - return { files: fileIds }; -}; - -export const scanMissingFileThumbnails = async (userId: number) => { - const fileIds = await getMissingFileThumbnails(userId); - return { files: fileIds }; -}; - export const uploadFile = async ( - params: Omit, + params: Omit, encContentStream: Readable, encContentHash: Promise, ) => { @@ -201,7 +104,7 @@ export const uploadFile = async ( throw new Error("Invalid checksum"); } - const { id: fileId } = await registerFile({ + const { id: fileId } = await FileRepo.registerFile({ ...params, path, encContentHash: hash, diff --git a/src/lib/services/category.ts b/src/lib/services/category.ts index 587df3f..c86c93b 100644 --- a/src/lib/services/category.ts +++ b/src/lib/services/category.ts @@ -1,31 +1,40 @@ -import { callPostApi } from "$lib/hooks"; import { generateDataKey, wrapDataKey, encryptString } from "$lib/modules/crypto"; -import type { CategoryCreateRequest, CategoryFileRemoveRequest } from "$lib/server/schemas"; import type { MasterKey } from "$lib/stores"; +import { useTRPC } from "$trpc/client"; export const requestCategoryCreation = async ( name: string, parentId: "root" | number, masterKey: MasterKey, ) => { + const trpc = useTRPC(); const { dataKey, dataKeyVersion } = await generateDataKey(); const nameEncrypted = await encryptString(name, dataKey); - const res = await callPostApi("/api/category/create", { - parent: parentId, - mekVersion: masterKey.version, - dek: await wrapDataKey(dataKey, masterKey.key), - dekVersion: dataKeyVersion.toISOString(), - name: nameEncrypted.ciphertext, - nameIv: nameEncrypted.iv, - }); - return res.ok; + try { + await trpc.category.create.mutate({ + parent: parentId, + mekVersion: masterKey.version, + dek: await wrapDataKey(dataKey, masterKey.key), + dekVersion: dataKeyVersion, + name: nameEncrypted.ciphertext, + nameIv: nameEncrypted.iv, + }); + return true; + } catch { + // TODO: Error Handling + return false; + } }; export const requestFileRemovalFromCategory = async (fileId: number, categoryId: number) => { - const res = await callPostApi( - `/api/category/${categoryId}/file/remove`, - { file: fileId }, - ); - return res.ok; + const trpc = useTRPC(); + + try { + await trpc.category.removeFile.mutate({ id: categoryId, file: fileId }); + return true; + } catch { + // TODO: Error Handling + return false; + } }; diff --git a/src/lib/services/file.ts b/src/lib/services/file.ts index bab3dac..f428e97 100644 --- a/src/lib/services/file.ts +++ b/src/lib/services/file.ts @@ -11,11 +11,8 @@ import { downloadFile, } from "$lib/modules/file"; import { getThumbnailUrl } from "$lib/modules/thumbnail"; -import type { - FileThumbnailInfoResponse, - FileThumbnailUploadRequest, - FileListResponse, -} from "$lib/server/schemas"; +import type { FileThumbnailUploadRequest } from "$lib/server/schemas"; +import { useTRPC } from "$trpc/client"; export const requestFileDownload = async ( fileId: number, @@ -52,12 +49,17 @@ export const requestFileThumbnailDownload = async (fileId: number, dataKey?: Cry const cache = await getFileThumbnailCache(fileId); if (cache || !dataKey) return cache; - let res = await callGetApi(`/api/file/${fileId}/thumbnail`); - if (!res.ok) return null; + const trpc = useTRPC(); + let thumbnailInfo; + try { + thumbnailInfo = await trpc.file.thumbnail.query({ id: fileId }); + } catch { + // TODO: Error Handling + return null; + } + const { contentIv: thumbnailEncryptedIv } = thumbnailInfo; - const { contentIv: thumbnailEncryptedIv }: FileThumbnailInfoResponse = await res.json(); - - res = await callGetApi(`/api/file/${fileId}/thumbnail/download`); + const res = await callGetApi(`/api/file/${fileId}/thumbnail/download`); if (!res.ok) return null; const thumbnailEncrypted = await res.arrayBuffer(); @@ -68,10 +70,15 @@ export const requestFileThumbnailDownload = async (fileId: number, dataKey?: Cry }; export const requestDeletedFilesCleanup = async () => { - const res = await callGetApi("/api/file/list"); - if (!res.ok) return; + const trpc = useTRPC(); + let liveFiles; + try { + liveFiles = await trpc.file.list.query(); + } catch { + // TODO: Error Handling + return; + } - const { files: liveFiles }: FileListResponse = await res.json(); const liveFilesSet = new Set(liveFiles); const maybeCachedFiles = await getAllFileInfos(); diff --git a/src/routes/(fullscreen)/file/[id]/service.ts b/src/routes/(fullscreen)/file/[id]/service.ts index 00614d6..73ca7f1 100644 --- a/src/routes/(fullscreen)/file/[id]/service.ts +++ b/src/routes/(fullscreen)/file/[id]/service.ts @@ -1,8 +1,7 @@ -import { callPostApi } from "$lib/hooks"; import { encryptData } from "$lib/modules/crypto"; import { storeFileThumbnailCache } from "$lib/modules/file"; -import type { CategoryFileAddRequest } from "$lib/server/schemas"; import { requestFileThumbnailUpload } from "$lib/services/file"; +import { useTRPC } from "$trpc/client"; export { requestCategoryCreation, requestFileRemovalFromCategory } from "$lib/services/category"; export { requestFileDownload } from "$lib/services/file"; @@ -23,8 +22,13 @@ export const requestThumbnailUpload = async ( }; export const requestFileAdditionToCategory = async (fileId: number, categoryId: number) => { - const res = await callPostApi(`/api/category/${categoryId}/file/add`, { - file: fileId, - }); - return res.ok; + const trpc = useTRPC(); + + try { + await trpc.category.addFile.mutate({ id: categoryId, file: fileId }); + return true; + } catch { + // TODO: Error Handling + return false; + } }; diff --git a/src/routes/(fullscreen)/settings/thumbnail/+page.ts b/src/routes/(fullscreen)/settings/thumbnail/+page.ts index a16cb8e..3bfa322 100644 --- a/src/routes/(fullscreen)/settings/thumbnail/+page.ts +++ b/src/routes/(fullscreen)/settings/thumbnail/+page.ts @@ -1,14 +1,14 @@ import { error } from "@sveltejs/kit"; -import { callPostApi } from "$lib/hooks"; -import type { MissingThumbnailFileScanResponse } from "$lib/server/schemas"; +import { useTRPC } from "$trpc/client"; import type { PageLoad } from "./$types"; export const load: PageLoad = async ({ fetch }) => { - const res = await callPostApi("/api/file/scanMissingThumbnails", undefined, fetch); - if (!res.ok) { + const trpc = useTRPC(fetch); + + try { + const files = await trpc.file.listWithoutThumbnail.query(); + return { files }; + } catch { error(500, "Internal server error"); } - - const { files }: MissingThumbnailFileScanResponse = await res.json(); - return { files }; }; diff --git a/src/routes/(main)/category/[[id]]/service.svelte.ts b/src/routes/(main)/category/[[id]]/service.svelte.ts index b573041..824de8a 100644 --- a/src/routes/(main)/category/[[id]]/service.svelte.ts +++ b/src/routes/(main)/category/[[id]]/service.svelte.ts @@ -1,8 +1,7 @@ import { getContext, setContext } from "svelte"; -import { callPostApi } from "$lib/hooks"; import { encryptString } from "$lib/modules/crypto"; import type { SelectedCategory } from "$lib/components/molecules"; -import type { CategoryRenameRequest } from "$lib/server/schemas"; +import { useTRPC } from "$trpc/client"; export { requestCategoryCreation, requestFileRemovalFromCategory } from "$lib/services/category"; @@ -18,17 +17,31 @@ export const useContext = () => { }; export const requestCategoryRename = async (category: SelectedCategory, newName: string) => { + const trpc = useTRPC(); const newNameEncrypted = await encryptString(newName, category.dataKey); - const res = await callPostApi(`/api/category/${category.id}/rename`, { - dekVersion: category.dataKeyVersion.toISOString(), - name: newNameEncrypted.ciphertext, - nameIv: newNameEncrypted.iv, - }); - return res.ok; + try { + await trpc.category.rename.mutate({ + id: category.id, + dekVersion: category.dataKeyVersion, + name: newNameEncrypted.ciphertext, + nameIv: newNameEncrypted.iv, + }); + return true; + } catch { + // TODO: Error Handling + return false; + } }; export const requestCategoryDeletion = async (category: SelectedCategory) => { - const res = await callPostApi(`/api/category/${category.id}/delete`); - return res.ok; + const trpc = useTRPC(); + + try { + await trpc.category.delete.mutate({ id: category.id }); + return true; + } catch { + // TODO: Error Handling + return false; + } }; diff --git a/src/routes/(main)/directory/[[id]]/service.svelte.ts b/src/routes/(main)/directory/[[id]]/service.svelte.ts index 40394f7..72a8fdb 100644 --- a/src/routes/(main)/directory/[[id]]/service.svelte.ts +++ b/src/routes/(main)/directory/[[id]]/service.svelte.ts @@ -1,5 +1,4 @@ import { getContext, setContext } from "svelte"; -import { callPostApi } from "$lib/hooks"; import { storeHmacSecrets } from "$lib/indexedDB"; import { generateDataKey, wrapDataKey, unwrapHmacSecret, encryptString } from "$lib/modules/crypto"; import { @@ -9,12 +8,6 @@ import { deleteFileThumbnailCache, uploadFile, } from "$lib/modules/file"; -import type { - DirectoryRenameRequest, - DirectoryCreateRequest, - FileRenameRequest, - DirectoryDeleteResponse, -} from "$lib/server/schemas"; import { hmacSecretStore, type MasterKey, type HmacSecret } from "$lib/stores"; import { useTRPC } from "$trpc/client"; @@ -68,18 +61,24 @@ export const requestDirectoryCreation = async ( parentId: "root" | number, masterKey: MasterKey, ) => { + const trpc = useTRPC(); const { dataKey, dataKeyVersion } = await generateDataKey(); const nameEncrypted = await encryptString(name, dataKey); - const res = await callPostApi("/api/directory/create", { - parent: parentId, - mekVersion: masterKey.version, - dek: await wrapDataKey(dataKey, masterKey.key), - dekVersion: dataKeyVersion.toISOString(), - name: nameEncrypted.ciphertext, - nameIv: nameEncrypted.iv, - }); - return res.ok; + try { + await trpc.directory.create.mutate({ + parent: parentId, + mekVersion: masterKey.version, + dek: await wrapDataKey(dataKey, masterKey.key), + dekVersion: dataKeyVersion, + name: nameEncrypted.ciphertext, + nameIv: nameEncrypted.iv, + }); + return true; + } catch { + // TODO: Error Handling + return false; + } }; export const requestFileUpload = async ( @@ -101,37 +100,51 @@ export const requestFileUpload = async ( }; export const requestEntryRename = async (entry: SelectedEntry, newName: string) => { + const trpc = useTRPC(); const newNameEncrypted = await encryptString(newName, entry.dataKey); - let res; - if (entry.type === "directory") { - res = await callPostApi(`/api/directory/${entry.id}/rename`, { - dekVersion: entry.dataKeyVersion.toISOString(), - name: newNameEncrypted.ciphertext, - nameIv: newNameEncrypted.iv, - }); - } else { - res = await callPostApi(`/api/file/${entry.id}/rename`, { - dekVersion: entry.dataKeyVersion.toISOString(), - name: newNameEncrypted.ciphertext, - nameIv: newNameEncrypted.iv, - }); + try { + if (entry.type === "directory") { + await trpc.directory.rename.mutate({ + id: entry.id, + dekVersion: entry.dataKeyVersion, + name: newNameEncrypted.ciphertext, + nameIv: newNameEncrypted.iv, + }); + } else { + await trpc.file.rename.mutate({ + id: entry.id, + dekVersion: entry.dataKeyVersion, + name: newNameEncrypted.ciphertext, + nameIv: newNameEncrypted.iv, + }); + } + return true; + } catch { + // TODO: Error Handling + return false; } - return res.ok; }; export const requestEntryDeletion = async (entry: SelectedEntry) => { - const res = await callPostApi(`/api/${entry.type}/${entry.id}/delete`); - if (!res.ok) return false; + const trpc = useTRPC(); - if (entry.type === "directory") { - const { deletedFiles }: DirectoryDeleteResponse = await res.json(); - await Promise.all( - deletedFiles.flatMap((fileId) => [deleteFileCache(fileId), deleteFileThumbnailCache(fileId)]), - ); - return true; - } else { - await Promise.all([deleteFileCache(entry.id), deleteFileThumbnailCache(entry.id)]); + try { + if (entry.type === "directory") { + const { deletedFiles } = await trpc.directory.delete.mutate({ id: entry.id }); + await Promise.all( + deletedFiles.flatMap((fileId) => [ + deleteFileCache(fileId), + deleteFileThumbnailCache(fileId), + ]), + ); + } else { + await trpc.file.delete.mutate({ id: entry.id }); + await Promise.all([deleteFileCache(entry.id), deleteFileThumbnailCache(entry.id)]); + } return true; + } catch { + // TODO: Error Handling + return false; } }; diff --git a/src/routes/(main)/menu/+page.ts b/src/routes/(main)/menu/+page.ts index b1582e7..ecd8f0b 100644 --- a/src/routes/(main)/menu/+page.ts +++ b/src/routes/(main)/menu/+page.ts @@ -6,7 +6,7 @@ export const load: PageLoad = async ({ fetch }) => { const trpc = useTRPC(fetch); try { - const { nickname } = await trpc.user.info.query(); + const { nickname } = await trpc.user.get.query(); return { nickname }; } catch { error(500, "Internal server error"); diff --git a/src/routes/api/category/[id]/+server.ts b/src/routes/api/category/[id]/+server.ts deleted file mode 100644 index 4a486fa..0000000 --- a/src/routes/api/category/[id]/+server.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { error, json } from "@sveltejs/kit"; -import { z } from "zod"; -import { authorize } from "$lib/server/modules/auth"; -import { categoryInfoResponse, type CategoryInfoResponse } from "$lib/server/schemas"; -import { getCategoryInformation } from "$lib/server/services/category"; -import type { RequestHandler } from "./$types"; - -export const GET: RequestHandler = async ({ locals, params }) => { - const { userId } = await authorize(locals, "activeClient"); - - const zodRes = z - .object({ - id: z.union([z.enum(["root"]), z.coerce.number().int().positive()]), - }) - .safeParse(params); - if (!zodRes.success) error(400, "Invalid path parameters"); - const { id } = zodRes.data; - - const { metadata, categories } = await getCategoryInformation(userId, id); - return json( - categoryInfoResponse.parse({ - metadata: metadata && { - parent: metadata.parentId, - mekVersion: metadata.mekVersion, - dek: metadata.encDek, - dekVersion: metadata.dekVersion.toISOString(), - name: metadata.encName.ciphertext, - nameIv: metadata.encName.iv, - }, - subCategories: categories, - } satisfies CategoryInfoResponse), - ); -}; diff --git a/src/routes/api/category/[id]/delete/+server.ts b/src/routes/api/category/[id]/delete/+server.ts deleted file mode 100644 index cbbe356..0000000 --- a/src/routes/api/category/[id]/delete/+server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { error, text } from "@sveltejs/kit"; -import { z } from "zod"; -import { authorize } from "$lib/server/modules/auth"; -import { deleteCategory } from "$lib/server/services/category"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, params }) => { - const { userId } = await authorize(locals, "activeClient"); - - const zodRes = z - .object({ - id: z.coerce.number().int().positive(), - }) - .safeParse(params); - if (!zodRes.success) error(400, "Invalid path parameters"); - const { id } = zodRes.data; - - await deleteCategory(userId, id); - return text("Category deleted", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/routes/api/category/[id]/file/add/+server.ts b/src/routes/api/category/[id]/file/add/+server.ts deleted file mode 100644 index 2eaf2f2..0000000 --- a/src/routes/api/category/[id]/file/add/+server.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { error, text } from "@sveltejs/kit"; -import { z } from "zod"; -import { authorize } from "$lib/server/modules/auth"; -import { categoryFileAddRequest } from "$lib/server/schemas"; -import { addCategoryFile } from "$lib/server/services/category"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, params, request }) => { - const { userId } = await authorize(locals, "activeClient"); - - const paramsZodRes = z - .object({ - id: z.coerce.number().int().positive(), - }) - .safeParse(params); - if (!paramsZodRes.success) error(400, "Invalid path parameters"); - const { id } = paramsZodRes.data; - - const bodyZodRes = categoryFileAddRequest.safeParse(await request.json()); - if (!bodyZodRes.success) error(400, "Invalid request body"); - const { file } = bodyZodRes.data; - - await addCategoryFile(userId, id, file); - return text("File added", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/routes/api/category/[id]/file/list/+server.ts b/src/routes/api/category/[id]/file/list/+server.ts deleted file mode 100644 index e354d8b..0000000 --- a/src/routes/api/category/[id]/file/list/+server.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { error, json } from "@sveltejs/kit"; -import { z } from "zod"; -import { authorize } from "$lib/server/modules/auth"; -import { categoryFileListResponse, type CategoryFileListResponse } from "$lib/server/schemas"; -import { getCategoryFiles } from "$lib/server/services/category"; -import type { RequestHandler } from "./$types"; - -export const GET: RequestHandler = async ({ locals, url, params }) => { - const { userId } = await authorize(locals, "activeClient"); - - const paramsZodRes = z - .object({ - id: z.coerce.number().int().positive(), - }) - .safeParse(params); - if (!paramsZodRes.success) error(400, "Invalid path parameters"); - const { id } = paramsZodRes.data; - - const queryZodRes = z - .object({ - recurse: z - .enum(["true", "false"]) - .transform((value) => value === "true") - .nullable(), - }) - .safeParse({ recurse: url.searchParams.get("recurse") }); - if (!queryZodRes.success) error(400, "Invalid query parameters"); - const { recurse } = queryZodRes.data; - - const { files } = await getCategoryFiles(userId, id, recurse ?? false); - return json( - categoryFileListResponse.parse({ - files: files.map(({ id, isRecursive }) => ({ file: id, isRecursive })), - } satisfies CategoryFileListResponse), - ); -}; diff --git a/src/routes/api/category/[id]/file/remove/+server.ts b/src/routes/api/category/[id]/file/remove/+server.ts deleted file mode 100644 index 6fdcccf..0000000 --- a/src/routes/api/category/[id]/file/remove/+server.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { error, text } from "@sveltejs/kit"; -import { z } from "zod"; -import { authorize } from "$lib/server/modules/auth"; -import { categoryFileRemoveRequest } from "$lib/server/schemas"; -import { removeCategoryFile } from "$lib/server/services/category"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, params, request }) => { - const { userId } = await authorize(locals, "activeClient"); - - const paramsZodRes = z - .object({ - id: z.coerce.number().int().positive(), - }) - .safeParse(params); - if (!paramsZodRes.success) error(400, "Invalid path parameters"); - const { id } = paramsZodRes.data; - - const bodyZodRes = categoryFileRemoveRequest.safeParse(await request.json()); - if (!bodyZodRes.success) error(400, "Invalid request body"); - const { file } = bodyZodRes.data; - - await removeCategoryFile(userId, id, file); - return text("File removed", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/routes/api/category/[id]/rename/+server.ts b/src/routes/api/category/[id]/rename/+server.ts deleted file mode 100644 index 5351544..0000000 --- a/src/routes/api/category/[id]/rename/+server.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { error, text } from "@sveltejs/kit"; -import { z } from "zod"; -import { authorize } from "$lib/server/modules/auth"; -import { categoryRenameRequest } from "$lib/server/schemas"; -import { renameCategory } from "$lib/server/services/category"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, params, request }) => { - const { userId } = await authorize(locals, "activeClient"); - - const paramsZodRes = z - .object({ - id: z.coerce.number().int().positive(), - }) - .safeParse(params); - if (!paramsZodRes.success) error(400, "Invalid path parameters"); - const { id } = paramsZodRes.data; - - const bodyZodRes = categoryRenameRequest.safeParse(await request.json()); - if (!bodyZodRes.success) error(400, "Invalid request body"); - const { dekVersion, name, nameIv } = bodyZodRes.data; - - await renameCategory(userId, id, new Date(dekVersion), { ciphertext: name, iv: nameIv }); - return text("Category renamed", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/routes/api/category/create/+server.ts b/src/routes/api/category/create/+server.ts deleted file mode 100644 index 216d850..0000000 --- a/src/routes/api/category/create/+server.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { error, text } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { categoryCreateRequest } from "$lib/server/schemas"; -import { createCategory } from "$lib/server/services/category"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, request }) => { - const { userId } = await authorize(locals, "activeClient"); - - const zodRes = categoryCreateRequest.safeParse(await request.json()); - if (!zodRes.success) error(400, "Invalid request body"); - const { parent, mekVersion, dek, dekVersion, name, nameIv } = zodRes.data; - - await createCategory({ - userId, - parentId: parent, - mekVersion, - encDek: dek, - dekVersion: new Date(dekVersion), - encName: { ciphertext: name, iv: nameIv }, - }); - return text("Category created", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/routes/api/directory/[id]/+server.ts b/src/routes/api/directory/[id]/+server.ts deleted file mode 100644 index 8189160..0000000 --- a/src/routes/api/directory/[id]/+server.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { error, json } from "@sveltejs/kit"; -import { z } from "zod"; -import { authorize } from "$lib/server/modules/auth"; -import { directoryInfoResponse, type DirectoryInfoResponse } from "$lib/server/schemas"; -import { getDirectoryInformation } from "$lib/server/services/directory"; -import type { RequestHandler } from "./$types"; - -export const GET: RequestHandler = async ({ locals, params }) => { - const { userId } = await authorize(locals, "activeClient"); - - const zodRes = z - .object({ - id: z.union([z.enum(["root"]), z.coerce.number().int().positive()]), - }) - .safeParse(params); - if (!zodRes.success) error(400, "Invalid path parameters"); - const { id } = zodRes.data; - - const { metadata, directories, files } = await getDirectoryInformation(userId, id); - return json( - directoryInfoResponse.parse({ - metadata: metadata && { - parent: metadata.parentId, - mekVersion: metadata.mekVersion, - dek: metadata.encDek, - dekVersion: metadata.dekVersion.toISOString(), - name: metadata.encName.ciphertext, - nameIv: metadata.encName.iv, - }, - subDirectories: directories, - files, - } satisfies DirectoryInfoResponse), - ); -}; diff --git a/src/routes/api/directory/[id]/delete/+server.ts b/src/routes/api/directory/[id]/delete/+server.ts deleted file mode 100644 index 4d29fd8..0000000 --- a/src/routes/api/directory/[id]/delete/+server.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { error, json } from "@sveltejs/kit"; -import { z } from "zod"; -import { authorize } from "$lib/server/modules/auth"; -import { directoryDeleteResponse, type DirectoryDeleteResponse } from "$lib/server/schemas"; -import { deleteDirectory } from "$lib/server/services/directory"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, params }) => { - const { userId } = await authorize(locals, "activeClient"); - - const zodRes = z - .object({ - id: z.coerce.number().int().positive(), - }) - .safeParse(params); - if (!zodRes.success) error(400, "Invalid path parameters"); - const { id } = zodRes.data; - - const { files } = await deleteDirectory(userId, id); - return json( - directoryDeleteResponse.parse({ deletedFiles: files } satisfies DirectoryDeleteResponse), - ); -}; diff --git a/src/routes/api/directory/[id]/rename/+server.ts b/src/routes/api/directory/[id]/rename/+server.ts deleted file mode 100644 index cc50b2f..0000000 --- a/src/routes/api/directory/[id]/rename/+server.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { error, text } from "@sveltejs/kit"; -import { z } from "zod"; -import { authorize } from "$lib/server/modules/auth"; -import { directoryRenameRequest } from "$lib/server/schemas"; -import { renameDirectory } from "$lib/server/services/directory"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, params, request }) => { - const { userId } = await authorize(locals, "activeClient"); - - const paramsZodRes = z - .object({ - id: z.coerce.number().int().positive(), - }) - .safeParse(params); - if (!paramsZodRes.success) error(400, "Invalid path parameters"); - const { id } = paramsZodRes.data; - - const bodyZodRes = directoryRenameRequest.safeParse(await request.json()); - if (!bodyZodRes.success) error(400, "Invalid request body"); - const { dekVersion, name, nameIv } = bodyZodRes.data; - - await renameDirectory(userId, id, new Date(dekVersion), { ciphertext: name, iv: nameIv }); - return text("Directory renamed", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/routes/api/directory/create/+server.ts b/src/routes/api/directory/create/+server.ts deleted file mode 100644 index 7c65436..0000000 --- a/src/routes/api/directory/create/+server.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { error, text } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { directoryCreateRequest } from "$lib/server/schemas"; -import { createDirectory } from "$lib/server/services/directory"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, request }) => { - const { userId } = await authorize(locals, "activeClient"); - - const zodRes = directoryCreateRequest.safeParse(await request.json()); - if (!zodRes.success) error(400, "Invalid request body"); - const { parent, mekVersion, dek, dekVersion, name, nameIv } = zodRes.data; - - await createDirectory({ - userId, - parentId: parent, - mekVersion, - encDek: dek, - dekVersion: new Date(dekVersion), - encName: { ciphertext: name, iv: nameIv }, - }); - return text("Directory created", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/routes/api/file/[id]/+server.ts b/src/routes/api/file/[id]/+server.ts deleted file mode 100644 index 23e9385..0000000 --- a/src/routes/api/file/[id]/+server.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { error, json } from "@sveltejs/kit"; -import { z } from "zod"; -import { authorize } from "$lib/server/modules/auth"; -import { fileInfoResponse, type FileInfoResponse } from "$lib/server/schemas"; -import { getFileInformation } from "$lib/server/services/file"; -import type { RequestHandler } from "./$types"; - -export const GET: RequestHandler = async ({ locals, params }) => { - const { userId } = await authorize(locals, "activeClient"); - - const zodRes = z - .object({ - id: z.coerce.number().int().positive(), - }) - .safeParse(params); - if (!zodRes.success) error(400, "Invalid path parameters"); - const { id } = zodRes.data; - - const { - parentId, - mekVersion, - encDek, - dekVersion, - contentType, - encContentIv, - encName, - encCreatedAt, - encLastModifiedAt, - categories, - } = await getFileInformation(userId, id); - return json( - fileInfoResponse.parse({ - parent: parentId, - mekVersion, - dek: encDek, - dekVersion: dekVersion.toISOString(), - contentType: contentType, - contentIv: encContentIv, - name: encName.ciphertext, - nameIv: encName.iv, - createdAt: encCreatedAt?.ciphertext, - createdAtIv: encCreatedAt?.iv, - lastModifiedAt: encLastModifiedAt.ciphertext, - lastModifiedAtIv: encLastModifiedAt.iv, - categories, - } satisfies FileInfoResponse), - ); -}; diff --git a/src/routes/api/file/[id]/delete/+server.ts b/src/routes/api/file/[id]/delete/+server.ts deleted file mode 100644 index 7baac25..0000000 --- a/src/routes/api/file/[id]/delete/+server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { error, text } from "@sveltejs/kit"; -import { z } from "zod"; -import { authorize } from "$lib/server/modules/auth"; -import { deleteFile } from "$lib/server/services/file"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, params }) => { - const { userId } = await authorize(locals, "activeClient"); - - const zodRes = z - .object({ - id: z.coerce.number().int().positive(), - }) - .safeParse(params); - if (!zodRes.success) error(400, "Invalid path parameters"); - const { id } = zodRes.data; - - await deleteFile(userId, id); - return text("File deleted", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/routes/api/file/[id]/rename/+server.ts b/src/routes/api/file/[id]/rename/+server.ts deleted file mode 100644 index 343f146..0000000 --- a/src/routes/api/file/[id]/rename/+server.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { error, text } from "@sveltejs/kit"; -import { z } from "zod"; -import { authorize } from "$lib/server/modules/auth"; -import { fileRenameRequest } from "$lib/server/schemas"; -import { renameFile } from "$lib/server/services/file"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, params, request }) => { - const { userId } = await authorize(locals, "activeClient"); - - const paramsZodRes = z - .object({ - id: z.coerce.number().int().positive(), - }) - .safeParse(params); - if (!paramsZodRes.success) error(400, "Invalid path parameters"); - const { id } = paramsZodRes.data; - - const bodyZodRes = fileRenameRequest.safeParse(await request.json()); - if (!bodyZodRes.success) error(400, "Invalid request body"); - const { dekVersion, name, nameIv } = bodyZodRes.data; - - await renameFile(userId, id, new Date(dekVersion), { ciphertext: name, iv: nameIv }); - return text("File renamed", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/routes/api/file/[id]/thumbnail/+server.ts b/src/routes/api/file/[id]/thumbnail/+server.ts deleted file mode 100644 index 12c9347..0000000 --- a/src/routes/api/file/[id]/thumbnail/+server.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { error, json } from "@sveltejs/kit"; -import { z } from "zod"; -import { authorize } from "$lib/server/modules/auth"; -import { fileThumbnailInfoResponse, type FileThumbnailInfoResponse } from "$lib/server/schemas"; -import { getFileThumbnailInformation } from "$lib/server/services/file"; -import type { RequestHandler } from "./$types"; - -export const GET: RequestHandler = async ({ locals, params }) => { - const { userId } = await authorize(locals, "activeClient"); - - const zodRes = z - .object({ - id: z.coerce.number().int().positive(), - }) - .safeParse(params); - if (!zodRes.success) error(400, "Invalid path parameters"); - const { id } = zodRes.data; - - const { updatedAt, encContentIv } = await getFileThumbnailInformation(userId, id); - return json( - fileThumbnailInfoResponse.parse({ - updatedAt: updatedAt.toISOString(), - contentIv: encContentIv, - } satisfies FileThumbnailInfoResponse), - ); -}; diff --git a/src/routes/api/file/list/+server.ts b/src/routes/api/file/list/+server.ts deleted file mode 100644 index c1b6888..0000000 --- a/src/routes/api/file/list/+server.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { json } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { fileListResponse, type FileListResponse } from "$lib/server/schemas"; -import { getFileList } from "$lib/server/services/file"; -import type { RequestHandler } from "./$types"; - -export const GET: RequestHandler = async ({ locals }) => { - const { userId } = await authorize(locals, "activeClient"); - const { files } = await getFileList(userId); - return json(fileListResponse.parse({ files } satisfies FileListResponse)); -}; diff --git a/src/routes/api/file/scanDuplicates/+server.ts b/src/routes/api/file/scanDuplicates/+server.ts deleted file mode 100644 index fb41b43..0000000 --- a/src/routes/api/file/scanDuplicates/+server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { error, json } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { - duplicateFileScanRequest, - duplicateFileScanResponse, - type DuplicateFileScanResponse, -} from "$lib/server/schemas"; -import { scanDuplicateFiles } from "$lib/server/services/file"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, request }) => { - const { userId } = await authorize(locals, "activeClient"); - - const zodRes = duplicateFileScanRequest.safeParse(await request.json()); - if (!zodRes.success) error(400, "Invalid request body"); - const { hskVersion, contentHmac } = zodRes.data; - - const { files } = await scanDuplicateFiles(userId, hskVersion, contentHmac); - return json(duplicateFileScanResponse.parse({ files } satisfies DuplicateFileScanResponse)); -}; diff --git a/src/routes/api/file/scanMissingThumbnails/+server.ts b/src/routes/api/file/scanMissingThumbnails/+server.ts deleted file mode 100644 index bf2a2a6..0000000 --- a/src/routes/api/file/scanMissingThumbnails/+server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { json } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { - missingThumbnailFileScanResponse, - type MissingThumbnailFileScanResponse, -} from "$lib/server/schemas/file"; -import { scanMissingFileThumbnails } from "$lib/server/services/file"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals }) => { - const { userId } = await authorize(locals, "activeClient"); - const { files } = await scanMissingFileThumbnails(userId); - return json( - missingThumbnailFileScanResponse.parse({ files } satisfies MissingThumbnailFileScanResponse), - ); -}; diff --git a/src/trpc/client.ts b/src/trpc/client.ts index dbf4e80..cb1e8c5 100644 --- a/src/trpc/client.ts +++ b/src/trpc/client.ts @@ -1,4 +1,5 @@ import { createTRPCClient, httpBatchLink } from "@trpc/client"; +import superjson from "superjson"; import { browser } from "$app/environment"; import type { AppRouter } from "./router.server"; @@ -7,6 +8,7 @@ const createClient = (fetch: typeof globalThis.fetch) => links: [ httpBatchLink({ url: "/api/trpc", + transformer: superjson, fetch, }), ], diff --git a/src/trpc/init.server.ts b/src/trpc/init.server.ts index 15a35fa..8b88157 100644 --- a/src/trpc/init.server.ts +++ b/src/trpc/init.server.ts @@ -1,10 +1,12 @@ import type { RequestEvent } from "@sveltejs/kit"; import { initTRPC, TRPCError } from "@trpc/server"; +import superjson from "superjson"; import { authorizeMiddleware, authorizeClientMiddleware } from "./middlewares/authorize"; -export const createContext = (event: RequestEvent) => event; +export type Context = Awaited>; -export const t = initTRPC.context>>().create(); +export const createContext = (event: RequestEvent) => event; +export const t = initTRPC.context().create({ transformer: superjson }); export const router = t.router; export const publicProcedure = t.procedure; diff --git a/src/trpc/router.server.ts b/src/trpc/router.server.ts index 3c44bea..3d05e93 100644 --- a/src/trpc/router.server.ts +++ b/src/trpc/router.server.ts @@ -1,10 +1,21 @@ import type { RequestEvent } from "@sveltejs/kit"; import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; import { createContext, router } from "./init.server"; -import { clientRouter, hskRouter, mekRouter, userRouter } from "./routers"; +import { + categoryRouter, + clientRouter, + directoryRouter, + fileRouter, + hskRouter, + mekRouter, + userRouter, +} from "./routers"; export const appRouter = router({ + category: categoryRouter, client: clientRouter, + directory: directoryRouter, + file: fileRouter, hsk: hskRouter, mek: mekRouter, user: userRouter, diff --git a/src/trpc/routers/category.ts b/src/trpc/routers/category.ts new file mode 100644 index 0000000..f002421 --- /dev/null +++ b/src/trpc/routers/category.ts @@ -0,0 +1,194 @@ +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { CategoryRepo, FileRepo, IntegrityError } from "$lib/server/db"; +import { categoryIdSchema } from "$lib/server/schemas"; +import { router, roleProcedure } from "../init.server"; + +const categoryRouter = router({ + get: roleProcedure["activeClient"] + .input( + z.object({ + id: categoryIdSchema, + }), + ) + .query(async ({ ctx, input }) => { + const category = + input.id !== "root" + ? await CategoryRepo.getCategory(ctx.session.userId, input.id) + : undefined; + if (category === null) { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid category id" }); + } + + const categories = await CategoryRepo.getAllCategoriesByParent(ctx.session.userId, input.id); + return { + metadata: category && { + parent: category.parentId, + mekVersion: category.mekVersion, + dek: category.encDek, + dekVersion: category.dekVersion, + name: category.encName.ciphertext, + nameIv: category.encName.iv, + }, + subCategories: categories.map(({ id }) => id), + }; + }), + + create: roleProcedure["activeClient"] + .input( + z.object({ + parent: categoryIdSchema, + mekVersion: z.number().int().positive(), + dek: z.string().base64().nonempty(), + dekVersion: z.date(), + name: z.string().base64().nonempty(), + nameIv: z.string().base64().nonempty(), + }), + ) + .mutation(async ({ ctx, input }) => { + const oneMinuteAgo = new Date(Date.now() - 60 * 1000); + const oneMinuteLater = new Date(Date.now() + 60 * 1000); + if (input.dekVersion <= oneMinuteAgo || input.dekVersion >= oneMinuteLater) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid DEK version" }); + } + + try { + await CategoryRepo.registerCategory({ + parentId: input.parent, + userId: ctx.session.userId, + mekVersion: input.mekVersion, + encDek: input.dek, + dekVersion: input.dekVersion, + encName: { ciphertext: input.name, iv: input.nameIv }, + }); + } catch (e) { + if (e instanceof IntegrityError && e.message === "Inactive MEK version") { + throw new TRPCError({ code: "BAD_REQUEST", message: e.message }); + } + throw e; + } + }), + + rename: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + dekVersion: z.date(), + name: z.string().base64().nonempty(), + nameIv: z.string().base64().nonempty(), + }), + ) + .mutation(async ({ ctx, input }) => { + try { + await CategoryRepo.setCategoryEncName(ctx.session.userId, input.id, input.dekVersion, { + ciphertext: input.name, + iv: input.nameIv, + }); + } catch (e) { + if (e instanceof IntegrityError) { + if (e.message === "Category not found") { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid category id" }); + } else if (e.message === "Invalid DEK version") { + throw new TRPCError({ code: "BAD_REQUEST", message: e.message }); + } + } + throw e; + } + }), + + delete: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + }), + ) + .mutation(async ({ ctx, input }) => { + try { + await CategoryRepo.unregisterCategory(ctx.session.userId, input.id); + } catch (e) { + if (e instanceof IntegrityError && e.message === "Category not found") { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid category id" }); + } + throw e; + } + }), + + files: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + recurse: z.boolean().default(false), + }), + ) + .query(async ({ ctx, input }) => { + const category = await CategoryRepo.getCategory(ctx.session.userId, input.id); + if (!category) { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid category id" }); + } + + const files = await FileRepo.getAllFilesByCategory( + ctx.session.userId, + input.id, + input.recurse, + ); + return files.map(({ id, isRecursive }) => ({ file: id, isRecursive })); + }), + + addFile: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + file: z.number().int().positive(), + }), + ) + .mutation(async ({ ctx, input }) => { + const [category, file] = await Promise.all([ + CategoryRepo.getCategory(ctx.session.userId, input.id), + FileRepo.getFile(ctx.session.userId, input.file), + ]); + if (!category) { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid category id" }); + } else if (!file) { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid file id" }); + } + + try { + await FileRepo.addFileToCategory(input.file, input.id); + } catch (e) { + if (e instanceof IntegrityError && e.message === "File already added to category") { + throw new TRPCError({ code: "BAD_REQUEST", message: "File already added" }); + } + throw e; + } + }), + + removeFile: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + file: z.number().int().positive(), + }), + ) + .mutation(async ({ ctx, input }) => { + const [category, file] = await Promise.all([ + CategoryRepo.getCategory(ctx.session.userId, input.id), + FileRepo.getFile(ctx.session.userId, input.file), + ]); + if (!category) { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid category id" }); + } else if (!file) { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid file id" }); + } + + try { + await FileRepo.removeFileFromCategory(input.file, input.id); + } catch (e) { + if (e instanceof IntegrityError && e.message === "File not found in category") { + throw new TRPCError({ code: "BAD_REQUEST", message: "File not added" }); + } + throw e; + } + }), +}); + +export default categoryRouter; diff --git a/src/trpc/routers/directory.ts b/src/trpc/routers/directory.ts new file mode 100644 index 0000000..70e3663 --- /dev/null +++ b/src/trpc/routers/directory.ts @@ -0,0 +1,125 @@ +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { FileRepo, IntegrityError } from "$lib/server/db"; +import { safeUnlink } from "$lib/server/modules/filesystem"; +import { directoryIdSchema } from "$lib/server/schemas"; +import { router, roleProcedure } from "../init.server"; + +const directoryRouter = router({ + get: roleProcedure["activeClient"] + .input( + z.object({ + id: directoryIdSchema, + }), + ) + .query(async ({ ctx, input }) => { + const directory = + input.id !== "root" ? await FileRepo.getDirectory(ctx.session.userId, input.id) : undefined; + if (directory === null) { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid directory id" }); + } + + const [directories, files] = await Promise.all([ + FileRepo.getAllDirectoriesByParent(ctx.session.userId, input.id), + FileRepo.getAllFilesByParent(ctx.session.userId, input.id), + ]); + return { + metadata: directory && { + parent: directory.parentId, + mekVersion: directory.mekVersion, + dek: directory.encDek, + dekVersion: directory.dekVersion, + name: directory.encName.ciphertext, + nameIv: directory.encName.iv, + }, + subDirectories: directories.map(({ id }) => id), + files: files.map(({ id }) => id), + }; + }), + + create: roleProcedure["activeClient"] + .input( + z.object({ + parent: directoryIdSchema, + mekVersion: z.number().int().positive(), + dek: z.string().base64().nonempty(), + dekVersion: z.date(), + name: z.string().base64().nonempty(), + nameIv: z.string().base64().nonempty(), + }), + ) + .mutation(async ({ ctx, input }) => { + const oneMinuteAgo = new Date(Date.now() - 60 * 1000); + const oneMinuteLater = new Date(Date.now() + 60 * 1000); + if (input.dekVersion <= oneMinuteAgo || input.dekVersion >= oneMinuteLater) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid DEK version" }); + } + + try { + await FileRepo.registerDirectory({ + parentId: input.parent, + userId: ctx.session.userId, + mekVersion: input.mekVersion, + encDek: input.dek, + dekVersion: input.dekVersion, + encName: { ciphertext: input.name, iv: input.nameIv }, + }); + } catch (e) { + if (e instanceof IntegrityError && e.message === "Inactive MEK version") { + throw new TRPCError({ code: "BAD_REQUEST", message: e.message }); + } + throw e; + } + }), + + rename: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + dekVersion: z.date(), + name: z.string().base64().nonempty(), + nameIv: z.string().base64().nonempty(), + }), + ) + .mutation(async ({ ctx, input }) => { + try { + await FileRepo.setDirectoryEncName(ctx.session.userId, input.id, input.dekVersion, { + ciphertext: input.name, + iv: input.nameIv, + }); + } catch (e) { + if (e instanceof IntegrityError) { + if (e.message === "Directory not found") { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid directory id" }); + } else if (e.message === "Invalid DEK version") { + throw new TRPCError({ code: "BAD_REQUEST", message: e.message }); + } + } + throw e; + } + }), + + delete: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + }), + ) + .mutation(async ({ ctx, input }) => { + try { + const files = await FileRepo.unregisterDirectory(ctx.session.userId, input.id); + files.forEach(({ path, thumbnailPath }) => { + safeUnlink(path); // Intended + safeUnlink(thumbnailPath); // Intended + }); + return { deletedFiles: files.map(({ id }) => id) }; + } catch (e) { + if (e instanceof IntegrityError && e.message === "Directory not found") { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid directory id" }); + } + throw e; + } + }), +}); + +export default directoryRouter; diff --git a/src/trpc/routers/file.ts b/src/trpc/routers/file.ts new file mode 100644 index 0000000..b37032b --- /dev/null +++ b/src/trpc/routers/file.ts @@ -0,0 +1,122 @@ +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { FileRepo, MediaRepo, IntegrityError } from "$lib/server/db"; +import { safeUnlink } from "$lib/server/modules/filesystem"; +import { router, roleProcedure } from "../init.server"; + +const fileRouter = router({ + get: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + }), + ) + .query(async ({ ctx, input }) => { + const file = await FileRepo.getFile(ctx.session.userId, input.id); + if (!file) { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid file id" }); + } + + const categories = await FileRepo.getAllFileCategories(input.id); + return { + parent: file.parentId, + mekVersion: file.mekVersion, + dek: file.encDek, + dekVersion: file.dekVersion, + contentType: file.contentType, + contentIv: file.encContentIv, + name: file.encName.ciphertext, + nameIv: file.encName.iv, + createdAt: file.encCreatedAt?.ciphertext, + createdAtIv: file.encCreatedAt?.iv, + lastModifiedAt: file.encLastModifiedAt.ciphertext, + lastModifiedAtIv: file.encLastModifiedAt.iv, + categories: categories.map(({ id }) => id), + }; + }), + + list: roleProcedure["activeClient"].query(async ({ ctx }) => { + return await FileRepo.getAllFileIds(ctx.session.userId); + }), + + listByHash: roleProcedure["activeClient"] + .input( + z.object({ + hskVersion: z.number().int().positive(), + contentHmac: z.string().base64().nonempty(), + }), + ) + .query(async ({ ctx, input }) => { + return await FileRepo.getAllFileIdsByContentHmac( + ctx.session.userId, + input.hskVersion, + input.contentHmac, + ); + }), + + listWithoutThumbnail: roleProcedure["activeClient"].query(async ({ ctx }) => { + return await MediaRepo.getMissingFileThumbnails(ctx.session.userId); + }), + + rename: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + dekVersion: z.date(), + name: z.string().base64().nonempty(), + nameIv: z.string().base64().nonempty(), + }), + ) + .mutation(async ({ ctx, input }) => { + try { + await FileRepo.setFileEncName(ctx.session.userId, input.id, input.dekVersion, { + ciphertext: input.name, + iv: input.nameIv, + }); + } catch (e) { + if (e instanceof IntegrityError) { + if (e.message === "File not found") { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid file id" }); + } else if (e.message === "Invalid DEK version") { + throw new TRPCError({ code: "BAD_REQUEST", message: e.message }); + } + } + throw e; + } + }), + + delete: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + }), + ) + .mutation(async ({ ctx, input }) => { + try { + const { path, thumbnailPath } = await FileRepo.unregisterFile(ctx.session.userId, input.id); + safeUnlink(path); // Intended + safeUnlink(thumbnailPath); // Intended + } catch (e) { + if (e instanceof IntegrityError && e.message === "File not found") { + throw new TRPCError({ code: "NOT_FOUND", message: "Invalid file id" }); + } + throw e; + } + }), + + thumbnail: roleProcedure["activeClient"] + .input( + z.object({ + id: z.number().int().positive(), + }), + ) + .query(async ({ ctx, input }) => { + const thumbnail = await MediaRepo.getFileThumbnail(ctx.session.userId, input.id); + if (!thumbnail) { + throw new TRPCError({ code: "NOT_FOUND", message: "File or its thumbnail not found" }); + } + return { updatedAt: thumbnail.updatedAt, contentIv: thumbnail.encContentIv }; + }), +}); + +export default fileRouter; diff --git a/src/trpc/routers/index.ts b/src/trpc/routers/index.ts index 26ac7b2..c943728 100644 --- a/src/trpc/routers/index.ts +++ b/src/trpc/routers/index.ts @@ -1,4 +1,7 @@ +export { default as categoryRouter } from "./category"; export { default as clientRouter } from "./client"; +export { default as directoryRouter } from "./directory"; +export { default as fileRouter } from "./file"; export { default as hskRouter } from "./hsk"; export { default as mekRouter } from "./mek"; export { default as userRouter } from "./user"; diff --git a/src/trpc/routers/user.ts b/src/trpc/routers/user.ts index 37b2460..ec514f1 100644 --- a/src/trpc/routers/user.ts +++ b/src/trpc/routers/user.ts @@ -1,10 +1,9 @@ import { TRPCError } from "@trpc/server"; -import { z } from "zod"; import { UserRepo } from "$lib/server/db"; import { router, roleProcedure } from "../init.server"; const userRouter = router({ - info: roleProcedure.any.query(async ({ ctx }) => { + get: roleProcedure["any"].query(async ({ ctx }) => { const user = await UserRepo.getUser(ctx.session.userId); if (!user) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid session id" }); @@ -12,16 +11,6 @@ const userRouter = router({ return { email: user.email, nickname: user.nickname }; }), - - changeNickname: roleProcedure.any - .input( - z.object({ - newNickname: z.string().trim().min(2).max(8), - }), - ) - .mutation(async ({ ctx, input }) => { - await UserRepo.setUserNickname(ctx.session.userId, input.newNickname); - }), }); export default userRouter; From b92b4a0b1bf52acf6d0f40ce4f1731f73454c448 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 25 Dec 2025 22:53:51 +0900 Subject: [PATCH 07/11] =?UTF-8?q?Zod=204=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 42 +- pnpm-lock.yaml | 1334 ++++++++++++--------------- src/lib/modules/key.ts | 10 +- src/lib/server/schemas/auth.ts | 14 +- src/lib/server/schemas/category.ts | 2 +- src/lib/server/schemas/directory.ts | 2 +- src/lib/server/schemas/file.ts | 30 +- src/trpc/routers/category.ts | 26 +- src/trpc/routers/client.ts | 8 +- src/trpc/routers/directory.ts | 16 +- src/trpc/routers/file.ts | 16 +- src/trpc/routers/hsk.ts | 4 +- src/trpc/routers/mek.ts | 4 +- 13 files changed, 655 insertions(+), 853 deletions(-) diff --git a/package.json b/package.json index b1425b0..ce46336 100644 --- a/package.json +++ b/package.json @@ -17,52 +17,52 @@ }, "devDependencies": { "@eslint/compat": "^1.4.1", - "@iconify-json/material-symbols": "^1.2.44", + "@iconify-json/material-symbols": "^1.2.50", "@sveltejs/adapter-node": "^5.4.0", - "@sveltejs/kit": "^2.48.4", + "@sveltejs/kit": "^2.49.2", "@sveltejs/vite-plugin-svelte": "^6.2.1", - "@trpc/client": "^11.7.1", + "@trpc/client": "^11.8.1", "@types/file-saver": "^2.0.7", "@types/ms": "^0.7.34", "@types/node-schedule": "^2.1.8", - "@types/pg": "^8.15.6", - "autoprefixer": "^10.4.21", - "axios": "^1.13.1", + "@types/pg": "^8.16.0", + "autoprefixer": "^10.4.23", + "axios": "^1.13.2", "dexie": "^4.2.1", - "eslint": "^9.39.0", + "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-svelte": "^3.13.0", + "eslint-plugin-svelte": "^3.13.1", "eslint-plugin-tailwindcss": "^3.18.2", - "exifreader": "^4.32.0", + "exifreader": "^4.33.1", "file-saver": "^2.0.5", "globals": "^16.5.0", "heic2any": "^0.0.4", "kysely-ctl": "^0.19.0", - "lru-cache": "^11.2.2", + "lru-cache": "^11.2.4", "mime": "^4.1.0", "p-limit": "^7.2.0", - "prettier": "^3.6.2", - "prettier-plugin-svelte": "^3.4.0", - "prettier-plugin-tailwindcss": "^0.7.1", - "svelte": "^5.43.2", - "svelte-check": "^4.3.3", - "tailwindcss": "^3.4.18", + "prettier": "^3.7.4", + "prettier-plugin-svelte": "^3.4.1", + "prettier-plugin-tailwindcss": "^0.7.2", + "svelte": "^5.46.1", + "svelte-check": "^4.3.5", + "tailwindcss": "^3.4.19", "typescript": "^5.9.3", - "typescript-eslint": "^8.46.2", + "typescript-eslint": "^8.50.1", "unplugin-icons": "^22.5.0", - "vite": "^7.1.12" + "vite": "^7.3.0" }, "dependencies": { "@fastify/busboy": "^3.2.0", - "@trpc/server": "^11.7.1", + "@trpc/server": "^11.8.1", "argon2": "^0.44.0", - "kysely": "^0.28.8", + "kysely": "^0.28.9", "ms": "^2.1.3", "node-schedule": "^2.1.1", "pg": "^8.16.3", "superjson": "^2.2.6", "uuid": "^13.0.0", - "zod": "^3.25.76" + "zod": "^4.2.1" }, "engines": { "node": "^22.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b428b0f..e3de21a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,14 +12,14 @@ importers: specifier: ^3.2.0 version: 3.2.0 '@trpc/server': - specifier: ^11.7.1 - version: 11.7.1(typescript@5.9.3) + specifier: ^11.8.1 + version: 11.8.1(typescript@5.9.3) argon2: specifier: ^0.44.0 version: 0.44.0 kysely: - specifier: ^0.28.8 - version: 0.28.8 + specifier: ^0.28.9 + version: 0.28.9 ms: specifier: ^2.1.3 version: 2.1.3 @@ -36,27 +36,27 @@ importers: specifier: ^13.0.0 version: 13.0.0 zod: - specifier: ^3.25.76 - version: 3.25.76 + specifier: ^4.2.1 + version: 4.2.1 devDependencies: '@eslint/compat': specifier: ^1.4.1 - version: 1.4.1(eslint@9.39.0(jiti@1.21.7)) + version: 1.4.1(eslint@9.39.2(jiti@1.21.7)) '@iconify-json/material-symbols': - specifier: ^1.2.44 - version: 1.2.44 + specifier: ^1.2.50 + version: 1.2.50 '@sveltejs/adapter-node': specifier: ^5.4.0 - version: 5.4.0(@sveltejs/kit@2.48.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0))) + version: 5.4.0(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0))) '@sveltejs/kit': - specifier: ^2.48.4 - version: 2.48.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0)) + specifier: ^2.49.2 + version: 2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0)) '@sveltejs/vite-plugin-svelte': specifier: ^6.2.1 - version: 6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0)) + version: 6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0)) '@trpc/client': - specifier: ^11.7.1 - version: 11.7.1(@trpc/server@11.7.1(typescript@5.9.3))(typescript@5.9.3) + specifier: ^11.8.1 + version: 11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3) '@types/file-saver': specifier: ^2.0.7 version: 2.0.7 @@ -67,32 +67,32 @@ importers: specifier: ^2.1.8 version: 2.1.8 '@types/pg': - specifier: ^8.15.6 - version: 8.15.6 + specifier: ^8.16.0 + version: 8.16.0 autoprefixer: - specifier: ^10.4.21 - version: 10.4.21(postcss@8.5.6) + specifier: ^10.4.23 + version: 10.4.23(postcss@8.5.6) axios: - specifier: ^1.13.1 - version: 1.13.1 + specifier: ^1.13.2 + version: 1.13.2 dexie: specifier: ^4.2.1 version: 4.2.1 eslint: - specifier: ^9.39.0 - version: 9.39.0(jiti@1.21.7) + specifier: ^9.39.2 + version: 9.39.2(jiti@1.21.7) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.0(jiti@1.21.7)) + version: 10.1.8(eslint@9.39.2(jiti@1.21.7)) eslint-plugin-svelte: - specifier: ^3.13.0 - version: 3.13.0(eslint@9.39.0(jiti@1.21.7))(svelte@5.43.2) + specifier: ^3.13.1 + version: 3.13.1(eslint@9.39.2(jiti@1.21.7))(svelte@5.46.1) eslint-plugin-tailwindcss: specifier: ^3.18.2 - version: 3.18.2(tailwindcss@3.4.18(yaml@2.8.0)) + version: 3.18.2(tailwindcss@3.4.19(yaml@2.8.0)) exifreader: - specifier: ^4.32.0 - version: 4.32.0 + specifier: ^4.33.1 + version: 4.33.1 file-saver: specifier: ^2.0.5 version: 2.0.5 @@ -104,10 +104,10 @@ importers: version: 0.0.4 kysely-ctl: specifier: ^0.19.0 - version: 0.19.0(kysely@0.28.8)(typescript@5.9.3) + version: 0.19.0(kysely@0.28.9)(typescript@5.9.3) lru-cache: - specifier: ^11.2.2 - version: 11.2.2 + specifier: ^11.2.4 + version: 11.2.4 mime: specifier: ^4.1.0 version: 4.1.0 @@ -115,35 +115,35 @@ importers: specifier: ^7.2.0 version: 7.2.0 prettier: - specifier: ^3.6.2 - version: 3.6.2 + specifier: ^3.7.4 + version: 3.7.4 prettier-plugin-svelte: - specifier: ^3.4.0 - version: 3.4.0(prettier@3.6.2)(svelte@5.43.2) + specifier: ^3.4.1 + version: 3.4.1(prettier@3.7.4)(svelte@5.46.1) prettier-plugin-tailwindcss: - specifier: ^0.7.1 - version: 0.7.1(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.43.2))(prettier@3.6.2) + specifier: ^0.7.2 + version: 0.7.2(prettier-plugin-svelte@3.4.1(prettier@3.7.4)(svelte@5.46.1))(prettier@3.7.4) svelte: - specifier: ^5.43.2 - version: 5.43.2 + specifier: ^5.46.1 + version: 5.46.1 svelte-check: - specifier: ^4.3.3 - version: 4.3.3(picomatch@4.0.3)(svelte@5.43.2)(typescript@5.9.3) + specifier: ^4.3.5 + version: 4.3.5(picomatch@4.0.3)(svelte@5.46.1)(typescript@5.9.3) tailwindcss: - specifier: ^3.4.18 - version: 3.4.18(yaml@2.8.0) + specifier: ^3.4.19 + version: 3.4.19(yaml@2.8.0) typescript: specifier: ^5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.46.2 - version: 8.46.2(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3) + specifier: ^8.50.1 + version: 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) unplugin-icons: specifier: ^22.5.0 - version: 22.5.0(svelte@5.43.2) + version: 22.5.0(svelte@5.46.1) vite: - specifier: ^7.1.12 - version: 7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0) + specifier: ^7.3.0 + version: 7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0) packages: @@ -154,164 +154,161 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@antfu/utils@9.3.0': - resolution: {integrity: sha512-9hFT4RauhcUzqOE4f1+frMKLZrgNog5b06I7VmZQV1BkvwvqrbC8EBZf3L1eEL2AKb6rNKjER0sEvJiSP1FXEA==} - '@epic-web/invariant@1.0.0': resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} - '@esbuild/aix-ppc64@0.25.11': - resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.11': - resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.11': - resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.11': - resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.11': - resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.11': - resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.11': - resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.11': - resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.11': - resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.11': - resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.11': - resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.11': - resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.11': - resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.11': - resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.11': - resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.11': - resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.11': - resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.11': - resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==} + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.11': - resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.11': - resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==} + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.11': - resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.11': - resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==} + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.11': - resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.11': - resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.11': - resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.11': - resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -347,12 +344,12 @@ packages: resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.39.0': - resolution: {integrity: sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==} + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.7': @@ -382,18 +379,14 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@iconify-json/material-symbols@1.2.44': - resolution: {integrity: sha512-NAJjhswaK9FxBeIzFFsNygws7wHtmAkBWhF4YEwn1NZIMbA+LNITqhUiq6sP5mOdKQqnoritFTlQaZ47a5BgBg==} + '@iconify-json/material-symbols@1.2.50': + resolution: {integrity: sha512-71tjHR70h46LHtBFab3fAd2V/wPTO7JMV5lKnRn3IcF303LaFgAlO0BZeTJDcmCv9d0snRZmnoLZAJVD7/eisw==} '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} - '@iconify/utils@3.0.2': - resolution: {integrity: sha512-EfJS0rLfVuRuJRn4psJHtK2A9TqVnkxPpHY6lYHiB9+8eSuudsxbwMiavocG45ujOo6FJ+CIRlRnlOGinzkaGQ==} - - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} + '@iconify/utils@3.1.0': + resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -427,10 +420,6 @@ packages: resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} engines: {node: '>=10'} - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -470,121 +459,121 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.52.5': - resolution: {integrity: sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==} + '@rollup/rollup-android-arm-eabi@4.54.0': + resolution: {integrity: sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.52.5': - resolution: {integrity: sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==} + '@rollup/rollup-android-arm64@4.54.0': + resolution: {integrity: sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.52.5': - resolution: {integrity: sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==} + '@rollup/rollup-darwin-arm64@4.54.0': + resolution: {integrity: sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.52.5': - resolution: {integrity: sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==} + '@rollup/rollup-darwin-x64@4.54.0': + resolution: {integrity: sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.52.5': - resolution: {integrity: sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==} + '@rollup/rollup-freebsd-arm64@4.54.0': + resolution: {integrity: sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.52.5': - resolution: {integrity: sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==} + '@rollup/rollup-freebsd-x64@4.54.0': + resolution: {integrity: sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.52.5': - resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==} + '@rollup/rollup-linux-arm-gnueabihf@4.54.0': + resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.52.5': - resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==} + '@rollup/rollup-linux-arm-musleabihf@4.54.0': + resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.52.5': - resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==} + '@rollup/rollup-linux-arm64-gnu@4.54.0': + resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.52.5': - resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==} + '@rollup/rollup-linux-arm64-musl@4.54.0': + resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loong64-gnu@4.52.5': - resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==} + '@rollup/rollup-linux-loong64-gnu@4.54.0': + resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.52.5': - resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==} + '@rollup/rollup-linux-ppc64-gnu@4.54.0': + resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.52.5': - resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==} + '@rollup/rollup-linux-riscv64-gnu@4.54.0': + resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.52.5': - resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==} + '@rollup/rollup-linux-riscv64-musl@4.54.0': + resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.52.5': - resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==} + '@rollup/rollup-linux-s390x-gnu@4.54.0': + resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.52.5': - resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==} + '@rollup/rollup-linux-x64-gnu@4.54.0': + resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.52.5': - resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==} + '@rollup/rollup-linux-x64-musl@4.54.0': + resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} cpu: [x64] os: [linux] - '@rollup/rollup-openharmony-arm64@4.52.5': - resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==} + '@rollup/rollup-openharmony-arm64@4.54.0': + resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.52.5': - resolution: {integrity: sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==} + '@rollup/rollup-win32-arm64-msvc@4.54.0': + resolution: {integrity: sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.52.5': - resolution: {integrity: sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==} + '@rollup/rollup-win32-ia32-msvc@4.54.0': + resolution: {integrity: sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.52.5': - resolution: {integrity: sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==} + '@rollup/rollup-win32-x64-gnu@4.54.0': + resolution: {integrity: sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.52.5': - resolution: {integrity: sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==} + '@rollup/rollup-win32-x64-msvc@4.54.0': + resolution: {integrity: sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==} cpu: [x64] os: [win32] - '@standard-schema/spec@1.0.0': - resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@sveltejs/acorn-typescript@1.0.6': - resolution: {integrity: sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==} + '@sveltejs/acorn-typescript@1.0.8': + resolution: {integrity: sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==} peerDependencies: acorn: ^8.9.0 @@ -593,8 +582,8 @@ packages: peerDependencies: '@sveltejs/kit': ^2.4.0 - '@sveltejs/kit@2.48.4': - resolution: {integrity: sha512-TGFX1pZUt9qqY20Cv5NyYvy0iLWHf2jXi8s+eCGsig7jQMdwZWKUFMR6TbvFNhfDSUpc1sH/Y5EHv20g3HHA3g==} + '@sveltejs/kit@2.49.2': + resolution: {integrity: sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -621,14 +610,14 @@ packages: svelte: ^5.0.0 vite: ^6.3.0 || ^7.0.0 - '@trpc/client@11.7.1': - resolution: {integrity: sha512-uOnAjElKI892/U6aQMcBHYs3x7mme3Cvv1F87ytBL56rBvs7+DyK7r43zgaXKf13+GtPEI6ex5xjVUfyDW8XcQ==} + '@trpc/client@11.8.1': + resolution: {integrity: sha512-L/SJFGanr9xGABmuDoeXR4xAdHJmsXsiF9OuH+apecJ+8sUITzVT1EPeqp0ebqA6lBhEl5pPfg3rngVhi/h60Q==} peerDependencies: - '@trpc/server': 11.7.1 + '@trpc/server': 11.8.1 typescript: '>=5.7.2' - '@trpc/server@11.7.1': - resolution: {integrity: sha512-N3U8LNLIP4g9C7LJ/sLkjuPHwqlvE3bnspzC4DEFVdvx2+usbn70P80E3wj5cjOTLhmhRiwJCSXhlB+MHfGeCw==} + '@trpc/server@11.8.1': + resolution: {integrity: sha512-P4rzZRpEL7zDFgjxK65IdyH0e41FMFfTkQkuq0BA5tKcr7E6v9/v38DEklCpoDN6sPiB1Sigy/PUEzHENhswDA==} peerDependencies: typescript: '>=5.7.2' @@ -650,72 +639,72 @@ packages: '@types/node-schedule@2.1.8': resolution: {integrity: sha512-k00g6Yj/oUg/CDC+MeLHUzu0+OFxWbIqrFfDiLi6OPKxTujvpv29mHGM8GtKr7B+9Vv92FcK/8mRqi1DK5f3hA==} - '@types/node@24.9.2': - resolution: {integrity: sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==} + '@types/node@25.0.3': + resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} - '@types/pg@8.15.6': - resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} + '@types/pg@8.16.0': + resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==} '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - '@typescript-eslint/eslint-plugin@8.46.2': - resolution: {integrity: sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==} + '@typescript-eslint/eslint-plugin@8.50.1': + resolution: {integrity: sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.46.2 + '@typescript-eslint/parser': ^8.50.1 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.46.2': - resolution: {integrity: sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==} + '@typescript-eslint/parser@8.50.1': + resolution: {integrity: sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.46.2': - resolution: {integrity: sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==} + '@typescript-eslint/project-service@8.50.1': + resolution: {integrity: sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.46.2': - resolution: {integrity: sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==} + '@typescript-eslint/scope-manager@8.50.1': + resolution: {integrity: sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.46.2': - resolution: {integrity: sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==} + '@typescript-eslint/tsconfig-utils@8.50.1': + resolution: {integrity: sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.46.2': - resolution: {integrity: sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==} + '@typescript-eslint/type-utils@8.50.1': + resolution: {integrity: sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.46.2': - resolution: {integrity: sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==} + '@typescript-eslint/types@8.50.1': + resolution: {integrity: sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.46.2': - resolution: {integrity: sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==} + '@typescript-eslint/typescript-estree@8.50.1': + resolution: {integrity: sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.46.2': - resolution: {integrity: sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==} + '@typescript-eslint/utils@8.50.1': + resolution: {integrity: sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.46.2': - resolution: {integrity: sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==} + '@typescript-eslint/visitor-keys@8.50.1': + resolution: {integrity: sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@xmldom/xmldom@0.9.8': @@ -735,22 +724,10 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} - engines: {node: '>=12'} - ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} - any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -775,15 +752,15 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - autoprefixer@10.4.21: - resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + autoprefixer@10.4.23: + resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: postcss: ^8.1.0 - axios@1.13.1: - resolution: {integrity: sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==} + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} @@ -792,8 +769,8 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.8.23: - resolution: {integrity: sha512-616V5YX4bepJFzNyOfce5Fa8fDJMfoxzOIzDCZwaGL8MKVpFrXqfNUoIpRn9YMI5pXf/VKgzjB4htFMsFKKdiQ==} + baseline-browser-mapping@2.9.11: + resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} hasBin: true binary-extensions@2.3.0: @@ -810,15 +787,15 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.27.0: - resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==} + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - c12@3.3.1: - resolution: {integrity: sha512-LcWQ01LT9tkoUINHgpIOv3mMs+Abv7oVCrtpMRi1PaapVEpWoMga5WuT7/DqFTu7URP9ftbOmimNw1KNIGh9DQ==} + c12@3.3.3: + resolution: {integrity: sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==} peerDependencies: - magicast: ^0.3.5 + magicast: '*' peerDependenciesMeta: magicast: optional: true @@ -835,8 +812,8 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001752: - resolution: {integrity: sha512-vKUk7beoukxE47P5gcVNKkDRzXdVofotshHwfR9vmpeFKxmI5PBpgOMC18LUJUA/DvJ70Y7RveasIBraqsyO/g==} + caniuse-lite@1.0.30001761: + resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -850,6 +827,10 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} @@ -940,8 +921,8 @@ packages: destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} - devalue@5.4.2: - resolution: {integrity: sha512-MwPZTKEPK2k8Qgfmqrd48ZKVvzSQjgW0lXLxiIBA8dQjtf/6mw6pggHNLcyDKyf+fI6eXxlQwPsfaCMTU5U+Bw==} + devalue@5.6.1: + resolution: {integrity: sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==} dexie@4.2.1: resolution: {integrity: sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg==} @@ -960,17 +941,8 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - - electron-to-chromium@1.5.244: - resolution: {integrity: sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + electron-to-chromium@1.5.267: + resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} @@ -988,8 +960,8 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - esbuild@0.25.11: - resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} engines: {node: '>=18'} hasBin: true @@ -1007,8 +979,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-svelte@3.13.0: - resolution: {integrity: sha512-2ohCCQJJTNbIpQCSDSTWj+FN0OVfPmSO03lmSNT7ytqMaWF6kpT86LdzDqtm4sh7TVPl/OEWJ/d7R87bXP2Vjg==} + eslint-plugin-svelte@3.13.1: + resolution: {integrity: sha512-Ng+kV/qGS8P/isbNYVE3sJORtubB+yLEcYICMkUWNaDTb0SwZni/JhAYXh/Dz/q2eThUwWY0VMPZ//KYD1n3eQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.1 || ^9.0.0 @@ -1035,8 +1007,8 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.39.0: - resolution: {integrity: sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==} + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -1056,8 +1028,8 @@ packages: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} - esrap@2.1.2: - resolution: {integrity: sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg==} + esrap@2.2.1: + resolution: {integrity: sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==} esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -1074,11 +1046,11 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - exifreader@4.32.0: - resolution: {integrity: sha512-sj1PzjpaPwSE/2MeUqoAYcfc2u7AZOGSby0FzmAkB4jjeCXgDryxzVgMwV+tJKGIkGdWkkWiUWoLSJoPHJ6V5Q==} + exifreader@4.33.1: + resolution: {integrity: sha512-KsVc4bRfZW255PSst5Opt5jUeLp+SD2+q6fmXQkMMkphpFCDBFjzNAvswgQa1YcMrXq+9Na6HJ6gS3wo2x7RRw==} - exsolve@1.0.7: - resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1093,8 +1065,8 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} @@ -1136,16 +1108,12 @@ packages: debug: optional: true - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - - form-data@4.0.4: - resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} - fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} @@ -1175,18 +1143,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} - hasBin: true - globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globals@15.15.0: - resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} - engines: {node: '>=18'} - globals@16.5.0: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} @@ -1195,9 +1155,6 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1245,10 +1202,6 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1273,9 +1226,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jiti@1.21.7: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true @@ -1284,8 +1234,8 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true json-buffer@3.0.1: @@ -1307,9 +1257,6 @@ packages: known-css-properties@0.37.0: resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} - kolorist@1.8.0: - resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} - kysely-ctl@0.19.0: resolution: {integrity: sha512-89hzOd1cy/H063jB2E9wYHq+uKYpaHv6Mb5RiNFpRZL6BYCah9ncsdl3x5b52eirxry4UyWSmGNN3sFv+gK+ig==} engines: {node: '>=20'} @@ -1327,8 +1274,8 @@ packages: kysely-prisma-postgres: optional: true - kysely@0.28.8: - resolution: {integrity: sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA==} + kysely@0.28.9: + resolution: {integrity: sha512-3BeXMoiOhpOwu62CiVpO6lxfq4eS6KMYfQdMsN/2kUCRNuF2YiEr7u0HLHaQU+O4Xu8YXE3bHVkwaQ85i72EuA==} engines: {node: '>=20.0.0'} levn@0.4.1: @@ -1363,11 +1310,8 @@ packages: long-timeout@0.1.1: resolution: {integrity: sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==} - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - - lru-cache@11.2.2: - resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} + lru-cache@11.2.4: + resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} engines: {node: 20 || >=22} luxon@3.7.2: @@ -1409,10 +1353,6 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -1460,10 +1400,6 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - nypm@0.6.2: resolution: {integrity: sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==} engines: {node: ^14.16.0 || >=16.10.0} @@ -1499,11 +1435,8 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - - package-manager-detector@1.5.0: - resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==} + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} @@ -1520,10 +1453,6 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1653,8 +1582,8 @@ packages: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} - postcss-selector-parser@7.1.0: - resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} engines: {node: '>=4'} postcss-value-parser@4.2.0: @@ -1668,8 +1597,8 @@ packages: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} - postgres-bytea@1.0.0: - resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} engines: {node: '>=0.10.0'} postgres-date@1.0.7: @@ -1684,14 +1613,14 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-plugin-svelte@3.4.0: - resolution: {integrity: sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==} + prettier-plugin-svelte@3.4.1: + resolution: {integrity: sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg==} peerDependencies: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - prettier-plugin-tailwindcss@0.7.1: - resolution: {integrity: sha512-Bzv1LZcuiR1Sk02iJTS1QzlFNp/o5l2p3xkopwOrbPmtMeh3fK9rVW5M3neBQzHq+kGKj/4LGQMTNcTH4NGPtQ==} + prettier-plugin-tailwindcss@0.7.2: + resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==} engines: {node: '>=20.19'} peerDependencies: '@ianvs/prettier-plugin-sort-imports': '*' @@ -1745,8 +1674,8 @@ packages: prettier-plugin-svelte: optional: true - prettier@3.6.2: - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + prettier@3.7.4: + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} engines: {node: '>=14'} hasBin: true @@ -1777,6 +1706,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1790,8 +1723,8 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.52.5: - resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==} + rollup@4.54.0: + resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -1818,10 +1751,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - sirv@3.0.2: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} @@ -1840,28 +1769,12 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-ansi@7.1.2: - resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} - engines: {node: '>=12'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - sucrase@3.35.0: - resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} hasBin: true @@ -1877,29 +1790,29 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svelte-check@4.3.3: - resolution: {integrity: sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg==} + svelte-check@4.3.5: + resolution: {integrity: sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==} engines: {node: '>= 18.0.0'} hasBin: true peerDependencies: svelte: ^4.0.0 || ^5.0.0-next.0 typescript: '>=5.0.0' - svelte-eslint-parser@1.4.0: - resolution: {integrity: sha512-fjPzOfipR5S7gQ/JvI9r2H8y9gMGXO3JtmrylHLLyahEMquXI0lrebcjT+9/hNgDej0H7abTyox5HpHmW1PSWA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.18.3} + svelte-eslint-parser@1.4.1: + resolution: {integrity: sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.24.0} peerDependencies: svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 peerDependenciesMeta: svelte: optional: true - svelte@5.43.2: - resolution: {integrity: sha512-ro1umEzX8rT5JpCmlf0PPv7ncD8MdVob9e18bhwqTKNoLjS8kDvhVpaoYVPc+qMwDAOfcwJtyY7ZFSDbOaNPgA==} + svelte@5.46.1: + resolution: {integrity: sha512-ynjfCHD3nP2el70kN5Pmg37sSi0EjOm9FgHYQdC4giWG/hzO3AatzXXJJgP305uIhGQxSufJLuYWtkY8uK/8RA==} engines: {node: '>=18'} - tailwindcss@3.4.18: - resolution: {integrity: sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==} + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} engines: {node: '>=14.0.0'} hasBin: true @@ -1910,8 +1823,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - tinyexec@1.0.1: - resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} @@ -1948,8 +1862,8 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typescript-eslint@8.46.2: - resolution: {integrity: sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==} + typescript-eslint@8.50.1: + resolution: {integrity: sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -1989,12 +1903,12 @@ packages: vue-template-es2015-compiler: optional: true - unplugin@2.3.10: - resolution: {integrity: sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==} + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} - update-browserslist-db@1.1.4: - resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -2009,8 +1923,8 @@ packages: resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} hasBin: true - vite@7.1.12: - resolution: {integrity: sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==} + vite@7.3.0: + resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -2069,14 +1983,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -2094,15 +2000,15 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yocto-queue@1.2.1: - resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} zimmerframe@1.1.4: resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.2.1: + resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==} snapshots: @@ -2110,103 +2016,101 @@ snapshots: '@antfu/install-pkg@1.1.0': dependencies: - package-manager-detector: 1.5.0 - tinyexec: 1.0.1 - - '@antfu/utils@9.3.0': {} + package-manager-detector: 1.6.0 + tinyexec: 1.0.2 '@epic-web/invariant@1.0.0': {} - '@esbuild/aix-ppc64@0.25.11': + '@esbuild/aix-ppc64@0.27.2': optional: true - '@esbuild/android-arm64@0.25.11': + '@esbuild/android-arm64@0.27.2': optional: true - '@esbuild/android-arm@0.25.11': + '@esbuild/android-arm@0.27.2': optional: true - '@esbuild/android-x64@0.25.11': + '@esbuild/android-x64@0.27.2': optional: true - '@esbuild/darwin-arm64@0.25.11': + '@esbuild/darwin-arm64@0.27.2': optional: true - '@esbuild/darwin-x64@0.25.11': + '@esbuild/darwin-x64@0.27.2': optional: true - '@esbuild/freebsd-arm64@0.25.11': + '@esbuild/freebsd-arm64@0.27.2': optional: true - '@esbuild/freebsd-x64@0.25.11': + '@esbuild/freebsd-x64@0.27.2': optional: true - '@esbuild/linux-arm64@0.25.11': + '@esbuild/linux-arm64@0.27.2': optional: true - '@esbuild/linux-arm@0.25.11': + '@esbuild/linux-arm@0.27.2': optional: true - '@esbuild/linux-ia32@0.25.11': + '@esbuild/linux-ia32@0.27.2': optional: true - '@esbuild/linux-loong64@0.25.11': + '@esbuild/linux-loong64@0.27.2': optional: true - '@esbuild/linux-mips64el@0.25.11': + '@esbuild/linux-mips64el@0.27.2': optional: true - '@esbuild/linux-ppc64@0.25.11': + '@esbuild/linux-ppc64@0.27.2': optional: true - '@esbuild/linux-riscv64@0.25.11': + '@esbuild/linux-riscv64@0.27.2': optional: true - '@esbuild/linux-s390x@0.25.11': + '@esbuild/linux-s390x@0.27.2': optional: true - '@esbuild/linux-x64@0.25.11': + '@esbuild/linux-x64@0.27.2': optional: true - '@esbuild/netbsd-arm64@0.25.11': + '@esbuild/netbsd-arm64@0.27.2': optional: true - '@esbuild/netbsd-x64@0.25.11': + '@esbuild/netbsd-x64@0.27.2': optional: true - '@esbuild/openbsd-arm64@0.25.11': + '@esbuild/openbsd-arm64@0.27.2': optional: true - '@esbuild/openbsd-x64@0.25.11': + '@esbuild/openbsd-x64@0.27.2': optional: true - '@esbuild/openharmony-arm64@0.25.11': + '@esbuild/openharmony-arm64@0.27.2': optional: true - '@esbuild/sunos-x64@0.25.11': + '@esbuild/sunos-x64@0.27.2': optional: true - '@esbuild/win32-arm64@0.25.11': + '@esbuild/win32-arm64@0.27.2': optional: true - '@esbuild/win32-ia32@0.25.11': + '@esbuild/win32-ia32@0.27.2': optional: true - '@esbuild/win32-x64@0.25.11': + '@esbuild/win32-x64@0.27.2': optional: true - '@eslint-community/eslint-utils@4.9.0(eslint@9.39.0(jiti@1.21.7))': + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2(jiti@1.21.7))': dependencies: - eslint: 9.39.0(jiti@1.21.7) + eslint: 9.39.2(jiti@1.21.7) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@1.4.1(eslint@9.39.0(jiti@1.21.7))': + '@eslint/compat@1.4.1(eslint@9.39.2(jiti@1.21.7))': dependencies: '@eslint/core': 0.17.0 optionalDependencies: - eslint: 9.39.0(jiti@1.21.7) + eslint: 9.39.2(jiti@1.21.7) '@eslint/config-array@0.21.1': dependencies: @@ -2224,7 +2128,7 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.1': + '@eslint/eslintrc@3.3.3': dependencies: ajv: 6.12.6 debug: 4.4.3 @@ -2232,13 +2136,13 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 minimatch: 3.1.2 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color - '@eslint/js@9.39.0': {} + '@eslint/js@9.39.2': {} '@eslint/object-schema@2.1.7': {} @@ -2260,33 +2164,17 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@iconify-json/material-symbols@1.2.44': + '@iconify-json/material-symbols@1.2.50': dependencies: '@iconify/types': 2.0.0 '@iconify/types@2.0.0': {} - '@iconify/utils@3.0.2': + '@iconify/utils@3.1.0': dependencies: '@antfu/install-pkg': 1.1.0 - '@antfu/utils': 9.3.0 '@iconify/types': 2.0.0 - debug: 4.4.3 - globals: 15.15.0 - kolorist: 1.8.0 - local-pkg: 1.1.2 mlly: 1.8.0 - transitivePeerDependencies: - - supports-color - - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.2 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -2317,18 +2205,15 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 + fastq: 1.20.1 '@phc/format@1.0.0': {} - '@pkgjs/parseargs@0.11.0': - optional: true - '@polka/url@1.0.0-next.29': {} - '@rollup/plugin-commonjs@28.0.9(rollup@4.52.5)': + '@rollup/plugin-commonjs@28.0.9(rollup@4.54.0)': dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.52.5) + '@rollup/pluginutils': 5.3.0(rollup@4.54.0) commondir: 1.0.1 estree-walker: 2.0.2 fdir: 6.5.0(picomatch@4.0.3) @@ -2336,121 +2221,121 @@ snapshots: magic-string: 0.30.21 picomatch: 4.0.3 optionalDependencies: - rollup: 4.52.5 + rollup: 4.54.0 - '@rollup/plugin-json@6.1.0(rollup@4.52.5)': + '@rollup/plugin-json@6.1.0(rollup@4.54.0)': dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.52.5) + '@rollup/pluginutils': 5.3.0(rollup@4.54.0) optionalDependencies: - rollup: 4.52.5 + rollup: 4.54.0 - '@rollup/plugin-node-resolve@16.0.3(rollup@4.52.5)': + '@rollup/plugin-node-resolve@16.0.3(rollup@4.54.0)': dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.52.5) + '@rollup/pluginutils': 5.3.0(rollup@4.54.0) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-module: 1.0.0 resolve: 1.22.11 optionalDependencies: - rollup: 4.52.5 + rollup: 4.54.0 - '@rollup/pluginutils@5.3.0(rollup@4.52.5)': + '@rollup/pluginutils@5.3.0(rollup@4.54.0)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.3 optionalDependencies: - rollup: 4.52.5 + rollup: 4.54.0 - '@rollup/rollup-android-arm-eabi@4.52.5': + '@rollup/rollup-android-arm-eabi@4.54.0': optional: true - '@rollup/rollup-android-arm64@4.52.5': + '@rollup/rollup-android-arm64@4.54.0': optional: true - '@rollup/rollup-darwin-arm64@4.52.5': + '@rollup/rollup-darwin-arm64@4.54.0': optional: true - '@rollup/rollup-darwin-x64@4.52.5': + '@rollup/rollup-darwin-x64@4.54.0': optional: true - '@rollup/rollup-freebsd-arm64@4.52.5': + '@rollup/rollup-freebsd-arm64@4.54.0': optional: true - '@rollup/rollup-freebsd-x64@4.52.5': + '@rollup/rollup-freebsd-x64@4.54.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.52.5': + '@rollup/rollup-linux-arm-gnueabihf@4.54.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.52.5': + '@rollup/rollup-linux-arm-musleabihf@4.54.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.52.5': + '@rollup/rollup-linux-arm64-gnu@4.54.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.52.5': + '@rollup/rollup-linux-arm64-musl@4.54.0': optional: true - '@rollup/rollup-linux-loong64-gnu@4.52.5': + '@rollup/rollup-linux-loong64-gnu@4.54.0': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.52.5': + '@rollup/rollup-linux-ppc64-gnu@4.54.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.52.5': + '@rollup/rollup-linux-riscv64-gnu@4.54.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.52.5': + '@rollup/rollup-linux-riscv64-musl@4.54.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.52.5': + '@rollup/rollup-linux-s390x-gnu@4.54.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.52.5': + '@rollup/rollup-linux-x64-gnu@4.54.0': optional: true - '@rollup/rollup-linux-x64-musl@4.52.5': + '@rollup/rollup-linux-x64-musl@4.54.0': optional: true - '@rollup/rollup-openharmony-arm64@4.52.5': + '@rollup/rollup-openharmony-arm64@4.54.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.52.5': + '@rollup/rollup-win32-arm64-msvc@4.54.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.52.5': + '@rollup/rollup-win32-ia32-msvc@4.54.0': optional: true - '@rollup/rollup-win32-x64-gnu@4.52.5': + '@rollup/rollup-win32-x64-gnu@4.54.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.52.5': + '@rollup/rollup-win32-x64-msvc@4.54.0': optional: true - '@standard-schema/spec@1.0.0': {} + '@standard-schema/spec@1.1.0': {} - '@sveltejs/acorn-typescript@1.0.6(acorn@8.15.0)': + '@sveltejs/acorn-typescript@1.0.8(acorn@8.15.0)': dependencies: acorn: 8.15.0 - '@sveltejs/adapter-node@5.4.0(@sveltejs/kit@2.48.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0)))': + '@sveltejs/adapter-node@5.4.0(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0)))': dependencies: - '@rollup/plugin-commonjs': 28.0.9(rollup@4.52.5) - '@rollup/plugin-json': 6.1.0(rollup@4.52.5) - '@rollup/plugin-node-resolve': 16.0.3(rollup@4.52.5) - '@sveltejs/kit': 2.48.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0)) - rollup: 4.52.5 + '@rollup/plugin-commonjs': 28.0.9(rollup@4.54.0) + '@rollup/plugin-json': 6.1.0(rollup@4.54.0) + '@rollup/plugin-node-resolve': 16.0.3(rollup@4.54.0) + '@sveltejs/kit': 2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0)) + rollup: 4.54.0 - '@sveltejs/kit@2.48.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0))': + '@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0))': dependencies: - '@standard-schema/spec': 1.0.0 - '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0)) + '@standard-schema/spec': 1.1.0 + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 - devalue: 5.4.2 + devalue: 5.6.1 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 @@ -2458,36 +2343,36 @@ snapshots: sade: 1.8.1 set-cookie-parser: 2.7.2 sirv: 3.0.2 - svelte: 5.43.2 - vite: 7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0) + svelte: 5.46.1 + vite: 7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0) - '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0))': + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0)) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0)) debug: 4.4.3 - svelte: 5.43.2 - vite: 7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0) + svelte: 5.46.1 + vite: 7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0))': + '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0)))(svelte@5.43.2)(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0)) + '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0)) debug: 4.4.3 deepmerge: 4.3.1 magic-string: 0.30.21 - svelte: 5.43.2 - vite: 7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0) - vitefu: 1.1.1(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0)) + svelte: 5.46.1 + vite: 7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0) + vitefu: 1.1.1(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0)) transitivePeerDependencies: - supports-color - '@trpc/client@11.7.1(@trpc/server@11.7.1(typescript@5.9.3))(typescript@5.9.3)': + '@trpc/client@11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3)': dependencies: - '@trpc/server': 11.7.1(typescript@5.9.3) + '@trpc/server': 11.8.1(typescript@5.9.3) typescript: 5.9.3 - '@trpc/server@11.7.1(typescript@5.9.3)': + '@trpc/server@11.8.1(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -2503,30 +2388,29 @@ snapshots: '@types/node-schedule@2.1.8': dependencies: - '@types/node': 24.9.2 + '@types/node': 25.0.3 - '@types/node@24.9.2': + '@types/node@25.0.3': dependencies: undici-types: 7.16.0 - '@types/pg@8.15.6': + '@types/pg@8.16.0': dependencies: - '@types/node': 24.9.2 + '@types/node': 25.0.3 pg-protocol: 1.10.3 pg-types: 2.2.0 '@types/resolve@1.20.2': {} - '@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.46.2(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/type-utils': 8.46.2(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.2 - eslint: 9.39.0(jiti@1.21.7) - graphemer: 1.4.0 + '@typescript-eslint/parser': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/type-utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.50.1 + eslint: 9.39.2(jiti@1.21.7) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.1.0(typescript@5.9.3) @@ -2534,80 +2418,79 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.2 + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.50.1 debug: 4.4.3 - eslint: 9.39.0(jiti@1.21.7) + eslint: 9.39.2(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.46.2(typescript@5.9.3)': + '@typescript-eslint/project-service@8.50.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3) - '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) + '@typescript-eslint/types': 8.50.1 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.46.2': + '@typescript-eslint/scope-manager@8.50.1': dependencies: - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/visitor-keys': 8.46.2 + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/visitor-keys': 8.50.1 - '@typescript-eslint/tsconfig-utils@8.46.2(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.50.1(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.46.2(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.0(jiti@1.21.7) + eslint: 9.39.2(jiti@1.21.7) ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.46.2': {} + '@typescript-eslint/types@8.50.1': {} - '@typescript-eslint/typescript-estree@8.46.2(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.50.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.46.2(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3) - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/visitor-keys': 8.46.2 + '@typescript-eslint/project-service': 8.50.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/visitor-keys': 8.50.1 debug: 4.4.3 - fast-glob: 3.3.3 - is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.7.3 + tinyglobby: 0.2.15 ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.46.2(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3)': + '@typescript-eslint/utils@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.0(jiti@1.21.7)) - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) - eslint: 9.39.0(jiti@1.21.7) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.46.2': + '@typescript-eslint/visitor-keys@8.50.1': dependencies: - '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/types': 8.50.1 eslint-visitor-keys: 4.2.1 '@xmldom/xmldom@0.9.8': @@ -2626,16 +2509,10 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ansi-regex@5.0.1: {} - - ansi-regex@6.2.2: {} - ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - ansi-styles@6.2.3: {} - any-promise@1.3.0: {} anymatch@3.1.3: @@ -2658,20 +2535,19 @@ snapshots: asynckit@0.4.0: {} - autoprefixer@10.4.21(postcss@8.5.6): + autoprefixer@10.4.23(postcss@8.5.6): dependencies: - browserslist: 4.27.0 - caniuse-lite: 1.0.30001752 - fraction.js: 4.3.7 - normalize-range: 0.1.2 + browserslist: 4.28.1 + caniuse-lite: 1.0.30001761 + fraction.js: 5.3.4 picocolors: 1.1.1 postcss: 8.5.6 postcss-value-parser: 4.2.0 - axios@1.13.1: + axios@1.13.2: dependencies: follow-redirects: 1.15.11 - form-data: 4.0.4 + form-data: 4.0.5 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug @@ -2680,7 +2556,7 @@ snapshots: balanced-match@1.0.2: {} - baseline-browser-mapping@2.8.23: {} + baseline-browser-mapping@2.9.11: {} binary-extensions@2.3.0: {} @@ -2697,21 +2573,21 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.27.0: + browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.8.23 - caniuse-lite: 1.0.30001752 - electron-to-chromium: 1.5.244 + baseline-browser-mapping: 2.9.11 + caniuse-lite: 1.0.30001761 + electron-to-chromium: 1.5.267 node-releases: 2.0.27 - update-browserslist-db: 1.1.4(browserslist@4.27.0) + update-browserslist-db: 1.2.3(browserslist@4.28.1) - c12@3.3.1: + c12@3.3.3: dependencies: - chokidar: 4.0.3 + chokidar: 5.0.0 confbox: 0.2.2 defu: 6.1.4 dotenv: 17.2.3 - exsolve: 1.0.7 + exsolve: 1.0.8 giget: 2.0.0 jiti: 2.6.1 ohash: 2.0.11 @@ -2729,7 +2605,7 @@ snapshots: camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001752: {} + caniuse-lite@1.0.30001761: {} chalk@4.1.2: dependencies: @@ -2752,6 +2628,10 @@ snapshots: dependencies: readdirp: 4.1.2 + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + citty@0.1.6: dependencies: consola: 3.4.2 @@ -2817,7 +2697,7 @@ snapshots: destr@2.0.5: {} - devalue@5.4.2: {} + devalue@5.6.1: {} dexie@4.2.1: {} @@ -2833,13 +2713,7 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - eastasianwidth@0.2.0: {} - - electron-to-chromium@1.5.244: {} - - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} + electron-to-chromium@1.5.267: {} es-define-property@1.0.1: {} @@ -2856,48 +2730,48 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 - esbuild@0.25.11: + esbuild@0.27.2: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.11 - '@esbuild/android-arm': 0.25.11 - '@esbuild/android-arm64': 0.25.11 - '@esbuild/android-x64': 0.25.11 - '@esbuild/darwin-arm64': 0.25.11 - '@esbuild/darwin-x64': 0.25.11 - '@esbuild/freebsd-arm64': 0.25.11 - '@esbuild/freebsd-x64': 0.25.11 - '@esbuild/linux-arm': 0.25.11 - '@esbuild/linux-arm64': 0.25.11 - '@esbuild/linux-ia32': 0.25.11 - '@esbuild/linux-loong64': 0.25.11 - '@esbuild/linux-mips64el': 0.25.11 - '@esbuild/linux-ppc64': 0.25.11 - '@esbuild/linux-riscv64': 0.25.11 - '@esbuild/linux-s390x': 0.25.11 - '@esbuild/linux-x64': 0.25.11 - '@esbuild/netbsd-arm64': 0.25.11 - '@esbuild/netbsd-x64': 0.25.11 - '@esbuild/openbsd-arm64': 0.25.11 - '@esbuild/openbsd-x64': 0.25.11 - '@esbuild/openharmony-arm64': 0.25.11 - '@esbuild/sunos-x64': 0.25.11 - '@esbuild/win32-arm64': 0.25.11 - '@esbuild/win32-ia32': 0.25.11 - '@esbuild/win32-x64': 0.25.11 + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 escalade@3.2.0: {} escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@9.39.0(jiti@1.21.7)): + eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@1.21.7)): dependencies: - eslint: 9.39.0(jiti@1.21.7) + eslint: 9.39.2(jiti@1.21.7) - eslint-plugin-svelte@3.13.0(eslint@9.39.0(jiti@1.21.7))(svelte@5.43.2): + eslint-plugin-svelte@3.13.1(eslint@9.39.2(jiti@1.21.7))(svelte@5.46.1): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) '@jridgewell/sourcemap-codec': 1.5.5 - eslint: 9.39.0(jiti@1.21.7) + eslint: 9.39.2(jiti@1.21.7) esutils: 2.0.3 globals: 16.5.0 known-css-properties: 0.37.0 @@ -2905,17 +2779,17 @@ snapshots: postcss-load-config: 3.1.4(postcss@8.5.6) postcss-safe-parser: 7.0.1(postcss@8.5.6) semver: 7.7.3 - svelte-eslint-parser: 1.4.0(svelte@5.43.2) + svelte-eslint-parser: 1.4.1(svelte@5.46.1) optionalDependencies: - svelte: 5.43.2 + svelte: 5.46.1 transitivePeerDependencies: - ts-node - eslint-plugin-tailwindcss@3.18.2(tailwindcss@3.4.18(yaml@2.8.0)): + eslint-plugin-tailwindcss@3.18.2(tailwindcss@3.4.19(yaml@2.8.0)): dependencies: fast-glob: 3.3.3 postcss: 8.5.6 - tailwindcss: 3.4.18(yaml@2.8.0) + tailwindcss: 3.4.19(yaml@2.8.0) eslint-scope@8.4.0: dependencies: @@ -2926,15 +2800,15 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.39.0(jiti@1.21.7): + eslint@9.39.2(jiti@1.21.7): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.1 '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.39.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 @@ -2979,7 +2853,7 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.1.2: + esrap@2.2.1: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2993,11 +2867,11 @@ snapshots: esutils@2.0.3: {} - exifreader@4.32.0: + exifreader@4.33.1: optionalDependencies: '@xmldom/xmldom': 0.9.8 - exsolve@1.0.7: {} + exsolve@1.0.8: {} fast-deep-equal@3.1.3: {} @@ -3013,7 +2887,7 @@ snapshots: fast-levenshtein@2.0.6: {} - fastq@1.19.1: + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -3045,12 +2919,7 @@ snapshots: follow-redirects@1.15.11: {} - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - - form-data@4.0.4: + form-data@4.0.5: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 @@ -3058,7 +2927,7 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 - fraction.js@4.3.7: {} + fraction.js@5.3.4: {} fsevents@2.3.3: optional: true @@ -3100,25 +2969,12 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@10.4.5: - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - globals@14.0.0: {} - globals@15.15.0: {} - globals@16.5.0: {} gopd@1.2.0: {} - graphemer@1.4.0: {} - has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -3154,8 +3010,6 @@ snapshots: is-extglob@2.1.1: {} - is-fullwidth-code-point@3.0.0: {} - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -3176,17 +3030,11 @@ snapshots: isexe@2.0.0: {} - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - jiti@1.21.7: {} jiti@2.6.1: {} - js-yaml@4.1.0: + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -3204,16 +3052,14 @@ snapshots: known-css-properties@0.37.0: {} - kolorist@1.8.0: {} - - kysely-ctl@0.19.0(kysely@0.28.8)(typescript@5.9.3): + kysely-ctl@0.19.0(kysely@0.28.9)(typescript@5.9.3): dependencies: - c12: 3.3.1 + c12: 3.3.3 citty: 0.1.6 confbox: 0.2.2 consola: 3.4.2 jiti: 2.6.1 - kysely: 0.28.8 + kysely: 0.28.9 nypm: 0.6.2 ofetch: 1.5.1 pathe: 2.0.3 @@ -3224,7 +3070,7 @@ snapshots: - magicast - typescript - kysely@0.28.8: {} + kysely@0.28.9: {} levn@0.4.1: dependencies: @@ -3253,9 +3099,7 @@ snapshots: long-timeout@0.1.1: {} - lru-cache@10.4.3: {} - - lru-cache@11.2.2: {} + lru-cache@11.2.4: {} luxon@3.7.2: {} @@ -3288,8 +3132,6 @@ snapshots: dependencies: brace-expansion: 2.0.2 - minipass@7.1.2: {} - mlly@1.8.0: dependencies: acorn: 8.15.0 @@ -3329,15 +3171,13 @@ snapshots: normalize-path@3.0.0: {} - normalize-range@0.1.2: {} - nypm@0.6.2: dependencies: citty: 0.1.6 consola: 3.4.2 pathe: 2.0.3 pkg-types: 2.3.0 - tinyexec: 1.0.1 + tinyexec: 1.0.2 object-assign@4.1.1: {} @@ -3366,15 +3206,13 @@ snapshots: p-limit@7.2.0: dependencies: - yocto-queue: 1.2.1 + yocto-queue: 1.2.2 p-locate@5.0.0: dependencies: p-limit: 3.1.0 - package-json-from-dist@1.0.1: {} - - package-manager-detector@1.5.0: {} + package-manager-detector@1.6.0: {} parent-module@1.0.1: dependencies: @@ -3386,11 +3224,6 @@ snapshots: path-parse@1.0.7: {} - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 - pathe@2.0.3: {} perfect-debounce@2.0.0: {} @@ -3412,7 +3245,7 @@ snapshots: dependencies: pg-int8: 1.0.1 postgres-array: 2.0.0 - postgres-bytea: 1.0.0 + postgres-bytea: 1.0.1 postgres-date: 1.0.7 postgres-interval: 1.2.0 @@ -3449,7 +3282,7 @@ snapshots: pkg-types@2.3.0: dependencies: confbox: 0.2.2 - exsolve: 1.0.7 + exsolve: 1.0.8 pathe: 2.0.3 postcss-import@15.1.0(postcss@8.5.6): @@ -3497,7 +3330,7 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-selector-parser@7.1.0: + postcss-selector-parser@7.1.1: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 @@ -3512,7 +3345,7 @@ snapshots: postgres-array@2.0.0: {} - postgres-bytea@1.0.0: {} + postgres-bytea@1.0.1: {} postgres-date@1.0.7: {} @@ -3522,18 +3355,18 @@ snapshots: prelude-ls@1.2.1: {} - prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.43.2): + prettier-plugin-svelte@3.4.1(prettier@3.7.4)(svelte@5.46.1): dependencies: - prettier: 3.6.2 - svelte: 5.43.2 + prettier: 3.7.4 + svelte: 5.46.1 - prettier-plugin-tailwindcss@0.7.1(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.43.2))(prettier@3.6.2): + prettier-plugin-tailwindcss@0.7.2(prettier-plugin-svelte@3.4.1(prettier@3.7.4)(svelte@5.46.1))(prettier@3.7.4): dependencies: - prettier: 3.6.2 + prettier: 3.7.4 optionalDependencies: - prettier-plugin-svelte: 3.4.0(prettier@3.6.2)(svelte@5.43.2) + prettier-plugin-svelte: 3.4.1(prettier@3.7.4)(svelte@5.46.1) - prettier@3.6.2: {} + prettier@3.7.4: {} proxy-from-env@1.1.0: {} @@ -3558,6 +3391,8 @@ snapshots: readdirp@4.1.2: {} + readdirp@5.0.0: {} + resolve-from@4.0.0: {} resolve@1.22.11: @@ -3568,32 +3403,32 @@ snapshots: reusify@1.1.0: {} - rollup@4.52.5: + rollup@4.54.0: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.52.5 - '@rollup/rollup-android-arm64': 4.52.5 - '@rollup/rollup-darwin-arm64': 4.52.5 - '@rollup/rollup-darwin-x64': 4.52.5 - '@rollup/rollup-freebsd-arm64': 4.52.5 - '@rollup/rollup-freebsd-x64': 4.52.5 - '@rollup/rollup-linux-arm-gnueabihf': 4.52.5 - '@rollup/rollup-linux-arm-musleabihf': 4.52.5 - '@rollup/rollup-linux-arm64-gnu': 4.52.5 - '@rollup/rollup-linux-arm64-musl': 4.52.5 - '@rollup/rollup-linux-loong64-gnu': 4.52.5 - '@rollup/rollup-linux-ppc64-gnu': 4.52.5 - '@rollup/rollup-linux-riscv64-gnu': 4.52.5 - '@rollup/rollup-linux-riscv64-musl': 4.52.5 - '@rollup/rollup-linux-s390x-gnu': 4.52.5 - '@rollup/rollup-linux-x64-gnu': 4.52.5 - '@rollup/rollup-linux-x64-musl': 4.52.5 - '@rollup/rollup-openharmony-arm64': 4.52.5 - '@rollup/rollup-win32-arm64-msvc': 4.52.5 - '@rollup/rollup-win32-ia32-msvc': 4.52.5 - '@rollup/rollup-win32-x64-gnu': 4.52.5 - '@rollup/rollup-win32-x64-msvc': 4.52.5 + '@rollup/rollup-android-arm-eabi': 4.54.0 + '@rollup/rollup-android-arm64': 4.54.0 + '@rollup/rollup-darwin-arm64': 4.54.0 + '@rollup/rollup-darwin-x64': 4.54.0 + '@rollup/rollup-freebsd-arm64': 4.54.0 + '@rollup/rollup-freebsd-x64': 4.54.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.54.0 + '@rollup/rollup-linux-arm-musleabihf': 4.54.0 + '@rollup/rollup-linux-arm64-gnu': 4.54.0 + '@rollup/rollup-linux-arm64-musl': 4.54.0 + '@rollup/rollup-linux-loong64-gnu': 4.54.0 + '@rollup/rollup-linux-ppc64-gnu': 4.54.0 + '@rollup/rollup-linux-riscv64-gnu': 4.54.0 + '@rollup/rollup-linux-riscv64-musl': 4.54.0 + '@rollup/rollup-linux-s390x-gnu': 4.54.0 + '@rollup/rollup-linux-x64-gnu': 4.54.0 + '@rollup/rollup-linux-x64-musl': 4.54.0 + '@rollup/rollup-openharmony-arm64': 4.54.0 + '@rollup/rollup-win32-arm64-msvc': 4.54.0 + '@rollup/rollup-win32-ia32-msvc': 4.54.0 + '@rollup/rollup-win32-x64-gnu': 4.54.0 + '@rollup/rollup-win32-x64-msvc': 4.54.0 fsevents: 2.3.3 run-parallel@1.2.0: @@ -3614,8 +3449,6 @@ snapshots: shebang-regex@3.0.0: {} - signal-exit@4.1.0: {} - sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 @@ -3630,36 +3463,16 @@ snapshots: std-env@3.10.0: {} - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.2 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-ansi@7.1.2: - dependencies: - ansi-regex: 6.2.2 - strip-json-comments@3.1.1: {} - sucrase@3.35.0: + sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 commander: 4.1.1 - glob: 10.4.5 lines-and-columns: 1.2.4 mz: 2.7.0 pirates: 4.0.7 + tinyglobby: 0.2.15 ts-interface-checker: 0.1.13 superjson@2.2.6: @@ -3672,47 +3485,48 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@4.3.3(picomatch@4.0.3)(svelte@5.43.2)(typescript@5.9.3): + svelte-check@4.3.5(picomatch@4.0.3)(svelte@5.46.1)(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 chokidar: 4.0.3 fdir: 6.5.0(picomatch@4.0.3) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.43.2 + svelte: 5.46.1 typescript: 5.9.3 transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.4.0(svelte@5.43.2): + svelte-eslint-parser@1.4.1(svelte@5.46.1): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 espree: 10.4.0 postcss: 8.5.6 postcss-scss: 4.0.9(postcss@8.5.6) - postcss-selector-parser: 7.1.0 + postcss-selector-parser: 7.1.1 optionalDependencies: - svelte: 5.43.2 + svelte: 5.46.1 - svelte@5.43.2: + svelte@5.46.1: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) '@types/estree': 1.0.8 acorn: 8.15.0 aria-query: 5.3.2 axobject-query: 4.1.0 clsx: 2.1.1 + devalue: 5.6.1 esm-env: 1.2.2 - esrap: 2.1.2 + esrap: 2.2.1 is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 zimmerframe: 1.1.4 - tailwindcss@3.4.18(yaml@2.8.0): + tailwindcss@3.4.19(yaml@2.8.0): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -3735,7 +3549,7 @@ snapshots: postcss-nested: 6.2.0(postcss@8.5.6) postcss-selector-parser: 6.1.2 resolve: 1.22.11 - sucrase: 3.35.0 + sucrase: 3.35.1 transitivePeerDependencies: - tsx - yaml @@ -3748,7 +3562,7 @@ snapshots: dependencies: any-promise: 1.3.0 - tinyexec@1.0.1: {} + tinyexec@1.0.2: {} tinyglobby@0.2.15: dependencies: @@ -3775,13 +3589,13 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.46.2(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3): + typescript-eslint@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/parser': 8.46.2(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.2(eslint@9.39.0(jiti@1.21.7))(typescript@5.9.3) - eslint: 9.39.0(jiti@1.21.7) + '@typescript-eslint/eslint-plugin': 8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.1(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.2(jiti@1.21.7) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -3792,28 +3606,28 @@ snapshots: undici-types@7.16.0: {} - unplugin-icons@22.5.0(svelte@5.43.2): + unplugin-icons@22.5.0(svelte@5.46.1): dependencies: '@antfu/install-pkg': 1.1.0 - '@iconify/utils': 3.0.2 + '@iconify/utils': 3.1.0 debug: 4.4.3 local-pkg: 1.1.2 - unplugin: 2.3.10 + unplugin: 2.3.11 optionalDependencies: - svelte: 5.43.2 + svelte: 5.46.1 transitivePeerDependencies: - supports-color - unplugin@2.3.10: + unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 acorn: 8.15.0 picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 - update-browserslist-db@1.1.4(browserslist@4.27.0): + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: - browserslist: 4.27.0 + browserslist: 4.28.1 escalade: 3.2.0 picocolors: 1.1.1 @@ -3825,23 +3639,23 @@ snapshots: uuid@13.0.0: {} - vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0): + vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0): dependencies: - esbuild: 0.25.11 + esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.52.5 + rollup: 4.54.0 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 24.9.2 + '@types/node': 25.0.3 fsevents: 2.3.3 jiti: 1.21.7 yaml: 2.8.0 - vitefu@1.1.1(vite@7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0)): + vitefu@1.1.1(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0)): optionalDependencies: - vite: 7.1.12(@types/node@24.9.2)(jiti@1.21.7)(yaml@2.8.0) + vite: 7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0) webpack-virtual-modules@0.6.2: {} @@ -3851,18 +3665,6 @@ snapshots: word-wrap@1.2.5: {} - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 5.1.2 - strip-ansi: 7.1.2 - xtend@4.0.2: {} yaml@1.10.2: {} @@ -3872,8 +3674,8 @@ snapshots: yocto-queue@0.1.0: {} - yocto-queue@1.2.1: {} + yocto-queue@1.2.2: {} zimmerframe@1.1.4: {} - zod@3.25.76: {} + zod@4.2.1: {} diff --git a/src/lib/modules/key.ts b/src/lib/modules/key.ts index 945902c..d5276a5 100644 --- a/src/lib/modules/key.ts +++ b/src/lib/modules/key.ts @@ -5,14 +5,14 @@ import type { ClientKeys } from "$lib/stores"; const serializedClientKeysSchema = z.intersection( z.object({ generator: z.literal("ArkVault"), - exportedAt: z.string().datetime(), + exportedAt: z.iso.datetime(), }), z.object({ version: z.literal(1), - encryptKey: z.string().base64().nonempty(), - decryptKey: z.string().base64().nonempty(), - signKey: z.string().base64().nonempty(), - verifyKey: z.string().base64().nonempty(), + encryptKey: z.base64().nonempty(), + decryptKey: z.base64().nonempty(), + signKey: z.base64().nonempty(), + verifyKey: z.base64().nonempty(), }), ); diff --git a/src/lib/server/schemas/auth.ts b/src/lib/server/schemas/auth.ts index b413f9d..d4972d1 100644 --- a/src/lib/server/schemas/auth.ts +++ b/src/lib/server/schemas/auth.ts @@ -7,26 +7,26 @@ export const passwordChangeRequest = z.object({ export type PasswordChangeRequest = z.input; export const loginRequest = z.object({ - email: z.string().email(), + email: z.email(), password: z.string().trim().nonempty(), }); export type LoginRequest = z.input; export const sessionUpgradeRequest = z.object({ - encPubKey: z.string().base64().nonempty(), - sigPubKey: z.string().base64().nonempty(), + encPubKey: z.base64().nonempty(), + sigPubKey: z.base64().nonempty(), }); export type SessionUpgradeRequest = z.input; export const sessionUpgradeResponse = z.object({ - id: z.number().int().positive(), - challenge: z.string().base64().nonempty(), + id: z.int().positive(), + challenge: z.base64().nonempty(), }); export type SessionUpgradeResponse = z.output; export const sessionUpgradeVerifyRequest = z.object({ - id: z.number().int().positive(), - answerSig: z.string().base64().nonempty(), + id: z.int().positive(), + answerSig: z.base64().nonempty(), force: z.boolean().default(false), }); export type SessionUpgradeVerifyRequest = z.input; diff --git a/src/lib/server/schemas/category.ts b/src/lib/server/schemas/category.ts index 408af7b..0bb07a7 100644 --- a/src/lib/server/schemas/category.ts +++ b/src/lib/server/schemas/category.ts @@ -1,3 +1,3 @@ import { z } from "zod"; -export const categoryIdSchema = z.union([z.literal("root"), z.number().int().positive()]); +export const categoryIdSchema = z.union([z.literal("root"), z.int().positive()]); diff --git a/src/lib/server/schemas/directory.ts b/src/lib/server/schemas/directory.ts index 107c3ee..dba44b9 100644 --- a/src/lib/server/schemas/directory.ts +++ b/src/lib/server/schemas/directory.ts @@ -1,3 +1,3 @@ import { z } from "zod"; -export const directoryIdSchema = z.union([z.literal("root"), z.number().int().positive()]); +export const directoryIdSchema = z.union([z.literal("root"), z.int().positive()]); diff --git a/src/lib/server/schemas/file.ts b/src/lib/server/schemas/file.ts index 5177bbd..811e590 100644 --- a/src/lib/server/schemas/file.ts +++ b/src/lib/server/schemas/file.ts @@ -3,34 +3,34 @@ import { z } from "zod"; import { directoryIdSchema } from "./directory"; export const fileThumbnailUploadRequest = z.object({ - dekVersion: z.string().datetime(), - contentIv: z.string().base64().nonempty(), + dekVersion: z.iso.datetime(), + contentIv: z.base64().nonempty(), }); export type FileThumbnailUploadRequest = z.input; export const fileUploadRequest = z.object({ parent: directoryIdSchema, - mekVersion: z.number().int().positive(), - dek: z.string().base64().nonempty(), - dekVersion: z.string().datetime(), - hskVersion: z.number().int().positive(), - contentHmac: z.string().base64().nonempty(), + mekVersion: z.int().positive(), + dek: z.base64().nonempty(), + dekVersion: z.iso.datetime(), + hskVersion: z.int().positive(), + contentHmac: z.base64().nonempty(), contentType: z .string() .trim() .nonempty() .refine((value) => mime.getExtension(value) !== null), // MIME type - contentIv: z.string().base64().nonempty(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), - createdAt: z.string().base64().nonempty().optional(), - createdAtIv: z.string().base64().nonempty().optional(), - lastModifiedAt: z.string().base64().nonempty(), - lastModifiedAtIv: z.string().base64().nonempty(), + contentIv: z.base64().nonempty(), + name: z.base64().nonempty(), + nameIv: z.base64().nonempty(), + createdAt: z.base64().nonempty().optional(), + createdAtIv: z.base64().nonempty().optional(), + lastModifiedAt: z.base64().nonempty(), + lastModifiedAtIv: z.base64().nonempty(), }); export type FileUploadRequest = z.input; export const fileUploadResponse = z.object({ - file: z.number().int().positive(), + file: z.int().positive(), }); export type FileUploadResponse = z.output; diff --git a/src/trpc/routers/category.ts b/src/trpc/routers/category.ts index f002421..2be80c8 100644 --- a/src/trpc/routers/category.ts +++ b/src/trpc/routers/category.ts @@ -38,11 +38,11 @@ const categoryRouter = router({ .input( z.object({ parent: categoryIdSchema, - mekVersion: z.number().int().positive(), - dek: z.string().base64().nonempty(), + mekVersion: z.int().positive(), + dek: z.base64().nonempty(), dekVersion: z.date(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), + name: z.base64().nonempty(), + nameIv: z.base64().nonempty(), }), ) .mutation(async ({ ctx, input }) => { @@ -72,10 +72,10 @@ const categoryRouter = router({ rename: roleProcedure["activeClient"] .input( z.object({ - id: z.number().int().positive(), + id: z.int().positive(), dekVersion: z.date(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), + name: z.base64().nonempty(), + nameIv: z.base64().nonempty(), }), ) .mutation(async ({ ctx, input }) => { @@ -99,7 +99,7 @@ const categoryRouter = router({ delete: roleProcedure["activeClient"] .input( z.object({ - id: z.number().int().positive(), + id: z.int().positive(), }), ) .mutation(async ({ ctx, input }) => { @@ -116,7 +116,7 @@ const categoryRouter = router({ files: roleProcedure["activeClient"] .input( z.object({ - id: z.number().int().positive(), + id: z.int().positive(), recurse: z.boolean().default(false), }), ) @@ -137,8 +137,8 @@ const categoryRouter = router({ addFile: roleProcedure["activeClient"] .input( z.object({ - id: z.number().int().positive(), - file: z.number().int().positive(), + id: z.int().positive(), + file: z.int().positive(), }), ) .mutation(async ({ ctx, input }) => { @@ -165,8 +165,8 @@ const categoryRouter = router({ removeFile: roleProcedure["activeClient"] .input( z.object({ - id: z.number().int().positive(), - file: z.number().int().positive(), + id: z.int().positive(), + file: z.int().positive(), }), ) .mutation(async ({ ctx, input }) => { diff --git a/src/trpc/routers/client.ts b/src/trpc/routers/client.ts index 8add385..37c5bc6 100644 --- a/src/trpc/routers/client.ts +++ b/src/trpc/routers/client.ts @@ -26,8 +26,8 @@ const clientRouter = router({ register: roleProcedure["notClient"] .input( z.object({ - encPubKey: z.string().base64().nonempty(), - sigPubKey: z.string().base64().nonempty(), + encPubKey: z.base64().nonempty(), + sigPubKey: z.base64().nonempty(), }), ) .mutation(async ({ ctx, input }) => { @@ -66,8 +66,8 @@ const clientRouter = router({ verify: roleProcedure["notClient"] .input( z.object({ - id: z.number().int().positive(), - answerSig: z.string().base64().nonempty(), + id: z.int().positive(), + answerSig: z.base64().nonempty(), }), ) .mutation(async ({ ctx, input }) => { diff --git a/src/trpc/routers/directory.ts b/src/trpc/routers/directory.ts index 70e3663..bcd31c7 100644 --- a/src/trpc/routers/directory.ts +++ b/src/trpc/routers/directory.ts @@ -41,11 +41,11 @@ const directoryRouter = router({ .input( z.object({ parent: directoryIdSchema, - mekVersion: z.number().int().positive(), - dek: z.string().base64().nonempty(), + mekVersion: z.int().positive(), + dek: z.base64().nonempty(), dekVersion: z.date(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), + name: z.base64().nonempty(), + nameIv: z.base64().nonempty(), }), ) .mutation(async ({ ctx, input }) => { @@ -75,10 +75,10 @@ const directoryRouter = router({ rename: roleProcedure["activeClient"] .input( z.object({ - id: z.number().int().positive(), + id: z.int().positive(), dekVersion: z.date(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), + name: z.base64().nonempty(), + nameIv: z.base64().nonempty(), }), ) .mutation(async ({ ctx, input }) => { @@ -102,7 +102,7 @@ const directoryRouter = router({ delete: roleProcedure["activeClient"] .input( z.object({ - id: z.number().int().positive(), + id: z.int().positive(), }), ) .mutation(async ({ ctx, input }) => { diff --git a/src/trpc/routers/file.ts b/src/trpc/routers/file.ts index b37032b..7c4e425 100644 --- a/src/trpc/routers/file.ts +++ b/src/trpc/routers/file.ts @@ -8,7 +8,7 @@ const fileRouter = router({ get: roleProcedure["activeClient"] .input( z.object({ - id: z.number().int().positive(), + id: z.int().positive(), }), ) .query(async ({ ctx, input }) => { @@ -42,8 +42,8 @@ const fileRouter = router({ listByHash: roleProcedure["activeClient"] .input( z.object({ - hskVersion: z.number().int().positive(), - contentHmac: z.string().base64().nonempty(), + hskVersion: z.int().positive(), + contentHmac: z.base64().nonempty(), }), ) .query(async ({ ctx, input }) => { @@ -61,10 +61,10 @@ const fileRouter = router({ rename: roleProcedure["activeClient"] .input( z.object({ - id: z.number().int().positive(), + id: z.int().positive(), dekVersion: z.date(), - name: z.string().base64().nonempty(), - nameIv: z.string().base64().nonempty(), + name: z.base64().nonempty(), + nameIv: z.base64().nonempty(), }), ) .mutation(async ({ ctx, input }) => { @@ -88,7 +88,7 @@ const fileRouter = router({ delete: roleProcedure["activeClient"] .input( z.object({ - id: z.number().int().positive(), + id: z.int().positive(), }), ) .mutation(async ({ ctx, input }) => { @@ -107,7 +107,7 @@ const fileRouter = router({ thumbnail: roleProcedure["activeClient"] .input( z.object({ - id: z.number().int().positive(), + id: z.int().positive(), }), ) .query(async ({ ctx, input }) => { diff --git a/src/trpc/routers/hsk.ts b/src/trpc/routers/hsk.ts index eed9d25..1e39af0 100644 --- a/src/trpc/routers/hsk.ts +++ b/src/trpc/routers/hsk.ts @@ -17,8 +17,8 @@ const hskRouter = router({ registerInitial: roleProcedure["activeClient"] .input( z.object({ - mekVersion: z.number().int().positive(), - hsk: z.string().base64().nonempty(), + mekVersion: z.int().positive(), + hsk: z.base64().nonempty(), }), ) .mutation(async ({ ctx, input }) => { diff --git a/src/trpc/routers/mek.ts b/src/trpc/routers/mek.ts index fa264a5..002d200 100644 --- a/src/trpc/routers/mek.ts +++ b/src/trpc/routers/mek.ts @@ -37,8 +37,8 @@ const mekRouter = router({ registerInitial: roleProcedure["pendingClient"] .input( z.object({ - mek: z.string().base64().nonempty(), - mekSig: z.string().base64().nonempty(), + mek: z.base64().nonempty(), + mekSig: z.base64().nonempty(), }), ) .mutation(async ({ ctx, input }) => { From 3fc29cf8dbd5623a79f52f00a5b40a130486beb3 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 25 Dec 2025 23:44:23 +0900 Subject: [PATCH 08/11] =?UTF-8?q?/api/auth=20=EC=95=84=EB=9E=98=EC=9D=98?= =?UTF-8?q?=20Endpoint=EB=93=A4=EC=9D=84=20tRPC=EB=A1=9C=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/middlewares/authenticate.ts | 6 +- src/lib/server/schemas/auth.ts | 32 ---- src/lib/server/schemas/index.ts | 1 - src/lib/server/services/auth.ts | 122 -------------- src/lib/services/auth.ts | 60 ++++--- .../auth/changePassword/service.ts | 17 +- src/routes/(fullscreen)/auth/login/service.ts | 14 +- src/routes/api/auth/changePassword/+server.ts | 16 -- src/routes/api/auth/login/+server.ts | 21 --- src/routes/api/auth/logout/+server.ts | 13 -- src/routes/api/auth/upgradeSession/+server.ts | 26 --- .../api/auth/upgradeSession/verify/+server.ts | 16 -- src/trpc/router.server.ts | 2 + src/trpc/routers/auth.ts | 153 ++++++++++++++++++ src/trpc/routers/index.ts | 1 + 15 files changed, 214 insertions(+), 286 deletions(-) delete mode 100644 src/lib/server/schemas/auth.ts delete mode 100644 src/lib/server/services/auth.ts delete mode 100644 src/routes/api/auth/changePassword/+server.ts delete mode 100644 src/routes/api/auth/login/+server.ts delete mode 100644 src/routes/api/auth/logout/+server.ts delete mode 100644 src/routes/api/auth/upgradeSession/+server.ts delete mode 100644 src/routes/api/auth/upgradeSession/verify/+server.ts create mode 100644 src/trpc/routers/auth.ts diff --git a/src/lib/server/middlewares/authenticate.ts b/src/lib/server/middlewares/authenticate.ts index cc635b4..49f6545 100644 --- a/src/lib/server/middlewares/authenticate.ts +++ b/src/lib/server/middlewares/authenticate.ts @@ -3,11 +3,6 @@ import env from "$lib/server/loadenv"; import { authenticate, AuthenticationError } from "$lib/server/modules/auth"; export const authenticateMiddleware: Handle = async ({ event, resolve }) => { - const { pathname, search } = event.url; - if (pathname === "/api/auth/login") { - return await resolve(event); - } - try { const sessionIdSigned = event.cookies.get("sessionId"); if (!sessionIdSigned) { @@ -24,6 +19,7 @@ export const authenticateMiddleware: Handle = async ({ event, resolve }) => { }); } catch (e) { if (e instanceof AuthenticationError) { + const { pathname, search } = event.url; if (pathname === "/auth/login" || pathname.startsWith("/api/trpc")) { return await resolve(event); } else if (pathname.startsWith("/api")) { diff --git a/src/lib/server/schemas/auth.ts b/src/lib/server/schemas/auth.ts deleted file mode 100644 index d4972d1..0000000 --- a/src/lib/server/schemas/auth.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { z } from "zod"; - -export const passwordChangeRequest = z.object({ - oldPassword: z.string().trim().nonempty(), - newPassword: z.string().trim().nonempty(), -}); -export type PasswordChangeRequest = z.input; - -export const loginRequest = z.object({ - email: z.email(), - password: z.string().trim().nonempty(), -}); -export type LoginRequest = z.input; - -export const sessionUpgradeRequest = z.object({ - encPubKey: z.base64().nonempty(), - sigPubKey: z.base64().nonempty(), -}); -export type SessionUpgradeRequest = z.input; - -export const sessionUpgradeResponse = z.object({ - id: z.int().positive(), - challenge: z.base64().nonempty(), -}); -export type SessionUpgradeResponse = z.output; - -export const sessionUpgradeVerifyRequest = z.object({ - id: z.int().positive(), - answerSig: z.base64().nonempty(), - force: z.boolean().default(false), -}); -export type SessionUpgradeVerifyRequest = z.input; diff --git a/src/lib/server/schemas/index.ts b/src/lib/server/schemas/index.ts index d9ddce7..f7a2bc1 100644 --- a/src/lib/server/schemas/index.ts +++ b/src/lib/server/schemas/index.ts @@ -1,4 +1,3 @@ -export * from "./auth"; export * from "./category"; export * from "./directory"; export * from "./file"; diff --git a/src/lib/server/services/auth.ts b/src/lib/server/services/auth.ts deleted file mode 100644 index 1c6867f..0000000 --- a/src/lib/server/services/auth.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { error } from "@sveltejs/kit"; -import argon2 from "argon2"; -import { getClient, getClientByPubKeys, getUserClient } from "$lib/server/db/client"; -import { IntegrityError } from "$lib/server/db/error"; -import { - upgradeSession, - deleteSession, - deleteAllOtherSessions, - registerSessionUpgradeChallenge, - consumeSessionUpgradeChallenge, -} from "$lib/server/db/session"; -import { getUser, getUserByEmail, setUserPassword } from "$lib/server/db/user"; -import env from "$lib/server/loadenv"; -import { startSession } from "$lib/server/modules/auth"; -import { verifySignature, generateChallenge } from "$lib/server/modules/crypto"; - -const hashPassword = async (password: string) => { - return await argon2.hash(password); -}; - -const verifyPassword = async (hash: string, password: string) => { - return await argon2.verify(hash, password); -}; - -export const changePassword = async ( - userId: number, - sessionId: string, - oldPassword: string, - newPassword: string, -) => { - if (oldPassword === newPassword) { - error(400, "Same passwords"); - } else if (newPassword.length < 8) { - error(400, "Too short password"); - } - - const user = await getUser(userId); - if (!user) { - error(500, "Invalid session id"); - } else if (!(await verifyPassword(user.password, oldPassword))) { - error(403, "Invalid password"); - } - - await setUserPassword(userId, await hashPassword(newPassword)); - await deleteAllOtherSessions(userId, sessionId); -}; - -export const login = async (email: string, password: string, ip: string, userAgent: string) => { - const user = await getUserByEmail(email); - if (!user || !(await verifyPassword(user.password, password))) { - error(401, "Invalid email or password"); - } - - return { sessionIdSigned: await startSession(user.id, ip, userAgent) }; -}; - -export const logout = async (sessionId: string) => { - await deleteSession(sessionId); -}; - -export const createSessionUpgradeChallenge = async ( - sessionId: string, - userId: number, - ip: string, - encPubKey: string, - sigPubKey: string, -) => { - const client = await getClientByPubKeys(encPubKey, sigPubKey); - const userClient = client ? await getUserClient(userId, client.id) : undefined; - if (!client) { - error(401, "Invalid public key(s)"); - } else if (!userClient || userClient.state === "challenging") { - error(403, "Unregistered client"); - } - - const { answer, challenge } = await generateChallenge(32, encPubKey); - const { id } = await registerSessionUpgradeChallenge( - sessionId, - client.id, - answer.toString("base64"), - ip, - new Date(Date.now() + env.challenge.sessionUpgradeExp), - ); - - return { id, challenge: challenge.toString("base64") }; -}; - -export const verifySessionUpgradeChallenge = async ( - sessionId: string, - userId: number, - ip: string, - challengeId: number, - answerSig: string, - force: boolean, -) => { - const challenge = await consumeSessionUpgradeChallenge(challengeId, sessionId, ip); - if (!challenge) { - error(403, "Invalid challenge answer"); - } - - const client = await getClient(challenge.clientId); - if (!client) { - error(500, "Invalid challenge answer"); - } else if ( - !verifySignature(Buffer.from(challenge.answer, "base64"), answerSig, client.sigPubKey) - ) { - error(403, "Invalid challenge answer signature"); - } - - try { - await upgradeSession(userId, sessionId, client.id, force); - } catch (e) { - if (e instanceof IntegrityError) { - if (e.message === "Session not found") { - error(500, "Invalid challenge answer"); - } else if (!force && e.message === "Session already exists") { - error(409, "Already logged in"); - } - } - throw e; - } -}; diff --git a/src/lib/services/auth.ts b/src/lib/services/auth.ts index d1975f4..5d2d01a 100644 --- a/src/lib/services/auth.ts +++ b/src/lib/services/auth.ts @@ -1,10 +1,6 @@ -import { callPostApi } from "$lib/hooks"; +import { TRPCClientError } from "@trpc/client"; import { encodeToBase64, decryptChallenge, signMessageRSA } from "$lib/modules/crypto"; -import type { - SessionUpgradeRequest, - SessionUpgradeResponse, - SessionUpgradeVerifyRequest, -} from "$lib/server/schemas"; +import { useTRPC } from "$trpc/client"; export const requestSessionUpgrade = async ( encryptKeyBase64: string, @@ -13,27 +9,45 @@ export const requestSessionUpgrade = async ( signKey: CryptoKey, force = false, ) => { - let res = await callPostApi("/api/auth/upgradeSession", { - encPubKey: encryptKeyBase64, - sigPubKey: verifyKeyBase64, - }); - if (res.status === 403) return [false, "Unregistered client"] as const; - else if (!res.ok) return [false] as const; - - const { id, challenge }: SessionUpgradeResponse = await res.json(); + const trpc = useTRPC(); + let id, challenge; + try { + ({ id, challenge } = await trpc.auth.upgradeSession.mutate({ + encPubKey: encryptKeyBase64, + sigPubKey: verifyKeyBase64, + })); + } catch (e) { + if (e instanceof TRPCClientError && e.data?.code === "FORBIDDEN") { + return [false, "Unregistered client"] as const; + } + return [false] as const; + } const answer = await decryptChallenge(challenge, decryptKey); const answerSig = await signMessageRSA(answer, signKey); - res = await callPostApi("/api/auth/upgradeSession/verify", { - id, - answerSig: encodeToBase64(answerSig), - force, - }); - if (res.status === 409) return [false, "Already logged in"] as const; - else return [res.ok] as const; + try { + await trpc.auth.verifySessionUpgrade.mutate({ + id, + answerSig: encodeToBase64(answerSig), + force, + }); + } catch (e) { + if (e instanceof TRPCClientError && e.data?.code === "CONFLICT") { + return [false, "Already logged in"] as const; + } + return [false] as const; + } + + return [true] as const; }; export const requestLogout = async () => { - const res = await callPostApi("/api/auth/logout"); - return res.ok; + const trpc = useTRPC(); + try { + await trpc.auth.logout.mutate(); + return true; + } catch { + // TODO: Error Handling + return false; + } }; diff --git a/src/routes/(fullscreen)/auth/changePassword/service.ts b/src/routes/(fullscreen)/auth/changePassword/service.ts index 37380a4..699ec7f 100644 --- a/src/routes/(fullscreen)/auth/changePassword/service.ts +++ b/src/routes/(fullscreen)/auth/changePassword/service.ts @@ -1,10 +1,13 @@ -import { callPostApi } from "$lib/hooks"; -import type { PasswordChangeRequest } from "$lib/server/schemas"; +import { useTRPC } from "$trpc/client"; export const requestPasswordChange = async (oldPassword: string, newPassword: string) => { - const res = await callPostApi("/api/auth/changePassword", { - oldPassword, - newPassword, - }); - return res.ok; + const trpc = useTRPC(); + + try { + await trpc.auth.changePassword.mutate({ oldPassword, newPassword }); + return true; + } catch { + // TODO: Error Handling + return false; + } }; diff --git a/src/routes/(fullscreen)/auth/login/service.ts b/src/routes/(fullscreen)/auth/login/service.ts index 88613f1..0d57545 100644 --- a/src/routes/(fullscreen)/auth/login/service.ts +++ b/src/routes/(fullscreen)/auth/login/service.ts @@ -1,5 +1,4 @@ -import { callPostApi } from "$lib/hooks"; -import type { LoginRequest } from "$lib/server/schemas"; +import { useTRPC } from "$trpc/client"; export { requestLogout } from "$lib/services/auth"; export { requestDeletedFilesCleanup } from "$lib/services/file"; @@ -9,6 +8,13 @@ export { } from "$lib/services/key"; export const requestLogin = async (email: string, password: string) => { - const res = await callPostApi("/api/auth/login", { email, password }); - return res.ok; + const trpc = useTRPC(); + + try { + await trpc.auth.login.mutate({ email, password }); + return true; + } catch { + // TODO: Error Handling + return false; + } }; diff --git a/src/routes/api/auth/changePassword/+server.ts b/src/routes/api/auth/changePassword/+server.ts deleted file mode 100644 index 59129b8..0000000 --- a/src/routes/api/auth/changePassword/+server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { error, text } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { passwordChangeRequest } from "$lib/server/schemas"; -import { changePassword } from "$lib/server/services/auth"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, request }) => { - const { sessionId, userId } = await authorize(locals, "any"); - - const zodRes = passwordChangeRequest.safeParse(await request.json()); - if (!zodRes.success) error(400, "Invalid request body"); - const { oldPassword, newPassword } = zodRes.data; - - await changePassword(userId, sessionId, oldPassword, newPassword); - return text("Password changed", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/routes/api/auth/login/+server.ts b/src/routes/api/auth/login/+server.ts deleted file mode 100644 index d748f6a..0000000 --- a/src/routes/api/auth/login/+server.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { error, text } from "@sveltejs/kit"; -import env from "$lib/server/loadenv"; -import { loginRequest } from "$lib/server/schemas"; -import { login } from "$lib/server/services/auth"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, request, cookies }) => { - const zodRes = loginRequest.safeParse(await request.json()); - if (!zodRes.success) error(400, "Invalid request body"); - const { email, password } = zodRes.data; - - const { sessionIdSigned } = await login(email, password, locals.ip, locals.userAgent); - cookies.set("sessionId", sessionIdSigned, { - path: "/", - maxAge: env.session.exp / 1000, - secure: true, - sameSite: "strict", - }); - - return text("Logged in", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/routes/api/auth/logout/+server.ts b/src/routes/api/auth/logout/+server.ts deleted file mode 100644 index b5c1f11..0000000 --- a/src/routes/api/auth/logout/+server.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { text } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { logout } from "$lib/server/services/auth"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, cookies }) => { - const { sessionId } = await authorize(locals, "any"); - - await logout(sessionId); - cookies.delete("sessionId", { path: "/" }); - - return text("Logged out", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/routes/api/auth/upgradeSession/+server.ts b/src/routes/api/auth/upgradeSession/+server.ts deleted file mode 100644 index fa0b6cf..0000000 --- a/src/routes/api/auth/upgradeSession/+server.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { error, json } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { - sessionUpgradeRequest, - sessionUpgradeResponse, - type SessionUpgradeResponse, -} from "$lib/server/schemas"; -import { createSessionUpgradeChallenge } from "$lib/server/services/auth"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, request }) => { - const { sessionId, userId } = await authorize(locals, "notClient"); - - const zodRes = sessionUpgradeRequest.safeParse(await request.json()); - if (!zodRes.success) error(400, "Invalid request body"); - const { encPubKey, sigPubKey } = zodRes.data; - - const { id, challenge } = await createSessionUpgradeChallenge( - sessionId, - userId, - locals.ip, - encPubKey, - sigPubKey, - ); - return json(sessionUpgradeResponse.parse({ id, challenge } satisfies SessionUpgradeResponse)); -}; diff --git a/src/routes/api/auth/upgradeSession/verify/+server.ts b/src/routes/api/auth/upgradeSession/verify/+server.ts deleted file mode 100644 index bbaedca..0000000 --- a/src/routes/api/auth/upgradeSession/verify/+server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { error, text } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { sessionUpgradeVerifyRequest } from "$lib/server/schemas"; -import { verifySessionUpgradeChallenge } from "$lib/server/services/auth"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, request }) => { - const { sessionId, userId } = await authorize(locals, "notClient"); - - const zodRes = sessionUpgradeVerifyRequest.safeParse(await request.json()); - if (!zodRes.success) error(400, "Invalid request body"); - const { id, answerSig, force } = zodRes.data; - - await verifySessionUpgradeChallenge(sessionId, userId, locals.ip, id, answerSig, force); - return text("Session upgraded", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/trpc/router.server.ts b/src/trpc/router.server.ts index 3d05e93..64d25c7 100644 --- a/src/trpc/router.server.ts +++ b/src/trpc/router.server.ts @@ -2,6 +2,7 @@ import type { RequestEvent } from "@sveltejs/kit"; import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; import { createContext, router } from "./init.server"; import { + authRouter, categoryRouter, clientRouter, directoryRouter, @@ -12,6 +13,7 @@ import { } from "./routers"; export const appRouter = router({ + auth: authRouter, category: categoryRouter, client: clientRouter, directory: directoryRouter, diff --git a/src/trpc/routers/auth.ts b/src/trpc/routers/auth.ts new file mode 100644 index 0000000..e0f3a7f --- /dev/null +++ b/src/trpc/routers/auth.ts @@ -0,0 +1,153 @@ +import { TRPCError } from "@trpc/server"; +import argon2 from "argon2"; +import { z } from "zod"; +import { ClientRepo, SessionRepo, UserRepo, IntegrityError } from "$lib/server/db"; +import env from "$lib/server/loadenv"; +import { startSession } from "$lib/server/modules/auth"; +import { generateChallenge, verifySignature } from "$lib/server/modules/crypto"; +import { publicProcedure, roleProcedure, router } from "../init.server"; + +const hashPassword = async (password: string) => { + return await argon2.hash(password); +}; + +const verifyPassword = async (hash: string, password: string) => { + return await argon2.verify(hash, password); +}; + +const authRouter = router({ + login: publicProcedure + .input( + z.object({ + email: z.email(), + password: z.string().trim().nonempty(), + }), + ) + .mutation(async ({ ctx, input }) => { + const user = await UserRepo.getUserByEmail(input.email); + if (!user || !(await verifyPassword(user.password, input.password))) { + throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid email or password" }); + } + + const sessionIdSigned = await startSession(user.id, ctx.locals.ip, ctx.locals.userAgent); + ctx.cookies.set("sessionId", sessionIdSigned, { + path: "/", + maxAge: env.session.exp / 1000, + secure: true, + sameSite: "strict", + }); + }), + + logout: roleProcedure["any"].mutation(async ({ ctx }) => { + await SessionRepo.deleteSession(ctx.session.sessionId); + ctx.cookies.delete("sessionId", { path: "/" }); + }), + + changePassword: roleProcedure["any"] + .input( + z.object({ + oldPassword: z.string().trim().nonempty(), + newPassword: z.string().trim().nonempty(), + }), + ) + .mutation(async ({ ctx, input }) => { + if (input.oldPassword === input.newPassword) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Same passwords" }); + } else if (input.newPassword.length < 8) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Too short password" }); + } + + const user = await UserRepo.getUser(ctx.session.userId); + if (!user) { + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid session id" }); + } else if (!(await verifyPassword(user.password, input.oldPassword))) { + throw new TRPCError({ code: "FORBIDDEN", message: "Invalid password" }); + } + + await UserRepo.setUserPassword(ctx.session.userId, await hashPassword(input.newPassword)); + await SessionRepo.deleteAllOtherSessions(ctx.session.userId, ctx.session.sessionId); + }), + + upgradeSession: roleProcedure["notClient"] + .input( + z.object({ + encPubKey: z.base64().nonempty(), + sigPubKey: z.base64().nonempty(), + }), + ) + .mutation(async ({ ctx, input }) => { + const client = await ClientRepo.getClientByPubKeys(input.encPubKey, input.sigPubKey); + const userClient = client + ? await ClientRepo.getUserClient(ctx.session.userId, client.id) + : undefined; + if (!client) { + throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid public key(s)" }); + } else if (!userClient || userClient.state === "challenging") { + throw new TRPCError({ code: "FORBIDDEN", message: "Unregistered client" }); + } + + const { answer, challenge } = await generateChallenge(32, input.encPubKey); + const { id } = await SessionRepo.registerSessionUpgradeChallenge( + ctx.session.sessionId, + client.id, + answer.toString("base64"), + ctx.locals.ip, + new Date(Date.now() + env.challenge.sessionUpgradeExp), + ); + return { id, challenge: challenge.toString("base64") }; + }), + + verifySessionUpgrade: roleProcedure["notClient"] + .input( + z.object({ + id: z.int().positive(), + answerSig: z.base64().nonempty(), + force: z.boolean().default(false), + }), + ) + .mutation(async ({ ctx, input }) => { + const challenge = await SessionRepo.consumeSessionUpgradeChallenge( + input.id, + ctx.session.sessionId, + ctx.locals.ip, + ); + if (!challenge) { + throw new TRPCError({ code: "FORBIDDEN", message: "Invalid challenge answer" }); + } + + const client = await ClientRepo.getClient(challenge.clientId); + if (!client) { + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid challenge answer" }); + } else if ( + !verifySignature(Buffer.from(challenge.answer, "base64"), input.answerSig, client.sigPubKey) + ) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Invalid challenge answer signature", + }); + } + + try { + await SessionRepo.upgradeSession( + ctx.session.userId, + ctx.session.sessionId, + client.id, + input.force, + ); + } catch (e) { + if (e instanceof IntegrityError) { + if (e.message === "Session not found") { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Invalid challenge answer", + }); + } else if (!input.force && e.message === "Session already exists") { + throw new TRPCError({ code: "CONFLICT", message: "Already logged in" }); + } + } + throw e; + } + }), +}); + +export default authRouter; diff --git a/src/trpc/routers/index.ts b/src/trpc/routers/index.ts index c943728..ab5b6a0 100644 --- a/src/trpc/routers/index.ts +++ b/src/trpc/routers/index.ts @@ -1,3 +1,4 @@ +export { default as authRouter } from "./auth"; export { default as categoryRouter } from "./category"; export { default as clientRouter } from "./client"; export { default as directoryRouter } from "./directory"; From d94d14cf83ddccc3948306ecad7f7b6e5ab229cc Mon Sep 17 00:00:00 2001 From: static Date: Fri, 26 Dec 2025 15:07:59 +0900 Subject: [PATCH 09/11] =?UTF-8?q?=EC=82=AC=EC=86=8C=ED=95=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/middlewares/authenticate.ts | 10 +---- src/lib/server/modules/auth.ts | 25 ++++++------- src/lib/services/auth.ts | 4 +- src/trpc/routers/auth.ts | 43 ++++++++-------------- src/trpc/routers/client.ts | 1 + 5 files changed, 33 insertions(+), 50 deletions(-) diff --git a/src/lib/server/middlewares/authenticate.ts b/src/lib/server/middlewares/authenticate.ts index 49f6545..ecd1adf 100644 --- a/src/lib/server/middlewares/authenticate.ts +++ b/src/lib/server/middlewares/authenticate.ts @@ -1,6 +1,5 @@ import { error, redirect, type Handle } from "@sveltejs/kit"; -import env from "$lib/server/loadenv"; -import { authenticate, AuthenticationError } from "$lib/server/modules/auth"; +import { cookieOptions, authenticate, AuthenticationError } from "$lib/server/modules/auth"; export const authenticateMiddleware: Handle = async ({ event, resolve }) => { try { @@ -11,12 +10,7 @@ export const authenticateMiddleware: Handle = async ({ event, resolve }) => { const { ip, userAgent } = event.locals; event.locals.session = await authenticate(sessionIdSigned, ip, userAgent); - event.cookies.set("sessionId", sessionIdSigned, { - path: "/", - maxAge: env.session.exp / 1000, - secure: true, - sameSite: "strict", - }); + event.cookies.set("sessionId", sessionIdSigned, cookieOptions); } catch (e) { if (e instanceof AuthenticationError) { const { pathname, search } = event.url; diff --git a/src/lib/server/modules/auth.ts b/src/lib/server/modules/auth.ts index 6ae3865..b736a49 100644 --- a/src/lib/server/modules/auth.ts +++ b/src/lib/server/modules/auth.ts @@ -1,11 +1,9 @@ import { error } from "@sveltejs/kit"; -import { getUserClient } from "$lib/server/db/client"; -import { IntegrityError } from "$lib/server/db/error"; -import { createSession, refreshSession } from "$lib/server/db/session"; +import { ClientRepo, SessionRepo, IntegrityError } from "$lib/server/db"; import env from "$lib/server/loadenv"; -import { issueSessionId, verifySessionId } from "$lib/server/modules/crypto"; +import { verifySessionId } from "$lib/server/modules/crypto"; -interface Session { +export interface Session { sessionId: string; userId: number; clientId?: number; @@ -42,11 +40,12 @@ export class AuthorizationError extends Error { } } -export const startSession = async (userId: number, ip: string, userAgent: string) => { - const { sessionId, sessionIdSigned } = await issueSessionId(32, env.session.secret); - await createSession(userId, sessionId, ip, userAgent); - return sessionIdSigned; -}; +export const cookieOptions = { + path: "/", + maxAge: env.session.exp / 1000, + secure: true, + sameSite: "strict", +} as const; export const authenticate = async (sessionIdSigned: string, ip: string, userAgent: string) => { const sessionId = verifySessionId(sessionIdSigned, env.session.secret); @@ -55,7 +54,7 @@ export const authenticate = async (sessionIdSigned: string, ip: string, userAgen } try { - const { userId, clientId } = await refreshSession(sessionId, ip, userAgent); + const { userId, clientId } = await SessionRepo.refreshSession(sessionId, ip, userAgent); return { id: sessionId, userId, @@ -96,7 +95,7 @@ export const authorizeInternal = async ( if (!clientId) { throw new AuthorizationError(403, "Forbidden"); } - const userClient = await getUserClient(userId, clientId); + const userClient = await ClientRepo.getUserClient(userId, clientId); if (!userClient) { throw new AuthorizationError(500, "Invalid session id"); } else if (userClient.state !== "pending") { @@ -108,7 +107,7 @@ export const authorizeInternal = async ( if (!clientId) { throw new AuthorizationError(403, "Forbidden"); } - const userClient = await getUserClient(userId, clientId); + const userClient = await ClientRepo.getUserClient(userId, clientId); if (!userClient) { throw new AuthorizationError(500, "Invalid session id"); } else if (userClient.state !== "active") { diff --git a/src/lib/services/auth.ts b/src/lib/services/auth.ts index 5d2d01a..2131943 100644 --- a/src/lib/services/auth.ts +++ b/src/lib/services/auth.ts @@ -12,7 +12,7 @@ export const requestSessionUpgrade = async ( const trpc = useTRPC(); let id, challenge; try { - ({ id, challenge } = await trpc.auth.upgradeSession.mutate({ + ({ id, challenge } = await trpc.auth.upgrade.mutate({ encPubKey: encryptKeyBase64, sigPubKey: verifyKeyBase64, })); @@ -26,7 +26,7 @@ export const requestSessionUpgrade = async ( const answerSig = await signMessageRSA(answer, signKey); try { - await trpc.auth.verifySessionUpgrade.mutate({ + await trpc.auth.verifyUpgrade.mutate({ id, answerSig: encodeToBase64(answerSig), force, diff --git a/src/trpc/routers/auth.ts b/src/trpc/routers/auth.ts index e0f3a7f..e0d3d50 100644 --- a/src/trpc/routers/auth.ts +++ b/src/trpc/routers/auth.ts @@ -3,51 +3,39 @@ import argon2 from "argon2"; import { z } from "zod"; import { ClientRepo, SessionRepo, UserRepo, IntegrityError } from "$lib/server/db"; import env from "$lib/server/loadenv"; -import { startSession } from "$lib/server/modules/auth"; -import { generateChallenge, verifySignature } from "$lib/server/modules/crypto"; -import { publicProcedure, roleProcedure, router } from "../init.server"; - -const hashPassword = async (password: string) => { - return await argon2.hash(password); -}; - -const verifyPassword = async (hash: string, password: string) => { - return await argon2.verify(hash, password); -}; +import { cookieOptions } from "$lib/server/modules/auth"; +import { generateChallenge, verifySignature, issueSessionId } from "$lib/server/modules/crypto"; +import { router, publicProcedure, roleProcedure } from "../init.server"; const authRouter = router({ login: publicProcedure .input( z.object({ email: z.email(), - password: z.string().trim().nonempty(), + password: z.string().nonempty(), }), ) .mutation(async ({ ctx, input }) => { const user = await UserRepo.getUserByEmail(input.email); - if (!user || !(await verifyPassword(user.password, input.password))) { + if (!user || !(await argon2.verify(user.password, input.password))) { throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid email or password" }); } - const sessionIdSigned = await startSession(user.id, ctx.locals.ip, ctx.locals.userAgent); - ctx.cookies.set("sessionId", sessionIdSigned, { - path: "/", - maxAge: env.session.exp / 1000, - secure: true, - sameSite: "strict", - }); + const { sessionId, sessionIdSigned } = await issueSessionId(32, env.session.secret); + await SessionRepo.createSession(user.id, sessionId, ctx.locals.ip, ctx.locals.userAgent); + ctx.cookies.set("sessionId", sessionIdSigned, cookieOptions); }), logout: roleProcedure["any"].mutation(async ({ ctx }) => { await SessionRepo.deleteSession(ctx.session.sessionId); - ctx.cookies.delete("sessionId", { path: "/" }); + ctx.cookies.delete("sessionId", cookieOptions); }), changePassword: roleProcedure["any"] .input( z.object({ - oldPassword: z.string().trim().nonempty(), - newPassword: z.string().trim().nonempty(), + oldPassword: z.string().nonempty(), + newPassword: z.string().nonempty(), }), ) .mutation(async ({ ctx, input }) => { @@ -60,15 +48,15 @@ const authRouter = router({ const user = await UserRepo.getUser(ctx.session.userId); if (!user) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid session id" }); - } else if (!(await verifyPassword(user.password, input.oldPassword))) { + } else if (!(await argon2.verify(user.password, input.oldPassword))) { throw new TRPCError({ code: "FORBIDDEN", message: "Invalid password" }); } - await UserRepo.setUserPassword(ctx.session.userId, await hashPassword(input.newPassword)); + await UserRepo.setUserPassword(ctx.session.userId, await argon2.hash(input.newPassword)); await SessionRepo.deleteAllOtherSessions(ctx.session.userId, ctx.session.sessionId); }), - upgradeSession: roleProcedure["notClient"] + upgrade: roleProcedure["notClient"] .input( z.object({ encPubKey: z.base64().nonempty(), @@ -94,10 +82,11 @@ const authRouter = router({ ctx.locals.ip, new Date(Date.now() + env.challenge.sessionUpgradeExp), ); + return { id, challenge: challenge.toString("base64") }; }), - verifySessionUpgrade: roleProcedure["notClient"] + verifyUpgrade: roleProcedure["notClient"] .input( z.object({ id: z.int().positive(), diff --git a/src/trpc/routers/client.ts b/src/trpc/routers/client.ts index 37c5bc6..db7229a 100644 --- a/src/trpc/routers/client.ts +++ b/src/trpc/routers/client.ts @@ -19,6 +19,7 @@ const createUserClientChallenge = async ( ip, new Date(Date.now() + env.challenge.userClientExp), ); + return { id, challenge: challenge.toString("base64") }; }; From c9d4b1035602cc3f837f9b6272636e43e00ac656 Mon Sep 17 00:00:00 2001 From: static Date: Fri, 26 Dec 2025 15:45:03 +0900 Subject: [PATCH 10/11] =?UTF-8?q?=EC=82=AC=EC=86=8C=ED=95=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/modules/file/upload.ts | 5 ++--- src/lib/modules/filesystem.ts | 13 +++++------- src/lib/services/auth.ts | 10 ++++------ src/lib/services/category.ts | 8 +++----- src/lib/services/file.ts | 8 +++----- src/lib/services/key.ts | 18 ++++++----------- .../auth/changePassword/service.ts | 6 ++---- src/routes/(fullscreen)/auth/login/service.ts | 6 ++---- src/routes/(fullscreen)/file/[id]/service.ts | 6 ++---- .../(fullscreen)/settings/thumbnail/+page.ts | 7 +++---- .../(main)/category/[[id]]/service.svelte.ts | 9 +++------ .../(main)/directory/[[id]]/service.svelte.ts | 20 +++++++------------ src/routes/(main)/menu/+page.ts | 7 +++---- src/trpc/client.ts | 2 +- src/trpc/routers/directory.ts | 12 ++++++----- src/trpc/routers/file.ts | 1 + 16 files changed, 54 insertions(+), 84 deletions(-) diff --git a/src/lib/modules/file/upload.ts b/src/lib/modules/file/upload.ts index 7c48503..31aabd8 100644 --- a/src/lib/modules/file/upload.ts +++ b/src/lib/modules/file/upload.ts @@ -23,15 +23,14 @@ import { type HmacSecret, type FileUploadStatus, } from "$lib/stores"; -import { useTRPC } from "$trpc/client"; +import { trpc } from "$trpc/client"; const requestDuplicateFileScan = limitFunction( async (file: File, hmacSecret: HmacSecret, onDuplicate: () => Promise) => { - const trpc = useTRPC(); const fileBuffer = await file.arrayBuffer(); const fileSigned = encodeToBase64(await signMessageHmac(fileBuffer, hmacSecret.secret)); - const files = await trpc.file.listByHash.query({ + const files = await trpc().file.listByHash.query({ hskVersion: hmacSecret.version, contentHmac: fileSigned, }); diff --git a/src/lib/modules/filesystem.ts b/src/lib/modules/filesystem.ts index 8b9b203..40e8cd4 100644 --- a/src/lib/modules/filesystem.ts +++ b/src/lib/modules/filesystem.ts @@ -18,7 +18,7 @@ import { type CategoryId, } from "$lib/indexedDB"; import { unwrapDataKey, decryptString } from "$lib/modules/crypto"; -import { useTRPC } from "$trpc/client"; +import { trpc } from "$trpc/client"; export type DirectoryInfo = | { @@ -101,10 +101,9 @@ const fetchDirectoryInfoFromServer = async ( info: Writable, masterKey: CryptoKey, ) => { - const trpc = useTRPC(); let data; try { - data = await trpc.directory.get.query({ id }); + data = await trpc().directory.get.query({ id }); } catch (e) { if (e instanceof TRPCClientError && e.data?.code === "NOT_FOUND") { info.set(null); @@ -174,10 +173,9 @@ const fetchFileInfoFromServer = async ( info: Writable, masterKey: CryptoKey, ) => { - const trpc = useTRPC(); let metadata; try { - metadata = await trpc.file.get.query({ id }); + metadata = await trpc().file.get.query({ id }); } catch (e) { if (e instanceof TRPCClientError && e.data?.code === "NOT_FOUND") { info.set(null); @@ -270,10 +268,9 @@ const fetchCategoryInfoFromServer = async ( info: Writable, masterKey: CryptoKey, ) => { - const trpc = useTRPC(); let data; try { - data = await trpc.category.get.query({ id }); + data = await trpc().category.get.query({ id }); } catch (e) { if (e instanceof TRPCClientError && e.data?.code === "NOT_FOUND") { info.set(null); @@ -293,7 +290,7 @@ const fetchCategoryInfoFromServer = async ( let files; try { - files = await trpc.category.files.query({ id, recurse: true }); + files = await trpc().category.files.query({ id, recurse: true }); } catch { throw new Error("Failed to fetch category files"); } diff --git a/src/lib/services/auth.ts b/src/lib/services/auth.ts index 2131943..56d67eb 100644 --- a/src/lib/services/auth.ts +++ b/src/lib/services/auth.ts @@ -1,6 +1,6 @@ import { TRPCClientError } from "@trpc/client"; import { encodeToBase64, decryptChallenge, signMessageRSA } from "$lib/modules/crypto"; -import { useTRPC } from "$trpc/client"; +import { trpc } from "$trpc/client"; export const requestSessionUpgrade = async ( encryptKeyBase64: string, @@ -9,10 +9,9 @@ export const requestSessionUpgrade = async ( signKey: CryptoKey, force = false, ) => { - const trpc = useTRPC(); let id, challenge; try { - ({ id, challenge } = await trpc.auth.upgrade.mutate({ + ({ id, challenge } = await trpc().auth.upgrade.mutate({ encPubKey: encryptKeyBase64, sigPubKey: verifyKeyBase64, })); @@ -26,7 +25,7 @@ export const requestSessionUpgrade = async ( const answerSig = await signMessageRSA(answer, signKey); try { - await trpc.auth.verifyUpgrade.mutate({ + await trpc().auth.verifyUpgrade.mutate({ id, answerSig: encodeToBase64(answerSig), force, @@ -42,9 +41,8 @@ export const requestSessionUpgrade = async ( }; export const requestLogout = async () => { - const trpc = useTRPC(); try { - await trpc.auth.logout.mutate(); + await trpc().auth.logout.mutate(); return true; } catch { // TODO: Error Handling diff --git a/src/lib/services/category.ts b/src/lib/services/category.ts index c86c93b..7e0443f 100644 --- a/src/lib/services/category.ts +++ b/src/lib/services/category.ts @@ -1,18 +1,17 @@ import { generateDataKey, wrapDataKey, encryptString } from "$lib/modules/crypto"; import type { MasterKey } from "$lib/stores"; -import { useTRPC } from "$trpc/client"; +import { trpc } from "$trpc/client"; export const requestCategoryCreation = async ( name: string, parentId: "root" | number, masterKey: MasterKey, ) => { - const trpc = useTRPC(); const { dataKey, dataKeyVersion } = await generateDataKey(); const nameEncrypted = await encryptString(name, dataKey); try { - await trpc.category.create.mutate({ + await trpc().category.create.mutate({ parent: parentId, mekVersion: masterKey.version, dek: await wrapDataKey(dataKey, masterKey.key), @@ -28,10 +27,9 @@ export const requestCategoryCreation = async ( }; export const requestFileRemovalFromCategory = async (fileId: number, categoryId: number) => { - const trpc = useTRPC(); try { - await trpc.category.removeFile.mutate({ id: categoryId, file: fileId }); + await trpc().category.removeFile.mutate({ id: categoryId, file: fileId }); return true; } catch { // TODO: Error Handling diff --git a/src/lib/services/file.ts b/src/lib/services/file.ts index f428e97..7e93c7a 100644 --- a/src/lib/services/file.ts +++ b/src/lib/services/file.ts @@ -12,7 +12,7 @@ import { } from "$lib/modules/file"; import { getThumbnailUrl } from "$lib/modules/thumbnail"; import type { FileThumbnailUploadRequest } from "$lib/server/schemas"; -import { useTRPC } from "$trpc/client"; +import { trpc } from "$trpc/client"; export const requestFileDownload = async ( fileId: number, @@ -49,10 +49,9 @@ export const requestFileThumbnailDownload = async (fileId: number, dataKey?: Cry const cache = await getFileThumbnailCache(fileId); if (cache || !dataKey) return cache; - const trpc = useTRPC(); let thumbnailInfo; try { - thumbnailInfo = await trpc.file.thumbnail.query({ id: fileId }); + thumbnailInfo = await trpc().file.thumbnail.query({ id: fileId }); } catch { // TODO: Error Handling return null; @@ -70,10 +69,9 @@ export const requestFileThumbnailDownload = async (fileId: number, dataKey?: Cry }; export const requestDeletedFilesCleanup = async () => { - const trpc = useTRPC(); let liveFiles; try { - liveFiles = await trpc.file.list.query(); + liveFiles = await trpc().file.list.query(); } catch { // TODO: Error Handling return; diff --git a/src/lib/services/key.ts b/src/lib/services/key.ts index 6149829..fd89f74 100644 --- a/src/lib/services/key.ts +++ b/src/lib/services/key.ts @@ -11,7 +11,7 @@ import { } from "$lib/modules/crypto"; import { requestSessionUpgrade } from "$lib/services/auth"; import { masterKeyStore, type ClientKeys } from "$lib/stores"; -import { useTRPC } from "$trpc/client"; +import { trpc } from "$trpc/client"; export const requestClientRegistration = async ( encryptKeyBase64: string, @@ -19,16 +19,14 @@ export const requestClientRegistration = async ( verifyKeyBase64: string, signKey: CryptoKey, ) => { - const trpc = useTRPC(); - try { - const { id, challenge } = await trpc.client.register.mutate({ + const { id, challenge } = await trpc().client.register.mutate({ encPubKey: encryptKeyBase64, sigPubKey: verifyKeyBase64, }); const answer = await decryptChallenge(challenge, decryptKey); const answerSig = await signMessageRSA(answer, signKey); - await trpc.client.verify.mutate({ + await trpc().client.verify.mutate({ id, answerSig: encodeToBase64(answerSig), }); @@ -69,11 +67,9 @@ export const requestClientRegistrationAndSessionUpgrade = async ( }; export const requestMasterKeyDownload = async (decryptKey: CryptoKey, verifyKey: CryptoKey) => { - const trpc = useTRPC(); - let masterKeysWrapped; try { - masterKeysWrapped = await trpc.mek.list.query(); + masterKeysWrapped = await trpc().mek.list.query(); } catch { // TODO: Error Handling return false; @@ -110,10 +106,8 @@ export const requestInitialMasterKeyAndHmacSecretRegistration = async ( hmacSecretWrapped: string, signKey: CryptoKey, ) => { - const trpc = useTRPC(); - try { - await trpc.mek.registerInitial.mutate({ + await trpc().mek.registerInitial.mutate({ mek: masterKeyWrapped, mekSig: await signMasterKeyWrapped(masterKeyWrapped, 1, signKey), }); @@ -129,7 +123,7 @@ export const requestInitialMasterKeyAndHmacSecretRegistration = async ( } try { - await trpc.hsk.registerInitial.mutate({ + await trpc().hsk.registerInitial.mutate({ mekVersion: 1, hsk: hmacSecretWrapped, }); diff --git a/src/routes/(fullscreen)/auth/changePassword/service.ts b/src/routes/(fullscreen)/auth/changePassword/service.ts index 699ec7f..4673611 100644 --- a/src/routes/(fullscreen)/auth/changePassword/service.ts +++ b/src/routes/(fullscreen)/auth/changePassword/service.ts @@ -1,10 +1,8 @@ -import { useTRPC } from "$trpc/client"; +import { trpc } from "$trpc/client"; export const requestPasswordChange = async (oldPassword: string, newPassword: string) => { - const trpc = useTRPC(); - try { - await trpc.auth.changePassword.mutate({ oldPassword, newPassword }); + await trpc().auth.changePassword.mutate({ oldPassword, newPassword }); return true; } catch { // TODO: Error Handling diff --git a/src/routes/(fullscreen)/auth/login/service.ts b/src/routes/(fullscreen)/auth/login/service.ts index 0d57545..5c8219a 100644 --- a/src/routes/(fullscreen)/auth/login/service.ts +++ b/src/routes/(fullscreen)/auth/login/service.ts @@ -1,4 +1,4 @@ -import { useTRPC } from "$trpc/client"; +import { trpc } from "$trpc/client"; export { requestLogout } from "$lib/services/auth"; export { requestDeletedFilesCleanup } from "$lib/services/file"; @@ -8,10 +8,8 @@ export { } from "$lib/services/key"; export const requestLogin = async (email: string, password: string) => { - const trpc = useTRPC(); - try { - await trpc.auth.login.mutate({ email, password }); + await trpc().auth.login.mutate({ email, password }); return true; } catch { // TODO: Error Handling diff --git a/src/routes/(fullscreen)/file/[id]/service.ts b/src/routes/(fullscreen)/file/[id]/service.ts index 73ca7f1..09ec86f 100644 --- a/src/routes/(fullscreen)/file/[id]/service.ts +++ b/src/routes/(fullscreen)/file/[id]/service.ts @@ -1,7 +1,7 @@ import { encryptData } from "$lib/modules/crypto"; import { storeFileThumbnailCache } from "$lib/modules/file"; import { requestFileThumbnailUpload } from "$lib/services/file"; -import { useTRPC } from "$trpc/client"; +import { trpc } from "$trpc/client"; export { requestCategoryCreation, requestFileRemovalFromCategory } from "$lib/services/category"; export { requestFileDownload } from "$lib/services/file"; @@ -22,10 +22,8 @@ export const requestThumbnailUpload = async ( }; export const requestFileAdditionToCategory = async (fileId: number, categoryId: number) => { - const trpc = useTRPC(); - try { - await trpc.category.addFile.mutate({ id: categoryId, file: fileId }); + await trpc().category.addFile.mutate({ id: categoryId, file: fileId }); return true; } catch { // TODO: Error Handling diff --git a/src/routes/(fullscreen)/settings/thumbnail/+page.ts b/src/routes/(fullscreen)/settings/thumbnail/+page.ts index 3bfa322..f435b72 100644 --- a/src/routes/(fullscreen)/settings/thumbnail/+page.ts +++ b/src/routes/(fullscreen)/settings/thumbnail/+page.ts @@ -1,14 +1,13 @@ import { error } from "@sveltejs/kit"; -import { useTRPC } from "$trpc/client"; +import { trpc } from "$trpc/client"; import type { PageLoad } from "./$types"; export const load: PageLoad = async ({ fetch }) => { - const trpc = useTRPC(fetch); - try { - const files = await trpc.file.listWithoutThumbnail.query(); + const files = await trpc(fetch).file.listWithoutThumbnail.query(); return { files }; } catch { + // TODO: Error Handling error(500, "Internal server error"); } }; diff --git a/src/routes/(main)/category/[[id]]/service.svelte.ts b/src/routes/(main)/category/[[id]]/service.svelte.ts index 824de8a..18f68fd 100644 --- a/src/routes/(main)/category/[[id]]/service.svelte.ts +++ b/src/routes/(main)/category/[[id]]/service.svelte.ts @@ -1,7 +1,7 @@ import { getContext, setContext } from "svelte"; import { encryptString } from "$lib/modules/crypto"; import type { SelectedCategory } from "$lib/components/molecules"; -import { useTRPC } from "$trpc/client"; +import { trpc } from "$trpc/client"; export { requestCategoryCreation, requestFileRemovalFromCategory } from "$lib/services/category"; @@ -17,11 +17,10 @@ export const useContext = () => { }; export const requestCategoryRename = async (category: SelectedCategory, newName: string) => { - const trpc = useTRPC(); const newNameEncrypted = await encryptString(newName, category.dataKey); try { - await trpc.category.rename.mutate({ + await trpc().category.rename.mutate({ id: category.id, dekVersion: category.dataKeyVersion, name: newNameEncrypted.ciphertext, @@ -35,10 +34,8 @@ export const requestCategoryRename = async (category: SelectedCategory, newName: }; export const requestCategoryDeletion = async (category: SelectedCategory) => { - const trpc = useTRPC(); - try { - await trpc.category.delete.mutate({ id: category.id }); + await trpc().category.delete.mutate({ id: category.id }); return true; } catch { // TODO: Error Handling diff --git a/src/routes/(main)/directory/[[id]]/service.svelte.ts b/src/routes/(main)/directory/[[id]]/service.svelte.ts index 72a8fdb..c94cc1e 100644 --- a/src/routes/(main)/directory/[[id]]/service.svelte.ts +++ b/src/routes/(main)/directory/[[id]]/service.svelte.ts @@ -9,7 +9,7 @@ import { uploadFile, } from "$lib/modules/file"; import { hmacSecretStore, type MasterKey, type HmacSecret } from "$lib/stores"; -import { useTRPC } from "$trpc/client"; +import { trpc } from "$trpc/client"; export interface SelectedEntry { type: "directory" | "file"; @@ -33,11 +33,9 @@ export const useContext = () => { export const requestHmacSecretDownload = async (masterKey: CryptoKey) => { // TODO: MEK rotation - const trpc = useTRPC(); - let hmacSecretsWrapped; try { - hmacSecretsWrapped = await trpc.hsk.list.query(); + hmacSecretsWrapped = await trpc().hsk.list.query(); } catch { // TODO: Error Handling return false; @@ -61,12 +59,11 @@ export const requestDirectoryCreation = async ( parentId: "root" | number, masterKey: MasterKey, ) => { - const trpc = useTRPC(); const { dataKey, dataKeyVersion } = await generateDataKey(); const nameEncrypted = await encryptString(name, dataKey); try { - await trpc.directory.create.mutate({ + await trpc().directory.create.mutate({ parent: parentId, mekVersion: masterKey.version, dek: await wrapDataKey(dataKey, masterKey.key), @@ -100,19 +97,18 @@ export const requestFileUpload = async ( }; export const requestEntryRename = async (entry: SelectedEntry, newName: string) => { - const trpc = useTRPC(); const newNameEncrypted = await encryptString(newName, entry.dataKey); try { if (entry.type === "directory") { - await trpc.directory.rename.mutate({ + await trpc().directory.rename.mutate({ id: entry.id, dekVersion: entry.dataKeyVersion, name: newNameEncrypted.ciphertext, nameIv: newNameEncrypted.iv, }); } else { - await trpc.file.rename.mutate({ + await trpc().file.rename.mutate({ id: entry.id, dekVersion: entry.dataKeyVersion, name: newNameEncrypted.ciphertext, @@ -127,11 +123,9 @@ export const requestEntryRename = async (entry: SelectedEntry, newName: string) }; export const requestEntryDeletion = async (entry: SelectedEntry) => { - const trpc = useTRPC(); - try { if (entry.type === "directory") { - const { deletedFiles } = await trpc.directory.delete.mutate({ id: entry.id }); + const { deletedFiles } = await trpc().directory.delete.mutate({ id: entry.id }); await Promise.all( deletedFiles.flatMap((fileId) => [ deleteFileCache(fileId), @@ -139,7 +133,7 @@ export const requestEntryDeletion = async (entry: SelectedEntry) => { ]), ); } else { - await trpc.file.delete.mutate({ id: entry.id }); + await trpc().file.delete.mutate({ id: entry.id }); await Promise.all([deleteFileCache(entry.id), deleteFileThumbnailCache(entry.id)]); } return true; diff --git a/src/routes/(main)/menu/+page.ts b/src/routes/(main)/menu/+page.ts index ecd8f0b..e97e395 100644 --- a/src/routes/(main)/menu/+page.ts +++ b/src/routes/(main)/menu/+page.ts @@ -1,14 +1,13 @@ import { error } from "@sveltejs/kit"; -import { useTRPC } from "$trpc/client"; +import { trpc } from "$trpc/client"; import type { PageLoad } from "./$types"; export const load: PageLoad = async ({ fetch }) => { - const trpc = useTRPC(fetch); - try { - const { nickname } = await trpc.user.get.query(); + const { nickname } = await trpc(fetch).user.get.query(); return { nickname }; } catch { + // TODO: Error Handling error(500, "Internal server error"); } }; diff --git a/src/trpc/client.ts b/src/trpc/client.ts index cb1e8c5..a24cd5d 100644 --- a/src/trpc/client.ts +++ b/src/trpc/client.ts @@ -16,7 +16,7 @@ const createClient = (fetch: typeof globalThis.fetch) => let browserClient: ReturnType; -export const useTRPC = (fetch = globalThis.fetch) => { +export const trpc = (fetch = globalThis.fetch) => { const client = browserClient ?? createClient(fetch); if (browser) { browserClient ??= client; diff --git a/src/trpc/routers/directory.ts b/src/trpc/routers/directory.ts index bcd31c7..e060c23 100644 --- a/src/trpc/routers/directory.ts +++ b/src/trpc/routers/directory.ts @@ -108,11 +108,13 @@ const directoryRouter = router({ .mutation(async ({ ctx, input }) => { try { const files = await FileRepo.unregisterDirectory(ctx.session.userId, input.id); - files.forEach(({ path, thumbnailPath }) => { - safeUnlink(path); // Intended - safeUnlink(thumbnailPath); // Intended - }); - return { deletedFiles: files.map(({ id }) => id) }; + return { + deletedFiles: files.map((file) => { + safeUnlink(file.path); // Intended + safeUnlink(file.thumbnailPath); // Intended + return file.id; + }), + }; } catch (e) { if (e instanceof IntegrityError && e.message === "Directory not found") { throw new TRPCError({ code: "NOT_FOUND", message: "Invalid directory id" }); diff --git a/src/trpc/routers/file.ts b/src/trpc/routers/file.ts index 7c4e425..decbc76 100644 --- a/src/trpc/routers/file.ts +++ b/src/trpc/routers/file.ts @@ -115,6 +115,7 @@ const fileRouter = router({ if (!thumbnail) { throw new TRPCError({ code: "NOT_FOUND", message: "File or its thumbnail not found" }); } + return { updatedAt: thumbnail.updatedAt, contentIv: thumbnail.encContentIv }; }), }); From 3eb7411438e873bdda42c6a76bfbf35bd37e192f Mon Sep 17 00:00:00 2001 From: static Date: Fri, 26 Dec 2025 15:57:05 +0900 Subject: [PATCH 11/11] =?UTF-8?q?=EC=82=AC=EC=86=8C=ED=95=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/hooks/callApi.ts | 11 ----------- src/lib/services/category.ts | 1 - src/lib/services/file.ts | 3 +-- src/lib/{hooks => utils}/gotoStateful.ts | 0 src/lib/{hooks => utils}/index.ts | 1 - src/routes/(fullscreen)/key/export/+page.ts | 2 +- src/routes/(fullscreen)/key/generate/+page.svelte | 2 +- src/trpc/init.server.ts | 3 +-- 8 files changed, 4 insertions(+), 19 deletions(-) delete mode 100644 src/lib/hooks/callApi.ts rename src/lib/{hooks => utils}/gotoStateful.ts (100%) rename src/lib/{hooks => utils}/index.ts (54%) diff --git a/src/lib/hooks/callApi.ts b/src/lib/hooks/callApi.ts deleted file mode 100644 index 1699ec2..0000000 --- a/src/lib/hooks/callApi.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const callGetApi = async (input: RequestInfo, fetchInternal = fetch) => { - return await fetchInternal(input); -}; - -export const callPostApi = async (input: RequestInfo, payload?: T, fetchInternal = fetch) => { - return await fetchInternal(input, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: payload ? JSON.stringify(payload) : undefined, - }); -}; diff --git a/src/lib/services/category.ts b/src/lib/services/category.ts index 7e0443f..c53a6f2 100644 --- a/src/lib/services/category.ts +++ b/src/lib/services/category.ts @@ -27,7 +27,6 @@ export const requestCategoryCreation = async ( }; export const requestFileRemovalFromCategory = async (fileId: number, categoryId: number) => { - try { await trpc().category.removeFile.mutate({ id: categoryId, file: fileId }); return true; diff --git a/src/lib/services/file.ts b/src/lib/services/file.ts index 7e93c7a..da05824 100644 --- a/src/lib/services/file.ts +++ b/src/lib/services/file.ts @@ -1,4 +1,3 @@ -import { callGetApi } from "$lib/hooks"; import { getAllFileInfos } from "$lib/indexedDB/filesystem"; import { decryptData } from "$lib/modules/crypto"; import { @@ -58,7 +57,7 @@ export const requestFileThumbnailDownload = async (fileId: number, dataKey?: Cry } const { contentIv: thumbnailEncryptedIv } = thumbnailInfo; - const res = await callGetApi(`/api/file/${fileId}/thumbnail/download`); + const res = await fetch(`/api/file/${fileId}/thumbnail/download`); if (!res.ok) return null; const thumbnailEncrypted = await res.arrayBuffer(); diff --git a/src/lib/hooks/gotoStateful.ts b/src/lib/utils/gotoStateful.ts similarity index 100% rename from src/lib/hooks/gotoStateful.ts rename to src/lib/utils/gotoStateful.ts diff --git a/src/lib/hooks/index.ts b/src/lib/utils/index.ts similarity index 54% rename from src/lib/hooks/index.ts rename to src/lib/utils/index.ts index e3b8dde..4c24322 100644 --- a/src/lib/hooks/index.ts +++ b/src/lib/utils/index.ts @@ -1,2 +1 @@ -export * from "./callApi"; export * from "./gotoStateful"; diff --git a/src/routes/(fullscreen)/key/export/+page.ts b/src/routes/(fullscreen)/key/export/+page.ts index a64ea53..5785158 100644 --- a/src/routes/(fullscreen)/key/export/+page.ts +++ b/src/routes/(fullscreen)/key/export/+page.ts @@ -1,5 +1,5 @@ import { error } from "@sveltejs/kit"; -import { keyExportState } from "$lib/hooks/gotoStateful"; +import { keyExportState } from "$lib/utils/gotoStateful"; import type { PageLoad } from "./$types"; export const load: PageLoad = async () => { diff --git a/src/routes/(fullscreen)/key/generate/+page.svelte b/src/routes/(fullscreen)/key/generate/+page.svelte index 6f7609c..7292d78 100644 --- a/src/routes/(fullscreen)/key/generate/+page.svelte +++ b/src/routes/(fullscreen)/key/generate/+page.svelte @@ -4,9 +4,9 @@ import { BottomDiv, Button, FullscreenDiv, TextButton } from "$lib/components/atoms"; import { TitledDiv } from "$lib/components/molecules"; import { ForceLoginModal } from "$lib/components/organisms"; - import { gotoStateful } from "$lib/hooks"; import { storeClientKeys } from "$lib/modules/key"; import { clientKeyStore } from "$lib/stores"; + import { gotoStateful } from "$lib/utils"; import Order from "./Order.svelte"; import { generateClientKeys, diff --git a/src/trpc/init.server.ts b/src/trpc/init.server.ts index 8b88157..8d5d7c5 100644 --- a/src/trpc/init.server.ts +++ b/src/trpc/init.server.ts @@ -3,11 +3,10 @@ import { initTRPC, TRPCError } from "@trpc/server"; import superjson from "superjson"; import { authorizeMiddleware, authorizeClientMiddleware } from "./middlewares/authorize"; +export const createContext = (event: RequestEvent) => event; export type Context = Awaited>; -export const createContext = (event: RequestEvent) => event; export const t = initTRPC.context().create({ transformer: superjson }); - export const router = t.router; export const publicProcedure = t.procedure;