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"