diff --git a/apps/app/package.json b/apps/app/package.json index 5054b762ba..c80be849be 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -8,7 +8,7 @@ "@ai-sdk/react": "^1.2.9", "@aws-sdk/client-s3": "^3.806.0", "@aws-sdk/client-sts": "^3.808.0", - "@aws-sdk/s3-request-presigner": "^3.806.0", + "@aws-sdk/s3-request-presigner": "^3.832.0", "@azure/core-rest-pipeline": "^1.21.0", "@browserbasehq/sdk": "^2.5.0", "@calcom/atoms": "^1.0.102-framer", diff --git a/apps/portal/package.json b/apps/portal/package.json index c00c502184..45ce5c2045 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -2,14 +2,17 @@ "name": "@comp/portal", "version": "0.1.0", "dependencies": { + "@aws-sdk/s3-request-presigner": "^3.832.0", "@comp/db": "workspace:*", "@comp/ui": "workspace:*", "@react-email/components": "^0.0.41", "@react-email/render": "^1.1.2", "@t3-oss/env-nextjs": "^0.13.8", + "@types/jszip": "^3.4.1", "archiver": "^7.0.1", "better-auth": "^1.2.8", "class-variance-authority": "^0.7.1", + "jszip": "^3.10.1", "next": "15.4.0-canary.85", "react-email": "^4.0.15" }, diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/DeviceAgentAccordionItem.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/DeviceAgentAccordionItem.tsx index d6c0ccfa0b..5cc375743b 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/DeviceAgentAccordionItem.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/components/tasks/DeviceAgentAccordionItem.tsx @@ -5,7 +5,7 @@ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@c import { Button } from '@comp/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@comp/ui/card'; import { cn } from '@comp/ui/cn'; -import { CheckCircle2, Circle, Download, XCircle } from 'lucide-react'; +import { CheckCircle2, Circle, Download, Loader2, XCircle } from 'lucide-react'; import Image from 'next/image'; import { useState } from 'react'; import { toast } from 'sonner'; @@ -31,8 +31,10 @@ export function DeviceAgentAccordionItem({ const handleDownload = async () => { setIsDownloading(true); + try { - const response = await fetch('/api/download-agent', { + // First, we need to get a download token/session from the API + const tokenResponse = await fetch('/api/download-agent/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -41,25 +43,52 @@ export function DeviceAgentAccordionItem({ }), }); - if (!response.ok) { - const errorText = await response.text(); - throw new Error(errorText || 'Failed to download agent.'); + if (!tokenResponse.ok) { + const errorText = await tokenResponse.text(); + throw new Error(errorText || 'Failed to prepare download.'); } - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); + const { token } = await tokenResponse.json(); + + // Now trigger the actual download using the browser's native download mechanism + // This will show in the browser's download UI immediately + const downloadUrl = `/api/download-agent?token=${encodeURIComponent(token)}`; + + // Method 1: Using a temporary link (most reliable) const a = document.createElement('a'); - a.href = url; + a.href = downloadUrl; a.download = 'compai-device-agent.zip'; document.body.appendChild(a); a.click(); - a.remove(); - window.URL.revokeObjectURL(url); + document.body.removeChild(a); + + toast.success('Download started! Check your downloads folder.'); } catch (error) { console.error(error); - toast.error(error instanceof Error ? error.message : 'An unknown error occurred.'); + toast.error(error instanceof Error ? error.message : 'Failed to download agent.'); } finally { - setIsDownloading(false); + // Reset after a short delay to allow download to start + setTimeout(() => { + setIsDownloading(false); + }, 1000); + } + }; + + const getButtonContent = () => { + if (isDownloading) { + return ( + <> + + Downloading... + + ); + } else { + return ( + <> + + Download Agent + + ); } }; @@ -104,8 +133,7 @@ export function DeviceAgentAccordionItem({ disabled={isDownloading || hasInstalledAgent} className="gap-2 mt-2" > - - {isDownloading ? 'Downloading...' : 'Download Agent'} + {getButtonContent()}
  • diff --git a/apps/portal/src/app/api/download-agent/route.ts b/apps/portal/src/app/api/download-agent/route.ts index 225384c179..90e6a74b7d 100644 --- a/apps/portal/src/app/api/download-agent/route.ts +++ b/apps/portal/src/app/api/download-agent/route.ts @@ -1,15 +1,117 @@ import { auth } from '@/app/lib/auth'; import { logger } from '@/utils/logger'; +import { s3Client } from '@/utils/s3'; +import { GetObjectCommand } from '@aws-sdk/client-s3'; +import { client as kv } from '@comp/kv'; +import archiver from 'archiver'; import { type NextRequest, NextResponse } from 'next/server'; -import { promises as fs } from 'node:fs'; -import { tmpdir } from 'node:os'; -import path from 'node:path'; -import { createAgentArchive } from './archive'; +import { PassThrough, Readable } from 'stream'; import { createFleetLabel } from './fleet-label'; -import { generateMacScript, generateWindowsScript } from './scripts'; +import { + generateMacScript, + generateWindowsScript, + getPackageFilename, + getReadmeContent, + getScriptFilename, +} from './scripts'; import type { DownloadAgentRequest, SupportedOS } from './types'; import { detectOSFromUserAgent, validateMemberAndOrg } from './utils'; +// GET handler for direct browser downloads using token +export async function GET(req: NextRequest) { + const searchParams = req.nextUrl.searchParams; + const token = searchParams.get('token'); + + if (!token) { + return new NextResponse('Missing download token', { status: 400 }); + } + + // Retrieve download info from KV store + const downloadInfo = await kv.get(`download:${token}`); + + if (!downloadInfo) { + return new NextResponse('Invalid or expired download token', { status: 403 }); + } + + // Delete token after retrieval (one-time use) + await kv.del(`download:${token}`); + + const { orgId, employeeId, os } = downloadInfo as { + orgId: string; + employeeId: string; + userId: string; + os: 'macos' | 'windows'; + }; + + // Check environment configuration + const fleetDevicePathMac = process.env.FLEET_DEVICE_PATH_MAC; + const fleetDevicePathWindows = process.env.FLEET_DEVICE_PATH_WINDOWS; + const fleetBucketName = process.env.FLEET_AGENT_BUCKET_NAME; + + if (!fleetDevicePathMac || !fleetDevicePathWindows || !fleetBucketName) { + return new NextResponse('Server configuration error', { status: 500 }); + } + + // Generate OS-specific script + const fleetDevicePath = os === 'macos' ? fleetDevicePathMac : fleetDevicePathWindows; + const script = + os === 'macos' + ? generateMacScript({ orgId, employeeId, fleetDevicePath }) + : generateWindowsScript({ orgId, employeeId, fleetDevicePath }); + + try { + // Create a passthrough stream for the response + const passThrough = new PassThrough(); + const archive = archiver('zip', { zlib: { level: 9 } }); + + // Pipe archive to passthrough + archive.pipe(passThrough); + + // Add script file + const scriptFilename = getScriptFilename(os); + archive.append(script, { name: scriptFilename, mode: 0o755 }); + + // Add README + const readmeContent = getReadmeContent(os); + archive.append(readmeContent, { name: 'README.txt' }); + + // Get package from S3 and stream it + const packageFilename = getPackageFilename(os); + const packageKey = `${os}/fleet-osquery.${os === 'macos' ? 'pkg' : 'msi'}`; + + const getObjectCommand = new GetObjectCommand({ + Bucket: fleetBucketName, + Key: packageKey, + }); + + const s3Response = await s3Client.send(getObjectCommand); + + if (s3Response.Body) { + const s3Stream = s3Response.Body as Readable; + archive.append(s3Stream, { name: packageFilename, store: true }); + } + + // Finalize the archive + archive.finalize(); + + // Convert Node.js stream to Web Stream for NextResponse + const webStream = Readable.toWeb(passThrough) as unknown as ReadableStream; + + // Return streaming response with headers that trigger browser download + return new NextResponse(webStream, { + headers: { + 'Content-Type': 'application/zip', + 'Content-Disposition': `attachment; filename="compai-device-agent-${os}.zip"`, + 'Cache-Control': 'no-cache, no-store, must-revalidate', + }, + }); + } catch (error) { + logger('Error creating agent download', { error }); + return new NextResponse('Failed to create download', { status: 500 }); + } +} + +// POST handler remains the same for backward compatibility or direct API usage export async function POST(req: NextRequest) { // Authentication const session = await auth.api.getSession({ @@ -44,6 +146,7 @@ export async function POST(req: NextRequest) { // Check environment configuration const fleetDevicePathMac = process.env.FLEET_DEVICE_PATH_MAC; const fleetDevicePathWindows = process.env.FLEET_DEVICE_PATH_WINDOWS; + const fleetBucketName = process.env.FLEET_AGENT_BUCKET_NAME; if (!fleetDevicePathMac || !fleetDevicePathWindows) { logger( @@ -57,6 +160,12 @@ export async function POST(req: NextRequest) { ); } + if (!fleetBucketName) { + return new NextResponse('Server configuration error: Fleet bucket name is missing.', { + status: 500, + }); + } + // Validate member and organization const member = await validateMemberAndOrg(session.user.id, orgId); if (!member) { @@ -70,18 +179,7 @@ export async function POST(req: NextRequest) { ? generateMacScript({ orgId, employeeId, fleetDevicePath }) : generateWindowsScript({ orgId, employeeId, fleetDevicePath }); - // Create temporary directory - const tempDir = path.join(tmpdir(), `compai-agent-${Date.now()}`); - await fs.mkdir(tempDir, { recursive: true }); - try { - // Create the archive - const stream = await createAgentArchive({ - os: os as SupportedOS, - script, - tempDir, - }); - // Create Fleet label await createFleetLabel({ employeeId, @@ -91,20 +189,53 @@ export async function POST(req: NextRequest) { fleetDevicePathWindows, }); - const filename = `compai-device-agent-${os}.zip`; + // Create a passthrough stream for the response + const passThrough = new PassThrough(); + const archive = archiver('zip', { zlib: { level: 9 } }); + + // Pipe archive to passthrough + archive.pipe(passThrough); - return new NextResponse(stream as unknown as ReadableStream, { + // Add script file + const scriptFilename = getScriptFilename(os); + archive.append(script, { name: scriptFilename, mode: 0o755 }); + + // Add README + const readmeContent = getReadmeContent(os); + archive.append(readmeContent, { name: 'README.txt' }); + + // Get package from S3 and stream it + const packageFilename = getPackageFilename(os); + const packageKey = `${os}/fleet-osquery.${os === 'macos' ? 'pkg' : 'msi'}`; + + const getObjectCommand = new GetObjectCommand({ + Bucket: fleetBucketName, + Key: packageKey, + }); + + const s3Response = await s3Client.send(getObjectCommand); + + if (s3Response.Body) { + const s3Stream = s3Response.Body as Readable; + archive.append(s3Stream, { name: packageFilename, store: true }); + } + + // Finalize the archive + archive.finalize(); + + // Convert Node.js stream to Web Stream for NextResponse + const webStream = Readable.toWeb(passThrough) as unknown as ReadableStream; + + // Return streaming response + return new NextResponse(webStream, { headers: { 'Content-Type': 'application/zip', - 'Content-Disposition': `attachment; filename="${filename}"`, + 'Content-Disposition': `attachment; filename="compai-device-agent-${os}.zip"`, + 'Cache-Control': 'no-cache', }, }); - } finally { - // Clean up temp directory - try { - await fs.rm(tempDir, { recursive: true, force: true }); - } catch (cleanupError) { - logger('Failed to clean up temp directory', { error: cleanupError, tempDir }); - } + } catch (error) { + logger('Error creating agent download', { error }); + return new NextResponse('Failed to create download', { status: 500 }); } } diff --git a/apps/portal/src/app/api/download-agent/token/route.ts b/apps/portal/src/app/api/download-agent/token/route.ts new file mode 100644 index 0000000000..602343662b --- /dev/null +++ b/apps/portal/src/app/api/download-agent/token/route.ts @@ -0,0 +1,59 @@ +import { auth } from '@/app/lib/auth'; +import { client as kv } from '@comp/kv'; +import { randomBytes } from 'crypto'; +import { type NextRequest, NextResponse } from 'next/server'; +import type { DownloadAgentRequest } from '../types'; +import { detectOSFromUserAgent, validateMemberAndOrg } from '../utils'; + +export async function POST(req: NextRequest) { + // Authentication + const session = await auth.api.getSession({ + headers: req.headers, + }); + + if (!session?.user) { + return new NextResponse('Unauthorized', { status: 401 }); + } + + // Validate request body + const { orgId, employeeId }: DownloadAgentRequest = await req.json(); + + if (!orgId || !employeeId) { + return new NextResponse('Missing orgId or employeeId', { status: 400 }); + } + + // Validate member and organization + const member = await validateMemberAndOrg(session.user.id, orgId); + if (!member) { + return new NextResponse('Member not found or organization invalid', { status: 404 }); + } + + // Auto-detect OS from User-Agent + const userAgent = req.headers.get('user-agent'); + const detectedOS = detectOSFromUserAgent(userAgent); + + if (!detectedOS) { + return new NextResponse( + 'Could not detect OS from User-Agent. Please use a standard browser on macOS or Windows.', + { status: 400 }, + ); + } + + // Generate a secure random token + const token = randomBytes(32).toString('hex'); + + // Store token with download info in KV store (expires in 5 minutes) + await kv.set( + `download:${token}`, + { + orgId, + employeeId, + userId: session.user.id, + os: detectedOS, + createdAt: Date.now(), + }, + { ex: 300 }, // 5 minutes + ); + + return NextResponse.json({ token }); +} diff --git a/apps/portal/src/utils/s3.ts b/apps/portal/src/utils/s3.ts index b73f2662e3..fe3498684f 100644 --- a/apps/portal/src/utils/s3.ts +++ b/apps/portal/src/utils/s3.ts @@ -1,4 +1,5 @@ -import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'; +import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; const AWS_REGION = process.env.AWS_REGION; const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID; @@ -138,3 +139,46 @@ export async function getFleetAgent({ os }: { os: 'macos' | 'windows' }) { const response = await s3Client.send(getFleetAgentCommand); return response.Body; } + +/** + * Generates a presigned URL for downloading a file from S3 + */ +export async function getPresignedDownloadUrl({ + bucketName, + key, + expiresIn = 3600, // 1 hour default +}: { + bucketName: string; + key: string; + expiresIn?: number; +}): Promise { + const command = new GetObjectCommand({ + Bucket: bucketName, + Key: key, + }); + + return await getSignedUrl(s3Client, command, { expiresIn }); +} + +/** + * Generates a presigned URL for uploading a file to S3 + */ +export async function getPresignedUploadUrl({ + bucketName, + key, + contentType, + expiresIn = 3600, // 1 hour default +}: { + bucketName: string; + key: string; + contentType?: string; + expiresIn?: number; +}): Promise { + const command = new PutObjectCommand({ + Bucket: bucketName, + Key: key, + ContentType: contentType, + }); + + return await getSignedUrl(s3Client, command, { expiresIn }); +} diff --git a/bun.lock b/bun.lock index efae11d292..490436d1a9 100644 --- a/bun.lock +++ b/bun.lock @@ -183,14 +183,17 @@ "name": "@comp/portal", "version": "0.1.0", "dependencies": { + "@aws-sdk/s3-request-presigner": "^3.832.0", "@comp/db": "workspace:*", "@comp/ui": "workspace:*", "@react-email/components": "^0.0.41", "@react-email/render": "^1.1.2", "@t3-oss/env-nextjs": "^0.13.8", + "@types/jszip": "^3.4.1", "archiver": "^7.0.1", "better-auth": "^1.2.8", "class-variance-authority": "^0.7.1", + "jszip": "^3.10.1", "next": "15.4.0-canary.85", "react-email": "^4.0.15", }, @@ -1677,6 +1680,8 @@ "@types/jsonwebtoken": ["@types/jsonwebtoken@8.5.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg=="], + "@types/jszip": ["@types/jszip@3.4.1", "", { "dependencies": { "jszip": "*" } }, "sha512-TezXjmf3lj+zQ651r6hPqvSScqBLvyPI9FxdXBqpEwBijNGQ2NXpaFW/7joGzveYkKQUil7iiDHLo6LV71Pc0A=="], + "@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="], "@types/lodash": ["@types/lodash@4.17.18", "", {}, "sha512-KJ65INaxqxmU6EoCiJmRPZC9H9RVWCRd349tXM2M3O5NA7cY6YL7c0bHAHQ93NOfTObEQ004kd2QVHs/r0+m4g=="], @@ -2667,6 +2672,8 @@ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "import-from-esm": ["import-from-esm@2.0.0", "", { "dependencies": { "debug": "^4.3.4", "import-meta-resolve": "^4.0.0" } }, "sha512-YVt14UZCgsX1vZQ3gKjkWVdBdHQ6eu3MPU1TBgL1H5orXe2+jWD006WCPPtOuwlQm10NuzOW5WawiF1Q9veW8g=="], @@ -2861,6 +2868,8 @@ "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], + "jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="], "jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="], @@ -2885,6 +2894,8 @@ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], + "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], @@ -3283,6 +3294,8 @@ "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], @@ -3675,6 +3688,8 @@ "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], @@ -4401,6 +4416,8 @@ "jsondiffpatch/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + "jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "load-json-file/parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="], @@ -5163,6 +5180,10 @@ "geist/next/sharp": ["sharp@0.34.2", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.4", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.2", "@img/sharp-darwin-x64": "0.34.2", "@img/sharp-libvips-darwin-arm64": "1.1.0", "@img/sharp-libvips-darwin-x64": "1.1.0", "@img/sharp-libvips-linux-arm": "1.1.0", "@img/sharp-libvips-linux-arm64": "1.1.0", "@img/sharp-libvips-linux-ppc64": "1.1.0", "@img/sharp-libvips-linux-s390x": "1.1.0", "@img/sharp-libvips-linux-x64": "1.1.0", "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", "@img/sharp-libvips-linuxmusl-x64": "1.1.0", "@img/sharp-linux-arm": "0.34.2", "@img/sharp-linux-arm64": "0.34.2", "@img/sharp-linux-s390x": "0.34.2", "@img/sharp-linux-x64": "0.34.2", "@img/sharp-linuxmusl-arm64": "0.34.2", "@img/sharp-linuxmusl-x64": "0.34.2", "@img/sharp-wasm32": "0.34.2", "@img/sharp-win32-arm64": "0.34.2", "@img/sharp-win32-ia32": "0.34.2", "@img/sharp-win32-x64": "0.34.2" } }, "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg=="], + "jszip/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], diff --git a/yarn.lock b/yarn.lock index 2674bb852d..c2788af917 100644 --- a/yarn.lock +++ b/yarn.lock @@ -636,7 +636,7 @@ "@smithy/util-middleware" "^4.0.4" tslib "^2.6.2" -"@aws-sdk/s3-request-presigner@^3.806.0": +"@aws-sdk/s3-request-presigner@^3.806.0", "@aws-sdk/s3-request-presigner@^3.832.0": version "3.832.0" resolved "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.832.0.tgz" integrity sha512-zXuwfaAYu99LUF7/6iBr3UlKCMaMImBwfmLXJQlvtE3ebrERXQuISME9Vjd2oG+hJ6XcX6RJqkeIvZBytMzvRw== @@ -1389,6 +1389,8 @@ react-dom "^19.1.0" tailwindcss "^4.1.8" typescript "^5.8.3" + dependencies: + "@aws-sdk/s3-request-presigner" "^3.832.0" dependencies: "@comp/db" "workspace:*" "@comp/ui" "workspace:*" @@ -1396,9 +1398,11 @@ "@react-email/components" "^0.0.41" "@react-email/render" "^1.1.2" "@t3-oss/env-nextjs" "^0.13.8" + "@types/jszip" "^3.4.1" archiver "^7.0.1" better-auth "^1.2.8" class-variance-authority "^0.7.1" + jszip "^3.10.1" next "15.4.0-canary.85" react-email "^4.0.15" @@ -5974,6 +5978,13 @@ dependencies: "@types/node" "*" +"@types/jszip@^3.4.1": + version "3.4.1" + resolved "https://registry.npmjs.org/@types/jszip/-/jszip-3.4.1.tgz" + integrity sha512-TezXjmf3lj+zQ651r6hPqvSScqBLvyPI9FxdXBqpEwBijNGQ2NXpaFW/7joGzveYkKQUil7iiDHLo6LV71Pc0A== + dependencies: + jszip "*" + "@types/linkify-it@^5": version "5.0.0" resolved "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz" @@ -10220,6 +10231,11 @@ ignore-walk@^7.0.0: dependencies: minimatch "^9.0.0" +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.1" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz" @@ -10979,6 +10995,16 @@ jsprim@^1.2.2: object.assign "^4.1.4" object.values "^1.1.6" +jszip@*, jszip@^3.10.1: + version "3.10.1" + resolved "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz" + integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + setimmediate "^1.0.5" + just-diff@^6.0.0: version "6.0.2" resolved "https://registry.npmjs.org/just-diff/-/just-diff-6.0.2.tgz" @@ -11178,6 +11204,13 @@ libnpmversion@^7.0.0: proc-log "^5.0.0" semver "^7.3.7" +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + lightningcss@1.30.1: version "1.30.1" resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz" @@ -13147,6 +13180,11 @@ pacote@^20.0.0: ssri "^12.0.0" tar "^6.1.11" +pako@~1.0.2: + version "1.0.11" + resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" @@ -14914,6 +14952,11 @@ set-proto@^1.0.0: es-errors "^1.3.0" es-object-atoms "^1.0.0" +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz"