From e420ed198b7ac730fd5542737f7fa2df840c5ade Mon Sep 17 00:00:00 2001 From: Robin Choice Date: Thu, 2 Apr 2026 13:23:10 +0200 Subject: [PATCH] Initial commit: Music Hub collaboration platform Full-stack music production collaboration tool with: - SvelteKit frontend with Design System (CSS vars, 8 shared components) - Hono API with auth, projects, tracks, versions, comments - PostgreSQL + Drizzle ORM (8 tables, roles, permissions) - S3-compatible storage with presigned upload URLs - wavesurfer.js audio player with waveform visualization - A/B version comparison with synchronized playback - Timestamped comments with threading and resolve workflow - Magic Link authentication with Resend email integration - Background audio processing (ffmpeg transcode + waveform peaks) - Role-based access control (Owner, Engineers, Artist, Label, Management, Viewer) - Toast notifications, skeleton loading, responsive layout - Docker deployment setup (API + Web + Postgres) Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.example | 8 + .gitignore | 41 + Dockerfile.api | 23 + Dockerfile.web | 23 + apps/api/package.json | 19 + apps/api/src/index.ts | 45 ++ apps/api/src/middleware/auth.ts | 37 + apps/api/src/routes/auth.ts | 159 ++++ apps/api/src/routes/comments.ts | 171 ++++ apps/api/src/routes/projects.ts | 294 +++++++ apps/api/src/routes/tracks.ts | 123 +++ apps/api/src/routes/versions.ts | 251 ++++++ apps/api/src/services/audio-processor.ts | 159 ++++ apps/api/src/services/email.ts | 71 ++ apps/api/src/storage/s3.ts | 49 ++ apps/api/src/types.ts | 8 + apps/api/tsconfig.json | 12 + apps/web/.gitignore | 23 + apps/web/.npmrc | 1 + apps/web/.vscode/extensions.json | 3 + apps/web/README.md | 42 + apps/web/package.json | 26 + apps/web/src/app.d.ts | 13 + apps/web/src/app.html | 12 + apps/web/src/lib/api/client.ts | 38 + apps/web/src/lib/assets/favicon.svg | 1 + .../src/lib/components/audio/ABCompare.svelte | 161 ++++ .../components/audio/UploadDropzone.svelte | 246 ++++++ .../components/audio/WaveformPlayer.svelte | 281 +++++++ apps/web/src/lib/components/ui/Avatar.svelte | 58 ++ apps/web/src/lib/components/ui/Badge.svelte | 52 ++ apps/web/src/lib/components/ui/Button.svelte | 129 +++ .../src/lib/components/ui/EmptyState.svelte | 57 ++ apps/web/src/lib/components/ui/Input.svelte | 80 ++ apps/web/src/lib/components/ui/Modal.svelte | 108 +++ .../web/src/lib/components/ui/Skeleton.svelte | 36 + .../lib/components/ui/ToastContainer.svelte | 104 +++ apps/web/src/lib/index.ts | 1 + apps/web/src/lib/stores/auth.ts | 38 + apps/web/src/lib/stores/toast.ts | 24 + apps/web/src/lib/utils/format.ts | 29 + apps/web/src/routes/+layout.svelte | 144 ++++ apps/web/src/routes/+page.svelte | 89 ++ apps/web/src/routes/auth/verify/+page.svelte | 56 ++ apps/web/src/routes/dashboard/+page.svelte | 182 +++++ .../routes/projects/[projectId]/+page.svelte | 218 +++++ .../[projectId]/settings/+page.svelte | 356 ++++++++ .../[projectId]/tracks/[trackId]/+page.svelte | 303 +++++++ .../[trackId]/components/CommentItem.svelte | 130 +++ .../components/CommentSection.svelte | 193 +++++ .../components/VersionHistory.svelte | 110 +++ .../[trackId]/components/VersionInfo.svelte | 86 ++ apps/web/src/routes/projects/new/+page.svelte | 108 +++ apps/web/static/robots.txt | 3 + apps/web/svelte.config.js | 24 + apps/web/tsconfig.json | 20 + apps/web/vite.config.ts | 14 + bun.lock | 719 +++++++++++++++++ docker-compose.prod.yml | 44 + docker-compose.yml | 27 + package.json | 19 + packages/db/drizzle.config.ts | 10 + packages/db/package.json | 21 + packages/db/src/index.ts | 12 + .../migrations/0000_magenta_apocalypse.sql | 108 +++ .../db/src/migrations/meta/0000_snapshot.json | 757 ++++++++++++++++++ packages/db/src/migrations/meta/_journal.json | 13 + packages/db/src/schema/auth.ts | 21 + packages/db/src/schema/comments.ts | 19 + packages/db/src/schema/index.ts | 5 + packages/db/src/schema/projects.ts | 54 ++ packages/db/src/schema/tracks.ts | 68 ++ packages/db/src/schema/users.ts | 10 + packages/db/tsconfig.json | 13 + packages/shared/package.json | 11 + packages/shared/src/constants/audio.ts | 24 + packages/shared/src/constants/index.ts | 3 + packages/shared/src/constants/permissions.ts | 69 ++ packages/shared/src/constants/roles.ts | 29 + packages/shared/src/index.ts | 2 + packages/shared/src/validation/auth.ts | 12 + packages/shared/src/validation/comment.ts | 14 + packages/shared/src/validation/index.ts | 4 + packages/shared/src/validation/project.ts | 30 + packages/shared/src/validation/track.ts | 32 + packages/shared/tsconfig.json | 13 + tsconfig.json | 29 + turbo.json | 22 + 88 files changed, 7306 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile.api create mode 100644 Dockerfile.web create mode 100644 apps/api/package.json create mode 100644 apps/api/src/index.ts create mode 100644 apps/api/src/middleware/auth.ts create mode 100644 apps/api/src/routes/auth.ts create mode 100644 apps/api/src/routes/comments.ts create mode 100644 apps/api/src/routes/projects.ts create mode 100644 apps/api/src/routes/tracks.ts create mode 100644 apps/api/src/routes/versions.ts create mode 100644 apps/api/src/services/audio-processor.ts create mode 100644 apps/api/src/services/email.ts create mode 100644 apps/api/src/storage/s3.ts create mode 100644 apps/api/src/types.ts create mode 100644 apps/api/tsconfig.json create mode 100644 apps/web/.gitignore create mode 100644 apps/web/.npmrc create mode 100644 apps/web/.vscode/extensions.json create mode 100644 apps/web/README.md create mode 100644 apps/web/package.json create mode 100644 apps/web/src/app.d.ts create mode 100644 apps/web/src/app.html create mode 100644 apps/web/src/lib/api/client.ts create mode 100644 apps/web/src/lib/assets/favicon.svg create mode 100644 apps/web/src/lib/components/audio/ABCompare.svelte create mode 100644 apps/web/src/lib/components/audio/UploadDropzone.svelte create mode 100644 apps/web/src/lib/components/audio/WaveformPlayer.svelte create mode 100644 apps/web/src/lib/components/ui/Avatar.svelte create mode 100644 apps/web/src/lib/components/ui/Badge.svelte create mode 100644 apps/web/src/lib/components/ui/Button.svelte create mode 100644 apps/web/src/lib/components/ui/EmptyState.svelte create mode 100644 apps/web/src/lib/components/ui/Input.svelte create mode 100644 apps/web/src/lib/components/ui/Modal.svelte create mode 100644 apps/web/src/lib/components/ui/Skeleton.svelte create mode 100644 apps/web/src/lib/components/ui/ToastContainer.svelte create mode 100644 apps/web/src/lib/index.ts create mode 100644 apps/web/src/lib/stores/auth.ts create mode 100644 apps/web/src/lib/stores/toast.ts create mode 100644 apps/web/src/lib/utils/format.ts create mode 100644 apps/web/src/routes/+layout.svelte create mode 100644 apps/web/src/routes/+page.svelte create mode 100644 apps/web/src/routes/auth/verify/+page.svelte create mode 100644 apps/web/src/routes/dashboard/+page.svelte create mode 100644 apps/web/src/routes/projects/[projectId]/+page.svelte create mode 100644 apps/web/src/routes/projects/[projectId]/settings/+page.svelte create mode 100644 apps/web/src/routes/projects/[projectId]/tracks/[trackId]/+page.svelte create mode 100644 apps/web/src/routes/projects/[projectId]/tracks/[trackId]/components/CommentItem.svelte create mode 100644 apps/web/src/routes/projects/[projectId]/tracks/[trackId]/components/CommentSection.svelte create mode 100644 apps/web/src/routes/projects/[projectId]/tracks/[trackId]/components/VersionHistory.svelte create mode 100644 apps/web/src/routes/projects/[projectId]/tracks/[trackId]/components/VersionInfo.svelte create mode 100644 apps/web/src/routes/projects/new/+page.svelte create mode 100644 apps/web/static/robots.txt create mode 100644 apps/web/svelte.config.js create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/vite.config.ts create mode 100644 bun.lock create mode 100644 docker-compose.prod.yml create mode 100644 docker-compose.yml create mode 100644 package.json create mode 100644 packages/db/drizzle.config.ts create mode 100644 packages/db/package.json create mode 100644 packages/db/src/index.ts create mode 100644 packages/db/src/migrations/0000_magenta_apocalypse.sql create mode 100644 packages/db/src/migrations/meta/0000_snapshot.json create mode 100644 packages/db/src/migrations/meta/_journal.json create mode 100644 packages/db/src/schema/auth.ts create mode 100644 packages/db/src/schema/comments.ts create mode 100644 packages/db/src/schema/index.ts create mode 100644 packages/db/src/schema/projects.ts create mode 100644 packages/db/src/schema/tracks.ts create mode 100644 packages/db/src/schema/users.ts create mode 100644 packages/db/tsconfig.json create mode 100644 packages/shared/package.json create mode 100644 packages/shared/src/constants/audio.ts create mode 100644 packages/shared/src/constants/index.ts create mode 100644 packages/shared/src/constants/permissions.ts create mode 100644 packages/shared/src/constants/roles.ts create mode 100644 packages/shared/src/index.ts create mode 100644 packages/shared/src/validation/auth.ts create mode 100644 packages/shared/src/validation/comment.ts create mode 100644 packages/shared/src/validation/index.ts create mode 100644 packages/shared/src/validation/project.ts create mode 100644 packages/shared/src/validation/track.ts create mode 100644 packages/shared/tsconfig.json create mode 100644 tsconfig.json create mode 100644 turbo.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6786b2a --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +DATABASE_URL=postgresql://musichub:musichub@localhost:5433/musichub +S3_ENDPOINT=http://localhost:9000 +S3_ACCESS_KEY=minioadmin +S3_SECRET_KEY=minioadmin +S3_BUCKET=music-hub +APP_URL=http://localhost:5173 +API_URL=http://localhost:3000 +MAGIC_LINK_SECRET=change-me-in-production diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5fb28c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +build +*.tgz + +# turbo +.turbo + +# svelte +.svelte-kit + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/Dockerfile.api b/Dockerfile.api new file mode 100644 index 0000000..1669f28 --- /dev/null +++ b/Dockerfile.api @@ -0,0 +1,23 @@ +FROM oven/bun:1 AS base +WORKDIR /app + +FROM base AS install +COPY package.json bun.lock ./ +COPY packages/shared/package.json ./packages/shared/ +COPY packages/db/package.json ./packages/db/ +COPY apps/api/package.json ./apps/api/ +RUN bun install --frozen-lockfile --production + +FROM base AS build +COPY --from=install /app/node_modules ./node_modules +COPY --from=install /app/packages/shared/node_modules ./packages/shared/node_modules +COPY --from=install /app/packages/db/node_modules ./packages/db/node_modules +COPY --from=install /app/apps/api/node_modules ./apps/api/node_modules +COPY . . + +FROM base AS production +RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/* +COPY --from=build /app . +EXPOSE 3000 +ENV NODE_ENV=production +CMD ["bun", "run", "apps/api/src/index.ts"] diff --git a/Dockerfile.web b/Dockerfile.web new file mode 100644 index 0000000..10420e2 --- /dev/null +++ b/Dockerfile.web @@ -0,0 +1,23 @@ +FROM oven/bun:1 AS base +WORKDIR /app + +FROM base AS install +COPY package.json bun.lock ./ +COPY packages/shared/package.json ./packages/shared/ +COPY apps/web/package.json ./apps/web/ +RUN bun install --frozen-lockfile + +FROM base AS build +COPY --from=install /app/node_modules ./node_modules +COPY --from=install /app/packages/shared/node_modules ./packages/shared/node_modules +COPY --from=install /app/apps/web/node_modules ./apps/web/node_modules +COPY . . +ENV PUBLIC_API_URL=/api +RUN cd apps/web && bun run build + +FROM base AS production +COPY --from=build /app/apps/web/build ./build +COPY --from=build /app/apps/web/package.json . +EXPOSE 3000 +ENV NODE_ENV=production +CMD ["bun", "./build/index.js"] diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..67e3129 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,19 @@ +{ + "name": "@music-hub/api", + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "bun run --hot src/index.ts", + "build": "bun build src/index.ts --outdir dist --target bun" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3", + "@aws-sdk/s3-request-presigner": "^3", + "@hono/zod-validator": "^0.5", + "@music-hub/db": "workspace:*", + "@music-hub/shared": "workspace:*", + "drizzle-orm": "^0.44", + "hono": "^4", + "resend": "^6.10.0" + } +} diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts new file mode 100644 index 0000000..0eb0c37 --- /dev/null +++ b/apps/api/src/index.ts @@ -0,0 +1,45 @@ +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { logger } from 'hono/logger'; +import { createDb } from '@music-hub/db'; +import { authRoutes } from './routes/auth.js'; +import { projectRoutes } from './routes/projects.js'; +import { trackRoutes } from './routes/tracks.js'; +import { versionRoutes } from './routes/versions.js'; +import { commentRoutes } from './routes/comments.js'; +import type { AppEnv } from './types.js'; + +const db = createDb(process.env.DATABASE_URL!); + +const app = new Hono() + .use('*', logger()) + .use( + '*', + cors({ + origin: process.env.APP_URL || 'http://localhost:5173', + credentials: true, + }), + ) + .use('*', async (c, next) => { + c.set('db', db); + await next(); + }) + .onError((err, c) => { + console.error('Unhandled error:', err); + return c.json({ error: err.message }, 500); + }) + .get('/health', (c) => c.json({ status: 'ok' })) + .basePath('/api/v1') + .route('/auth', authRoutes) + .route('/projects', projectRoutes) + .route('/tracks', trackRoutes) + .route('/versions', versionRoutes) + .route('/comments', commentRoutes); + +const port = parseInt(process.env.PORT || '3000'); +console.log(`Music Hub API running on port ${port}`); + +export default { + port, + fetch: app.fetch, +}; diff --git a/apps/api/src/middleware/auth.ts b/apps/api/src/middleware/auth.ts new file mode 100644 index 0000000..8235785 --- /dev/null +++ b/apps/api/src/middleware/auth.ts @@ -0,0 +1,37 @@ +import { createMiddleware } from 'hono/factory'; +import { getCookie } from 'hono/cookie'; +import { eq, gt } from 'drizzle-orm'; +import { sessions } from '@music-hub/db'; +import type { AppEnv } from '../types.js'; + +export const requireAuth = createMiddleware(async (c, next) => { + const sessionToken = getCookie(c, 'session'); + if (!sessionToken) { + return c.json({ error: 'Unauthorized' }, 401); + } + + const tokenHash = await hashToken(sessionToken); + const db = c.get('db'); + + const [session] = await db + .select() + .from(sessions) + .where(eq(sessions.tokenHash, tokenHash)) + .limit(1); + + if (!session || session.expiresAt < new Date()) { + return c.json({ error: 'Unauthorized' }, 401); + } + + c.set('userId', session.userId); + await next(); +}); + +export async function hashToken(token: string): Promise { + const encoder = new TextEncoder(); + const data = encoder.encode(token); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + return Array.from(new Uint8Array(hashBuffer)) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); +} diff --git a/apps/api/src/routes/auth.ts b/apps/api/src/routes/auth.ts new file mode 100644 index 0000000..a27e9be --- /dev/null +++ b/apps/api/src/routes/auth.ts @@ -0,0 +1,159 @@ +import { Hono } from 'hono'; +import { zValidator } from '@hono/zod-validator'; +import { setCookie, deleteCookie, getCookie } from 'hono/cookie'; +import { eq } from 'drizzle-orm'; +import { magicLinkSchema, verifyTokenSchema } from '@music-hub/shared'; +import { users, magicLinks, sessions } from '@music-hub/db'; +import { hashToken } from '../middleware/auth.js'; +import { sendMagicLinkEmail } from '../services/email.js'; +import type { AppEnv } from '../types.js'; + +export const authRoutes = new Hono() + .post('/magic-link', zValidator('json', magicLinkSchema), async (c) => { + const { email } = c.req.valid('json'); + const db = c.get('db'); + + const token = generateToken(); + const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 min + + await db.insert(magicLinks).values({ + email, + token, + expiresAt, + }); + + await sendMagicLinkEmail(email, token); + + return c.json({ message: 'Magic link sent' }); + }) + + .post('/verify', zValidator('json', verifyTokenSchema), async (c) => { + const { token } = c.req.valid('json'); + const db = c.get('db'); + + const [link] = await db + .select() + .from(magicLinks) + .where(eq(magicLinks.token, token)) + .limit(1); + + if (!link || link.expiresAt < new Date() || link.usedAt) { + return c.json({ error: 'Invalid or expired token' }, 400); + } + + await db + .update(magicLinks) + .set({ usedAt: new Date() }) + .where(eq(magicLinks.id, link.id)); + + // Find or create user + let [user] = await db + .select() + .from(users) + .where(eq(users.email, link.email)) + .limit(1); + + if (!user) { + const name = link.email.split('@')[0]; + [user] = await db + .insert(users) + .values({ email: link.email, name }) + .returning(); + } + + // Create session + const sessionToken = generateToken(); + const tokenHash = await hashToken(sessionToken); + const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days + + await db.insert(sessions).values({ + userId: user.id, + tokenHash, + expiresAt, + }); + + setCookie(c, 'session', sessionToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'Lax', + path: '/', + maxAge: 30 * 24 * 60 * 60, + }); + + return c.json({ user: { id: user.id, email: user.email, name: user.name } }); + }) + + .post('/logout', async (c) => { + const sessionToken = getCookie(c, 'session'); + if (sessionToken) { + const db = c.get('db'); + const tokenHash = await hashToken(sessionToken); + await db.delete(sessions).where(eq(sessions.tokenHash, tokenHash)); + } + deleteCookie(c, 'session'); + return c.json({ message: 'Logged out' }); + }) + + .get('/me', async (c) => { + const sessionToken = getCookie(c, 'session'); + if (!sessionToken) { + return c.json({ user: null }); + } + + const db = c.get('db'); + const tokenHash = await hashToken(sessionToken); + + const [session] = await db + .select() + .from(sessions) + .where(eq(sessions.tokenHash, tokenHash)) + .limit(1); + + if (!session || session.expiresAt < new Date()) { + return c.json({ user: null }); + } + + const [user] = await db + .select({ id: users.id, email: users.email, name: users.name, avatarUrl: users.avatarUrl }) + .from(users) + .where(eq(users.id, session.userId)) + .limit(1); + + return c.json({ user: user || null }); + }) + + .patch('/me', async (c) => { + const sessionToken = getCookie(c, 'session'); + if (!sessionToken) return c.json({ error: 'Unauthorized' }, 401); + + const db = c.get('db'); + const tokenHash = await hashToken(sessionToken); + const [session] = await db + .select() + .from(sessions) + .where(eq(sessions.tokenHash, tokenHash)) + .limit(1); + + if (!session || session.expiresAt < new Date()) { + return c.json({ error: 'Unauthorized' }, 401); + } + + const body = await c.req.json<{ name?: string }>(); + if (!body.name?.trim()) return c.json({ error: 'Name is required' }, 400); + + const [user] = await db + .update(users) + .set({ name: body.name.trim(), updatedAt: new Date() }) + .where(eq(users.id, session.userId)) + .returning({ id: users.id, email: users.email, name: users.name, avatarUrl: users.avatarUrl }); + + return c.json({ user }); + }); + +function generateToken(): string { + const bytes = new Uint8Array(32); + crypto.getRandomValues(bytes); + return Array.from(bytes) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); +} diff --git a/apps/api/src/routes/comments.ts b/apps/api/src/routes/comments.ts new file mode 100644 index 0000000..66a83eb --- /dev/null +++ b/apps/api/src/routes/comments.ts @@ -0,0 +1,171 @@ +import { Hono } from 'hono'; +import { zValidator } from '@hono/zod-validator'; +import { eq, and, asc } from 'drizzle-orm'; +import { createCommentSchema, updateCommentSchema } from '@music-hub/shared'; +import { comments, versions, tracks, projectMembers, users } from '@music-hub/db'; +import { requireAuth } from '../middleware/auth.js'; +import type { AppEnv } from '../types.js'; + +export const commentRoutes = new Hono() + .use('*', requireAuth) + + // Get comments for a version + .get('/version/:versionId', async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + const versionId = c.req.param('versionId'); + + const [version] = await db + .select() + .from(versions) + .where(eq(versions.id, versionId)) + .limit(1); + + if (!version) return c.json({ error: 'Not found' }, 404); + + const [track] = await db + .select() + .from(tracks) + .where(eq(tracks.id, version.trackId)) + .limit(1); + + const [membership] = await db + .select() + .from(projectMembers) + .where( + and(eq(projectMembers.projectId, track!.projectId), eq(projectMembers.userId, userId)), + ) + .limit(1); + + if (!membership) return c.json({ error: 'Not found' }, 404); + + const versionComments = await db + .select({ + id: comments.id, + body: comments.body, + timestampSeconds: comments.timestampSeconds, + parentId: comments.parentId, + resolvedAt: comments.resolvedAt, + createdAt: comments.createdAt, + user: { + id: users.id, + name: users.name, + avatarUrl: users.avatarUrl, + }, + }) + .from(comments) + .innerJoin(users, eq(users.id, comments.userId)) + .where(eq(comments.versionId, versionId)) + .orderBy(asc(comments.createdAt)); + + return c.json({ comments: versionComments }); + }) + + // Create comment + .post( + '/version/:versionId', + zValidator('json', createCommentSchema), + async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + const versionId = c.req.param('versionId'); + const input = c.req.valid('json'); + + const [version] = await db + .select() + .from(versions) + .where(eq(versions.id, versionId)) + .limit(1); + + if (!version) return c.json({ error: 'Not found' }, 404); + + const [track] = await db + .select() + .from(tracks) + .where(eq(tracks.id, version.trackId)) + .limit(1); + + const [membership] = await db + .select() + .from(projectMembers) + .where( + and(eq(projectMembers.projectId, track!.projectId), eq(projectMembers.userId, userId)), + ) + .limit(1); + + if (!membership || !membership.canComment) { + return c.json({ error: 'Forbidden' }, 403); + } + + const [comment] = await db + .insert(comments) + .values({ + versionId, + userId, + body: input.body, + timestampSeconds: input.timestampSeconds, + parentId: input.parentId, + }) + .returning(); + + return c.json({ comment }, 201); + }, + ) + + // Update comment + .patch('/:id', zValidator('json', updateCommentSchema), async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + const commentId = c.req.param('id'); + const input = c.req.valid('json'); + + const [comment] = await db + .select() + .from(comments) + .where(and(eq(comments.id, commentId), eq(comments.userId, userId))) + .limit(1); + + if (!comment) return c.json({ error: 'Not found' }, 404); + + const [updated] = await db + .update(comments) + .set({ body: input.body, updatedAt: new Date() }) + .where(eq(comments.id, commentId)) + .returning(); + + return c.json({ comment: updated }); + }) + + // Delete comment + .delete('/:id', async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + const commentId = c.req.param('id'); + + const [comment] = await db + .select() + .from(comments) + .where(and(eq(comments.id, commentId), eq(comments.userId, userId))) + .limit(1); + + if (!comment) return c.json({ error: 'Not found' }, 404); + + await db.delete(comments).where(eq(comments.id, commentId)); + return c.json({ message: 'Comment deleted' }); + }) + + // Resolve comment + .post('/:id/resolve', async (c) => { + const db = c.get('db'); + const commentId = c.req.param('id'); + + const [updated] = await db + .update(comments) + .set({ resolvedAt: new Date() }) + .where(eq(comments.id, commentId)) + .returning(); + + if (!updated) return c.json({ error: 'Not found' }, 404); + + return c.json({ comment: updated }); + }); diff --git a/apps/api/src/routes/projects.ts b/apps/api/src/routes/projects.ts new file mode 100644 index 0000000..b005ae5 --- /dev/null +++ b/apps/api/src/routes/projects.ts @@ -0,0 +1,294 @@ +import { Hono } from 'hono'; +import { zValidator } from '@hono/zod-validator'; +import { eq, and } from 'drizzle-orm'; +import { + createProjectSchema, + updateProjectSchema, + inviteMemberSchema, + updateMemberSchema, +} from '@music-hub/shared'; +import { projects, projectMembers, users } from '@music-hub/db'; +import { requireAuth } from '../middleware/auth.js'; +import type { AppEnv } from '../types.js'; + +export const projectRoutes = new Hono() + .use('*', requireAuth) + + .get('/', async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + + const memberships = await db + .select({ + project: projects, + role: projectMembers.role, + }) + .from(projectMembers) + .innerJoin(projects, eq(projects.id, projectMembers.projectId)) + .where(and(eq(projectMembers.userId, userId), eq(projects.isArchived, false))); + + return c.json({ projects: memberships }); + }) + + .post('/', zValidator('json', createProjectSchema), async (c) => { + const input = c.req.valid('json'); + const db = c.get('db'); + const userId = c.get('userId'); + + const [project] = await db + .insert(projects) + .values({ ...input, createdById: userId }) + .returning(); + + await db.insert(projectMembers).values({ + projectId: project.id, + userId, + role: 'owner', + canUpload: true, + canComment: true, + canApprove: true, + }); + + return c.json({ project }, 201); + }) + + .get('/:id', async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + const projectId = c.req.param('id'); + + const [membership] = await db + .select() + .from(projectMembers) + .where(and(eq(projectMembers.projectId, projectId), eq(projectMembers.userId, userId))) + .limit(1); + + if (!membership) { + return c.json({ error: 'Not found' }, 404); + } + + const [project] = await db + .select() + .from(projects) + .where(eq(projects.id, projectId)) + .limit(1); + + return c.json({ project, role: membership.role }); + }) + + .patch('/:id', zValidator('json', updateProjectSchema), async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + const projectId = c.req.param('id'); + const input = c.req.valid('json'); + + const [membership] = await db + .select() + .from(projectMembers) + .where( + and( + eq(projectMembers.projectId, projectId), + eq(projectMembers.userId, userId), + eq(projectMembers.role, 'owner'), + ), + ) + .limit(1); + + if (!membership) { + return c.json({ error: 'Forbidden' }, 403); + } + + const [project] = await db + .update(projects) + .set({ ...input, updatedAt: new Date() }) + .where(eq(projects.id, projectId)) + .returning(); + + return c.json({ project }); + }) + + .delete('/:id', async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + const projectId = c.req.param('id'); + + const [membership] = await db + .select() + .from(projectMembers) + .where( + and( + eq(projectMembers.projectId, projectId), + eq(projectMembers.userId, userId), + eq(projectMembers.role, 'owner'), + ), + ) + .limit(1); + + if (!membership) { + return c.json({ error: 'Forbidden' }, 403); + } + + await db + .update(projects) + .set({ isArchived: true, updatedAt: new Date() }) + .where(eq(projects.id, projectId)); + + return c.json({ message: 'Project archived' }); + }) + + // Members + .get('/:id/members', async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + const projectId = c.req.param('id'); + + // Check access + const [membership] = await db + .select() + .from(projectMembers) + .where(and(eq(projectMembers.projectId, projectId), eq(projectMembers.userId, userId))) + .limit(1); + + if (!membership) { + return c.json({ error: 'Not found' }, 404); + } + + const members = await db + .select({ + id: projectMembers.id, + role: projectMembers.role, + canUpload: projectMembers.canUpload, + canComment: projectMembers.canComment, + canApprove: projectMembers.canApprove, + user: { + id: users.id, + email: users.email, + name: users.name, + avatarUrl: users.avatarUrl, + }, + }) + .from(projectMembers) + .innerJoin(users, eq(users.id, projectMembers.userId)) + .where(eq(projectMembers.projectId, projectId)); + + return c.json({ members }); + }) + + .post('/:id/members', zValidator('json', inviteMemberSchema), async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + const projectId = c.req.param('id'); + const { email, role } = c.req.valid('json'); + + // Check permission (owner or management) + const [membership] = await db + .select() + .from(projectMembers) + .where(and(eq(projectMembers.projectId, projectId), eq(projectMembers.userId, userId))) + .limit(1); + + if (!membership || (membership.role !== 'owner' && membership.role !== 'management')) { + return c.json({ error: 'Forbidden' }, 403); + } + + // Find or create user + let [invitedUser] = await db + .select() + .from(users) + .where(eq(users.email, email)) + .limit(1); + + if (!invitedUser) { + [invitedUser] = await db + .insert(users) + .values({ email, name: email.split('@')[0] }) + .returning(); + } + + const defaults = getRoleDefaults(role); + const [member] = await db + .insert(projectMembers) + .values({ + projectId, + userId: invitedUser.id, + role, + ...defaults, + }) + .onConflictDoNothing() + .returning(); + + if (!member) { + return c.json({ error: 'User already a member' }, 409); + } + + return c.json({ member }, 201); + }) + + .patch('/:id/members/:memberId', zValidator('json', updateMemberSchema), async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + const projectId = c.req.param('id'); + const memberId = c.req.param('memberId'); + const { role: newRole } = c.req.valid('json'); + + const [membership] = await db + .select() + .from(projectMembers) + .where( + and( + eq(projectMembers.projectId, projectId), + eq(projectMembers.userId, userId), + eq(projectMembers.role, 'owner'), + ), + ) + .limit(1); + + if (!membership) { + return c.json({ error: 'Forbidden' }, 403); + } + + const defaults = getRoleDefaults(newRole); + const [updated] = await db + .update(projectMembers) + .set({ role: newRole, ...defaults }) + .where(eq(projectMembers.id, memberId)) + .returning(); + + return c.json({ member: updated }); + }) + + .delete('/:id/members/:memberId', async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + const projectId = c.req.param('id'); + const memberId = c.req.param('memberId'); + + const [membership] = await db + .select() + .from(projectMembers) + .where( + and( + eq(projectMembers.projectId, projectId), + eq(projectMembers.userId, userId), + eq(projectMembers.role, 'owner'), + ), + ) + .limit(1); + + if (!membership) { + return c.json({ error: 'Forbidden' }, 403); + } + + await db.delete(projectMembers).where(eq(projectMembers.id, memberId)); + return c.json({ message: 'Member removed' }); + }); + +function getRoleDefaults(role: string) { + const engineerRoles = ['recording_engineer', 'mixing_engineer', 'mastering_engineer']; + if (role === 'owner') return { canUpload: true, canComment: true, canApprove: true }; + if (engineerRoles.includes(role)) return { canUpload: true, canComment: true, canApprove: false }; + if (role === 'artist') return { canUpload: false, canComment: true, canApprove: true }; + if (role === 'label') return { canUpload: false, canComment: true, canApprove: true }; + if (role === 'management') return { canUpload: false, canComment: true, canApprove: true }; + return { canUpload: false, canComment: false, canApprove: false }; // viewer +} diff --git a/apps/api/src/routes/tracks.ts b/apps/api/src/routes/tracks.ts new file mode 100644 index 0000000..6ec1a2a --- /dev/null +++ b/apps/api/src/routes/tracks.ts @@ -0,0 +1,123 @@ +import { Hono } from 'hono'; +import { zValidator } from '@hono/zod-validator'; +import { eq, and, asc } from 'drizzle-orm'; +import { createTrackSchema, updateTrackSchema } from '@music-hub/shared'; +import { tracks, projectMembers } from '@music-hub/db'; +import { requireAuth } from '../middleware/auth.js'; +import type { AppEnv } from '../types.js'; + +export const trackRoutes = new Hono() + .use('*', requireAuth) + + // Get all tracks for a project + .get('/project/:projectId', async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + const projectId = c.req.param('projectId'); + + const [membership] = await db + .select() + .from(projectMembers) + .where(and(eq(projectMembers.projectId, projectId), eq(projectMembers.userId, userId))) + .limit(1); + + if (!membership) return c.json({ error: 'Not found' }, 404); + + const projectTracks = await db + .select() + .from(tracks) + .where(eq(tracks.projectId, projectId)) + .orderBy(asc(tracks.sortOrder), asc(tracks.createdAt)); + + return c.json({ tracks: projectTracks }); + }) + + .post('/:projectId', zValidator('json', createTrackSchema), async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + const projectId = c.req.param('projectId'); + const input = c.req.valid('json'); + + const [membership] = await db + .select() + .from(projectMembers) + .where(and(eq(projectMembers.projectId, projectId), eq(projectMembers.userId, userId))) + .limit(1); + + if (!membership || !membership.canUpload) { + return c.json({ error: 'Forbidden' }, 403); + } + + const [track] = await db + .insert(tracks) + .values({ ...input, projectId, createdById: userId }) + .returning(); + + return c.json({ track }, 201); + }) + + .patch('/:id', zValidator('json', updateTrackSchema), async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + const trackId = c.req.param('id'); + const input = c.req.valid('json'); + + const [track] = await db + .select() + .from(tracks) + .where(eq(tracks.id, trackId)) + .limit(1); + + if (!track) return c.json({ error: 'Not found' }, 404); + + const [membership] = await db + .select() + .from(projectMembers) + .where(and(eq(projectMembers.projectId, track.projectId), eq(projectMembers.userId, userId))) + .limit(1); + + if (!membership || !membership.canUpload) { + return c.json({ error: 'Forbidden' }, 403); + } + + const [updated] = await db + .update(tracks) + .set({ ...input, updatedAt: new Date() }) + .where(eq(tracks.id, trackId)) + .returning(); + + return c.json({ track: updated }); + }) + + .delete('/:id', async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + const trackId = c.req.param('id'); + + const [track] = await db + .select() + .from(tracks) + .where(eq(tracks.id, trackId)) + .limit(1); + + if (!track) return c.json({ error: 'Not found' }, 404); + + const [membership] = await db + .select() + .from(projectMembers) + .where( + and( + eq(projectMembers.projectId, track.projectId), + eq(projectMembers.userId, userId), + eq(projectMembers.role, 'owner'), + ), + ) + .limit(1); + + if (!membership) { + return c.json({ error: 'Forbidden' }, 403); + } + + await db.delete(tracks).where(eq(tracks.id, trackId)); + return c.json({ message: 'Track deleted' }); + }); diff --git a/apps/api/src/routes/versions.ts b/apps/api/src/routes/versions.ts new file mode 100644 index 0000000..7022def --- /dev/null +++ b/apps/api/src/routes/versions.ts @@ -0,0 +1,251 @@ +import { Hono } from 'hono'; +import { zValidator } from '@hono/zod-validator'; +import { eq, and, desc, sql } from 'drizzle-orm'; +import { requestUploadUrlSchema, createVersionSchema } from '@music-hub/shared'; +import { tracks, versions, projectMembers } from '@music-hub/db'; +import { requireAuth } from '../middleware/auth.js'; +import { createUploadUrl, createDownloadUrl } from '../storage/s3.js'; +import { processVersion } from '../services/audio-processor.js'; +import type { AppEnv } from '../types.js'; + +export const versionRoutes = new Hono() + .use('*', requireAuth) + + // Get all versions for a track + .get('/track/:trackId', async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + const trackId = c.req.param('trackId'); + + const [track] = await db.select().from(tracks).where(eq(tracks.id, trackId)).limit(1); + if (!track) return c.json({ error: 'Not found' }, 404); + + const [membership] = await db + .select() + .from(projectMembers) + .where(and(eq(projectMembers.projectId, track.projectId), eq(projectMembers.userId, userId))) + .limit(1); + + if (!membership) return c.json({ error: 'Not found' }, 404); + + const trackVersions = await db + .select() + .from(versions) + .where(eq(versions.trackId, trackId)) + .orderBy(desc(versions.versionNumber)); + + return c.json({ versions: trackVersions }); + }) + + // Request presigned upload URL + .post( + '/track/:trackId/upload-url', + zValidator('json', requestUploadUrlSchema), + async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + const trackId = c.req.param('trackId'); + const { fileName, mimeType, fileSize } = c.req.valid('json'); + + const [track] = await db.select().from(tracks).where(eq(tracks.id, trackId)).limit(1); + if (!track) return c.json({ error: 'Not found' }, 404); + + const [membership] = await db + .select() + .from(projectMembers) + .where( + and(eq(projectMembers.projectId, track.projectId), eq(projectMembers.userId, userId)), + ) + .limit(1); + + if (!membership || !membership.canUpload) { + return c.json({ error: 'Forbidden' }, 403); + } + + const versionId = crypto.randomUUID(); + const fileKey = `projects/${track.projectId}/tracks/${trackId}/versions/${versionId}/original/${fileName}`; + + const uploadUrl = await createUploadUrl(fileKey, mimeType, fileSize); + + return c.json({ uploadUrl, fileKey, versionId }); + }, + ) + + // Register version after upload + .post('/track/:trackId', zValidator('json', createVersionSchema), async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + const trackId = c.req.param('trackId'); + const input = c.req.valid('json'); + + const [track] = await db.select().from(tracks).where(eq(tracks.id, trackId)).limit(1); + if (!track) return c.json({ error: 'Not found' }, 404); + + // Get next version number + const [latest] = await db + .select({ maxVersion: sql`coalesce(max(${versions.versionNumber}), 0)` }) + .from(versions) + .where(eq(versions.trackId, trackId)); + + const versionNumber = (latest?.maxVersion ?? 0) + 1; + + const [version] = await db + .insert(versions) + .values({ + trackId, + versionNumber, + label: input.label, + notes: input.notes, + status: 'uploaded', + originalFileName: input.originalFileName, + mimeType: input.mimeType, + fileSize: input.fileSize, + originalFileKey: input.fileKey, + createdById: userId, + }) + .returning(); + + // Background processing (fire and forget) + processVersion(db, version.id).catch((err) => + console.error(`[Worker] Failed: ${err.message}`), + ); + + return c.json({ version }, 201); + }) + + // Get stream URL + .get('/:id/stream-url', async (c) => { + const db = c.get('db'); + const versionId = c.req.param('id'); + + const [version] = await db + .select() + .from(versions) + .where(eq(versions.id, versionId)) + .limit(1); + + if (!version) return c.json({ error: 'Not found' }, 404); + + const key = version.streamFileKey || version.originalFileKey; + const url = await createDownloadUrl(key); + + return c.json({ url }); + }) + + // Get download URL + .get('/:id/download-url', async (c) => { + const db = c.get('db'); + const versionId = c.req.param('id'); + + const [version] = await db + .select() + .from(versions) + .where(eq(versions.id, versionId)) + .limit(1); + + if (!version) return c.json({ error: 'Not found' }, 404); + + const url = await createDownloadUrl(version.originalFileKey); + return c.json({ url }); + }) + + // Get waveform data + .get('/:id/waveform', async (c) => { + const db = c.get('db'); + const versionId = c.req.param('id'); + + const [version] = await db + .select() + .from(versions) + .where(eq(versions.id, versionId)) + .limit(1); + + if (!version || !version.waveformDataKey) { + return c.json({ error: 'Not found' }, 404); + } + + const url = await createDownloadUrl(version.waveformDataKey); + return c.json({ url }); + }) + + // Approve version + .post('/:id/approve', async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + const versionId = c.req.param('id'); + + const [version] = await db + .select() + .from(versions) + .where(eq(versions.id, versionId)) + .limit(1); + + if (!version) return c.json({ error: 'Not found' }, 404); + + const [track] = await db + .select() + .from(tracks) + .where(eq(tracks.id, version.trackId)) + .limit(1); + + const [membership] = await db + .select() + .from(projectMembers) + .where( + and(eq(projectMembers.projectId, track!.projectId), eq(projectMembers.userId, userId)), + ) + .limit(1); + + if (!membership || !membership.canApprove) { + return c.json({ error: 'Forbidden' }, 403); + } + + const [updated] = await db + .update(versions) + .set({ status: 'approved' }) + .where(eq(versions.id, versionId)) + .returning(); + + return c.json({ version: updated }); + }) + + // Reject version + .post('/:id/reject', async (c) => { + const db = c.get('db'); + const userId = c.get('userId'); + const versionId = c.req.param('id'); + + const [version] = await db + .select() + .from(versions) + .where(eq(versions.id, versionId)) + .limit(1); + + if (!version) return c.json({ error: 'Not found' }, 404); + + const [track] = await db + .select() + .from(tracks) + .where(eq(tracks.id, version.trackId)) + .limit(1); + + const [membership] = await db + .select() + .from(projectMembers) + .where( + and(eq(projectMembers.projectId, track!.projectId), eq(projectMembers.userId, userId)), + ) + .limit(1); + + if (!membership || !membership.canApprove) { + return c.json({ error: 'Forbidden' }, 403); + } + + const [updated] = await db + .update(versions) + .set({ status: 'rejected' }) + .where(eq(versions.id, versionId)) + .returning(); + + return c.json({ version: updated }); + }); diff --git a/apps/api/src/services/audio-processor.ts b/apps/api/src/services/audio-processor.ts new file mode 100644 index 0000000..3d15506 --- /dev/null +++ b/apps/api/src/services/audio-processor.ts @@ -0,0 +1,159 @@ +import { eq } from 'drizzle-orm'; +import { versions } from '@music-hub/db'; +import { createUploadUrl, createDownloadUrl } from '../storage/s3.js'; +import type { Database } from '@music-hub/db'; + +export async function processVersion(db: Database, versionId: string) { + console.log(`[Worker] Processing version ${versionId}`); + + // Mark as processing + const [version] = await db + .update(versions) + .set({ status: 'processing' }) + .where(eq(versions.id, versionId)) + .returning(); + + if (!version) { + console.error(`[Worker] Version ${versionId} not found`); + return; + } + + try { + const originalUrl = await createDownloadUrl(version.originalFileKey, 3600); + + // Extract metadata with ffprobe + const metadata = await extractMetadata(originalUrl); + + // Generate waveform peaks + const peaks = await generateWaveformPeaks(originalUrl, metadata.duration); + const waveformKey = version.originalFileKey.replace(/\/original\/.*$/, '/waveform/peaks.json'); + + // Upload waveform data to S3 + const waveformUploadUrl = await createUploadUrl(waveformKey, 'application/json', 10 * 1024 * 1024); + await fetch(waveformUploadUrl, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(peaks), + }); + + // Transcode to MP3 for streaming + const streamKey = version.originalFileKey.replace(/\/original\/.*$/, '/stream/audio.mp3'); + await transcodeToMp3(originalUrl, streamKey); + + // Update version with metadata + await db + .update(versions) + .set({ + status: 'ready', + duration: metadata.duration, + sampleRate: metadata.sampleRate, + bitDepth: metadata.bitDepth, + streamFileKey: streamKey, + waveformDataKey: waveformKey, + }) + .where(eq(versions.id, versionId)); + + console.log(`[Worker] Version ${versionId} ready`); + } catch (error) { + console.error(`[Worker] Failed to process version ${versionId}:`, error); + // Still mark as ready so user can listen to original + await db + .update(versions) + .set({ status: 'ready' }) + .where(eq(versions.id, versionId)); + } +} + +async function extractMetadata(url: string): Promise<{ + duration: number; + sampleRate: number; + bitDepth: number; +}> { + const proc = Bun.spawn([ + 'ffprobe', + '-v', 'quiet', + '-print_format', 'json', + '-show_format', + '-show_streams', + url, + ]); + + const output = await new Response(proc.stdout).text(); + await proc.exited; + + const data = JSON.parse(output); + const audioStream = data.streams?.find((s: any) => s.codec_type === 'audio'); + + return { + duration: parseFloat(data.format?.duration || '0'), + sampleRate: parseInt(audioStream?.sample_rate || '44100'), + bitDepth: parseInt(audioStream?.bits_per_raw_sample || audioStream?.bits_per_sample || '16'), + }; +} + +async function generateWaveformPeaks(url: string, duration: number): Promise { + // Generate raw PCM samples with ffmpeg, then compute peaks + const samplesPerPixel = Math.max(1, Math.floor(duration * 44100 / 800)); // ~800 peaks + + const proc = Bun.spawn([ + 'ffmpeg', + '-i', url, + '-ac', '1', // mono + '-ar', '8000', // low sample rate for peaks + '-f', 'f32le', // raw 32-bit float + '-v', 'quiet', + 'pipe:1', + ]); + + const buffer = await new Response(proc.stdout).arrayBuffer(); + await proc.exited; + + const samples = new Float32Array(buffer); + const numPeaks = 800; + const blockSize = Math.max(1, Math.floor(samples.length / numPeaks)); + const peaks: number[] = []; + + for (let i = 0; i < numPeaks && i * blockSize < samples.length; i++) { + let max = 0; + const start = i * blockSize; + const end = Math.min(start + blockSize, samples.length); + for (let j = start; j < end; j++) { + const abs = Math.abs(samples[j]); + if (abs > max) max = abs; + } + peaks.push(Math.round(max * 1000) / 1000); + } + + return peaks; +} + +async function transcodeToMp3(inputUrl: string, outputKey: string) { + // Transcode to temp file, then upload + const tmpFile = `/tmp/musichub-${crypto.randomUUID()}.mp3`; + + const proc = Bun.spawn([ + 'ffmpeg', + '-i', inputUrl, + '-codec:a', 'libmp3lame', + '-b:a', '128k', + '-v', 'quiet', + '-y', + tmpFile, + ]); + + await proc.exited; + + // Upload to S3 + const file = Bun.file(tmpFile); + const fileSize = file.size; + const uploadUrl = await createUploadUrl(outputKey, 'audio/mpeg', fileSize); + + await fetch(uploadUrl, { + method: 'PUT', + headers: { 'Content-Type': 'audio/mpeg' }, + body: file, + }); + + // Cleanup + await Bun.file(tmpFile).exists() && (await Bun.spawn(['rm', tmpFile]).exited); +} diff --git a/apps/api/src/services/email.ts b/apps/api/src/services/email.ts new file mode 100644 index 0000000..3c01d26 --- /dev/null +++ b/apps/api/src/services/email.ts @@ -0,0 +1,71 @@ +import { Resend } from 'resend'; + +const resend = process.env.RESEND_API_KEY + ? new Resend(process.env.RESEND_API_KEY) + : null; + +const fromEmail = process.env.EMAIL_FROM || 'Music Hub '; + +export async function sendMagicLinkEmail(email: string, token: string) { + const url = `${process.env.APP_URL}/auth/verify?token=${token}`; + + if (!resend) { + console.log(`[DEV] Magic link for ${email}: ${url}`); + return; + } + + await resend.emails.send({ + from: fromEmail, + to: email, + subject: 'Your Music Hub Login Link', + html: ` +
+

Music Hub

+

Click the button below to log in:

+ Log in to Music Hub +

This link expires in 15 minutes.

+

If you didn't request this, ignore this email.

+
+ `, + }); +} + +export async function sendInviteEmail(email: string, projectName: string, inviterName: string) { + const url = `${process.env.APP_URL}`; + + if (!resend) { + console.log(`[DEV] Invite ${email} to project "${projectName}" by ${inviterName}`); + return; + } + + await resend.emails.send({ + from: fromEmail, + to: email, + subject: `You've been invited to "${projectName}" on Music Hub`, + html: ` +
+

Music Hub

+

${inviterName} invited you to collaborate on "${projectName}".

+ Open Music Hub +
+ `, + }); +} diff --git a/apps/api/src/storage/s3.ts b/apps/api/src/storage/s3.ts new file mode 100644 index 0000000..bef8878 --- /dev/null +++ b/apps/api/src/storage/s3.ts @@ -0,0 +1,49 @@ +import { + S3Client, + PutObjectCommand, + GetObjectCommand, + DeleteObjectCommand, +} from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; + +const s3 = new S3Client({ + region: 'eu-central-1', + endpoint: process.env.S3_ENDPOINT, + credentials: { + accessKeyId: process.env.S3_ACCESS_KEY!, + secretAccessKey: process.env.S3_SECRET_KEY!, + }, + forcePathStyle: true, +}); + +const bucket = process.env.S3_BUCKET!; + +export async function createUploadUrl( + key: string, + contentType: string, + maxSize: number, +): Promise { + const command = new PutObjectCommand({ + Bucket: bucket, + Key: key, + ContentType: contentType, + ContentLength: maxSize, + }); + return getSignedUrl(s3, command, { expiresIn: 900 }); // 15 min +} + +export async function createDownloadUrl(key: string, expiresIn = 3600): Promise { + const command = new GetObjectCommand({ + Bucket: bucket, + Key: key, + }); + return getSignedUrl(s3, command, { expiresIn }); +} + +export async function deleteObject(key: string): Promise { + const command = new DeleteObjectCommand({ + Bucket: bucket, + Key: key, + }); + await s3.send(command); +} diff --git a/apps/api/src/types.ts b/apps/api/src/types.ts new file mode 100644 index 0000000..2212d59 --- /dev/null +++ b/apps/api/src/types.ts @@ -0,0 +1,8 @@ +import type { Database } from '@music-hub/db'; + +export type AppEnv = { + Variables: { + db: Database; + userId: string; + }; +}; diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..60eef80 --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 0000000..3b462cb --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/apps/web/.npmrc b/apps/web/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/apps/web/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/apps/web/.vscode/extensions.json b/apps/web/.vscode/extensions.json new file mode 100644 index 0000000..28d1e67 --- /dev/null +++ b/apps/web/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["svelte.svelte-vscode"] +} diff --git a/apps/web/README.md b/apps/web/README.md new file mode 100644 index 0000000..7469b45 --- /dev/null +++ b/apps/web/README.md @@ -0,0 +1,42 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```sh +# create a new project +npx sv create my-app +``` + +To recreate this project with the same configuration: + +```sh +# recreate this project +npx sv@0.13.1 create --template minimal --types ts --no-install apps/web +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```sh +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```sh +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..5c85383 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,26 @@ +{ + "name": "@music-hub/web", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" + }, + "dependencies": { + "@music-hub/shared": "workspace:*", + "wavesurfer.js": "^7.12.5" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^7.0.0", + "@sveltejs/kit": "^2.50.2", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "svelte": "^5.54.0", + "svelte-check": "^4.4.2", + "typescript": "^5.9.3", + "vite": "^7.3.1" + } +} diff --git a/apps/web/src/app.d.ts b/apps/web/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/apps/web/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/apps/web/src/app.html b/apps/web/src/app.html new file mode 100644 index 0000000..6a2bb58 --- /dev/null +++ b/apps/web/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/apps/web/src/lib/api/client.ts b/apps/web/src/lib/api/client.ts new file mode 100644 index 0000000..08b3c75 --- /dev/null +++ b/apps/web/src/lib/api/client.ts @@ -0,0 +1,38 @@ +import { toastError } from '$lib/stores/toast.js'; + +type FetchOptions = { + method?: string; + body?: unknown; + headers?: Record; + silent?: boolean; +}; + +async function request(path: string, options: FetchOptions = {}): Promise { + const { method = 'GET', body, headers = {}, silent = false } = options; + + const res = await fetch(`/api/v1${path}`, { + method, + headers: { + 'Content-Type': 'application/json', + ...headers, + }, + credentials: 'include', + body: body ? JSON.stringify(body) : undefined, + }); + + if (!res.ok) { + const error = await res.json().catch(() => ({ error: res.statusText })); + const message = error.error || 'Request failed'; + if (!silent) toastError(message); + throw new Error(message); + } + + return res.json(); +} + +export const api = { + get: (path: string, silent = false) => request(path, { silent }), + post: (path: string, body?: unknown) => request(path, { method: 'POST', body }), + patch: (path: string, body?: unknown) => request(path, { method: 'PATCH', body }), + delete: (path: string) => request(path, { method: 'DELETE' }), +}; diff --git a/apps/web/src/lib/assets/favicon.svg b/apps/web/src/lib/assets/favicon.svg new file mode 100644 index 0000000..cc5dc66 --- /dev/null +++ b/apps/web/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/apps/web/src/lib/components/audio/ABCompare.svelte b/apps/web/src/lib/components/audio/ABCompare.svelte new file mode 100644 index 0000000..0baf5f9 --- /dev/null +++ b/apps/web/src/lib/components/audio/ABCompare.svelte @@ -0,0 +1,161 @@ + + +
+
+

A/B Compare

+
+ + +
+ +
+ +
+
+ +
+
+ +
+
+
+ + diff --git a/apps/web/src/lib/components/audio/UploadDropzone.svelte b/apps/web/src/lib/components/audio/UploadDropzone.svelte new file mode 100644 index 0000000..e028c36 --- /dev/null +++ b/apps/web/src/lib/components/audio/UploadDropzone.svelte @@ -0,0 +1,246 @@ + + +
+
+ +
+ +
!uploading && document.getElementById(`file-input-${trackId}`)?.click()} + onkeydown={(e) => e.key === 'Enter' && !uploading && document.getElementById(`file-input-${trackId}`)?.click()} + > + + + {#if uploading} +
+
+ {progress}% +
+ {:else} +
+ 🎵 +

Drop audio file here or click to browse

+ WAV, MP3, FLAC, AIFF — max 500 MB +
+ {/if} +
+ + {#if error} +

{error}

+ {/if} +
+ + diff --git a/apps/web/src/lib/components/audio/WaveformPlayer.svelte b/apps/web/src/lib/components/audio/WaveformPlayer.svelte new file mode 100644 index 0000000..a1e7623 --- /dev/null +++ b/apps/web/src/lib/components/audio/WaveformPlayer.svelte @@ -0,0 +1,281 @@ + + +
+ {#if label} + {label} + {/if} + +
+
+ + {#if duration > 0 && markers.length > 0} +
+ {#each markers as marker} + + {/each} +
+ {/if} +
+ +
+
+ {#if !compact} + + {/if} + + {#if !compact} + + {/if} +
+ +
+ {formatTime(currentTime)} / {formatTime(duration)} +
+ + {#if !compact} +
+ setVol(Number((e.target as HTMLInputElement).value))} + class="volume-slider" + title="Volume" + /> +
+ {/if} +
+
+ + diff --git a/apps/web/src/lib/components/ui/Avatar.svelte b/apps/web/src/lib/components/ui/Avatar.svelte new file mode 100644 index 0000000..3f375b5 --- /dev/null +++ b/apps/web/src/lib/components/ui/Avatar.svelte @@ -0,0 +1,58 @@ + + +
+ {#if src} + {name} + {:else} + {initials} + {/if} +
+ + diff --git a/apps/web/src/lib/components/ui/Badge.svelte b/apps/web/src/lib/components/ui/Badge.svelte new file mode 100644 index 0000000..aebacaa --- /dev/null +++ b/apps/web/src/lib/components/ui/Badge.svelte @@ -0,0 +1,52 @@ + + + + {@render children()} + + + diff --git a/apps/web/src/lib/components/ui/Button.svelte b/apps/web/src/lib/components/ui/Button.svelte new file mode 100644 index 0000000..a38074d --- /dev/null +++ b/apps/web/src/lib/components/ui/Button.svelte @@ -0,0 +1,129 @@ + + +{#if href} + + {@render children()} + +{:else} + +{/if} + + diff --git a/apps/web/src/lib/components/ui/EmptyState.svelte b/apps/web/src/lib/components/ui/EmptyState.svelte new file mode 100644 index 0000000..c25a02a --- /dev/null +++ b/apps/web/src/lib/components/ui/EmptyState.svelte @@ -0,0 +1,57 @@ + + +
+ {icon} +

{title}

+ {#if description} +

{description}

+ {/if} + {#if action} +
+ {@render action()} +
+ {/if} +
+ + diff --git a/apps/web/src/lib/components/ui/Input.svelte b/apps/web/src/lib/components/ui/Input.svelte new file mode 100644 index 0000000..a413174 --- /dev/null +++ b/apps/web/src/lib/components/ui/Input.svelte @@ -0,0 +1,80 @@ + + +
+ {#if label} + + {/if} + + {#if error} + {error} + {/if} +
+ + diff --git a/apps/web/src/lib/components/ui/Modal.svelte b/apps/web/src/lib/components/ui/Modal.svelte new file mode 100644 index 0000000..1c9eecf --- /dev/null +++ b/apps/web/src/lib/components/ui/Modal.svelte @@ -0,0 +1,108 @@ + + + + +{#if open} + +{/if} + + diff --git a/apps/web/src/lib/components/ui/Skeleton.svelte b/apps/web/src/lib/components/ui/Skeleton.svelte new file mode 100644 index 0000000..df195d9 --- /dev/null +++ b/apps/web/src/lib/components/ui/Skeleton.svelte @@ -0,0 +1,36 @@ + + +
+ + diff --git a/apps/web/src/lib/components/ui/ToastContainer.svelte b/apps/web/src/lib/components/ui/ToastContainer.svelte new file mode 100644 index 0000000..5c1fbce --- /dev/null +++ b/apps/web/src/lib/components/ui/ToastContainer.svelte @@ -0,0 +1,104 @@ + + +{#if $toasts.length > 0} +
+ {#each $toasts as t (t.id)} + + {/each} +
+{/if} + + diff --git a/apps/web/src/lib/index.ts b/apps/web/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/apps/web/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/apps/web/src/lib/stores/auth.ts b/apps/web/src/lib/stores/auth.ts new file mode 100644 index 0000000..d498b3b --- /dev/null +++ b/apps/web/src/lib/stores/auth.ts @@ -0,0 +1,38 @@ +import { writable } from 'svelte/store'; +import { api } from '$lib/api/client.js'; + +type User = { + id: string; + email: string; + name: string; + avatarUrl?: string; +} | null; + +export const user = writable(null); +export const authLoading = writable(true); + +export async function checkAuth() { + try { + const res = await api.get<{ user: User }>('/auth/me', true); + user.set(res.user); + } catch { + user.set(null); + } finally { + authLoading.set(false); + } +} + +export async function sendMagicLink(email: string) { + return api.post('/auth/magic-link', { email }); +} + +export async function verifyToken(token: string) { + const res = await api.post<{ user: User }>('/auth/verify', { token }); + user.set(res.user); + return res.user; +} + +export async function logout() { + await api.post('/auth/logout'); + user.set(null); +} diff --git a/apps/web/src/lib/stores/toast.ts b/apps/web/src/lib/stores/toast.ts new file mode 100644 index 0000000..f066a21 --- /dev/null +++ b/apps/web/src/lib/stores/toast.ts @@ -0,0 +1,24 @@ +import { writable } from 'svelte/store'; + +export type ToastType = 'success' | 'error' | 'info' | 'warning'; + +type Toast = { + id: string; + message: string; + type: ToastType; +}; + +export const toasts = writable([]); + +export function toast(message: string, type: ToastType = 'info', duration = 4000) { + const id = crypto.randomUUID(); + toasts.update((t) => [...t, { id, message, type }]); + setTimeout(() => removeToast(id), duration); +} + +export function removeToast(id: string) { + toasts.update((t) => t.filter((x) => x.id !== id)); +} + +export const toastSuccess = (msg: string) => toast(msg, 'success'); +export const toastError = (msg: string) => toast(msg, 'error', 6000); diff --git a/apps/web/src/lib/utils/format.ts b/apps/web/src/lib/utils/format.ts new file mode 100644 index 0000000..f19e48b --- /dev/null +++ b/apps/web/src/lib/utils/format.ts @@ -0,0 +1,29 @@ +export function formatTime(seconds: number): string { + const m = Math.floor(seconds / 60); + const s = Math.floor(seconds % 60); + return `${m}:${s.toString().padStart(2, '0')}`; +} + +export function timeAgo(dateStr: string): string { + const now = Date.now(); + const then = new Date(dateStr).getTime(); + const diff = now - then; + + const seconds = Math.floor(diff / 1000); + if (seconds < 60) return 'just now'; + + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + + const days = Math.floor(hours / 24); + if (days < 30) return `${days}d ago`; + + return new Date(dateStr).toLocaleDateString('de-DE', { + day: '2-digit', + month: '2-digit', + year: '2-digit', + }); +} diff --git a/apps/web/src/routes/+layout.svelte b/apps/web/src/routes/+layout.svelte new file mode 100644 index 0000000..64ac6c5 --- /dev/null +++ b/apps/web/src/routes/+layout.svelte @@ -0,0 +1,144 @@ + + + + Music Hub + + + +{#if $authLoading} +
+
+
+{:else} + {@render children()} +{/if} + + + + diff --git a/apps/web/src/routes/+page.svelte b/apps/web/src/routes/+page.svelte new file mode 100644 index 0000000..9a492a7 --- /dev/null +++ b/apps/web/src/routes/+page.svelte @@ -0,0 +1,89 @@ + + + + + diff --git a/apps/web/src/routes/auth/verify/+page.svelte b/apps/web/src/routes/auth/verify/+page.svelte new file mode 100644 index 0000000..869d089 --- /dev/null +++ b/apps/web/src/routes/auth/verify/+page.svelte @@ -0,0 +1,56 @@ + + +
+ {#if error} +
+

Login Failed

+

{error}

+ Try again +
+ {:else} +

Verifying...

+ {/if} +
+ + diff --git a/apps/web/src/routes/dashboard/+page.svelte b/apps/web/src/routes/dashboard/+page.svelte new file mode 100644 index 0000000..2242a3a --- /dev/null +++ b/apps/web/src/routes/dashboard/+page.svelte @@ -0,0 +1,182 @@ + + +
+
+

Music Hub

+
+ {#if $user} + + {$user.name} + {/if} + +
+
+ +
+
+

Projects

+ +
+ + {#if loading} +
+ {#each [1, 2, 3] as _} +
+ + + +
+ {/each} +
+ {:else if projects.length === 0} + + {#snippet action()} + + {/snippet} + + {:else} + + {/if} +
+
+ + diff --git a/apps/web/src/routes/projects/[projectId]/+page.svelte b/apps/web/src/routes/projects/[projectId]/+page.svelte new file mode 100644 index 0000000..32e6b68 --- /dev/null +++ b/apps/web/src/routes/projects/[projectId]/+page.svelte @@ -0,0 +1,218 @@ + + +
+
+ ← Projects + {#if loading} + + {:else if project} +
+

{project.name}

+ {#if role === 'owner' || role === 'management'} + + {/if} +
+ {#if project.description} +

{project.description}

+ {/if} + {/if} +
+ +
+

Tracks

+ {#if canUpload} + + {/if} +
+ + {#if showNewTrack} +
{ e.preventDefault(); createTrack(); }}> + + +
+ {/if} + + {#if loading} +
+ {#each [1, 2] as _} +
+ +
+ {/each} +
+ {:else if tracks.length === 0} + + {:else} +
+ {#each tracks as track} + + {track.name} + + {/each} +
+ {/if} +
+ + diff --git a/apps/web/src/routes/projects/[projectId]/settings/+page.svelte b/apps/web/src/routes/projects/[projectId]/settings/+page.svelte new file mode 100644 index 0000000..e9e1fc8 --- /dev/null +++ b/apps/web/src/routes/projects/[projectId]/settings/+page.svelte @@ -0,0 +1,356 @@ + + +
+
+ ← Back to project +

Settings

+
+ + {#if !loading && project} + +
+

Project Details

+
{ e.preventDefault(); saveProject(); }}> + +
+ + +
+ +
+
+ + +
+

Members

+ +
+ {#each members as member} +
+ +
+ {member.user.name} + {member.user.email} +
+ {#if member.role === 'owner'} + Owner + {:else if role === 'owner'} + + + {:else} + {ROLE_LABELS[member.role as keyof typeof ROLE_LABELS] || member.role} + {/if} +
+ {/each} +
+ + + {#if role === 'owner' || role === 'management'} +
{ e.preventDefault(); inviteMember(); }}> + + + +
+ {/if} +
+ + + {#if role === 'owner'} +
+

Danger Zone

+
+
+ Archive this project +

The project will be hidden from all members.

+
+ +
+
+ {/if} + {/if} +
+ + +

Are you sure you want to archive {project?.name}? This will hide it from all members.

+ {#snippet actions()} + + + {/snippet} +
+ + diff --git a/apps/web/src/routes/projects/[projectId]/tracks/[trackId]/+page.svelte b/apps/web/src/routes/projects/[projectId]/tracks/[trackId]/+page.svelte new file mode 100644 index 0000000..8697368 --- /dev/null +++ b/apps/web/src/routes/projects/[projectId]/tracks/[trackId]/+page.svelte @@ -0,0 +1,303 @@ + + +
+
+ ← Back to project + {#if loading} + + {:else} +

{trackName}

+ {/if} +
+ + + {#if selectedVersion && streamUrl} + {#key streamUrl} + c.timestampSeconds !== null) + .map((c) => ({ + id: c.id, + timestampSeconds: c.timestampSeconds!, + body: c.body, + userName: c.user.name, + }))} + onTimeClick={(time) => commentTimestamp = Math.round(time * 10) / 10} + /> + {/key} + + + +
+ {#if canUpload} + + {/if} + + {#if versions.length > 1} + + {/if} +
+ {/if} + + + {#if compareVersion && compareStreamUrl && selectedVersion && streamUrl} + + {/if} + + {#if showUpload} + { showUpload = false; loadVersions(); toastSuccess('Version uploaded'); }} /> + {/if} + + {#if loading} + + {:else if versions.length === 0} + + {#snippet action()} + {#if canUpload} + + {/if} + {/snippet} + + {/if} + + {#if selectedVersion} + playerRef?.seekToTime(time)} + /> + {/if} + + +
+ + diff --git a/apps/web/src/routes/projects/[projectId]/tracks/[trackId]/components/CommentItem.svelte b/apps/web/src/routes/projects/[projectId]/tracks/[trackId]/components/CommentItem.svelte new file mode 100644 index 0000000..bcddc63 --- /dev/null +++ b/apps/web/src/routes/projects/[projectId]/tracks/[trackId]/components/CommentItem.svelte @@ -0,0 +1,130 @@ + + +
+
+ + {comment.user.name} + {#if comment.timestampSeconds !== null} + + {/if} + {timeAgo(comment.createdAt)} +
+ {#if onReply} + + {/if} + {#if !comment.resolvedAt} + + {/if} +
+
+

{comment.body}

+
+ + diff --git a/apps/web/src/routes/projects/[projectId]/tracks/[trackId]/components/CommentSection.svelte b/apps/web/src/routes/projects/[projectId]/tracks/[trackId]/components/CommentSection.svelte new file mode 100644 index 0000000..46a3afd --- /dev/null +++ b/apps/web/src/routes/projects/[projectId]/tracks/[trackId]/components/CommentSection.svelte @@ -0,0 +1,193 @@ + + +
+

Comments

+ + {#if canComment} +
{ e.preventDefault(); handleSubmit(); }}> + {#if commentTimestamp !== null} + + + + + {/if} + {#if replyingTo} + + Replying... + + + {/if} +
+ + +
+
+ {/if} + +
+ {#each topLevel as comment} + + {#each replies(comment.id) as reply} +
+ +
+ {/each} + {/each} + + {#if comments.length === 0} + + {/if} +
+
+ + diff --git a/apps/web/src/routes/projects/[projectId]/tracks/[trackId]/components/VersionHistory.svelte b/apps/web/src/routes/projects/[projectId]/tracks/[trackId]/components/VersionHistory.svelte new file mode 100644 index 0000000..1d19796 --- /dev/null +++ b/apps/web/src/routes/projects/[projectId]/tracks/[trackId]/components/VersionHistory.svelte @@ -0,0 +1,110 @@ + + +{#if versions.length > 1} +
+

Version History

+
+ {#each versions as version} + + {/each} +
+
+{/if} + + diff --git a/apps/web/src/routes/projects/[projectId]/tracks/[trackId]/components/VersionInfo.svelte b/apps/web/src/routes/projects/[projectId]/tracks/[trackId]/components/VersionInfo.svelte new file mode 100644 index 0000000..0916e7b --- /dev/null +++ b/apps/web/src/routes/projects/[projectId]/tracks/[trackId]/components/VersionInfo.svelte @@ -0,0 +1,86 @@ + + +
+
+ + V{version.versionNumber} + {#if version.label} — {version.label}{/if} + + {version.status} +
+ + {#if showActions} +
+ + +
+ {/if} +
+ +{#if version.notes} +

{version.notes}

+{/if} + + diff --git a/apps/web/src/routes/projects/new/+page.svelte b/apps/web/src/routes/projects/new/+page.svelte new file mode 100644 index 0000000..9f21e60 --- /dev/null +++ b/apps/web/src/routes/projects/new/+page.svelte @@ -0,0 +1,108 @@ + + +
+
+

New Project

+ +
+ + +
+ + +
+ +
+ + +
+
+
+
+ + diff --git a/apps/web/static/robots.txt b/apps/web/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/apps/web/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/apps/web/svelte.config.js b/apps/web/svelte.config.js new file mode 100644 index 0000000..58d330b --- /dev/null +++ b/apps/web/svelte.config.js @@ -0,0 +1,24 @@ +import adapter from '@sveltejs/adapter-auto'; +import { relative, sep } from 'node:path'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + compilerOptions: { + // defaults to rune mode for the project, except for `node_modules`. Can be removed in svelte 6. + runes: ({ filename }) => { + const relativePath = relative(import.meta.dirname, filename); + const pathSegments = relativePath.toLowerCase().split(sep); + const isExternalLibrary = pathSegments.includes('node_modules'); + + return isExternalLibrary ? undefined : true; + } + }, + 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() + } +}; + +export default config; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..2c2ed3c --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..1ab0e91 --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,14 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()], + server: { + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + }, + }, + }, +}); diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..1059dc4 --- /dev/null +++ b/bun.lock @@ -0,0 +1,719 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "music-hub", + "devDependencies": { + "@types/bun": "latest", + "turbo": "latest", + "typescript": "^5", + }, + }, + "apps/api": { + "name": "@music-hub/api", + "version": "0.0.1", + "dependencies": { + "@aws-sdk/client-s3": "^3", + "@aws-sdk/s3-request-presigner": "^3", + "@hono/zod-validator": "^0.5", + "@music-hub/db": "workspace:*", + "@music-hub/shared": "workspace:*", + "drizzle-orm": "^0.44", + "hono": "^4", + "resend": "^6.10.0", + }, + }, + "apps/web": { + "name": "@music-hub/web", + "version": "0.0.1", + "dependencies": { + "@music-hub/shared": "workspace:*", + "wavesurfer.js": "^7.12.5", + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^7.0.0", + "@sveltejs/kit": "^2.50.2", + "@sveltejs/vite-plugin-svelte": "^6.2.4", + "svelte": "^5.54.0", + "svelte-check": "^4.4.2", + "typescript": "^5.9.3", + "vite": "^7.3.1", + }, + }, + "packages/db": { + "name": "@music-hub/db", + "version": "0.0.1", + "dependencies": { + "@music-hub/shared": "workspace:*", + "drizzle-orm": "^0.44", + "postgres": "^3.4", + }, + "devDependencies": { + "drizzle-kit": "^0.31", + }, + }, + "packages/shared": { + "name": "@music-hub/shared", + "version": "0.0.1", + "dependencies": { + "zod": "^3.24", + }, + }, + }, + "packages": { + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], + + "@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="], + + "@aws-crypto/sha1-browser": ["@aws-crypto/sha1-browser@5.2.0", "", { "dependencies": { "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg=="], + + "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], + + "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], + + "@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="], + + "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + + "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1022.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.26", "@aws-sdk/credential-provider-node": "^3.972.29", "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", "@aws-sdk/middleware-expect-continue": "^3.972.8", "@aws-sdk/middleware-flexible-checksums": "^3.974.6", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-location-constraint": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-sdk-s3": "^3.972.27", "@aws-sdk/middleware-ssec": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.28", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/signature-v4-multi-region": "^3.996.15", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.14", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.13", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-blob-browser": "^4.2.13", "@smithy/hash-node": "^4.2.12", "@smithy/hash-stream-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/md5-js": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-retry": "^4.4.46", "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.44", "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.13", "@smithy/util-stream": "^4.5.21", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.14", "tslib": "^2.6.2" } }, "sha512-PhdIW0LxjzcMlBiCldRefnyZk84wtYGnEV0sNGOD55DZTvZsibG2XHvQiL1aFliKugfAhuIpNmFkctI2n2I3Dg=="], + + "@aws-sdk/core": ["@aws-sdk/core@3.973.26", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/xml-builder": "^3.972.16", "@smithy/core": "^3.23.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-A/E6n2W42ruU+sfWk+mMUOyVXbsSgGrY3MJ9/0Az5qUdG67y8I6HYzzoAa+e/lzxxl1uCYmEL6BTMi9ZiZnplQ=="], + + "@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.972.5", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.24", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-FWg8uFmT6vQM7VuzELzwVo5bzExGaKHdubn0StjgrcU5FvuLExUe+k06kn/40uKv59rYzhez8eFNM4yYE/Yb/w=="], + + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.26", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.1", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.21", "tslib": "^2.6.2" } }, "sha512-CY4ppZ+qHYqcXqBVi//sdHST1QK3KzOEiLtpLsc9W2k2vfZPKExGaQIsOwcyvjpjUEolotitmd3mUNY56IwDEA=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/credential-provider-env": "^3.972.24", "@aws-sdk/credential-provider-http": "^3.972.26", "@aws-sdk/credential-provider-login": "^3.972.28", "@aws-sdk/credential-provider-process": "^3.972.24", "@aws-sdk/credential-provider-sso": "^3.972.28", "@aws-sdk/credential-provider-web-identity": "^3.972.28", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wXYvq3+uQcZV7k+bE4yDXCTBdzWTU9x/nMiKBfzInmv6yYK1veMK0AKvRfRBd72nGWYKcL6AxwiPg9z/pYlgpw=="], + + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ZSTfO6jqUTCysbdBPtEX5OUR//3rbD0lN7jO3sQeS2Gjr/Y+DT6SbIJ0oT2cemNw3UzKu97sNONd1CwNMthuZQ=="], + + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.29", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.24", "@aws-sdk/credential-provider-http": "^3.972.26", "@aws-sdk/credential-provider-ini": "^3.972.28", "@aws-sdk/credential-provider-process": "^3.972.24", "@aws-sdk/credential-provider-sso": "^3.972.28", "@aws-sdk/credential-provider-web-identity": "^3.972.28", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-clSzDcvndpFJAggLDnDb36sPdlZYyEs5Zm6zgZjjUhwsJgSWiWKwFIXUVBcbruidNyBdbpOv2tNDL9sX8y3/0g=="], + + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.24", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Q2k/XLrFXhEztPHqj4SLCNID3hEPdlhh1CDLBpNnM+1L8fq7P+yON9/9M1IGN/dA5W45v44ylERfXtDAlmMNmw=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/token-providers": "3.1021.0", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-IoUlmKMLEITFn1SiCTjPfR6KrE799FBo5baWyk/5Ppar2yXZoUdaRqZzJzK6TcJxx450M8m8DbpddRVYlp5R/A=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-d+6h0SD8GGERzKe27v5rOzNGKOl0D+l0bWJdqrxH8WSQzHzjsQFIAPgIeOTUwBHVsKKwtSxc91K/SWax6XgswQ=="], + + "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-WR525Rr2QJSETa9a050isktyWi/4yIGcmY3BQ1kpHqb0LqUglQHCS8R27dTJxxWNZvQ0RVGtEZjTCbZJpyF3Aw=="], + + "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ=="], + + "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.974.6", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.973.26", "@aws-sdk/crc64-nvme": "^3.972.5", "@aws-sdk/types": "^3.973.6", "@smithy/is-array-buffer": "^4.2.2", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.21", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-YckB8k1ejbyCg/g36gUMFLNzE4W5cERIa4MtsdO+wpTmJEP0+TB7okWIt7d8TDOvnb7SwvxJ21E4TGOBxFpSWQ=="], + + "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ=="], + + "@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-KaUoFuoFPziIa98DSQsTPeke1gvGXlc5ZGMhy+b+nLxZ4A7jmJgLzjEF95l8aOQN2T/qlPP3MrAyELm8ExXucw=="], + + "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA=="], + + "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ=="], + + "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.27", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/core": "^3.23.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.21", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-gomO6DZwx+1D/9mbCpcqO5tPBqYBK7DtdgjTIjZ4yvfh/S7ETwAPS0XbJgP2JD8Ycr5CwVrEkV1sFtu3ShXeOw=="], + + "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw=="], + + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@smithy/core": "^3.23.13", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-retry": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-cfWZFlVh7Va9lRay4PN2A9ARFzaBYcA097InT5M2CdRS05ECF5yaz86jET8Wsl2WcyKYEvVr/QNmKtYtafUHtQ=="], + + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.18", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.26", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.28", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.14", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.13", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-retry": "^4.4.46", "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.44", "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-c7ZSIXrESxHKx2Mcopgd8AlzZgoXMr20fkx5ViPWPOLBvmyhw9VwJx/Govg8Ef/IhEon5R9l53Z8fdYSEmp6VA=="], + + "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/config-resolver": "^4.4.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-1dq9ToC6e070QvnVhhbAs3bb5r6cQ10gTVc6cyRV5uvQe7P138TV2uG2i6+Yok4bAkVAcx5AqkTEBUvWEtBlsQ=="], + + "@aws-sdk/s3-request-presigner": ["@aws-sdk/s3-request-presigner@3.1022.0", "", { "dependencies": { "@aws-sdk/signature-v4-multi-region": "^3.996.15", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-format-url": "^3.972.8", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-2arKiJswYGEOScAhEeOuy/1A1wScfgbfmU/6NAn0UK0/LDxqsOTc4/bCEuUK+/LtB+Lxu3rODqbY8V5gTGPLaQ=="], + + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.15", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.27", "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Ukw2RpqvaL96CjfH/FgfBmy/ZosHBqoHBCFsN61qGg99F33vpntIVii8aNeh65XuOja73arSduskoa4OJea9RQ=="], + + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1021.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/nested-clients": "^3.996.18", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-TKY6h9spUk3OLs5v1oAgW9mAeBE3LAGNBwJokLy96wwmd4W2v/tYlXseProyed9ValDj2u1jK/4Rg1T+1NXyJA=="], + + "@aws-sdk/types": ["@aws-sdk/types@3.973.6", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw=="], + + "@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA=="], + + "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.5", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-endpoints": "^3.3.3", "tslib": "^2.6.2" } }, "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw=="], + + "@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A=="], + + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.5", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ=="], + + "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA=="], + + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.14", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.28", "@aws-sdk/types": "^3.973.6", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-vNSB/DYaPOyujVZBg/zUznH9QC142MaTHVmaFlF7uzzfg3CgT9f/l4C0Yi+vU/tbBhxVcXVB90Oohk5+o+ZbWw=="], + + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.16", "", { "dependencies": { "@smithy/types": "^4.13.1", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A=="], + + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], + + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@hono/zod-validator": ["@hono/zod-validator@0.5.0", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-ds5bW6DCgAnNHP33E3ieSbaZFd5dkV52ZjyaXtGoR06APFrCtzAsKZxTHwOrJNBdXsi0e5wNwo5L4nVEVnJUdg=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@music-hub/api": ["@music-hub/api@workspace:apps/api"], + + "@music-hub/db": ["@music-hub/db@workspace:packages/db"], + + "@music-hub/shared": ["@music-hub/shared@workspace:packages/shared"], + + "@music-hub/web": ["@music-hub/web@workspace:apps/web"], + + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.1", "", { "os": "android", "cpu": "arm64" }, "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ=="], + + "@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw=="], + + "@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.2.3", "", { "dependencies": { "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw=="], + + "@smithy/config-resolver": ["@smithy/config-resolver@4.4.13", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg=="], + + "@smithy/core": ["@smithy/core@3.23.13", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.21", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q=="], + + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.12", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg=="], + + "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.12", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA=="], + + "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.12", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A=="], + + "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q=="], + + "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.12", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA=="], + + "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.12", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ=="], + + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.15", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A=="], + + "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.13", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", "@smithy/chunked-blob-reader-native": "^4.2.3", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g=="], + + "@smithy/hash-node": ["@smithy/hash-node@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w=="], + + "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw=="], + + "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g=="], + + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow=="], + + "@smithy/md5-js": ["@smithy/md5-js@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ=="], + + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA=="], + + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.28", "", { "dependencies": { "@smithy/core": "^3.23.13", "@smithy/middleware-serde": "^4.2.16", "@smithy/node-config-provider": "^4.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-p1gfYpi91CHcs5cBq982UlGlDrxoYUX6XdHSo91cQ2KFuz6QloHosO7Jc60pJiVmkWrKOV8kFYlGFFbQ2WUKKQ=="], + + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.46", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/service-error-classification": "^4.2.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.13", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-SpvWNNOPOrKQGUqZbEPO+es+FRXMWvIyzUKUOYdDgdlA6BdZj/R58p4umoQ76c2oJC44PiM7mKizyyex1IJzow=="], + + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.16", "", { "dependencies": { "@smithy/core": "^3.23.13", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-beqfV+RZ9RSv+sQqor3xroUUYgRFCGRw6niGstPG8zO9LgTl0B0MCucxjmrH/2WwksQN7UUgI7KNANoZv+KALA=="], + + "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw=="], + + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.12", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw=="], + + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.1", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ejjxdAXjkPIs9lyYyVutOGNOraqUE9v/NjGMKwwFrfOM354wfSD8lmlj8hVwUzQmlLLF4+udhfCX9Exnbmvfzw=="], + + "@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="], + + "@smithy/protocol-http": ["@smithy/protocol-http@5.3.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw=="], + + "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg=="], + + "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw=="], + + "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1" } }, "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ=="], + + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.7", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw=="], + + "@smithy/signature-v4": ["@smithy/signature-v4@5.3.12", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw=="], + + "@smithy/smithy-client": ["@smithy/smithy-client@4.12.8", "", { "dependencies": { "@smithy/core": "^3.23.13", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-stack": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.21", "tslib": "^2.6.2" } }, "sha512-aJaAX7vHe5i66smoSSID7t4rKY08PbD8EBU7DOloixvhOozfYWdcSYE4l6/tjkZ0vBZhGjheWzB2mh31sLgCMA=="], + + "@smithy/types": ["@smithy/types@4.13.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g=="], + + "@smithy/url-parser": ["@smithy/url-parser@4.2.12", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA=="], + + "@smithy/util-base64": ["@smithy/util-base64@4.3.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ=="], + + "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ=="], + + "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g=="], + + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="], + + "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ=="], + + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.44", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-eZg6XzaCbVr2S5cAErU5eGBDaOVTuTo1I65i4tQcHENRcZ8rMWhQy1DaIYUSLyZjsfXvmCqZrstSMYyGFocvHA=="], + + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.48", "", { "dependencies": { "@smithy/config-resolver": "^4.4.13", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-FqOKTlqSaoV3nzO55pMs5NBnZX8EhoI0DGmn9kbYeXWppgHD6dchyuj2HLqp4INJDJbSrj6OFYJkAh/WhSzZPg=="], + + "@smithy/util-endpoints": ["@smithy/util-endpoints@3.3.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig=="], + + "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg=="], + + "@smithy/util-middleware": ["@smithy/util-middleware@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ=="], + + "@smithy/util-retry": ["@smithy/util-retry@4.2.13", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-qQQsIvL0MGIbUjeSrg0/VlQ3jGNKyM3/2iU3FPNgy01z+Sp4OvcaxbgIoFOTvB61ZoohtutuOvOcgmhbD0katQ=="], + + "@smithy/util-stream": ["@smithy/util-stream@4.5.21", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.1", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q=="], + + "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw=="], + + "@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@smithy/util-waiter": ["@smithy/util-waiter@4.2.14", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-2zqq5o/oizvMaFUlNiTyZ7dbgYv1a893aGut2uaxtbzTx/VYYnRxWzDHuD/ftgcw94ffenua+ZNLrbqwUYE+Bg=="], + + "@smithy/uuid": ["@smithy/uuid@1.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g=="], + + "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.9", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA=="], + + "@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@7.0.1", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-dvuPm1E7M9NI/+canIQ6KKQDU2AkEefEZ2Dp7cY6uKoPq9Z/PhOXABe526UdW2mN986gjVkuSLkOYIBnS/M2LQ=="], + + "@sveltejs/kit": ["@sveltejs/kit@2.55.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.4", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-MdFRjevVxmAknf2NbaUkDF16jSIzXMWd4Nfah0Qp8TtQVoSp3bV4jKt8mX7z7qTUTWvgSaxtR0EG5WJf53gcuA=="], + + "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="], + + "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="], + + "@turbo/darwin-64": ["@turbo/darwin-64@2.9.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-P8foouaP+y/p+hhEGBoZpzMbpVvUMwPjDpcy6wN7EYfvvyISD1USuV27qWkczecihwuPJzQ1lDBuL8ERcavTyg=="], + + "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-SIzEkvtNdzdI50FJDaIQ6kQGqgSSdFPcdn0wqmmONN6iGKjy6hsT+EH99GP65FsfV7DLZTh2NmtTIRl2kdoz5Q=="], + + "@turbo/linux-64": ["@turbo/linux-64@2.9.3", "", { "os": "linux", "cpu": "x64" }, "sha512-pLRwFmcHHNBvsCySLS6OFabr/07kDT2pxEt/k6eBf/3asiVQZKJ7Rk88AafQx2aYA641qek4RsXvYO3JYpiBug=="], + + "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-gy6ApUroC2Nzv+qjGtE/uPNkhHAFU4c8God+zd5Aiv9L9uBgHlxVJpHT3XWl5xwlJZ2KWuMrlHTaS5kmNB+q1Q=="], + + "@turbo/windows-64": ["@turbo/windows-64@2.9.3", "", { "os": "win32", "cpu": "x64" }, "sha512-d0YelTX6hAsB7kIEtGB3PzIzSfAg3yDoUlHwuwJc3adBXUsyUIs0YLG+1NNtuhcDOUGnWQeKUoJ2pGWvbpRj7w=="], + + "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-/08CwpKJl3oRY8nOlh2YgilZVJDHsr60XTNxRhuDeuFXONpUZ5X+Nv65izbG/xBew9qxcJFbDX9/sAmAX+ITcQ=="], + + "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], + + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "devalue": ["devalue@5.6.4", "", {}, "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA=="], + + "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], + + "drizzle-orm": ["drizzle-orm@0.44.7", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], + + "esrap": ["esrap@2.2.4", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@typescript-eslint/types": "^8.2.0" } }, "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg=="], + + "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], + + "fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="], + + "fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="], + + "hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="], + + "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "path-expression-matcher": ["path-expression-matcher@1.2.0", "", {}, "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "postal-mime": ["postal-mime@2.7.4", "", {}, "sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g=="], + + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "resend": ["resend@6.10.0", "", { "dependencies": { "postal-mime": "2.7.4", "svix": "1.88.0" }, "peerDependencies": { "@react-email/render": "*" }, "optionalPeers": ["@react-email/render"] }, "sha512-i7CwZpYj4Oho1RxsTpLcCUkO08+HiL4NXrm6jLJ2WzJ89UGI8eROSieLONJA3hnUrf1OYnCyfq5F6POnHUMv1Q=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="], + + "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], + + "set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], + + "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="], + + "strnum": ["strnum@2.2.2", "", {}, "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA=="], + + "svelte": ["svelte@5.55.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.4", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-QjvU7EFemf6mRzdMGlAFttMWtAAVXrax61SZYHdkD6yoVGQ89VeyKfZD4H1JrV1WLmJBxWhFch9H6ig/87VGjw=="], + + "svelte-check": ["svelte-check@4.4.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-kP1zG81EWaFe9ZyTv4ZXv44Csi6Pkdpb7S3oj6m+K2ec/IcDg/a8LsFsnVLqm2nxtkSwsd5xPj/qFkTBgXHXjg=="], + + "svix": ["svix@1.88.0", "", { "dependencies": { "standardwebhooks": "1.0.0", "uuid": "^10.0.0" } }, "sha512-vm/JrrUd3bVyBE+3L33TIyVSs8gS5fYx7lrISvKlDJXTYX1ACH4REX8P1tHxsSKoZi/rvifM1t0XRc5Vc45THw=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + + "turbo": ["turbo@2.9.3", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.3", "@turbo/darwin-arm64": "2.9.3", "@turbo/linux-64": "2.9.3", "@turbo/linux-arm64": "2.9.3", "@turbo/windows-64": "2.9.3", "@turbo/windows-arm64": "2.9.3" }, "bin": { "turbo": "bin/turbo" } }, "sha512-J/VUvsGRykPb9R8Kh8dHVBOqioDexLk9BhLCU/ZybRR+HN9UR3cURdazFvNgMDt9zPP8TF6K73Z+tplfmi0PqQ=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + + "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], + + "wavesurfer.js": ["wavesurfer.js@7.12.5", "", {}, "sha512-MSZcA13R9ZlxgYpzfakaSYf8dz5tCdZKYbjtN1qnKbCi+UoyfaTuhvjlXHrITi/fgeO3qWfsH7U3BP1AKnwRNg=="], + + "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + + "tsx/esbuild": ["esbuild@0.27.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.5", "@esbuild/android-arm": "0.27.5", "@esbuild/android-arm64": "0.27.5", "@esbuild/android-x64": "0.27.5", "@esbuild/darwin-arm64": "0.27.5", "@esbuild/darwin-x64": "0.27.5", "@esbuild/freebsd-arm64": "0.27.5", "@esbuild/freebsd-x64": "0.27.5", "@esbuild/linux-arm": "0.27.5", "@esbuild/linux-arm64": "0.27.5", "@esbuild/linux-ia32": "0.27.5", "@esbuild/linux-loong64": "0.27.5", "@esbuild/linux-mips64el": "0.27.5", "@esbuild/linux-ppc64": "0.27.5", "@esbuild/linux-riscv64": "0.27.5", "@esbuild/linux-s390x": "0.27.5", "@esbuild/linux-x64": "0.27.5", "@esbuild/netbsd-arm64": "0.27.5", "@esbuild/netbsd-x64": "0.27.5", "@esbuild/openbsd-arm64": "0.27.5", "@esbuild/openbsd-x64": "0.27.5", "@esbuild/openharmony-arm64": "0.27.5", "@esbuild/sunos-x64": "0.27.5", "@esbuild/win32-arm64": "0.27.5", "@esbuild/win32-ia32": "0.27.5", "@esbuild/win32-x64": "0.27.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-zdQoHBjuDqKsvV5OPaWansOwfSQ0Js+Uj9J85TBvj3bFW1JjWTSULMRwdQAc8qMeIScbClxeMK0jlrtB9linhA=="], + + "vite/esbuild": ["esbuild@0.27.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.5", "@esbuild/android-arm": "0.27.5", "@esbuild/android-arm64": "0.27.5", "@esbuild/android-x64": "0.27.5", "@esbuild/darwin-arm64": "0.27.5", "@esbuild/darwin-x64": "0.27.5", "@esbuild/freebsd-arm64": "0.27.5", "@esbuild/freebsd-x64": "0.27.5", "@esbuild/linux-arm": "0.27.5", "@esbuild/linux-arm64": "0.27.5", "@esbuild/linux-ia32": "0.27.5", "@esbuild/linux-loong64": "0.27.5", "@esbuild/linux-mips64el": "0.27.5", "@esbuild/linux-ppc64": "0.27.5", "@esbuild/linux-riscv64": "0.27.5", "@esbuild/linux-s390x": "0.27.5", "@esbuild/linux-x64": "0.27.5", "@esbuild/netbsd-arm64": "0.27.5", "@esbuild/netbsd-x64": "0.27.5", "@esbuild/openbsd-arm64": "0.27.5", "@esbuild/openbsd-x64": "0.27.5", "@esbuild/openharmony-arm64": "0.27.5", "@esbuild/sunos-x64": "0.27.5", "@esbuild/win32-arm64": "0.27.5", "@esbuild/win32-ia32": "0.27.5", "@esbuild/win32-x64": "0.27.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-zdQoHBjuDqKsvV5OPaWansOwfSQ0Js+Uj9J85TBvj3bFW1JjWTSULMRwdQAc8qMeIScbClxeMK0jlrtB9linhA=="], + + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-nGsF/4C7uzUj+Nj/4J+Zt0bYQ6bz33Phz8Lb2N80Mti1HjGclTJdXZ+9APC4kLvONbjxN1zfvYNd8FEcbBK/MQ=="], + + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.5", "", { "os": "android", "cpu": "arm" }, "sha512-Cv781jd0Rfj/paoNrul1/r4G0HLvuFKYh7C9uHZ2Pl8YXstzvCyyeWENTFR9qFnRzNMCjXmsulZuvosDg10Mog=="], + + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.5", "", { "os": "android", "cpu": "arm64" }, "sha512-Oeghq+XFgh1pUGd1YKs4DDoxzxkoUkvko+T/IVKwlghKLvvjbGFB3ek8VEDBmNvqhwuL0CQS3cExdzpmUyIrgA=="], + + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.5", "", { "os": "android", "cpu": "x64" }, "sha512-nQD7lspbzerlmtNOxYMFAGmhxgzn8Z7m9jgFkh6kpkjsAhZee1w8tJW3ZlW+N9iRePz0oPUDrYrXidCPSImD0Q=="], + + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-I+Ya/MgC6rr8oRWGRDF3BXDfP8K1BVUggHqN6VI2lUZLdDi1IM1v2cy0e3lCPbP+pVcK3Tv8cgUhHse1kaNZZw=="], + + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-MCjQUtC8wWJn/pIPM7vQaO69BFgwPD1jriEdqwTCKzWjGgkMbcg+M5HzrOhPhuYe1AJjXlHmD142KQf+jnYj8A=="], + + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-X6xVS+goSH0UelYXnuf4GHLwpOdc8rgK/zai+dKzBMnncw7BTQIwquOodE7EKvY2UVUetSqyAfyZC1D+oqLQtg=="], + + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-233X1FGo3a8x1ekLB6XT69LfZ83vqz+9z3TSEQCTYfMNY880A97nr81KbPcAMl9rmOFp11wO0dP+eB18KU/Ucg=="], + + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.5", "", { "os": "linux", "cpu": "arm" }, "sha512-0wkVrYHG4sdCCN/bcwQ7yYMXACkaHc3UFeaEOwSVW6e5RycMageYAFv+JS2bKLwHyeKVUvtoVH+5/RHq0fgeFw=="], + + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-euKkilsNOv7x/M1NKsx5znyprbpsRFIzTV6lWziqJch7yWYayfLtZzDxDTl+LSQDJYAjd9TVb/Kt5UKIrj2e4A=="], + + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-hVRQX4+P3MS36NxOy24v/Cdsimy/5HYePw+tmPqnNN1fxV0bPrFWR6TMqwXPwoTM2VzbkA+4lbHWUKDd5ZDA/w=="], + + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.5", "", { "os": "linux", "cpu": "none" }, "sha512-mKqqRuOPALI8nDzhOBmIS0INvZOOFGGg5n1osGIXAx8oersceEbKd4t1ACNTHM3sJBXGFAlEgqM+svzjPot+ZQ=="], + + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.5", "", { "os": "linux", "cpu": "none" }, "sha512-EE/QXH9IyaAj1qeuIV5+/GZkBTipgGO782Ff7Um3vPS9cvLhJJeATy4Ggxikz2inZ46KByamMn6GqtqyVjhenA=="], + + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-0V2iF1RGxBf1b7/BjurA5jfkl7PtySjom1r6xOK2q9KWw/XCpAdtB6KNMO+9xx69yYfSCRR9FE0TyKfHA2eQMw=="], + + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.5", "", { "os": "linux", "cpu": "none" }, "sha512-rYxThBx6G9HN6tFNuvB/vykeLi4VDsm5hE5pVwzqbAjZEARQrWu3noZSfbEnPZ/CRXP3271GyFk/49up2W190g=="], + + "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-uEP2q/4qgd8goEUc4QIdU/1P2NmEtZ/zX5u3OpLlCGhJIuBIv0s0wr7TB2nBrd3/A5XIdEkkS5ZLF0ULuvaaYQ=="], + + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.5", "", { "os": "linux", "cpu": "x64" }, "sha512-+Gq47Wqq6PLOOZuBzVSII2//9yyHNKZLuwfzCemqexqOQCSz0zy0O26kIzyp9EMNMK+nZ0tFHBZrCeVUuMs/ew=="], + + "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.5", "", { "os": "none", "cpu": "arm64" }, "sha512-3F/5EG8VHfN/I+W5cO1/SV2H9Q/5r7vcHabMnBqhHK2lTWOh3F8vixNzo8lqxrlmBtZVFpW8pmITHnq54+Tq4g=="], + + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.5", "", { "os": "none", "cpu": "x64" }, "sha512-28t+Sj3CPN8vkMOlZotOmDgilQwVvxWZl7b8rxpn73Tt/gCnvrHxQUMng4uu3itdFvrtba/1nHejvxqz8xgEMA=="], + + "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Doz/hKtiuVAi9hMsBMpwBANhIZc8l238U2Onko3t2xUp8xtM0ZKdDYHMnm/qPFVthY8KtxkXaocwmMh6VolzMA=="], + + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-WfGVaa1oz5A7+ZFPkERIbIhKT4olvGl1tyzTRaB5yoZRLqC0KwaO95FeZtOdQj/oKkjW57KcVF944m62/0GYtA=="], + + "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.5", "", { "os": "none", "cpu": "arm64" }, "sha512-Xh+VRuh6OMh3uJ0JkCjI57l+DVe7VRGBYymen8rFPnTVgATBwA6nmToxM2OwTlSvrnWpPKkrQUj93+K9huYC6A=="], + + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-aC1gpJkkaUADHuAdQfuVTnqVUTLqqUNhAvEwHwVWcnVVZvNlDPGA0UveZsfXJJ9T6k9Po4eHi3c02gbdwO3g6w=="], + + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-0UNx2aavV0fk6UpZcwXFLztA2r/k9jTUa7OW7SAea1VYUhkug99MW1uZeXEnPn5+cHOd0n8myQay6TlFnBR07w=="], + + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-5nlJ3AeJWCTSzR7AEqVjT/faWyqKU86kCi1lLmxVqmNR+j4HrYdns+eTGjS/vmrzCIe8inGQckUadvS0+JkKdQ=="], + + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.5", "", { "os": "win32", "cpu": "x64" }, "sha512-PWypQR+d4FLfkhBIV+/kHsUELAnMpx1bRvvsn3p+/sAERbnCzFrtDRG2Xw5n+2zPxBK2+iaP+vetsRl4Ti7WgA=="], + + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-nGsF/4C7uzUj+Nj/4J+Zt0bYQ6bz33Phz8Lb2N80Mti1HjGclTJdXZ+9APC4kLvONbjxN1zfvYNd8FEcbBK/MQ=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.5", "", { "os": "android", "cpu": "arm" }, "sha512-Cv781jd0Rfj/paoNrul1/r4G0HLvuFKYh7C9uHZ2Pl8YXstzvCyyeWENTFR9qFnRzNMCjXmsulZuvosDg10Mog=="], + + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.5", "", { "os": "android", "cpu": "arm64" }, "sha512-Oeghq+XFgh1pUGd1YKs4DDoxzxkoUkvko+T/IVKwlghKLvvjbGFB3ek8VEDBmNvqhwuL0CQS3cExdzpmUyIrgA=="], + + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.5", "", { "os": "android", "cpu": "x64" }, "sha512-nQD7lspbzerlmtNOxYMFAGmhxgzn8Z7m9jgFkh6kpkjsAhZee1w8tJW3ZlW+N9iRePz0oPUDrYrXidCPSImD0Q=="], + + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-I+Ya/MgC6rr8oRWGRDF3BXDfP8K1BVUggHqN6VI2lUZLdDi1IM1v2cy0e3lCPbP+pVcK3Tv8cgUhHse1kaNZZw=="], + + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-MCjQUtC8wWJn/pIPM7vQaO69BFgwPD1jriEdqwTCKzWjGgkMbcg+M5HzrOhPhuYe1AJjXlHmD142KQf+jnYj8A=="], + + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-X6xVS+goSH0UelYXnuf4GHLwpOdc8rgK/zai+dKzBMnncw7BTQIwquOodE7EKvY2UVUetSqyAfyZC1D+oqLQtg=="], + + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-233X1FGo3a8x1ekLB6XT69LfZ83vqz+9z3TSEQCTYfMNY880A97nr81KbPcAMl9rmOFp11wO0dP+eB18KU/Ucg=="], + + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.5", "", { "os": "linux", "cpu": "arm" }, "sha512-0wkVrYHG4sdCCN/bcwQ7yYMXACkaHc3UFeaEOwSVW6e5RycMageYAFv+JS2bKLwHyeKVUvtoVH+5/RHq0fgeFw=="], + + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-euKkilsNOv7x/M1NKsx5znyprbpsRFIzTV6lWziqJch7yWYayfLtZzDxDTl+LSQDJYAjd9TVb/Kt5UKIrj2e4A=="], + + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-hVRQX4+P3MS36NxOy24v/Cdsimy/5HYePw+tmPqnNN1fxV0bPrFWR6TMqwXPwoTM2VzbkA+4lbHWUKDd5ZDA/w=="], + + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.5", "", { "os": "linux", "cpu": "none" }, "sha512-mKqqRuOPALI8nDzhOBmIS0INvZOOFGGg5n1osGIXAx8oersceEbKd4t1ACNTHM3sJBXGFAlEgqM+svzjPot+ZQ=="], + + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.5", "", { "os": "linux", "cpu": "none" }, "sha512-EE/QXH9IyaAj1qeuIV5+/GZkBTipgGO782Ff7Um3vPS9cvLhJJeATy4Ggxikz2inZ46KByamMn6GqtqyVjhenA=="], + + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-0V2iF1RGxBf1b7/BjurA5jfkl7PtySjom1r6xOK2q9KWw/XCpAdtB6KNMO+9xx69yYfSCRR9FE0TyKfHA2eQMw=="], + + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.5", "", { "os": "linux", "cpu": "none" }, "sha512-rYxThBx6G9HN6tFNuvB/vykeLi4VDsm5hE5pVwzqbAjZEARQrWu3noZSfbEnPZ/CRXP3271GyFk/49up2W190g=="], + + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-uEP2q/4qgd8goEUc4QIdU/1P2NmEtZ/zX5u3OpLlCGhJIuBIv0s0wr7TB2nBrd3/A5XIdEkkS5ZLF0ULuvaaYQ=="], + + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.5", "", { "os": "linux", "cpu": "x64" }, "sha512-+Gq47Wqq6PLOOZuBzVSII2//9yyHNKZLuwfzCemqexqOQCSz0zy0O26kIzyp9EMNMK+nZ0tFHBZrCeVUuMs/ew=="], + + "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.5", "", { "os": "none", "cpu": "arm64" }, "sha512-3F/5EG8VHfN/I+W5cO1/SV2H9Q/5r7vcHabMnBqhHK2lTWOh3F8vixNzo8lqxrlmBtZVFpW8pmITHnq54+Tq4g=="], + + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.5", "", { "os": "none", "cpu": "x64" }, "sha512-28t+Sj3CPN8vkMOlZotOmDgilQwVvxWZl7b8rxpn73Tt/gCnvrHxQUMng4uu3itdFvrtba/1nHejvxqz8xgEMA=="], + + "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Doz/hKtiuVAi9hMsBMpwBANhIZc8l238U2Onko3t2xUp8xtM0ZKdDYHMnm/qPFVthY8KtxkXaocwmMh6VolzMA=="], + + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-WfGVaa1oz5A7+ZFPkERIbIhKT4olvGl1tyzTRaB5yoZRLqC0KwaO95FeZtOdQj/oKkjW57KcVF944m62/0GYtA=="], + + "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.5", "", { "os": "none", "cpu": "arm64" }, "sha512-Xh+VRuh6OMh3uJ0JkCjI57l+DVe7VRGBYymen8rFPnTVgATBwA6nmToxM2OwTlSvrnWpPKkrQUj93+K9huYC6A=="], + + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-aC1gpJkkaUADHuAdQfuVTnqVUTLqqUNhAvEwHwVWcnVVZvNlDPGA0UveZsfXJJ9T6k9Po4eHi3c02gbdwO3g6w=="], + + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-0UNx2aavV0fk6UpZcwXFLztA2r/k9jTUa7OW7SAea1VYUhkug99MW1uZeXEnPn5+cHOd0n8myQay6TlFnBR07w=="], + + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-5nlJ3AeJWCTSzR7AEqVjT/faWyqKU86kCi1lLmxVqmNR+j4HrYdns+eTGjS/vmrzCIe8inGQckUadvS0+JkKdQ=="], + + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.5", "", { "os": "win32", "cpu": "x64" }, "sha512-PWypQR+d4FLfkhBIV+/kHsUELAnMpx1bRvvsn3p+/sAERbnCzFrtDRG2Xw5n+2zPxBK2+iaP+vetsRl4Ti7WgA=="], + + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + } +} diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..da0211f --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,44 @@ +services: + api: + build: + context: . + dockerfile: Dockerfile.api + ports: + - "3000:3000" + environment: + - DATABASE_URL=${DATABASE_URL} + - S3_ENDPOINT=${S3_ENDPOINT} + - S3_ACCESS_KEY=${S3_ACCESS_KEY} + - S3_SECRET_KEY=${S3_SECRET_KEY} + - S3_BUCKET=${S3_BUCKET} + - APP_URL=${APP_URL} + - MAGIC_LINK_SECRET=${MAGIC_LINK_SECRET} + - RESEND_API_KEY=${RESEND_API_KEY} + - EMAIL_FROM=${EMAIL_FROM} + - NODE_ENV=production + depends_on: + - postgres + restart: unless-stopped + + web: + build: + context: . + dockerfile: Dockerfile.web + ports: + - "5173:3000" + environment: + - NODE_ENV=production + restart: unless-stopped + + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: musichub + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: musichub + volumes: + - pgdata:/var/lib/postgresql/data + restart: unless-stopped + +volumes: + pgdata: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9c45558 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +services: + postgres: + image: postgres:16-alpine + ports: + - "5433:5432" + environment: + POSTGRES_USER: musichub + POSTGRES_PASSWORD: musichub + POSTGRES_DB: musichub + volumes: + - pgdata:/var/lib/postgresql/data + + minio: + image: minio/minio + command: server /data --console-address ":9001" + ports: + - "9000:9000" + - "9001:9001" + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + volumes: + - miniodata:/data + +volumes: + pgdata: + miniodata: diff --git a/package.json b/package.json new file mode 100644 index 0000000..765e16d --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "music-hub", + "private": true, + "packageManager": "bun@1.3.11", + "type": "module", + "workspaces": ["packages/*", "apps/*"], + "scripts": { + "dev": "turbo dev", + "build": "turbo build", + "db:generate": "turbo db:generate --filter=@music-hub/db", + "db:migrate": "turbo db:migrate --filter=@music-hub/db", + "db:seed": "turbo db:seed --filter=@music-hub/db" + }, + "devDependencies": { + "@types/bun": "latest", + "turbo": "latest", + "typescript": "^5" + } +} diff --git a/packages/db/drizzle.config.ts b/packages/db/drizzle.config.ts new file mode 100644 index 0000000..a89537c --- /dev/null +++ b/packages/db/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './src/schema/index.ts', + out: './src/migrations', + dialect: 'postgresql', + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}); diff --git a/packages/db/package.json b/packages/db/package.json new file mode 100644 index 0000000..ad37263 --- /dev/null +++ b/packages/db/package.json @@ -0,0 +1,21 @@ +{ + "name": "@music-hub/db", + "version": "0.0.1", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:seed": "bun run seed.ts" + }, + "dependencies": { + "@music-hub/shared": "workspace:*", + "drizzle-orm": "^0.44", + "postgres": "^3.4" + }, + "devDependencies": { + "drizzle-kit": "^0.31" + } +} diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts new file mode 100644 index 0000000..01c3bb3 --- /dev/null +++ b/packages/db/src/index.ts @@ -0,0 +1,12 @@ +import { drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import * as schema from './schema/index.js'; + +export function createDb(connectionString: string) { + const client = postgres(connectionString); + return drizzle(client, { schema }); +} + +export type Database = ReturnType; + +export * from './schema/index.js'; diff --git a/packages/db/src/migrations/0000_magenta_apocalypse.sql b/packages/db/src/migrations/0000_magenta_apocalypse.sql new file mode 100644 index 0000000..cb4de65 --- /dev/null +++ b/packages/db/src/migrations/0000_magenta_apocalypse.sql @@ -0,0 +1,108 @@ +CREATE TYPE "public"."project_role" AS ENUM('owner', 'recording_engineer', 'mixing_engineer', 'mastering_engineer', 'artist', 'label', 'management', 'viewer');--> statement-breakpoint +CREATE TYPE "public"."version_status" AS ENUM('uploaded', 'processing', 'ready', 'approved', 'rejected');--> statement-breakpoint +CREATE TABLE "users" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "email" varchar(255) NOT NULL, + "name" varchar(255) NOT NULL, + "avatar_url" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "users_email_unique" UNIQUE("email") +); +--> statement-breakpoint +CREATE TABLE "magic_links" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "email" varchar(255) NOT NULL, + "token" varchar(64) NOT NULL, + "expires_at" timestamp NOT NULL, + "used_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "magic_links_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE "sessions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "token_hash" varchar(128) NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "sessions_token_hash_unique" UNIQUE("token_hash") +); +--> statement-breakpoint +CREATE TABLE "project_members" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "project_id" uuid NOT NULL, + "user_id" uuid NOT NULL, + "role" "project_role" NOT NULL, + "can_upload" boolean DEFAULT false NOT NULL, + "can_comment" boolean DEFAULT true NOT NULL, + "can_approve" boolean DEFAULT false NOT NULL, + "invited_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "project_members_project_id_user_id_unique" UNIQUE("project_id","user_id") +); +--> statement-breakpoint +CREATE TABLE "projects" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" varchar(255) NOT NULL, + "description" text, + "cover_image_url" text, + "created_by_id" uuid NOT NULL, + "is_archived" boolean DEFAULT false NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "tracks" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "project_id" uuid NOT NULL, + "name" varchar(255) NOT NULL, + "description" text, + "sort_order" integer DEFAULT 0 NOT NULL, + "created_by_id" uuid NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "versions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "track_id" uuid NOT NULL, + "version_number" integer NOT NULL, + "label" varchar(100), + "notes" text, + "status" "version_status" DEFAULT 'uploaded' NOT NULL, + "original_file_name" varchar(500) NOT NULL, + "mime_type" varchar(100) NOT NULL, + "file_size" bigint NOT NULL, + "duration" real, + "sample_rate" integer, + "bit_depth" integer, + "original_file_key" text NOT NULL, + "stream_file_key" text, + "waveform_data_key" text, + "created_by_id" uuid NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "versions_track_id_version_number_unique" UNIQUE("track_id","version_number") +); +--> statement-breakpoint +CREATE TABLE "comments" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "version_id" uuid NOT NULL, + "user_id" uuid NOT NULL, + "body" text NOT NULL, + "timestamp_seconds" real, + "parent_id" uuid, + "resolved_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "project_members" ADD CONSTRAINT "project_members_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "project_members" ADD CONSTRAINT "project_members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "projects" ADD CONSTRAINT "projects_created_by_id_users_id_fk" FOREIGN KEY ("created_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "tracks" ADD CONSTRAINT "tracks_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "tracks" ADD CONSTRAINT "tracks_created_by_id_users_id_fk" FOREIGN KEY ("created_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "versions" ADD CONSTRAINT "versions_track_id_tracks_id_fk" FOREIGN KEY ("track_id") REFERENCES "public"."tracks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "versions" ADD CONSTRAINT "versions_created_by_id_users_id_fk" FOREIGN KEY ("created_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "comments" ADD CONSTRAINT "comments_version_id_versions_id_fk" FOREIGN KEY ("version_id") REFERENCES "public"."versions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "comments" ADD CONSTRAINT "comments_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0000_snapshot.json b/packages/db/src/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..dc68b53 --- /dev/null +++ b/packages/db/src/migrations/meta/0000_snapshot.json @@ -0,0 +1,757 @@ +{ + "id": "4e5be5fd-2fae-43d2-8273-5a372d714cc5", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.magic_links": { + "name": "magic_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "magic_links_token_unique": { + "name": "magic_links_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_hash_unique": { + "name": "sessions_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_members": { + "name": "project_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "project_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "can_upload": { + "name": "can_upload", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "can_comment": { + "name": "can_comment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "can_approve": { + "name": "can_approve", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "invited_at": { + "name": "invited_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "project_members_project_id_projects_id_fk": { + "name": "project_members_project_id_projects_id_fk", + "tableFrom": "project_members", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_members_user_id_users_id_fk": { + "name": "project_members_user_id_users_id_fk", + "tableFrom": "project_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "project_members_project_id_user_id_unique": { + "name": "project_members_project_id_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "project_id", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_image_url": { + "name": "cover_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_id": { + "name": "created_by_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "projects_created_by_id_users_id_fk": { + "name": "projects_created_by_id_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tracks": { + "name": "tracks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_by_id": { + "name": "created_by_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tracks_project_id_projects_id_fk": { + "name": "tracks_project_id_projects_id_fk", + "tableFrom": "tracks", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tracks_created_by_id_users_id_fk": { + "name": "tracks_created_by_id_users_id_fk", + "tableFrom": "tracks", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.versions": { + "name": "versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "track_id": { + "name": "track_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_number": { + "name": "version_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'uploaded'" + }, + "original_file_name": { + "name": "original_file_name", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "duration": { + "name": "duration", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "sample_rate": { + "name": "sample_rate", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "bit_depth": { + "name": "bit_depth", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "original_file_key": { + "name": "original_file_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream_file_key": { + "name": "stream_file_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "waveform_data_key": { + "name": "waveform_data_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_id": { + "name": "created_by_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "versions_track_id_tracks_id_fk": { + "name": "versions_track_id_tracks_id_fk", + "tableFrom": "versions", + "tableTo": "tracks", + "columnsFrom": [ + "track_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "versions_created_by_id_users_id_fk": { + "name": "versions_created_by_id_users_id_fk", + "tableFrom": "versions", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "versions_track_id_version_number_unique": { + "name": "versions_track_id_version_number_unique", + "nullsNotDistinct": false, + "columns": [ + "track_id", + "version_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.comments": { + "name": "comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "version_id": { + "name": "version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp_seconds": { + "name": "timestamp_seconds", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "comments_version_id_versions_id_fk": { + "name": "comments_version_id_versions_id_fk", + "tableFrom": "comments", + "tableTo": "versions", + "columnsFrom": [ + "version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comments_user_id_users_id_fk": { + "name": "comments_user_id_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.project_role": { + "name": "project_role", + "schema": "public", + "values": [ + "owner", + "recording_engineer", + "mixing_engineer", + "mastering_engineer", + "artist", + "label", + "management", + "viewer" + ] + }, + "public.version_status": { + "name": "version_status", + "schema": "public", + "values": [ + "uploaded", + "processing", + "ready", + "approved", + "rejected" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json new file mode 100644 index 0000000..c0adc18 --- /dev/null +++ b/packages/db/src/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1775122377765, + "tag": "0000_magenta_apocalypse", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/packages/db/src/schema/auth.ts b/packages/db/src/schema/auth.ts new file mode 100644 index 0000000..ba036bb --- /dev/null +++ b/packages/db/src/schema/auth.ts @@ -0,0 +1,21 @@ +import { pgTable, uuid, varchar, timestamp } from 'drizzle-orm/pg-core'; +import { users } from './users.js'; + +export const magicLinks = pgTable('magic_links', { + id: uuid('id').defaultRandom().primaryKey(), + email: varchar('email', { length: 255 }).notNull(), + token: varchar('token', { length: 64 }).notNull().unique(), + expiresAt: timestamp('expires_at').notNull(), + usedAt: timestamp('used_at'), + createdAt: timestamp('created_at').defaultNow().notNull(), +}); + +export const sessions = pgTable('sessions', { + id: uuid('id').defaultRandom().primaryKey(), + userId: uuid('user_id') + .references(() => users.id, { onDelete: 'cascade' }) + .notNull(), + tokenHash: varchar('token_hash', { length: 128 }).notNull().unique(), + expiresAt: timestamp('expires_at').notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), +}); diff --git a/packages/db/src/schema/comments.ts b/packages/db/src/schema/comments.ts new file mode 100644 index 0000000..21a996d --- /dev/null +++ b/packages/db/src/schema/comments.ts @@ -0,0 +1,19 @@ +import { pgTable, uuid, text, real, timestamp } from 'drizzle-orm/pg-core'; +import { users } from './users.js'; +import { versions } from './tracks.js'; + +export const comments = pgTable('comments', { + id: uuid('id').defaultRandom().primaryKey(), + versionId: uuid('version_id') + .references(() => versions.id, { onDelete: 'cascade' }) + .notNull(), + userId: uuid('user_id') + .references(() => users.id) + .notNull(), + body: text('body').notNull(), + timestampSeconds: real('timestamp_seconds'), + parentId: uuid('parent_id'), + resolvedAt: timestamp('resolved_at'), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}); diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts new file mode 100644 index 0000000..dd8f410 --- /dev/null +++ b/packages/db/src/schema/index.ts @@ -0,0 +1,5 @@ +export * from './users.js'; +export * from './auth.js'; +export * from './projects.js'; +export * from './tracks.js'; +export * from './comments.js'; diff --git a/packages/db/src/schema/projects.ts b/packages/db/src/schema/projects.ts new file mode 100644 index 0000000..f1ad37b --- /dev/null +++ b/packages/db/src/schema/projects.ts @@ -0,0 +1,54 @@ +import { + pgTable, + pgEnum, + uuid, + varchar, + text, + boolean, + timestamp, + unique, +} from 'drizzle-orm/pg-core'; +import { users } from './users.js'; + +export const projectRoleEnum = pgEnum('project_role', [ + 'owner', + 'recording_engineer', + 'mixing_engineer', + 'mastering_engineer', + 'artist', + 'label', + 'management', + 'viewer', +]); + +export const projects = pgTable('projects', { + id: uuid('id').defaultRandom().primaryKey(), + name: varchar('name', { length: 255 }).notNull(), + description: text('description'), + coverImageUrl: text('cover_image_url'), + createdById: uuid('created_by_id') + .references(() => users.id) + .notNull(), + isArchived: boolean('is_archived').default(false).notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}); + +export const projectMembers = pgTable( + 'project_members', + { + id: uuid('id').defaultRandom().primaryKey(), + projectId: uuid('project_id') + .references(() => projects.id, { onDelete: 'cascade' }) + .notNull(), + userId: uuid('user_id') + .references(() => users.id, { onDelete: 'cascade' }) + .notNull(), + role: projectRoleEnum('role').notNull(), + canUpload: boolean('can_upload').default(false).notNull(), + canComment: boolean('can_comment').default(true).notNull(), + canApprove: boolean('can_approve').default(false).notNull(), + invitedAt: timestamp('invited_at').defaultNow().notNull(), + }, + (table) => [unique().on(table.projectId, table.userId)], +); diff --git a/packages/db/src/schema/tracks.ts b/packages/db/src/schema/tracks.ts new file mode 100644 index 0000000..64a99fd --- /dev/null +++ b/packages/db/src/schema/tracks.ts @@ -0,0 +1,68 @@ +import { + pgTable, + pgEnum, + uuid, + varchar, + text, + integer, + bigint, + real, + timestamp, + unique, +} from 'drizzle-orm/pg-core'; +import { users } from './users.js'; +import { projects } from './projects.js'; + +export const versionStatusEnum = pgEnum('version_status', [ + 'uploaded', + 'processing', + 'ready', + 'approved', + 'rejected', +]); + +export const tracks = pgTable('tracks', { + id: uuid('id').defaultRandom().primaryKey(), + projectId: uuid('project_id') + .references(() => projects.id, { onDelete: 'cascade' }) + .notNull(), + name: varchar('name', { length: 255 }).notNull(), + description: text('description'), + sortOrder: integer('sort_order').default(0).notNull(), + createdById: uuid('created_by_id') + .references(() => users.id) + .notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}); + +export const versions = pgTable( + 'versions', + { + id: uuid('id').defaultRandom().primaryKey(), + trackId: uuid('track_id') + .references(() => tracks.id, { onDelete: 'cascade' }) + .notNull(), + versionNumber: integer('version_number').notNull(), + label: varchar('label', { length: 100 }), + notes: text('notes'), + status: versionStatusEnum('status').default('uploaded').notNull(), + + originalFileName: varchar('original_file_name', { length: 500 }).notNull(), + mimeType: varchar('mime_type', { length: 100 }).notNull(), + fileSize: bigint('file_size', { mode: 'number' }).notNull(), + duration: real('duration'), + sampleRate: integer('sample_rate'), + bitDepth: integer('bit_depth'), + + originalFileKey: text('original_file_key').notNull(), + streamFileKey: text('stream_file_key'), + waveformDataKey: text('waveform_data_key'), + + createdById: uuid('created_by_id') + .references(() => users.id) + .notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + }, + (table) => [unique().on(table.trackId, table.versionNumber)], +); diff --git a/packages/db/src/schema/users.ts b/packages/db/src/schema/users.ts new file mode 100644 index 0000000..91013f2 --- /dev/null +++ b/packages/db/src/schema/users.ts @@ -0,0 +1,10 @@ +import { pgTable, uuid, varchar, text, timestamp } from 'drizzle-orm/pg-core'; + +export const users = pgTable('users', { + id: uuid('id').defaultRandom().primaryKey(), + email: varchar('email', { length: 255 }).notNull().unique(), + name: varchar('name', { length: 255 }).notNull(), + avatarUrl: text('avatar_url'), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}); diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json new file mode 100644 index 0000000..f50aa12 --- /dev/null +++ b/packages/db/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..15b220f --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,11 @@ +{ + "name": "@music-hub/shared", + "version": "0.0.1", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "zod": "^3.24" + } +} diff --git a/packages/shared/src/constants/audio.ts b/packages/shared/src/constants/audio.ts new file mode 100644 index 0000000..6e7d520 --- /dev/null +++ b/packages/shared/src/constants/audio.ts @@ -0,0 +1,24 @@ +export const SUPPORTED_AUDIO_FORMATS = [ + 'audio/wav', + 'audio/x-wav', + 'audio/mp3', + 'audio/mpeg', + 'audio/flac', + 'audio/x-flac', + 'audio/aiff', + 'audio/x-aiff', +] as const; + +export const SUPPORTED_EXTENSIONS = ['.wav', '.mp3', '.flac', '.aiff', '.aif'] as const; + +export const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500 MB + +export const VERSION_STATUSES = [ + 'uploaded', + 'processing', + 'ready', + 'approved', + 'rejected', +] as const; + +export type VersionStatus = (typeof VERSION_STATUSES)[number]; diff --git a/packages/shared/src/constants/index.ts b/packages/shared/src/constants/index.ts new file mode 100644 index 0000000..d59f76f --- /dev/null +++ b/packages/shared/src/constants/index.ts @@ -0,0 +1,3 @@ +export * from './roles.js'; +export * from './permissions.js'; +export * from './audio.js'; diff --git a/packages/shared/src/constants/permissions.ts b/packages/shared/src/constants/permissions.ts new file mode 100644 index 0000000..6696763 --- /dev/null +++ b/packages/shared/src/constants/permissions.ts @@ -0,0 +1,69 @@ +import type { ProjectRole } from './roles.js'; + +export type Permission = + | 'project.edit' + | 'project.invite' + | 'track.upload' + | 'track.listen' + | 'track.download' + | 'version.comment' + | 'version.approve'; + +const PERMISSIONS: Record = { + owner: [ + 'project.edit', + 'project.invite', + 'track.upload', + 'track.listen', + 'track.download', + 'version.comment', + 'version.approve', + ], + recording_engineer: [ + 'track.upload', + 'track.listen', + 'track.download', + 'version.comment', + ], + mixing_engineer: [ + 'track.upload', + 'track.listen', + 'track.download', + 'version.comment', + ], + mastering_engineer: [ + 'track.upload', + 'track.listen', + 'track.download', + 'version.comment', + ], + artist: [ + 'track.listen', + 'track.download', + 'version.comment', + 'version.approve', + ], + label: [ + 'track.listen', + 'version.comment', + 'version.approve', + ], + management: [ + 'project.invite', + 'track.listen', + 'track.download', + 'version.comment', + 'version.approve', + ], + viewer: [ + 'track.listen', + ], +}; + +export function hasPermission(role: ProjectRole, permission: Permission): boolean { + return PERMISSIONS[role].includes(permission); +} + +export function getPermissions(role: ProjectRole): Permission[] { + return PERMISSIONS[role]; +} diff --git a/packages/shared/src/constants/roles.ts b/packages/shared/src/constants/roles.ts new file mode 100644 index 0000000..d26c9a3 --- /dev/null +++ b/packages/shared/src/constants/roles.ts @@ -0,0 +1,29 @@ +export const PROJECT_ROLES = [ + 'owner', + 'recording_engineer', + 'mixing_engineer', + 'mastering_engineer', + 'artist', + 'label', + 'management', + 'viewer', +] as const; + +export type ProjectRole = (typeof PROJECT_ROLES)[number]; + +export const ROLE_LABELS: Record = { + owner: 'Owner', + recording_engineer: 'Recording Engineer', + mixing_engineer: 'Mixing Engineer', + mastering_engineer: 'Mastering Engineer', + artist: 'Artist', + label: 'Label', + management: 'Management', + viewer: 'Viewer', +}; + +export const ENGINEER_ROLES: ProjectRole[] = [ + 'recording_engineer', + 'mixing_engineer', + 'mastering_engineer', +]; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..74866d1 --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,2 @@ +export * from './constants/index.js'; +export * from './validation/index.js'; diff --git a/packages/shared/src/validation/auth.ts b/packages/shared/src/validation/auth.ts new file mode 100644 index 0000000..9b29732 --- /dev/null +++ b/packages/shared/src/validation/auth.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +export const magicLinkSchema = z.object({ + email: z.string().email(), +}); + +export const verifyTokenSchema = z.object({ + token: z.string().min(1), +}); + +export type MagicLinkInput = z.infer; +export type VerifyTokenInput = z.infer; diff --git a/packages/shared/src/validation/comment.ts b/packages/shared/src/validation/comment.ts new file mode 100644 index 0000000..dfaa290 --- /dev/null +++ b/packages/shared/src/validation/comment.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +export const createCommentSchema = z.object({ + body: z.string().min(1).max(5000), + timestampSeconds: z.number().nonnegative().optional(), + parentId: z.string().uuid().optional(), +}); + +export const updateCommentSchema = z.object({ + body: z.string().min(1).max(5000), +}); + +export type CreateCommentInput = z.infer; +export type UpdateCommentInput = z.infer; diff --git a/packages/shared/src/validation/index.ts b/packages/shared/src/validation/index.ts new file mode 100644 index 0000000..9a518b2 --- /dev/null +++ b/packages/shared/src/validation/index.ts @@ -0,0 +1,4 @@ +export * from './auth.js'; +export * from './project.js'; +export * from './track.js'; +export * from './comment.js'; diff --git a/packages/shared/src/validation/project.ts b/packages/shared/src/validation/project.ts new file mode 100644 index 0000000..0b3bdee --- /dev/null +++ b/packages/shared/src/validation/project.ts @@ -0,0 +1,30 @@ +import { z } from 'zod'; +import { PROJECT_ROLES } from '../constants/roles.js'; + +export const createProjectSchema = z.object({ + name: z.string().min(1).max(255), + description: z.string().max(2000).optional(), +}); + +export const updateProjectSchema = z.object({ + name: z.string().min(1).max(255).optional(), + description: z.string().max(2000).optional(), +}); + +export const inviteMemberSchema = z.object({ + email: z.string().email(), + role: z.enum(PROJECT_ROLES).refine((r) => r !== 'owner', { + message: 'Cannot invite as owner', + }), +}); + +export const updateMemberSchema = z.object({ + role: z.enum(PROJECT_ROLES).refine((r) => r !== 'owner', { + message: 'Cannot change role to owner', + }), +}); + +export type CreateProjectInput = z.infer; +export type UpdateProjectInput = z.infer; +export type InviteMemberInput = z.infer; +export type UpdateMemberInput = z.infer; diff --git a/packages/shared/src/validation/track.ts b/packages/shared/src/validation/track.ts new file mode 100644 index 0000000..d2ba0f2 --- /dev/null +++ b/packages/shared/src/validation/track.ts @@ -0,0 +1,32 @@ +import { z } from 'zod'; +import { SUPPORTED_AUDIO_FORMATS, MAX_FILE_SIZE } from '../constants/audio.js'; + +export const createTrackSchema = z.object({ + name: z.string().min(1).max(255), + description: z.string().max(2000).optional(), +}); + +export const updateTrackSchema = z.object({ + name: z.string().min(1).max(255).optional(), + description: z.string().max(2000).optional(), +}); + +export const requestUploadUrlSchema = z.object({ + fileName: z.string().min(1), + mimeType: z.enum(SUPPORTED_AUDIO_FORMATS), + fileSize: z.number().int().positive().max(MAX_FILE_SIZE), +}); + +export const createVersionSchema = z.object({ + fileKey: z.string().min(1), + label: z.string().max(100).optional(), + notes: z.string().max(2000).optional(), + originalFileName: z.string().min(1), + mimeType: z.string().min(1), + fileSize: z.number().int().positive(), +}); + +export type CreateTrackInput = z.infer; +export type UpdateTrackInput = z.infer; +export type RequestUploadUrlInput = z.infer; +export type CreateVersionInput = z.infer; diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..f50aa12 --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..b7fca0a --- /dev/null +++ b/turbo.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://turbo.build/schema.json", + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**"] + }, + "dev": { + "cache": false, + "persistent": true + }, + "db:generate": { + "cache": false + }, + "db:migrate": { + "cache": false + }, + "db:seed": { + "cache": false + } + } +}