diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6c9aa06 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +DATABASE_URL="postgres://drop:drop@127.0.0.1:5432/drop" + +CLIENT_CERTIFICATES="./.data/ca" + +GIANT_BOMB_API_KEY="" \ No newline at end of file diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 0000000..6370fa5 --- /dev/null +++ b/.yarnrc @@ -0,0 +1 @@ +"@drop:registry" "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/" \ No newline at end of file diff --git a/app.vue b/app.vue index f8eacfa..b34046f 100644 --- a/app.vue +++ b/app.vue @@ -3,3 +3,7 @@ + + diff --git a/assets/core.scss b/assets/core.scss index 36bfbbe..61d4df6 100644 --- a/assets/core.scss +++ b/assets/core.scss @@ -36,4 +36,16 @@ $helvetica: ( font-weight: $weight; font-style: $style; } +} + +@font-face { + font-family: "Inter"; + src: url("/fonts/inter/InterVariable.ttf"); + font-style: normal; +} + +@font-face { + font-family: "Inter"; + src: url("/fonts/inter/InterVariable-Italic.ttf"); + font-style: italic; } \ No newline at end of file diff --git a/components/HeaderUserWidget.vue b/components/HeaderUserWidget.vue new file mode 100644 index 0000000..72c8828 --- /dev/null +++ b/components/HeaderUserWidget.vue @@ -0,0 +1,68 @@ + + + \ No newline at end of file diff --git a/components/InlineUserWidget.vue b/components/InlineUserWidget.vue deleted file mode 100644 index c73cda6..0000000 --- a/components/InlineUserWidget.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - \ No newline at end of file diff --git a/components/LoadingButton.vue b/components/LoadingButton.vue new file mode 100644 index 0000000..b61303f --- /dev/null +++ b/components/LoadingButton.vue @@ -0,0 +1,31 @@ + + + diff --git a/components/PanelWidget.vue b/components/PanelWidget.vue new file mode 100644 index 0000000..be7ba0f --- /dev/null +++ b/components/PanelWidget.vue @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/components/UserFooter.vue b/components/UserFooter.vue index cb9705b..e5c22ef 100644 --- a/components/UserFooter.vue +++ b/components/UserFooter.vue @@ -18,18 +18,18 @@
-

Solutions

+

Games

-

Support

+

Community

-

Company

+

Documentation

-

Legal

+

About

    -
  • +
  • {{ item.name }}
  • @@ -67,29 +67,26 @@ import GithubLogo from './GithubLogo.vue'; import DiscordLogo from './DiscordLogo.vue'; const navigation = { - solutions: [ - { name: 'Marketing', href: '#' }, - { name: 'Analytics', href: '#' }, - { name: 'Commerce', href: '#' }, - { name: 'Insights', href: '#' }, + games: [ + { name: 'Newly Added', href: '#' }, + { name: 'New Releases', href: '#' }, + { name: 'Top Sellers', href: '#' }, + { name: 'Find a Game', href: '#' }, ], - support: [ - { name: 'Pricing', href: '#' }, - { name: 'Documentation', href: '#' }, - { name: 'Guides', href: '#' }, - { name: 'API Status', href: '#' }, + community: [ + { name: 'Friends', href: '#' }, + { name: 'Groups', href: '#' }, + { name: 'Servers', href: '#' }, ], - company: [ - { name: 'About', href: '#' }, - { name: 'Blog', href: '#' }, - { name: 'Jobs', href: '#' }, - { name: 'Press', href: '#' }, - { name: 'Partners', href: '#' }, + documentation: [ + { name: 'API', href: '#' }, + { name: 'Server Docs', href: '#' }, + { name: 'Client Docs', href: '#' }, ], - legal: [ - { name: 'Claim', href: '#' }, - { name: 'Privacy', href: '#' }, - { name: 'Terms', href: '#' }, + about: [ + { name: 'About Drop', href: '#' }, + { name: 'Features', href: '#' }, + { name: 'FAQ', href: '#' }, ], social: [ { diff --git a/components/UserHeader.vue b/components/UserHeader.vue index a58743e..9bbaeef 100644 --- a/components/UserHeader.vue +++ b/components/UserHeader.vue @@ -3,7 +3,7 @@
@@ -26,8 +26,7 @@ \ No newline at end of file + titleTemplate(title) { + if (title) return `${title} | Drop`; + return `Drop`; + }, +}); + diff --git a/middleware/require-user.global.ts b/middleware/require-user.global.ts new file mode 100644 index 0000000..8273279 --- /dev/null +++ b/middleware/require-user.global.ts @@ -0,0 +1,15 @@ +const whitelistedPrefixes = ["/signin", "/register"]; + +export default defineNuxtRouteMiddleware(async (to, from) => { + if (import.meta.server) return; + if (whitelistedPrefixes.findIndex((e) => to.fullPath.startsWith(e)) != -1) + return; + + const user = useUser(); + if (user === undefined) { + await updateUser(); + } + if (!user.value) { + return navigateTo({ path: "/signin", query: { redirect: to.fullPath } }); + } +}); diff --git a/package.json b/package.json index b733015..906ec72 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "postinstall": "nuxt prepare" }, "dependencies": { + "@drop/droplet": "^0.2.0", + "@drop/droplet-linux-x64-gnu": "^0.2.0", "@headlessui/vue": "^1.7.23", "@heroicons/vue": "^2.1.5", "@prisma/client": "5.20.0", @@ -25,6 +27,7 @@ }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", "devDependencies": { + "@tailwindcss/forms": "^0.5.9", "@types/bcrypt": "^5.0.2", "@types/turndown": "^5.0.5", "@types/uuid": "^10.0.0", diff --git a/pages/admin/index.vue b/pages/admin/index.vue new file mode 100644 index 0000000..ad33254 --- /dev/null +++ b/pages/admin/index.vue @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/pages/index.vue b/pages/index.vue index 99393bc..48b5df7 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -1,9 +1,11 @@ \ No newline at end of file + title: "Home", +}); + +const user = useUser(); + diff --git a/pages/register.vue b/pages/register.vue index 983f2de..210fbc3 100644 --- a/pages/register.vue +++ b/pages/register.vue @@ -12,7 +12,7 @@ const username = ref(""); const password = ref(""); async function register() { - await $fetch('/api/v1/signup/simple', { + await $fetch('/api/v1/auth/signup/simple', { method: "POST", body: { username: username.value, diff --git a/pages/signin.vue b/pages/signin.vue index 89d67a1..0057975 100644 --- a/pages/signin.vue +++ b/pages/signin.vue @@ -1,23 +1,172 @@ \ No newline at end of file + +async function signin() { + await $fetch("/api/v1/auth/signin/simple", { + method: "POST", + body: { + username: username.value, + password: password.value, + rememberMe: rememberMe.value, + }, + }); + const user = useUser(); + user.value = await $fetch("/api/v1/whoami"); +} + +definePageMeta({ + layout: false, +}); + +useHead({ + title: "Sign in to Drop", +}); + diff --git a/prisma/migrations/20241007043002_add_user_admin/migration.sql b/prisma/migrations/20241007043002_add_user_admin/migration.sql new file mode 100644 index 0000000..554507b --- /dev/null +++ b/prisma/migrations/20241007043002_add_user_admin/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "admin" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/migrations/20241007065541_add_client/migration.sql b/prisma/migrations/20241007065541_add_client/migration.sql new file mode 100644 index 0000000..efca156 --- /dev/null +++ b/prisma/migrations/20241007065541_add_client/migration.sql @@ -0,0 +1,15 @@ +-- CreateEnum +CREATE TYPE "ClientCapabilities" AS ENUM ('DownloadAggregation'); + +-- CreateTable +CREATE TABLE "Client" ( + "sharedToken" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "endpoint" TEXT NOT NULL, + "capabilities" "ClientCapabilities"[], + + CONSTRAINT "Client_pkey" PRIMARY KEY ("sharedToken") +); + +-- AddForeignKey +ALTER TABLE "Client" ADD CONSTRAINT "Client_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 31f6a07..4dddcb5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,10 +14,12 @@ datasource db { } model User { - id String @id @default(uuid()) - username String @unique + id String @id @default(uuid()) + username String @unique + admin Boolean @default(false) authMecs LinkedAuthMec[] + clients Client[] } enum AuthMec { @@ -35,6 +37,20 @@ model LinkedAuthMec { @@id([userId, mec]) } +enum ClientCapabilities { + DownloadAggregation +} + +// References a device +model Client { + sharedToken String @id @default(uuid()) + userId String + user User @relation(fields: [userId], references: [id]) + + endpoint String + capabilities ClientCapabilities[] +} + enum MetadataSource { Custom GiantBomb diff --git a/public/fonts/inter/InterVariable-Italic.ttf b/public/fonts/inter/InterVariable-Italic.ttf new file mode 100644 index 0000000..ed674e7 Binary files /dev/null and b/public/fonts/inter/InterVariable-Italic.ttf differ diff --git a/public/fonts/inter/InterVariable.ttf b/public/fonts/inter/InterVariable.ttf new file mode 100644 index 0000000..2d4b470 Binary files /dev/null and b/public/fonts/inter/InterVariable.ttf differ diff --git a/public/wallpapers/signin.jpg b/public/wallpapers/signin.jpg new file mode 100644 index 0000000..509343e Binary files /dev/null and b/public/wallpapers/signin.jpg differ diff --git a/server/api/v1/signin/simple.post.ts b/server/api/v1/auth/signin/simple.post.ts similarity index 91% rename from server/api/v1/signin/simple.post.ts rename to server/api/v1/auth/signin/simple.post.ts index 7f649c2..54e9f14 100644 --- a/server/api/v1/signin/simple.post.ts +++ b/server/api/v1/auth/signin/simple.post.ts @@ -8,6 +8,7 @@ export default defineEventHandler(async (h3) => { const username = body.username; const password = body.password; + const rememberMe = body.rememberMe ?? false; if (username === undefined || password === undefined) throw createError({ statusCode: 403, statusMessage: "Username or password missing from request." }); @@ -30,7 +31,7 @@ export default defineEventHandler(async (h3) => { if (!await checkHash(password, hash.toString())) throw createError({ statusCode: 401, statusMessage: "Invalid username or password." }); - await h3.context.session.setUserId(h3, authMek.userId); + await h3.context.session.setUserId(h3, authMek.userId, rememberMe); return { result: true, userId: authMek.userId } }); \ No newline at end of file diff --git a/server/api/v1/signup/simple.post.ts b/server/api/v1/auth/signup/simple.post.ts similarity index 100% rename from server/api/v1/signup/simple.post.ts rename to server/api/v1/auth/signup/simple.post.ts diff --git a/server/api/v1/client/handshake.post.ts b/server/api/v1/client/handshake.post.ts new file mode 100644 index 0000000..7b67c41 --- /dev/null +++ b/server/api/v1/client/handshake.post.ts @@ -0,0 +1,3 @@ +export default defineEventHandler((h3) => { + +}) \ No newline at end of file diff --git a/server/api/v1/client/initiate.post.ts b/server/api/v1/client/initiate.post.ts new file mode 100644 index 0000000..5475725 --- /dev/null +++ b/server/api/v1/client/initiate.post.ts @@ -0,0 +1,3 @@ +export default defineEventHandler(async (h3) => { + +}); \ No newline at end of file diff --git a/server/api/v1/client/session.post.ts b/server/api/v1/client/session.post.ts new file mode 100644 index 0000000..7b67c41 --- /dev/null +++ b/server/api/v1/client/session.post.ts @@ -0,0 +1,3 @@ +export default defineEventHandler((h3) => { + +}) \ No newline at end of file diff --git a/server/api/v1/whoami.get.ts b/server/api/v1/whoami.get.ts index 02fc5db..6c34792 100644 --- a/server/api/v1/whoami.get.ts +++ b/server/api/v1/whoami.get.ts @@ -1,5 +1,4 @@ export default defineEventHandler(async (h3) => { const user = await h3.context.session.getUser(h3); - - return user ?? {}; + return user ?? null; }); \ No newline at end of file diff --git a/server/internal/clients/README.md b/server/internal/clients/README.md new file mode 100644 index 0000000..666c5b7 --- /dev/null +++ b/server/internal/clients/README.md @@ -0,0 +1,26 @@ +# Client Handshake process + +Drop clients need to complete a handshake in order to connect to a Drop server. It also trades certificates for encrypted P2P connections. + +## 1. Client requests a handshake +Client makes request: `POST /api/v1/client/initiate` with information about the client. + +Server responds with a URL to send the user to. It generates a device ID, which has all the metadata attached. + +## 2. User signs in +Client sends user to the provided URL (in external browser). User signs in using the existing authentication stack. + +Server sends redirect to drop://handshake/[id]/[token], where the token is an authentication token to generate the necessary certificates, and the ID is the client ID as generated by the server. + +## 3. Client requests certificates +Client makes request: `POST /api/v1/client/handshake` with the token recieved in the previous step. + +The server uses it's CA to generate a public-private key pair, the CN of the client ID. It then sends that pair, plus the CA's public key, to the client, which stores it all. + +The certificate lasts for a year, and is rotated when it has 3 months or less left on it's expiry. + +## 4.a Client requests one-time device endpoint +The client generates a nonce and signs it with their private key. This is then attached to any device-related request. + +## 4.b Client wants a long-lived session +The client does the same as above, but instead makes the request to `POST /api/v1/client/session`, which generates a session token that lasts for a day. This can then be used in the request to provide authentication. \ No newline at end of file diff --git a/server/internal/clients/ca.ts b/server/internal/clients/ca.ts new file mode 100644 index 0000000..15b9ab9 --- /dev/null +++ b/server/internal/clients/ca.ts @@ -0,0 +1,34 @@ +import path from "path"; +import droplet from "@drop/droplet"; +import { CertificateStore } from "./store"; + +export type CertificateBundle = { + priv: string; + pub: string; + cert: string; +}; + +/* +This is designed to handle client certificates, as described in the README.md +*/ +export class CertificateAuthority { + private certificateStore: CertificateStore; + + private root: CertificateBundle; + + constructor(store: CertificateStore, root: CertificateBundle) { + this.certificateStore = store; + this.root = root; + } + + static async new(store: CertificateStore) { + const root = await store.fetch("ca"); + if (root === undefined) { + const [priv, pub, cert] = droplet.generateRootCa(); + const bundle: CertificateBundle = { priv, pub, cert }; + await store.store("ca", bundle); + return new CertificateAuthority(store, bundle); + } + return new CertificateAuthority(store, root); + } +} diff --git a/server/internal/clients/store.ts b/server/internal/clients/store.ts new file mode 100644 index 0000000..29f323d --- /dev/null +++ b/server/internal/clients/store.ts @@ -0,0 +1,23 @@ +import path from "path"; +import fs from "fs"; +import { CertificateBundle } from "./ca"; + +export type CertificateStore = { + store(name: string, data: CertificateBundle): Promise; + fetch(name: string): Promise; +}; + +export const fsCertificateStore = (base: string) => { + const store: CertificateStore = { + async store(name: string, data: CertificateBundle) { + const filepath = path.join(base, name); + fs.writeFileSync(filepath, JSON.stringify(data)); + }, + async fetch(name: string) { + const filepath = path.join(base, name); + if (!fs.existsSync(filepath)) return undefined; + return JSON.parse(fs.readFileSync(filepath, "utf-8")); + }, + }; + return store; +}; diff --git a/server/internal/downloads/README.md b/server/internal/downloads/README.md new file mode 100644 index 0000000..7369010 --- /dev/null +++ b/server/internal/downloads/README.md @@ -0,0 +1,5 @@ +# Drop Download System +The Drop download system uses a torrent-*like* system. It is not torrenting, nor is it compatible with torrenting clients. + +## Clients +Drop clients have built-in HTTP APIs that they forward with UPnP. This API exposes different capabilities for different Drop features, like download aggegration and P2P networking. When they sign on, they send a list of supported capabilities to the server. \ No newline at end of file diff --git a/server/internal/downloads/coordinator.ts b/server/internal/downloads/coordinator.ts new file mode 100644 index 0000000..4eaac7b --- /dev/null +++ b/server/internal/downloads/coordinator.ts @@ -0,0 +1,10 @@ +/* +The download co-ordinator's job is to keep track of all the currently online clients. + +When a client signs on and registers itself as a peer + +*/ + +class DownloadCoordinator { + +} \ No newline at end of file diff --git a/server/internal/session/index.ts b/server/internal/session/index.ts index 34f2bdf..794dadd 100644 --- a/server/internal/session/index.ts +++ b/server/internal/session/index.ts @@ -26,11 +26,11 @@ export class SessionHandler { return data[userSessionKey]; } - async setSession(h3: H3Event, data: any) { + async setSession(h3: H3Event, data: any, expend = false) { const result = await this.sessionProvider.updateSession(h3, userSessionKey, data); if (!result) { const toCreate = { [userSessionKey]: data }; - await this.sessionProvider.setSession(h3, toCreate); + await this.sessionProvider.setSession(h3, toCreate, expend); } } async clearSession(h3: H3Event) { @@ -52,11 +52,11 @@ export class SessionHandler { return user; } - async setUserId(h3: H3Event, userId: string) { + async setUserId(h3: H3Event, userId: string, extend = false) { const result = await this.sessionProvider.updateSession(h3, userIdKey, userId); if (!result) { const toCreate = { [userIdKey]: userId }; - await this.sessionProvider.setSession(h3, toCreate); + await this.sessionProvider.setSession(h3, toCreate, extend); } } } diff --git a/server/internal/session/memory.ts b/server/internal/session/memory.ts index c4e486c..0ca3a50 100644 --- a/server/internal/session/memory.ts +++ b/server/internal/session/memory.ts @@ -1,45 +1,45 @@ import moment from "moment"; import { Session, SessionProvider } from "./types"; -import { v4 as uuidv4 } from 'uuid'; +import { v4 as uuidv4 } from "uuid"; export default function createMemorySessionHandler() { - const sessions: { [key: string]: Session } = {} + const sessions: { [key: string]: Session } = {}; - const sessionCookieName = "drop-session"; + const sessionCookieName = "drop-session"; - const memoryProvider: SessionProvider = { - async setSession(h3, data) { - const existingCookie = getCookie(h3, sessionCookieName); - if (existingCookie) delete sessions[existingCookie]; // Clear any previous session + const memoryProvider: SessionProvider = { + async setSession(h3, data, extend = false) { + const existingCookie = getCookie(h3, sessionCookieName); + if (existingCookie) delete sessions[existingCookie]; // Clear any previous session - const cookie = uuidv4(); - const expiry = moment().add(31, 'day'); - setCookie(h3, sessionCookieName, cookie, { expires: expiry.toDate() }); + const cookie = uuidv4(); + const expiry = moment().add(31, extend ? "month" : "day"); + setCookie(h3, sessionCookieName, cookie, { expires: expiry.toDate() }); - sessions[cookie] = data; - return true; - }, - async updateSession(h3, key, data) { - const cookie = getCookie(h3, sessionCookieName); - if (!cookie) return false; + sessions[cookie] = data; + return true; + }, + async updateSession(h3, key, data) { + const cookie = getCookie(h3, sessionCookieName); + if (!cookie) return false; - sessions[cookie] = Object.assign({}, sessions[cookie], { [key]: data }); - return true; - }, - async getSession(h3) { - const cookie = getCookie(h3, sessionCookieName); - if (!cookie) return undefined; + sessions[cookie] = Object.assign({}, sessions[cookie], { [key]: data }); + return true; + }, + async getSession(h3) { + const cookie = getCookie(h3, sessionCookieName); + if (!cookie) return undefined; - return sessions[cookie] as any; // Wild type cast because we let the user specify types if they want - }, - async clearSession(h3) { - const cookie = getCookie(h3, sessionCookieName); - if (!cookie) return; + return sessions[cookie] as any; // Wild type cast because we let the user specify types if they want + }, + async clearSession(h3) { + const cookie = getCookie(h3, sessionCookieName); + if (!cookie) return; - delete sessions[cookie]; - deleteCookie(h3, sessionCookieName); - }, - }; + delete sessions[cookie]; + deleteCookie(h3, sessionCookieName); + }, + }; - return memoryProvider; -} \ No newline at end of file + return memoryProvider; +} diff --git a/server/internal/session/types.d.ts b/server/internal/session/types.d.ts index 10da2fd..7a6a602 100644 --- a/server/internal/session/types.d.ts +++ b/server/internal/session/types.d.ts @@ -3,8 +3,12 @@ import { H3Event } from "h3"; export type Session = { [key: string]: any }; export interface SessionProvider { - setSession: (h3: H3Event, data: Session) => Promise; - updateSession: (h3: H3Event, key: string, data: any) => Promise; - getSession: (h3: H3Event) => Promise; - clearSession: (h3: H3Event) => Promise; -} \ No newline at end of file + setSession: ( + h3: H3Event, + data: Session, + extend?: boolean + ) => Promise; + updateSession: (h3: H3Event, key: string, data: any) => Promise; + getSession: (h3: H3Event) => Promise; + clearSession: (h3: H3Event) => Promise; +} diff --git a/server/plugins/ca.ts b/server/plugins/ca.ts new file mode 100644 index 0000000..369d06f --- /dev/null +++ b/server/plugins/ca.ts @@ -0,0 +1,18 @@ +import { CertificateAuthority } from "../internal/clients/ca"; +import fs from "fs"; +import { fsCertificateStore } from "../internal/clients/store"; + +let ca: CertificateAuthority | undefined; + +export const useGlobalCertificateAuthority = () => { + if (!ca) throw new Error("CA not initialised"); + return ca; +}; + +export default defineNitroPlugin(async (nitro) => { + const basePath = process.env.CLIENT_CERTIFICATES ?? "./certs"; + fs.mkdirSync(basePath, { recursive: true }); + const store = fsCertificateStore(basePath); + + ca = await CertificateAuthority.new(store); +}); diff --git a/server/routes/signout.get.ts b/server/routes/signout.get.ts new file mode 100644 index 0000000..36b5a6f --- /dev/null +++ b/server/routes/signout.get.ts @@ -0,0 +1,5 @@ +export default defineEventHandler(async (h3) => { + await h3.context.session.clearSession(h3); + + return sendRedirect(h3, "/signin"); +}); diff --git a/tailwind.config.js b/tailwind.config.js index 02d554d..c510cf2 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -11,11 +11,13 @@ export default { theme: { extend: { fontFamily: { - sans: ["Helvetica"], + sans: ["Inter"], display: ["Motiva Sans"] } }, }, - plugins: [], + plugins: [ + require('@tailwindcss/forms'), + ], } diff --git a/yarn.lock b/yarn.lock index 6456fcf..f00a47b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -296,6 +296,24 @@ dependencies: mime "^3.0.0" +"@drop/droplet-linux-x64-gnu@0.2.0", "@drop/droplet-linux-x64-gnu@^0.2.0": + version "0.2.0" + resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-linux-x64-gnu/-/@drop/droplet-linux-x64-gnu-0.2.0.tgz#e1c0133abc38cf63cc8beaf5826db1946beb1165" + integrity sha1-4cATOrw4z2PMi+r1gm2xlGvrEWU= + +"@drop/droplet-win32-x64-msvc@0.2.0": + version "0.2.0" + resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet-win32-x64-msvc/-/@drop/droplet-win32-x64-msvc-0.2.0.tgz#0531e51e225530c277afcc7ac4230c8d99c8365e" + integrity sha1-BTHlHiJVMMJ3r8x6xCMMjZnINl4= + +"@drop/droplet@^0.2.0": + version "0.2.0" + resolved "https://lab.deepcore.dev/api/v4/projects/57/packages/npm/@drop/droplet/-/@drop/droplet-0.2.0.tgz#e4b6d2cf2bd5c0416fd3452ffa5b7c34267e160a" + integrity sha1-5LbSzyvVwEFv00Uv+lt8NCZ+Fgo= + optionalDependencies: + "@drop/droplet-linux-x64-gnu" "0.2.0" + "@drop/droplet-win32-x64-msvc" "0.2.0" + "@esbuild/aix-ppc64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" @@ -1279,6 +1297,13 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz#719df7fb41766bc143369eaa0dd56d8dc87c9958" integrity sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg== +"@tailwindcss/forms@^0.5.9": + version "0.5.9" + resolved "https://registry.yarnpkg.com/@tailwindcss/forms/-/forms-0.5.9.tgz#b495c12575d6eae5865b2cbd9876b26d89f16f61" + integrity sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg== + dependencies: + mini-svg-data-uri "^1.2.3" + "@tanstack/virtual-core@3.10.8": version "3.10.8" resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz#975446a667755222f62884c19e5c3c66d959b8b4" @@ -3502,6 +3527,11 @@ mimic-fn@^4.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== +mini-svg-data-uri@^1.2.3: + version "1.4.4" + resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz#8ab0aabcdf8c29ad5693ca595af19dd2ead09939" + integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg== + minimatch@^3.0.4, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"