diff options
37 files changed, 405 insertions, 148 deletions
diff --git a/packages/trpc/auth.ts b/packages/trpc/auth.ts new file mode 100644 index 00000000..6854303b --- /dev/null +++ b/packages/trpc/auth.ts @@ -0,0 +1,99 @@ +import { randomBytes } from "crypto"; +import { apiKeys } from "@hoarder/db/schema"; +import * as bcrypt from "bcrypt"; +import { db } from "@hoarder/db"; + +// API Keys + +const BCRYPT_SALT_ROUNDS = 10; +const API_KEY_PREFIX = "ak1"; + +export async function generateApiKey(name: string, userId: string) { + const id = randomBytes(10).toString("hex"); + const secret = randomBytes(10).toString("hex"); + const secretHash = await bcrypt.hash(secret, BCRYPT_SALT_ROUNDS); + + const plain = `${API_KEY_PREFIX}_${id}_${secret}`; + + const key = ( + await db + .insert(apiKeys) + .values({ + name: name, + userId: userId, + keyId: id, + keyHash: secretHash, + }) + .returning() + )[0]; + + return { + id: key.id, + name: key.name, + createdAt: key.createdAt, + key: plain, + }; +} +function parseApiKey(plain: string) { + const parts = plain.split("_"); + if (parts.length != 3) { + throw new Error( + `Malformd API key. API keys should have 3 segments, found ${parts.length} instead.`, + ); + } + if (parts[0] !== API_KEY_PREFIX) { + throw new Error(`Malformd API key. Got unexpected key prefix.`); + } + return { + keyId: parts[1], + keySecret: parts[2], + }; +} + +export async function authenticateApiKey(key: string) { + const { keyId, keySecret } = parseApiKey(key); + const apiKey = await db.query.apiKeys.findFirst({ + where: (k, { eq }) => eq(k.keyId, keyId), + with: { + user: true, + }, + }); + + if (!apiKey) { + throw new Error("API key not found"); + } + + const hash = apiKey.keyHash; + + const validation = await bcrypt.compare(keySecret, hash); + if (!validation) { + throw new Error("Invalid API Key"); + } + + return apiKey.user; +} + +export async function hashPassword(password: string) { + return bcrypt.hash(password, BCRYPT_SALT_ROUNDS); +} + +export async function validatePassword(email: string, password: string) { + const user = await db.query.users.findFirst({ + where: (u, { eq }) => eq(u.email, email), + }); + + if (!user) { + throw new Error("User not found"); + } + + if (!user.password) { + throw new Error("This user doesn't have a password defined"); + } + + const validation = await bcrypt.compare(password, user.password); + if (!validation) { + throw new Error("Wrong password"); + } + + return user; +} diff --git a/packages/web/server/api/trpc.ts b/packages/trpc/index.ts index 0ba09e94..a32eb871 100644 --- a/packages/web/server/api/trpc.ts +++ b/packages/trpc/index.ts @@ -1,9 +1,13 @@ import { db } from "@hoarder/db"; import serverConfig from "@hoarder/shared/config"; import { TRPCError, initTRPC } from "@trpc/server"; -import { User } from "next-auth"; import superjson from "superjson"; +type User = { + id: string; + role: "admin" | "user" | null; +}; + export type Context = { user: User | null; db: typeof db; diff --git a/packages/trpc/package.json b/packages/trpc/package.json new file mode 100644 index 00000000..1e33eff0 --- /dev/null +++ b/packages/trpc/package.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@hoarder/trpc", + "version": "0.1.0", + "private": true, + "scripts": { + "typecheck": "tsc --noEmit", + "test": "vitest" + }, + "dependencies": { + "@hoarder/db": "workspace:*", + "@hoarder/shared": "workspace:*", + "@trpc/server": "11.0.0-next-beta.304", + "bcrypt": "^5.1.1", + "drizzle-orm": "^0.29.4", + "superjson": "^2.2.1", + "zod": "^3.22.4" + }, + "devDependencies": { + "@tsconfig/node21": "^21.0.1", + "@types/bcrypt": "^5.0.2", + "aws-sdk": "^2.1570.0", + "mock-aws-s3": "^4.0.2", + "nock": "^13.5.4", + "vite-tsconfig-paths": "^4.3.1", + "vitest": "^1.3.1" + } +} diff --git a/packages/web/server/api/routers/_app.ts b/packages/trpc/routers/_app.ts index 43ab6f5d..6e5dd91d 100644 --- a/packages/web/server/api/routers/_app.ts +++ b/packages/trpc/routers/_app.ts @@ -1,4 +1,4 @@ -import { router } from "../trpc"; +import { router } from "../index"; import { adminAppRouter } from "./admin"; import { apiKeysAppRouter } from "./apiKeys"; import { bookmarksAppRouter } from "./bookmarks"; diff --git a/packages/web/server/api/routers/admin.ts b/packages/trpc/routers/admin.ts index c3f6235a..8a7b592d 100644 --- a/packages/web/server/api/routers/admin.ts +++ b/packages/trpc/routers/admin.ts @@ -1,4 +1,4 @@ -import { adminProcedure, router } from "../trpc"; +import { adminProcedure, router } from "../index"; import { z } from "zod"; import { count } from "drizzle-orm"; import { bookmarks, users } from "@hoarder/db/schema"; diff --git a/packages/web/server/api/routers/apiKeys.ts b/packages/trpc/routers/apiKeys.ts index 9eb36974..d13f87fb 100644 --- a/packages/web/server/api/routers/apiKeys.ts +++ b/packages/trpc/routers/apiKeys.ts @@ -1,5 +1,5 @@ -import { generateApiKey } from "@/server/auth"; -import { authedProcedure, router } from "../trpc"; +import { generateApiKey } from "../auth"; +import { authedProcedure, router } from "../index"; import { z } from "zod"; import { apiKeys } from "@hoarder/db/schema"; import { eq, and } from "drizzle-orm"; diff --git a/packages/web/server/api/routers/bookmarks.test.ts b/packages/trpc/routers/bookmarks.test.ts index 626a7250..724a9998 100644 --- a/packages/web/server/api/routers/bookmarks.test.ts +++ b/packages/trpc/routers/bookmarks.test.ts @@ -1,4 +1,4 @@ -import { CustomTestContext, defaultBeforeEach } from "@/lib/testUtils"; +import { CustomTestContext, defaultBeforeEach } from "../testUtils"; import { expect, describe, test, beforeEach, assert } from "vitest"; beforeEach<CustomTestContext>(defaultBeforeEach(true)); diff --git a/packages/web/server/api/routers/bookmarks.ts b/packages/trpc/routers/bookmarks.ts index 73818508..ea7ffef8 100644 --- a/packages/web/server/api/routers/bookmarks.ts +++ b/packages/trpc/routers/bookmarks.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { Context, authedProcedure, router } from "../trpc"; +import { Context, authedProcedure, router } from "../index"; import { getSearchIdxClient } from "@hoarder/shared/search"; import { ZBookmark, @@ -10,7 +10,7 @@ import { zGetBookmarksResponseSchema, zNewBookmarkRequestSchema, zUpdateBookmarksRequestSchema, -} from "@/lib/types/api/bookmarks"; +} from "../types/bookmarks"; import { bookmarkLinks, bookmarkTags, @@ -25,7 +25,7 @@ import { } from "@hoarder/shared/queues"; import { TRPCError, experimental_trpcMiddleware } from "@trpc/server"; import { and, desc, eq, inArray } from "drizzle-orm"; -import { ZBookmarkTags } from "@/lib/types/api/tags"; +import { ZBookmarkTags } from "../types/tags"; import { db as DONT_USE_db } from "@hoarder/db"; diff --git a/packages/web/server/api/routers/lists.ts b/packages/trpc/routers/lists.ts index 7bf5eed5..fa97929d 100644 --- a/packages/web/server/api/routers/lists.ts +++ b/packages/trpc/routers/lists.ts @@ -1,10 +1,10 @@ -import { Context, authedProcedure, router } from "../trpc"; +import { Context, authedProcedure, router } from "../index"; import { SqliteError } from "@hoarder/db"; import { z } from "zod"; import { TRPCError, experimental_trpcMiddleware } from "@trpc/server"; import { bookmarkLists, bookmarksInLists } from "@hoarder/db/schema"; import { and, eq } from "drizzle-orm"; -import { zBookmarkListSchema } from "@/lib/types/api/lists"; +import { zBookmarkListSchema } from "../types/lists"; const ensureListOwnership = experimental_trpcMiddleware<{ ctx: Context; diff --git a/packages/web/server/api/routers/users.test.ts b/packages/trpc/routers/users.test.ts index 1ee04f99..87814407 100644 --- a/packages/web/server/api/routers/users.test.ts +++ b/packages/trpc/routers/users.test.ts @@ -2,7 +2,7 @@ import { CustomTestContext, defaultBeforeEach, getApiCaller, -} from "@/lib/testUtils"; +} from "../testUtils"; import { expect, describe, test, beforeEach, assert } from "vitest"; beforeEach<CustomTestContext>(defaultBeforeEach(false)); diff --git a/packages/web/server/api/routers/users.ts b/packages/trpc/routers/users.ts index 32d10860..b5334f99 100644 --- a/packages/web/server/api/routers/users.ts +++ b/packages/trpc/routers/users.ts @@ -1,8 +1,8 @@ -import { zSignUpSchema } from "@/lib/types/api/users"; -import { adminProcedure, publicProcedure, router } from "../trpc"; +import { zSignUpSchema } from "../types/users"; +import { adminProcedure, publicProcedure, router } from "../index"; import { SqliteError } from "@hoarder/db"; import { z } from "zod"; -import { hashPassword } from "@/server/auth"; +import { hashPassword } from "../auth"; import { TRPCError } from "@trpc/server"; import { users } from "@hoarder/db/schema"; import { count, eq } from "drizzle-orm"; diff --git a/packages/web/lib/testUtils.ts b/packages/trpc/testUtils.ts index bad78463..d5f24def 100644 --- a/packages/web/lib/testUtils.ts +++ b/packages/trpc/testUtils.ts @@ -1,7 +1,7 @@ import { users } from "@hoarder/db/schema"; import { getInMemoryDB } from "@hoarder/db/drizzle"; -import { appRouter } from "@/server/api/routers/_app"; -import { createCallerFactory } from "@/server/api/trpc"; +import { appRouter } from "./routers/_app"; +import { createCallerFactory } from "./index"; export function getTestDB() { return getInMemoryDB(true); diff --git a/packages/trpc/tsconfig.json b/packages/trpc/tsconfig.json new file mode 100644 index 00000000..bf020b01 --- /dev/null +++ b/packages/trpc/tsconfig.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/node21/tsconfig.json", + "include": ["**/*.ts"], + "exclude": ["node_modules"], + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "node", + "baseUrl": "./", + "esModuleInterop": true + } +} + diff --git a/packages/web/lib/types/api/bookmarks.ts b/packages/trpc/types/bookmarks.ts index 5fabc7ca..b61ab0e0 100644 --- a/packages/web/lib/types/api/bookmarks.ts +++ b/packages/trpc/types/bookmarks.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { zBookmarkTagSchema } from "@/lib/types/api/tags"; +import { zBookmarkTagSchema } from "./tags"; export const zBookmarkedLinkSchema = z.object({ type: z.literal("link"), diff --git a/packages/web/lib/types/api/lists.ts b/packages/trpc/types/lists.ts index 4b0ccaca..4b0ccaca 100644 --- a/packages/web/lib/types/api/lists.ts +++ b/packages/trpc/types/lists.ts diff --git a/packages/web/lib/types/api/tags.ts b/packages/trpc/types/tags.ts index 7a99dad4..7a99dad4 100644 --- a/packages/web/lib/types/api/tags.ts +++ b/packages/trpc/types/tags.ts diff --git a/packages/web/lib/types/api/users.ts b/packages/trpc/types/users.ts index c2fe182a..c2fe182a 100644 --- a/packages/web/lib/types/api/users.ts +++ b/packages/trpc/types/users.ts diff --git a/packages/trpc/vitest.config.ts b/packages/trpc/vitest.config.ts new file mode 100644 index 00000000..c3d02f71 --- /dev/null +++ b/packages/trpc/vitest.config.ts @@ -0,0 +1,14 @@ +/// <reference types="vitest" /> + +import { defineConfig } from "vitest/config"; +import tsconfigPaths from "vite-tsconfig-paths"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [tsconfigPaths()], + test: { + alias: { + "@/*": "./*", + }, + }, +}); diff --git a/packages/web/app/api/trpc/[trpc]/route.ts b/packages/web/app/api/trpc/[trpc]/route.ts index 7d56cadc..b6753101 100644 --- a/packages/web/app/api/trpc/[trpc]/route.ts +++ b/packages/web/app/api/trpc/[trpc]/route.ts @@ -1,7 +1,7 @@ import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; -import { appRouter } from "@/server/api/routers/_app"; +import { appRouter } from "@hoarder/trpc/routers/_app"; import { createContext } from "@/server/api/client"; -import { authenticateApiKey } from "@/server/auth"; +import { authenticateApiKey } from "@hoarder/trpc/auth"; import { db } from "@hoarder/db"; const handler = (req: Request) => diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx index 584e8708..4f08ebee 100644 --- a/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx +++ b/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx @@ -2,7 +2,7 @@ import { useToast } from "@/components/ui/use-toast"; import { api } from "@/lib/trpc"; -import { ZBookmark, ZBookmarkedLink } from "@/lib/types/api/bookmarks"; +import { ZBookmark, ZBookmarkedLink } from "@hoarder/trpc/types/bookmarks"; import { Button } from "@/components/ui/button"; import { DropdownMenu, diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarkedTextEditor.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarkedTextEditor.tsx index c449fae3..a5b58f1a 100644 --- a/packages/web/app/dashboard/bookmarks/components/BookmarkedTextEditor.tsx +++ b/packages/web/app/dashboard/bookmarks/components/BookmarkedTextEditor.tsx @@ -1,4 +1,4 @@ -import { ZBookmark } from "@/lib/types/api/bookmarks"; +import { ZBookmark } from "@hoarder/trpc/types/bookmarks"; import { Dialog, DialogClose, diff --git a/packages/web/app/dashboard/bookmarks/components/Bookmarks.tsx b/packages/web/app/dashboard/bookmarks/components/Bookmarks.tsx index 62b93dc8..1ad3670c 100644 --- a/packages/web/app/dashboard/bookmarks/components/Bookmarks.tsx +++ b/packages/web/app/dashboard/bookmarks/components/Bookmarks.tsx @@ -1,6 +1,6 @@ import { redirect } from "next/navigation"; import BookmarksGrid from "./BookmarksGrid"; -import { ZGetBookmarksRequest } from "@/lib/types/api/bookmarks"; +import { ZGetBookmarksRequest } from "@hoarder/trpc/types/bookmarks"; import { api } from "@/server/api/client"; import { getServerAuthSession } from "@/server/auth"; diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx index 554d20a0..4d5b6b0a 100644 --- a/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx +++ b/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx @@ -1,7 +1,7 @@ "use client"; import LinkCard from "./LinkCard"; -import { ZBookmark, ZGetBookmarksRequest } from "@/lib/types/api/bookmarks"; +import { ZBookmark, ZGetBookmarksRequest } from "@hoarder/trpc/types/bookmarks"; import { api } from "@/lib/trpc"; import TextCard from "./TextCard"; import { Slot } from "@radix-ui/react-slot"; diff --git a/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx b/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx index 5af11aa3..76d3f1b8 100644 --- a/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx +++ b/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx @@ -8,7 +8,7 @@ import { ImageCardFooter, ImageCardTitle, } from "@/components/ui/imageCard"; -import { ZBookmark } from "@/lib/types/api/bookmarks"; +import { ZBookmark } from "@hoarder/trpc/types/bookmarks"; import Link from "next/link"; import BookmarkOptions from "./BookmarkOptions"; import { api } from "@/lib/trpc"; diff --git a/packages/web/app/dashboard/bookmarks/components/TagList.tsx b/packages/web/app/dashboard/bookmarks/components/TagList.tsx index 82d9f376..6c9d2d22 100644 --- a/packages/web/app/dashboard/bookmarks/components/TagList.tsx +++ b/packages/web/app/dashboard/bookmarks/components/TagList.tsx @@ -1,7 +1,7 @@ import { badgeVariants } from "@/components/ui/badge"; import Link from "next/link"; import { Skeleton } from "@/components/ui/skeleton"; -import { ZBookmark } from "@/lib/types/api/bookmarks"; +import { ZBookmark } from "@hoarder/trpc/types/bookmarks"; import { cn } from "@/lib/utils"; export default function TagList({ diff --git a/packages/web/app/dashboard/bookmarks/components/TagModal.tsx b/packages/web/app/dashboard/bookmarks/components/TagModal.tsx index 703c4221..8c09d00e 100644 --- a/packages/web/app/dashboard/bookmarks/components/TagModal.tsx +++ b/packages/web/app/dashboard/bookmarks/components/TagModal.tsx @@ -11,8 +11,8 @@ import { import { Input } from "@/components/ui/input"; import { toast } from "@/components/ui/use-toast"; import { api } from "@/lib/trpc"; -import { ZBookmark } from "@/lib/types/api/bookmarks"; -import { ZAttachedByEnum } from "@/lib/types/api/tags"; +import { ZBookmark } from "@hoarder/trpc/types/bookmarks"; +import { ZAttachedByEnum } from "@hoarder/trpc/types/tags"; import { cn } from "@/lib/utils"; import { Sparkles, X } from "lucide-react"; import { useState, KeyboardEvent, useEffect } from "react"; diff --git a/packages/web/app/dashboard/bookmarks/components/TextCard.tsx b/packages/web/app/dashboard/bookmarks/components/TextCard.tsx index 029800ac..5e0ba3f9 100644 --- a/packages/web/app/dashboard/bookmarks/components/TextCard.tsx +++ b/packages/web/app/dashboard/bookmarks/components/TextCard.tsx @@ -1,6 +1,6 @@ "use client"; -import { ZBookmark } from "@/lib/types/api/bookmarks"; +import { ZBookmark } from "@hoarder/trpc/types/bookmarks"; import BookmarkOptions from "./BookmarkOptions"; import { api } from "@/lib/trpc"; import { Maximize2, Star } from "lucide-react"; diff --git a/packages/web/app/dashboard/components/AllLists.tsx b/packages/web/app/dashboard/components/AllLists.tsx index 8903c82a..a77252d0 100644 --- a/packages/web/app/dashboard/components/AllLists.tsx +++ b/packages/web/app/dashboard/components/AllLists.tsx @@ -5,7 +5,7 @@ import SidebarItem from "./SidebarItem"; import NewListModal, { useNewListModal } from "./NewListModal"; import { Plus } from "lucide-react"; import Link from "next/link"; -import { ZBookmarkList } from "@/lib/types/api/lists"; +import { ZBookmarkList } from "@hoarder/trpc/types/lists"; export default function AllLists({ initialData, diff --git a/packages/web/app/dashboard/lists/[listId]/components/DeleteListButton.tsx b/packages/web/app/dashboard/lists/[listId]/components/DeleteListButton.tsx index 32a7facf..5303b217 100644 --- a/packages/web/app/dashboard/lists/[listId]/components/DeleteListButton.tsx +++ b/packages/web/app/dashboard/lists/[listId]/components/DeleteListButton.tsx @@ -16,7 +16,7 @@ import { toast } from "@/components/ui/use-toast"; import { api } from "@/lib/trpc"; import { ActionButton } from "@/components/ui/action-button"; import { useState } from "react"; -import { ZBookmarkList } from "@/lib/types/api/lists"; +import { ZBookmarkList } from "@hoarder/trpc/types/lists"; export default function DeleteListButton({ list }: { list: ZBookmarkList }) { const [isDialogOpen, setDialogOpen] = useState(false); diff --git a/packages/web/app/dashboard/lists/[listId]/components/ListView.tsx b/packages/web/app/dashboard/lists/[listId]/components/ListView.tsx index 6489e9f0..979b522f 100644 --- a/packages/web/app/dashboard/lists/[listId]/components/ListView.tsx +++ b/packages/web/app/dashboard/lists/[listId]/components/ListView.tsx @@ -1,8 +1,8 @@ "use client"; import BookmarksGrid from "@/app/dashboard/bookmarks/components/BookmarksGrid"; -import { ZBookmark } from "@/lib/types/api/bookmarks"; -import { ZBookmarkListWithBookmarks } from "@/lib/types/api/lists"; +import { ZBookmark } from "@hoarder/trpc/types/bookmarks"; +import { ZBookmarkListWithBookmarks } from "@hoarder/trpc/types/lists"; import { api } from "@/lib/trpc"; export default function ListView({ diff --git a/packages/web/app/dashboard/lists/components/AllListsView.tsx b/packages/web/app/dashboard/lists/components/AllListsView.tsx index d81f5fca..0e2f898b 100644 --- a/packages/web/app/dashboard/lists/components/AllListsView.tsx +++ b/packages/web/app/dashboard/lists/components/AllListsView.tsx @@ -2,7 +2,7 @@ import { Button } from "@/components/ui/button"; import { api } from "@/lib/trpc"; -import { ZBookmarkList } from "@/lib/types/api/lists"; +import { ZBookmarkList } from "@hoarder/trpc/types/lists"; import { keepPreviousData } from "@tanstack/react-query"; import { Plus } from "lucide-react"; import Link from "next/link"; diff --git a/packages/web/app/signin/components/CredentialsForm.tsx b/packages/web/app/signin/components/CredentialsForm.tsx index f47708f6..5296e163 100644 --- a/packages/web/app/signin/components/CredentialsForm.tsx +++ b/packages/web/app/signin/components/CredentialsForm.tsx @@ -13,7 +13,7 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { ActionButton } from "@/components/ui/action-button"; -import { zSignUpSchema } from "@/lib/types/api/users"; +import { zSignUpSchema } from "@hoarder/trpc/types/users"; import { signIn } from "next-auth/react"; import { useState } from "react"; import { api } from "@/lib/trpc"; diff --git a/packages/web/lib/trpc.tsx b/packages/web/lib/trpc.tsx index aa246047..79a2a9fe 100644 --- a/packages/web/lib/trpc.tsx +++ b/packages/web/lib/trpc.tsx @@ -1,5 +1,5 @@ "use client"; -import type { AppRouter } from "@/server/api/routers/_app"; +import type { AppRouter } from "@hoarder/trpc/routers/_app"; import { createTRPCReact } from "@trpc/react-query"; export const api = createTRPCReact<AppRouter>(); diff --git a/packages/web/package.json b/packages/web/package.json index 5367d189..e0c9d407 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -8,7 +8,8 @@ "build": "next build", "start": "next start", "lint": "next lint", - "test": "vitest" + "test": "vitest", + "typecheck": "tsc --noEmit" }, "dependencies": { "@auth/drizzle-adapter": "^0.8.0", @@ -16,6 +17,7 @@ "@emoji-mart/react": "^1.1.1", "@hoarder/db": "0.1.0", "@hoarder/shared": "0.1.0", + "@hoarder/trpc": "0.1.0", "@hookform/resolvers": "^3.3.4", "@next/eslint-plugin-next": "^14.1.1", "@radix-ui/react-dialog": "^1.0.5", @@ -34,7 +36,6 @@ "@trpc/next": "11.0.0-next-beta.304", "@trpc/react-query": "^11.0.0-next-beta.304", "@trpc/server": "11.0.0-next-beta.304", - "bcrypt": "^5.1.1", "better-sqlite3": "^9.4.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", @@ -60,7 +61,6 @@ }, "devDependencies": { "@tailwindcss/typography": "^0.5.10", - "@types/bcrypt": "^5.0.2", "@types/emoji-mart": "^3.0.14", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/packages/web/server/api/client.ts b/packages/web/server/api/client.ts index 130f4f87..88ea7a0e 100644 --- a/packages/web/server/api/client.ts +++ b/packages/web/server/api/client.ts @@ -1,6 +1,6 @@ -import { appRouter } from "./routers/_app"; +import { appRouter } from "@hoarder/trpc/routers/_app"; import { getServerAuthSession } from "@/server/auth"; -import { Context, createCallerFactory } from "./trpc"; +import { Context, createCallerFactory } from "@hoarder/trpc"; import { db } from "@hoarder/db"; export const createContext = async (database?: typeof db): Promise<Context> => { diff --git a/packages/web/server/auth.ts b/packages/web/server/auth.ts index 1810c87d..950443b9 100644 --- a/packages/web/server/auth.ts +++ b/packages/web/server/auth.ts @@ -2,15 +2,13 @@ import NextAuth, { NextAuthOptions, getServerSession } from "next-auth"; import type { Adapter } from "next-auth/adapters"; import AuthentikProvider from "next-auth/providers/authentik"; import serverConfig from "@hoarder/shared/config"; +import { validatePassword } from "@hoarder/trpc/auth"; import { db } from "@hoarder/db"; import { DefaultSession } from "next-auth"; -import * as bcrypt from "bcrypt"; import CredentialsProvider from "next-auth/providers/credentials"; import { DrizzleAdapter } from "@auth/drizzle-adapter"; -import { randomBytes } from "crypto"; import { Provider } from "next-auth/providers/index"; -import { apiKeys } from "@hoarder/db/schema"; declare module "next-auth/jwt" { export interface JWT { @@ -96,99 +94,3 @@ export const authOptions: NextAuthOptions = { export const authHandler = NextAuth(authOptions); export const getServerAuthSession = () => getServerSession(authOptions); - -// API Keys - -const BCRYPT_SALT_ROUNDS = 10; -const API_KEY_PREFIX = "ak1"; - -export async function generateApiKey(name: string, userId: string) { - const id = randomBytes(10).toString("hex"); - const secret = randomBytes(10).toString("hex"); - const secretHash = await bcrypt.hash(secret, BCRYPT_SALT_ROUNDS); - - const plain = `${API_KEY_PREFIX}_${id}_${secret}`; - - const key = ( - await db - .insert(apiKeys) - .values({ - name: name, - userId: userId, - keyId: id, - keyHash: secretHash, - }) - .returning() - )[0]; - - return { - id: key.id, - name: key.name, - createdAt: key.createdAt, - key: plain, - }; -} - -function parseApiKey(plain: string) { - const parts = plain.split("_"); - if (parts.length != 3) { - throw new Error( - `Malformd API key. API keys should have 3 segments, found ${parts.length} instead.`, - ); - } - if (parts[0] !== API_KEY_PREFIX) { - throw new Error(`Malformd API key. Got unexpected key prefix.`); - } - return { - keyId: parts[1], - keySecret: parts[2], - }; -} - -export async function authenticateApiKey(key: string) { - const { keyId, keySecret } = parseApiKey(key); - const apiKey = await db.query.apiKeys.findFirst({ - where: (k, { eq }) => eq(k.keyId, keyId), - with: { - user: true, - }, - }); - - if (!apiKey) { - throw new Error("API key not found"); - } - - const hash = apiKey.keyHash; - - const validation = await bcrypt.compare(keySecret, hash); - if (!validation) { - throw new Error("Invalid API Key"); - } - - return apiKey.user; -} - -export async function hashPassword(password: string) { - return bcrypt.hash(password, BCRYPT_SALT_ROUNDS); -} - -export async function validatePassword(email: string, password: string) { - const user = await db.query.users.findFirst({ - where: (u, { eq }) => eq(u.email, email), - }); - - if (!user) { - throw new Error("User not found"); - } - - if (!user.password) { - throw new Error("This user doesn't have a password defined"); - } - - const validation = await bcrypt.compare(password, user.password); - if (!validation) { - throw new Error("Wrong password"); - } - - return user; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f7a22a6..dc333de5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,6 +177,52 @@ importers: specifier: ^3.22.4 version: 3.22.4 + packages/trpc: + dependencies: + '@hoarder/db': + specifier: workspace:* + version: link:../db + '@hoarder/shared': + specifier: workspace:* + version: link:../shared + '@trpc/server': + specifier: 11.0.0-next-beta.304 + version: 11.0.0-next-beta.304 + bcrypt: + specifier: ^5.1.1 + version: 5.1.1 + drizzle-orm: + specifier: ^0.29.4 + version: 0.29.4(@types/react@18.2.58)(better-sqlite3@9.4.3)(react@18.2.0) + superjson: + specifier: ^2.2.1 + version: 2.2.1 + zod: + specifier: ^3.22.4 + version: 3.22.4 + devDependencies: + '@tsconfig/node21': + specifier: ^21.0.1 + version: 21.0.1 + '@types/bcrypt': + specifier: ^5.0.2 + version: 5.0.2 + aws-sdk: + specifier: ^2.1570.0 + version: 2.1570.0 + mock-aws-s3: + specifier: ^4.0.2 + version: 4.0.2 + nock: + specifier: ^13.5.4 + version: 13.5.4 + vite-tsconfig-paths: + specifier: ^4.3.1 + version: 4.3.1(typescript@5.3.3) + vitest: + specifier: ^1.3.1 + version: 1.3.1(@types/node@20.11.20) + packages/web: dependencies: '@auth/drizzle-adapter': @@ -194,6 +240,9 @@ importers: '@hoarder/shared': specifier: 0.1.0 version: link:../shared + '@hoarder/trpc': + specifier: 0.1.0 + version: link:../trpc '@hookform/resolvers': specifier: ^3.3.4 version: 3.3.4(react-hook-form@7.50.1) @@ -248,9 +297,6 @@ importers: '@trpc/server': specifier: 11.0.0-next-beta.304 version: 11.0.0-next-beta.304 - bcrypt: - specifier: ^5.1.1 - version: 5.1.1 better-sqlite3: specifier: ^9.4.3 version: 9.4.3 @@ -321,9 +367,6 @@ importers: '@tailwindcss/typography': specifier: ^0.5.10 version: 0.5.10(tailwindcss@3.4.1) - '@types/bcrypt': - specifier: ^5.0.2 - version: 5.0.2 '@types/emoji-mart': specifier: ^3.0.14 version: 3.0.14 @@ -4804,6 +4847,22 @@ packages: dependencies: possible-typed-array-names: 1.0.0 + /aws-sdk@2.1570.0: + resolution: {integrity: sha512-WySdibC3YOPCFcXNSevX7cGp6Nc0Ksv7m6aaz6YoqSrmSn7mZhkWaVXqfd14nsjJuyEbEgX+gAiZaahyvkUYJw==} + engines: {node: '>= 10.0.0'} + dependencies: + buffer: 4.9.2 + events: 1.1.1 + ieee754: 1.1.13 + jmespath: 0.16.0 + querystring: 0.2.0 + sax: 1.2.1 + url: 0.10.3 + util: 0.12.5 + uuid: 8.0.0 + xml2js: 0.6.2 + dev: true + /axe-core@4.7.0: resolution: {integrity: sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==} engines: {node: '>=4'} @@ -4914,7 +4973,6 @@ packages: /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: false /basic-ftp@5.0.4: resolution: {integrity: sha512-8PzkB0arJFV4jJWSGOYR+OEic6aeKMu/osRhBULN6RY0ykby6LKhbmuQ5ublvaas5BOwboah5D87nrHyuh8PPA==} @@ -4963,6 +5021,10 @@ packages: readable-stream: 3.6.2 dev: false + /bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + dev: true + /boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -5000,6 +5062,14 @@ packages: /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + /buffer@4.9.2: + resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + isarray: 1.0.0 + dev: true + /buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} dependencies: @@ -6647,6 +6717,11 @@ packages: engines: {node: '>=6'} dev: false + /events@1.1.1: + resolution: {integrity: sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==} + engines: {node: '>=0.4.x'} + dev: true + /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -6879,6 +6954,15 @@ packages: universalify: 2.0.1 dev: false + /fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + dev: true + /fs-extra@9.1.0: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} @@ -7311,9 +7395,12 @@ packages: resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} dev: false + /ieee754@1.1.13: + resolution: {integrity: sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==} + dev: true + /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: false /ignore@5.3.1: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} @@ -7426,6 +7513,14 @@ packages: is-decimal: 2.0.1 dev: false + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + dev: true + /is-array-buffer@3.0.4: resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} engines: {node: '>= 0.4'} @@ -7677,6 +7772,10 @@ packages: resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} engines: {node: '>=12.13'} + /isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + dev: true + /isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -7751,6 +7850,11 @@ packages: resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} hasBin: true + /jmespath@0.16.0: + resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} + engines: {node: '>= 0.6.0'} + dev: true + /jose@4.15.4: resolution: {integrity: sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==} dev: false @@ -7859,6 +7963,10 @@ packages: /json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + /json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + dev: true + /json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -7876,6 +7984,12 @@ packages: resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} dev: true + /jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + /jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} dependencies: @@ -8791,6 +8905,15 @@ packages: ufo: 1.4.0 dev: true + /mock-aws-s3@4.0.2: + resolution: {integrity: sha512-J6g3MMCuKHeuqVEOgvQfRGIfVmg6KKrED48Bux/L9rTY3NPK9TFRh/9bCf5AuzjJm9PIlwhDEO99tD8+smnTyQ==} + engines: {node: '>=10.0.0'} + dependencies: + bluebird: 3.7.2 + fs-extra: 7.0.1 + underscore: 1.12.1 + dev: true + /ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} dev: true @@ -8950,6 +9073,17 @@ packages: - babel-plugin-macros dev: false + /nock@13.5.4: + resolution: {integrity: sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw==} + engines: {node: '>= 10.13'} + dependencies: + debug: 4.3.4 + json-stringify-safe: 5.0.1 + propagate: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: true + /node-abi@3.56.0: resolution: {integrity: sha512-fZjdhDOeRcaS+rcpve7XuwHBmktS1nS1gzgghwKUQQ8nTy2FdSDr6ZT8k6YhvlJeHmmQMYiT/IH9hfco5zeW2Q==} engines: {node: '>=10'} @@ -9660,6 +9794,11 @@ packages: object-assign: 4.1.1 react-is: 16.13.1 + /propagate@2.0.1: + resolution: {integrity: sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==} + engines: {node: '>= 8'} + dev: true + /property-information@6.4.1: resolution: {integrity: sha512-OHYtXfu5aI2sS2LWFSN5rgJjrQ4pCy8i1jubJLe2QvMF8JJ++HXTUIVWFLfXJoaOfvYYjk2SN8J2wFUWIGXT4w==} dev: false @@ -9700,6 +9839,10 @@ packages: engines: {node: '>=0.10'} dev: false + /punycode@1.3.2: + resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} + dev: true + /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -9875,6 +10018,12 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} dev: true + /querystring@0.2.0: + resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} + engines: {node: '>=0.4.x'} + deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. + dev: true + /querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} dev: false @@ -10342,6 +10491,10 @@ packages: requiresBuild: true dev: false + /sax@1.2.1: + resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==} + dev: true + /saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -11238,6 +11391,10 @@ packages: through: 2.3.8 dev: false + /underscore@1.12.1: + resolution: {integrity: sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==} + dev: true + /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} @@ -11337,6 +11494,11 @@ packages: unist-util-visit-parents: 6.0.1 dev: false + /universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + dev: true + /universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} @@ -11387,6 +11549,13 @@ packages: tlds: 1.250.0 dev: false + /url@0.10.3: + resolution: {integrity: sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==} + dependencies: + punycode: 1.3.2 + querystring: 0.2.0 + dev: true + /urlpattern-polyfill@10.0.0: resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==} dev: false @@ -11441,6 +11610,21 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + /util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + dependencies: + inherits: 2.0.4 + is-arguments: 1.1.1 + is-generator-function: 1.0.10 + is-typed-array: 1.1.13 + which-typed-array: 1.1.14 + dev: true + + /uuid@8.0.0: + resolution: {integrity: sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==} + hasBin: true + dev: true + /uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -12042,6 +12226,19 @@ packages: engines: {node: '>=18'} dev: false + /xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + dependencies: + sax: 1.2.1 + xmlbuilder: 11.0.1 + dev: true + + /xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + dev: true + /xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} dev: false |
