From d5a70e531ee96eab7b81a78caf6ab272f3b66ba0 Mon Sep 17 00:00:00 2001 From: coodos Date: Mon, 16 Mar 2026 15:50:17 +0530 Subject: [PATCH 1/7] feat: switch to s3 storage --- platforms/esigner/api/package.json | 6 +- .../api/src/controllers/FileController.ts | 81 ++ .../api/src/controllers/WebhookController.ts | 16 +- .../esigner/api/src/database/entities/File.ts | 7 +- platforms/esigner/api/src/index.ts | 2 + .../esigner/api/src/services/FileService.ts | 36 + .../esigner/api/src/services/S3Service.ts | 97 ++ .../web3adapter/mappings/file.mapping.json | 1 + .../esigner/client/src/lib/stores/files.ts | 40 +- .../(protected)/files/[id]/+page.svelte | 63 +- platforms/file-manager/api/package.json | 5 +- .../api/src/controllers/FileController.ts | 104 ++ .../api/src/controllers/WebhookController.ts | 16 +- .../api/src/database/entities/File.ts | 7 +- platforms/file-manager/api/src/index.ts | 2 + .../api/src/services/FileService.ts | 65 +- .../api/src/services/S3Service.ts | 97 ++ .../web3adapter/mappings/file.mapping.json | 1 + .../client/src/lib/stores/files.ts | 1 + .../src/routes/(protected)/files/+page.svelte | 52 +- .../(protected)/files/[id]/+page.svelte | 40 +- pnpm-lock.yaml | 1278 ++++++++++++++++- services/ontology/schemas/file.json | 7 +- 23 files changed, 1866 insertions(+), 158 deletions(-) create mode 100644 platforms/esigner/api/src/services/S3Service.ts create mode 100644 platforms/file-manager/api/src/services/S3Service.ts diff --git a/platforms/esigner/api/package.json b/platforms/esigner/api/package.json index 80bccf0ff..69ac504b1 100644 --- a/platforms/esigner/api/package.json +++ b/platforms/esigner/api/package.json @@ -13,6 +13,8 @@ "migration:revert": "npm run typeorm migration:revert -- -d src/database/data-source.ts" }, "dependencies": { + "@aws-sdk/client-s3": "^3.1009.0", + "@aws-sdk/s3-request-presigner": "^3.1009.0", "axios": "^1.6.7", "cors": "^2.8.5", "dotenv": "^16.4.5", @@ -23,9 +25,9 @@ "multer": "^1.4.5-lts.1", "pg": "^8.11.3", "reflect-metadata": "^0.2.1", + "signature-validator": "workspace:*", "typeorm": "^0.3.24", "uuid": "^9.0.1", - "signature-validator": "workspace:*", "web3-adapter": "workspace:*" }, "devDependencies": { @@ -44,5 +46,3 @@ "typescript": "^5.3.3" } } - - diff --git a/platforms/esigner/api/src/controllers/FileController.ts b/platforms/esigner/api/src/controllers/FileController.ts index 1fbea406a..c44439996 100644 --- a/platforms/esigner/api/src/controllers/FileController.ts +++ b/platforms/esigner/api/src/controllers/FileController.ts @@ -1,6 +1,7 @@ import { Request, Response } from "express"; import { FileService, ReservedFileNameError } from "../services/FileService"; import multer from "multer"; +import { v4 as uuidv4 } from "uuid"; export const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20MB limit @@ -16,6 +17,80 @@ export class FileController { this.fileService = new FileService(); } + presignUpload = async (req: Request, res: Response) => { + try { + if (!req.user) { + return res.status(401).json({ error: "Authentication required" }); + } + + const { filename, mimeType, size } = req.body; + + if (!filename || !mimeType || !size) { + return res.status(400).json({ error: "filename, mimeType, and size are required" }); + } + + const fileId = uuidv4(); + const key = this.fileService.s3Service.generateKey(req.user.id, fileId, filename); + const uploadUrl = await this.fileService.s3Service.generateUploadUrl(key, mimeType); + + res.json({ uploadUrl, key, fileId }); + } catch (error) { + console.error("Error generating presigned URL:", error); + res.status(500).json({ error: "Failed to generate upload URL" }); + } + }; + + confirmUpload = async (req: Request, res: Response) => { + try { + if (!req.user) { + return res.status(401).json({ error: "Authentication required" }); + } + + const { key, fileId, filename, mimeType, size, displayName, description } = req.body; + + if (!key || !fileId || !filename || !mimeType || !size) { + return res.status(400).json({ error: "key, fileId, filename, mimeType, and size are required" }); + } + + const head = await this.fileService.s3Service.headObject(key); + const md5Hash = head.etag; + const url = this.fileService.s3Service.getPublicUrl(key); + + const file = await this.fileService.createFileWithUrl( + fileId, + filename, + mimeType, + size, + md5Hash, + url, + req.user.id, + displayName, + description, + ); + + res.status(201).json({ + id: file.id, + name: file.name, + displayName: file.displayName, + description: file.description, + mimeType: file.mimeType, + size: file.size, + md5Hash: file.md5Hash, + url: file.url, + createdAt: file.createdAt, + }); + } catch (error) { + console.error("Error confirming upload:", error); + if (error instanceof ReservedFileNameError) { + return res.status(400).json({ error: error.message }); + } + if (error instanceof Error) { + return res.status(400).json({ error: error.message }); + } + res.status(500).json({ error: "Failed to confirm upload" }); + } + }; + uploadFile = [ upload.single('file'), async (req: Request, res: Response) => { @@ -97,6 +172,7 @@ export class FileController { mimeType: file.mimeType, size: file.size, md5Hash: file.md5Hash, + url: file.url, ownerId: file.ownerId, createdAt: file.createdAt, updatedAt: file.updatedAt, @@ -158,6 +234,11 @@ export class FileController { return res.status(404).json({ error: "File not found" }); } + if (file.url) { + return res.redirect(file.url); + } + + // Legacy fallback for files still in DB res.setHeader('Content-Type', file.mimeType); res.setHeader('Content-Disposition', `attachment; filename="${file.name}"`); res.setHeader('Content-Length', file.size.toString()); diff --git a/platforms/esigner/api/src/controllers/WebhookController.ts b/platforms/esigner/api/src/controllers/WebhookController.ts index 4ea2fc5b1..233ea9e09 100644 --- a/platforms/esigner/api/src/controllers/WebhookController.ts +++ b/platforms/esigner/api/src/controllers/WebhookController.ts @@ -299,7 +299,11 @@ export class WebhookController { file.md5Hash = local.data.md5Hash as string; file.ownerId = owner.id; - // Decode base64 data if provided + // Store URL if provided + if (local.data.url && typeof local.data.url === "string") { + file.url = local.data.url; + } + // Legacy: decode base64 data if provided (backward compat) if (local.data.data && typeof local.data.data === "string") { file.data = Buffer.from(local.data.data, "base64"); } @@ -307,13 +311,6 @@ export class WebhookController { this.adapter.addToLockedIds(localId); await this.fileRepository.save(file); } else { - // Create new file with binary data - // Decode base64 data if provided - let fileData: Buffer = Buffer.alloc(0); - if (local.data.data && typeof local.data.data === "string") { - fileData = Buffer.from(local.data.data, "base64"); - } - const file = this.fileRepository.create({ name: local.data.name as string, displayName: local.data.displayName as string | null, @@ -322,7 +319,8 @@ export class WebhookController { size: local.data.size as number, md5Hash: local.data.md5Hash as string, ownerId: owner.id, - data: fileData, + url: (local.data.url as string) || null, + data: local.data.data ? Buffer.from(local.data.data as string, "base64") : null, }); this.adapter.addToLockedIds(file.id); diff --git a/platforms/esigner/api/src/database/entities/File.ts b/platforms/esigner/api/src/database/entities/File.ts index 1a6966808..2b3b22bea 100644 --- a/platforms/esigner/api/src/database/entities/File.ts +++ b/platforms/esigner/api/src/database/entities/File.ts @@ -35,8 +35,11 @@ export class File { @Column({ type: "text" }) md5Hash!: string; - @Column({ type: "bytea" }) - data!: Buffer; + @Column({ type: "bytea", nullable: true }) + data!: Buffer | null; + + @Column({ type: "text", nullable: true }) + url!: string | null; @Column() ownerId!: string; diff --git a/platforms/esigner/api/src/index.ts b/platforms/esigner/api/src/index.ts index 746a817a4..ad669f419 100644 --- a/platforms/esigner/api/src/index.ts +++ b/platforms/esigner/api/src/index.ts @@ -83,6 +83,8 @@ app.post("/api/webhook", webhookController.handleWebhook); app.use(authMiddleware); // File routes +app.post("/api/files/presign", authGuard, fileController.presignUpload); +app.post("/api/files/confirm", authGuard, fileController.confirmUpload); app.post("/api/files", authGuard, fileController.uploadFile); app.get("/api/files", authGuard, fileController.getFiles); app.get("/api/files/:id", authGuard, fileController.getFile); diff --git a/platforms/esigner/api/src/services/FileService.ts b/platforms/esigner/api/src/services/FileService.ts index f6b8ab5c4..d70956394 100644 --- a/platforms/esigner/api/src/services/FileService.ts +++ b/platforms/esigner/api/src/services/FileService.ts @@ -2,6 +2,7 @@ import { AppDataSource } from "../database/data-source"; import { File } from "../database/entities/File"; import { FileSignee } from "../database/entities/FileSignee"; import { SignatureContainer } from "../database/entities/SignatureContainer"; +import { S3Service } from "./S3Service"; import crypto from "crypto"; /** Soft-deleted marker from File Manager (no delete webhook); hide these in eSigner. */ @@ -19,6 +20,7 @@ export class FileService { private fileRepository = AppDataSource.getRepository(File); private fileSigneeRepository = AppDataSource.getRepository(FileSignee); private signatureRepository = AppDataSource.getRepository(SignatureContainer); + public s3Service = new S3Service(); /** * Validates that the given filename is not the reserved soft-delete sentinel. @@ -67,6 +69,39 @@ export class FileService { return savedFile; } + async createFileWithUrl( + id: string, + name: string, + mimeType: string, + size: number, + md5Hash: string, + url: string, + ownerId: string, + displayName?: string, + description?: string + ): Promise { + this.validateFileName(name); + + const fileData: Partial = { + id, + name, + displayName: displayName || name, + mimeType, + size, + md5Hash, + url, + ownerId, + }; + + if (description !== undefined) { + fileData.description = description || null; + } + + const file = this.fileRepository.create(fileData); + const savedFile = await this.fileRepository.save(file); + return savedFile; + } + async getFileById(id: string, userId?: string): Promise { const file = await this.fileRepository.findOne({ where: { id }, @@ -174,6 +209,7 @@ export class FileService { mimeType: file.mimeType, size: file.size, md5Hash: file.md5Hash, + url: file.url, ownerId: file.ownerId, owner: file.owner ? { id: file.owner.id, diff --git a/platforms/esigner/api/src/services/S3Service.ts b/platforms/esigner/api/src/services/S3Service.ts new file mode 100644 index 000000000..a8a437bec --- /dev/null +++ b/platforms/esigner/api/src/services/S3Service.ts @@ -0,0 +1,97 @@ +import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, HeadObjectCommand } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { Readable } from "stream"; + +export class S3Service { + private client: S3Client; + private bucket: string; + private cdnEndpoint: string; + private originEndpoint: string; + + constructor() { + // S3_ORIGIN = https://..digitaloceanspaces.com + // S3_CDN = https://..cdn.digitaloceanspaces.com + // S3_ACCESS_KEY, S3_SECRET_KEY + this.originEndpoint = process.env.S3_ORIGIN || ""; + this.cdnEndpoint = process.env.S3_CDN || ""; + + // Extract bucket name and region from origin URL + // e.g. https://myspace.nyc3.digitaloceanspaces.com → bucket=myspace, region=nyc3 + const originUrl = new URL(this.originEndpoint); + const hostParts = originUrl.hostname.split("."); + this.bucket = hostParts[0]; + const region = hostParts[1]; + + this.client = new S3Client({ + endpoint: `https://${region}.digitaloceanspaces.com`, + region, + credentials: { + accessKeyId: process.env.S3_ACCESS_KEY || "", + secretAccessKey: process.env.S3_SECRET_KEY || "", + }, + forcePathStyle: false, + }); + } + + generateKey(userId: string, fileId: string, filename: string): string { + return `files/${userId}/${fileId}/${filename}`; + } + + async generateUploadUrl(key: string, contentType: string): Promise { + const command = new PutObjectCommand({ + Bucket: this.bucket, + Key: key, + ContentType: contentType, + ACL: "public-read", + }); + + return getSignedUrl(this.client, command, { expiresIn: 900 }); // 15 minutes + } + + getPublicUrl(key: string): string { + return `${this.cdnEndpoint}/${key}`; + } + + extractKeyFromUrl(url: string): string { + if (url.startsWith(this.cdnEndpoint)) { + return url.slice(this.cdnEndpoint.length + 1); + } + if (url.startsWith(this.originEndpoint)) { + return url.slice(this.originEndpoint.length + 1); + } + // Fallback: extract path from URL + const urlObj = new URL(url); + return urlObj.pathname.slice(1); + } + + async headObject(key: string): Promise<{ contentLength: number; etag: string }> { + const command = new HeadObjectCommand({ + Bucket: this.bucket, + Key: key, + }); + + const response = await this.client.send(command); + return { + contentLength: response.ContentLength || 0, + etag: (response.ETag || "").replace(/"/g, ""), + }; + } + + async deleteObject(key: string): Promise { + const command = new DeleteObjectCommand({ + Bucket: this.bucket, + Key: key, + }); + await this.client.send(command); + } + + async getObjectStream(key: string): Promise { + const command = new GetObjectCommand({ + Bucket: this.bucket, + Key: key, + }); + + const response = await this.client.send(command); + return response.Body as Readable; + } +} diff --git a/platforms/esigner/api/src/web3adapter/mappings/file.mapping.json b/platforms/esigner/api/src/web3adapter/mappings/file.mapping.json index 2b92c6d5f..305cf2ad1 100644 --- a/platforms/esigner/api/src/web3adapter/mappings/file.mapping.json +++ b/platforms/esigner/api/src/web3adapter/mappings/file.mapping.json @@ -11,6 +11,7 @@ "size": "size", "md5Hash": "md5Hash", "data": "data", + "url": "url", "ownerId": "users(owner.id),ownerId", "createdAt": "__date(createdAt)", "updatedAt": "__date(updatedAt)" diff --git a/platforms/esigner/client/src/lib/stores/files.ts b/platforms/esigner/client/src/lib/stores/files.ts index 5ca5ec9a9..94b8530ce 100644 --- a/platforms/esigner/client/src/lib/stores/files.ts +++ b/platforms/esigner/client/src/lib/stores/files.ts @@ -22,6 +22,7 @@ export interface Document { mimeType: string; size: number; md5Hash: string; + url: string | null; ownerId: string; owner?: { id: string; @@ -98,19 +99,38 @@ export const uploadFile = async ( try { isLoading.set(true); error.set(null); - const formData = new FormData(); - formData.append("file", file); - if (displayName) { - formData.append("displayName", displayName); - } - if (description) { - formData.append("description", description); - } - const response = await apiClient.post("/api/files", formData, { + + // Step 1: Get presigned upload URL + const presignResponse = await apiClient.post("/api/files/presign", { + filename: file.name, + mimeType: file.type || "application/octet-stream", + size: file.size, + }); + const { uploadUrl, key, fileId } = presignResponse.data; + + // Step 2: Upload directly to S3 + const uploadResponse = await fetch(uploadUrl, { + method: "PUT", + body: file, headers: { - "Content-Type": "multipart/form-data", + "Content-Type": file.type || "application/octet-stream", }, }); + if (!uploadResponse.ok) { + throw new Error(`S3 upload failed with status ${uploadResponse.status}`); + } + + // Step 3: Confirm upload + const response = await apiClient.post("/api/files/confirm", { + key, + fileId, + filename: file.name, + mimeType: file.type || "application/octet-stream", + size: file.size, + displayName, + description, + }); + await fetchDocuments(); return response.data; } catch (err: unknown) { diff --git a/platforms/esigner/client/src/routes/(protected)/files/[id]/+page.svelte b/platforms/esigner/client/src/routes/(protected)/files/[id]/+page.svelte index d8379ec38..06c033ee2 100644 --- a/platforms/esigner/client/src/routes/(protected)/files/[id]/+page.svelte +++ b/platforms/esigner/client/src/routes/(protected)/files/[id]/+page.svelte @@ -102,23 +102,26 @@ async function createPreview() { if (!file) return; - + const isImage = file.mimeType?.startsWith('image/'); const isPDF = file.mimeType === 'application/pdf'; - + if (isImage || isPDF) { - try { - const response = await apiClient.get(`/api/files/${file.id}/download`, { - responseType: 'blob' - }); - const blob = new Blob([response.data], { type: file.mimeType }); - // Clean up old URL if exists - if (previewUrl) { - URL.revokeObjectURL(previewUrl); + if (file.url) { + previewUrl = file.url; + } else { + try { + const response = await apiClient.get(`/api/files/${file.id}/download`, { + responseType: 'blob' + }); + const blob = new Blob([response.data], { type: file.mimeType }); + if (previewUrl) { + URL.revokeObjectURL(previewUrl); + } + previewUrl = URL.createObjectURL(blob); + } catch (err) { + console.error('Failed to load preview:', err); } - previewUrl = URL.createObjectURL(blob); - } catch (err) { - console.error('Failed to load preview:', err); } } } @@ -245,18 +248,28 @@ async function downloadDocument() { try { - const response = await apiClient.get(`/api/files/${file.id}/download`, { - responseType: 'blob' - }); - const blob = new Blob([response.data], { type: file.mimeType }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = file.name; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); + if (file.url) { + const a = document.createElement('a'); + a.href = file.url; + a.download = file.name; + a.target = '_blank'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } else { + const response = await apiClient.get(`/api/files/${file.id}/download`, { + responseType: 'blob' + }); + const blob = new Blob([response.data], { type: file.mimeType }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = file.name; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } showDownloadModal = false; } catch (err) { console.error('Failed to download document:', err); diff --git a/platforms/file-manager/api/package.json b/platforms/file-manager/api/package.json index 9c3f48ea9..99c282834 100644 --- a/platforms/file-manager/api/package.json +++ b/platforms/file-manager/api/package.json @@ -13,6 +13,8 @@ "migration:revert": "npm run typeorm migration:revert -- -d src/database/data-source.ts" }, "dependencies": { + "@aws-sdk/client-s3": "^3.1009.0", + "@aws-sdk/s3-request-presigner": "^3.1009.0", "archiver": "^7.0.1", "axios": "^1.6.7", "cors": "^2.8.5", @@ -24,9 +26,9 @@ "multer": "^1.4.5-lts.1", "pg": "^8.11.3", "reflect-metadata": "^0.2.1", + "signature-validator": "workspace:*", "typeorm": "^0.3.24", "uuid": "^9.0.1", - "signature-validator": "workspace:*", "web3-adapter": "workspace:*" }, "devDependencies": { @@ -46,4 +48,3 @@ "typescript": "^5.3.3" } } - diff --git a/platforms/file-manager/api/src/controllers/FileController.ts b/platforms/file-manager/api/src/controllers/FileController.ts index f4a08d1eb..a9af2540d 100644 --- a/platforms/file-manager/api/src/controllers/FileController.ts +++ b/platforms/file-manager/api/src/controllers/FileController.ts @@ -4,6 +4,7 @@ import archiver from "archiver"; import fs from "fs"; import path from "path"; import os from "os"; +import { v4 as uuidv4 } from "uuid"; import { FileService } from "../services/FileService"; const upload = multer({ @@ -27,6 +28,97 @@ export class FileController { } } + presignUpload = async (req: Request, res: Response) => { + try { + if (!req.user) { + return res.status(401).json({ error: "Authentication required" }); + } + + const { filename, mimeType, size, folderId, displayName, description } = req.body; + + if (!filename || !mimeType || !size) { + return res.status(400).json({ error: "filename, mimeType, and size are required" }); + } + + // Check user's storage quota + const { used, limit } = await this.fileService.getUserStorageUsage(req.user.id); + if (used + size > limit) { + return res.status(413).json({ + error: "Storage quota exceeded", + used, + limit, + fileSize: size, + available: limit - used, + }); + } + + const fileId = uuidv4(); + const key = this.fileService.s3Service.generateKey(req.user.id, fileId, filename); + const uploadUrl = await this.fileService.s3Service.generateUploadUrl(key, mimeType); + + res.json({ uploadUrl, key, fileId }); + } catch (error) { + console.error("Error generating presigned URL:", error); + res.status(500).json({ error: "Failed to generate upload URL" }); + } + }; + + confirmUpload = async (req: Request, res: Response) => { + try { + if (!req.user) { + return res.status(401).json({ error: "Authentication required" }); + } + + const { key, fileId, filename, mimeType, size, folderId, displayName, description } = req.body; + + if (!key || !fileId || !filename || !mimeType || !size) { + return res.status(400).json({ error: "key, fileId, filename, mimeType, and size are required" }); + } + + // Verify file exists in S3 and get ETag for md5Hash + const head = await this.fileService.s3Service.headObject(key); + const md5Hash = head.etag; + const url = this.fileService.s3Service.getPublicUrl(key); + + const normalizedFolderId = + folderId === "null" || folderId === "" || folderId === null || folderId === undefined + ? null + : folderId; + + const file = await this.fileService.createFileWithUrl( + fileId, + filename, + mimeType, + size, + md5Hash, + url, + req.user.id, + normalizedFolderId, + displayName, + description, + ); + + res.status(201).json({ + id: file.id, + name: file.name, + displayName: file.displayName, + description: file.description, + mimeType: file.mimeType, + size: file.size, + md5Hash: file.md5Hash, + url: file.url, + folderId: file.folderId, + createdAt: file.createdAt, + }); + } catch (error) { + console.error("Error confirming upload:", error); + if (error instanceof Error) { + return res.status(400).json({ error: error.message }); + } + res.status(500).json({ error: "Failed to confirm upload" }); + } + }; + uploadFile = [ upload.single("file"), async (req: Request, res: Response) => { @@ -264,6 +356,7 @@ export class FileController { mimeType: file.mimeType, size: file.size, md5Hash: file.md5Hash, + url: file.url, ownerId: file.ownerId, owner: file.owner ? { @@ -314,6 +407,7 @@ export class FileController { mimeType: file.mimeType, size: file.size, md5Hash: file.md5Hash, + url: file.url, ownerId: file.ownerId, folderId: file.folderId, createdAt: file.createdAt, @@ -416,6 +510,11 @@ export class FileController { return res.status(404).json({ error: "File not found" }); } + if (file.url) { + return res.redirect(file.url); + } + + // Legacy fallback for files still in DB res.setHeader("Content-Type", file.mimeType); res.setHeader( "Content-Disposition", @@ -450,6 +549,11 @@ export class FileController { .json({ error: "File type cannot be previewed" }); } + if (file.url) { + return res.redirect(file.url); + } + + // Legacy fallback for files still in DB res.setHeader("Content-Type", file.mimeType); res.setHeader( "Content-Disposition", diff --git a/platforms/file-manager/api/src/controllers/WebhookController.ts b/platforms/file-manager/api/src/controllers/WebhookController.ts index 5d5391323..f389c9f17 100644 --- a/platforms/file-manager/api/src/controllers/WebhookController.ts +++ b/platforms/file-manager/api/src/controllers/WebhookController.ts @@ -312,7 +312,11 @@ export class WebhookController { file.folderId = folderId; } - // Decode base64 data if provided + // Store URL if provided + if (local.data.url && typeof local.data.url === "string") { + file.url = local.data.url; + } + // Legacy: decode base64 data if provided (backward compat) if (local.data.data && typeof local.data.data === "string") { file.data = Buffer.from(local.data.data, "base64"); } @@ -320,13 +324,6 @@ export class WebhookController { this.adapter.addToLockedIds(localId); await this.fileRepository.save(file); } else { - // Create new file with binary data - // Decode base64 data if provided - let fileData: Buffer = Buffer.alloc(0); - if (local.data.data && typeof local.data.data === "string") { - fileData = Buffer.from(local.data.data, "base64"); - } - const file = this.fileRepository.create({ name: local.data.name as string, displayName: local.data.displayName as string | null, @@ -336,7 +333,8 @@ export class WebhookController { md5Hash: local.data.md5Hash as string, ownerId: owner.id, folderId: folderId, - data: fileData, + url: (local.data.url as string) || null, + data: local.data.data ? Buffer.from(local.data.data as string, "base64") : null, }); this.adapter.addToLockedIds(file.id); diff --git a/platforms/file-manager/api/src/database/entities/File.ts b/platforms/file-manager/api/src/database/entities/File.ts index b963e0e8c..5cbf9e488 100644 --- a/platforms/file-manager/api/src/database/entities/File.ts +++ b/platforms/file-manager/api/src/database/entities/File.ts @@ -38,8 +38,11 @@ export class File { @Column({ type: "text" }) md5Hash!: string; - @Column({ type: "bytea" }) - data!: Buffer; + @Column({ type: "bytea", nullable: true }) + data!: Buffer | null; + + @Column({ type: "text", nullable: true }) + url!: string | null; @Column() ownerId!: string; diff --git a/platforms/file-manager/api/src/index.ts b/platforms/file-manager/api/src/index.ts index 9b3993f3e..a8dcee982 100644 --- a/platforms/file-manager/api/src/index.ts +++ b/platforms/file-manager/api/src/index.ts @@ -85,6 +85,8 @@ app.post("/api/webhook", webhookController.handleWebhook); app.use(authMiddleware); // File routes +app.post("/api/files/presign", authGuard, fileController.presignUpload); +app.post("/api/files/confirm", authGuard, fileController.confirmUpload); app.post("/api/files", authGuard, fileController.uploadFile); app.post("/api/files/download-zip", authGuard, fileController.downloadFilesAsZip); app.get("/api/files", authGuard, fileController.getFiles); diff --git a/platforms/file-manager/api/src/services/FileService.ts b/platforms/file-manager/api/src/services/FileService.ts index 59db7de6a..bf71b464b 100644 --- a/platforms/file-manager/api/src/services/FileService.ts +++ b/platforms/file-manager/api/src/services/FileService.ts @@ -8,6 +8,7 @@ import { User } from "../database/entities/User"; import { In, IsNull, Not } from "typeorm"; import crypto from "crypto"; import { Readable } from "stream"; +import { S3Service } from "./S3Service"; /** Soft-delete marker: file is hidden and syncs to eSigner so they can hide it too (no delete webhook). */ export const SOFT_DELETED_FILE_NAME = "[[deleted]]"; @@ -18,6 +19,7 @@ export class FileService { private folderRepository = AppDataSource.getRepository(Folder); private signatureRepository = AppDataSource.getRepository(SignatureContainer); private userRepository = AppDataSource.getRepository(User); + public s3Service = new S3Service(); async calculateMD5(buffer: Buffer): Promise { return crypto.createHash('md5').update(buffer).digest('hex'); @@ -68,6 +70,50 @@ export class FileService { return savedFile; } + async createFileWithUrl( + id: string, + name: string, + mimeType: string, + size: number, + md5Hash: string, + url: string, + ownerId: string, + folderId?: string | null, + displayName?: string, + description?: string + ): Promise { + const normalizedFolderId = folderId === 'null' || folderId === '' || folderId === null || folderId === undefined ? null : folderId; + + if (normalizedFolderId) { + const folder = await this.folderRepository.findOne({ + where: { id: normalizedFolderId, ownerId }, + }); + if (!folder) { + throw new Error("Folder not found or user is not the owner"); + } + } + + const fileData: Partial = { + id, + name, + displayName: displayName || name, + mimeType, + size, + md5Hash, + url, + ownerId, + folderId: normalizedFolderId, + }; + + if (description !== undefined) { + fileData.description = description || null; + } + + const file = this.fileRepository.create(fileData); + const savedFile = await this.fileRepository.save(file); + return savedFile; + } + async getFileById(id: string, userId: string): Promise { const file = await this.fileRepository.findOne({ where: { id }, @@ -374,7 +420,7 @@ export class FileService { async getFileMetadataById(id: string, userId: string): Promise | null> { const file = await this.fileRepository.findOne({ where: { id }, - select: ["id", "name", "displayName", "mimeType", "size", "md5Hash", "ownerId", "folderId", "createdAt", "updatedAt"], + select: ["id", "name", "displayName", "mimeType", "size", "md5Hash", "url", "ownerId", "folderId", "createdAt", "updatedAt"], }); if (!file || file.name === SOFT_DELETED_FILE_NAME) { @@ -413,13 +459,24 @@ export class FileService { * PostgreSQL large objects or file system storage. */ async getFileDataStream(id: string, userId: string): Promise<{ stream: Readable; size: number; name: string; mimeType: string } | null> { - // First verify access with metadata only const metadata = await this.getFileMetadataById(id, userId); if (!metadata) { return null; } - // Fetch only the data column + if (metadata.url) { + // Stream from S3 + const key = this.s3Service.extractKeyFromUrl(metadata.url); + const stream = await this.s3Service.getObjectStream(key); + return { + stream, + size: Number(metadata.size), + name: metadata.displayName || metadata.name, + mimeType: metadata.mimeType, + }; + } + + // Fallback: read from DB (legacy files not yet migrated) const result = await this.fileRepository .createQueryBuilder('file') .select('file.data') @@ -430,9 +487,7 @@ export class FileService { return null; } - // Convert Buffer to Readable stream const stream = Readable.from(result.file_data); - return { stream, size: Number(metadata.size), diff --git a/platforms/file-manager/api/src/services/S3Service.ts b/platforms/file-manager/api/src/services/S3Service.ts new file mode 100644 index 000000000..a8a437bec --- /dev/null +++ b/platforms/file-manager/api/src/services/S3Service.ts @@ -0,0 +1,97 @@ +import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, HeadObjectCommand } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { Readable } from "stream"; + +export class S3Service { + private client: S3Client; + private bucket: string; + private cdnEndpoint: string; + private originEndpoint: string; + + constructor() { + // S3_ORIGIN = https://..digitaloceanspaces.com + // S3_CDN = https://..cdn.digitaloceanspaces.com + // S3_ACCESS_KEY, S3_SECRET_KEY + this.originEndpoint = process.env.S3_ORIGIN || ""; + this.cdnEndpoint = process.env.S3_CDN || ""; + + // Extract bucket name and region from origin URL + // e.g. https://myspace.nyc3.digitaloceanspaces.com → bucket=myspace, region=nyc3 + const originUrl = new URL(this.originEndpoint); + const hostParts = originUrl.hostname.split("."); + this.bucket = hostParts[0]; + const region = hostParts[1]; + + this.client = new S3Client({ + endpoint: `https://${region}.digitaloceanspaces.com`, + region, + credentials: { + accessKeyId: process.env.S3_ACCESS_KEY || "", + secretAccessKey: process.env.S3_SECRET_KEY || "", + }, + forcePathStyle: false, + }); + } + + generateKey(userId: string, fileId: string, filename: string): string { + return `files/${userId}/${fileId}/${filename}`; + } + + async generateUploadUrl(key: string, contentType: string): Promise { + const command = new PutObjectCommand({ + Bucket: this.bucket, + Key: key, + ContentType: contentType, + ACL: "public-read", + }); + + return getSignedUrl(this.client, command, { expiresIn: 900 }); // 15 minutes + } + + getPublicUrl(key: string): string { + return `${this.cdnEndpoint}/${key}`; + } + + extractKeyFromUrl(url: string): string { + if (url.startsWith(this.cdnEndpoint)) { + return url.slice(this.cdnEndpoint.length + 1); + } + if (url.startsWith(this.originEndpoint)) { + return url.slice(this.originEndpoint.length + 1); + } + // Fallback: extract path from URL + const urlObj = new URL(url); + return urlObj.pathname.slice(1); + } + + async headObject(key: string): Promise<{ contentLength: number; etag: string }> { + const command = new HeadObjectCommand({ + Bucket: this.bucket, + Key: key, + }); + + const response = await this.client.send(command); + return { + contentLength: response.ContentLength || 0, + etag: (response.ETag || "").replace(/"/g, ""), + }; + } + + async deleteObject(key: string): Promise { + const command = new DeleteObjectCommand({ + Bucket: this.bucket, + Key: key, + }); + await this.client.send(command); + } + + async getObjectStream(key: string): Promise { + const command = new GetObjectCommand({ + Bucket: this.bucket, + Key: key, + }); + + const response = await this.client.send(command); + return response.Body as Readable; + } +} diff --git a/platforms/file-manager/api/src/web3adapter/mappings/file.mapping.json b/platforms/file-manager/api/src/web3adapter/mappings/file.mapping.json index 1379c2ceb..f18e37f28 100644 --- a/platforms/file-manager/api/src/web3adapter/mappings/file.mapping.json +++ b/platforms/file-manager/api/src/web3adapter/mappings/file.mapping.json @@ -11,6 +11,7 @@ "size": "size", "md5Hash": "md5Hash", "data": "data", + "url": "url", "ownerId": "users(owner.id),ownerId", "folderId": "folderId", "createdAt": "__date(createdAt)", diff --git a/platforms/file-manager/client/src/lib/stores/files.ts b/platforms/file-manager/client/src/lib/stores/files.ts index 1a1672394..0efcb6211 100644 --- a/platforms/file-manager/client/src/lib/stores/files.ts +++ b/platforms/file-manager/client/src/lib/stores/files.ts @@ -12,6 +12,7 @@ export interface File { mimeType: string; size: number; md5Hash: string; + url: string | null; ownerId: string; folderId: string | null; createdAt: string; diff --git a/platforms/file-manager/client/src/routes/(protected)/files/+page.svelte b/platforms/file-manager/client/src/routes/(protected)/files/+page.svelte index 0e91f9168..d63aaad4f 100644 --- a/platforms/file-manager/client/src/routes/(protected)/files/+page.svelte +++ b/platforms/file-manager/client/src/routes/(protected)/files/+page.svelte @@ -257,24 +257,48 @@ try { fileUploadStatus[file.name] = "uploading"; - const formData = new FormData(); - formData.append("file", file); - if (currentFolderId !== undefined) { - formData.append("folderId", currentFolderId || "null"); - } + // Step 1: Get presigned upload URL + const presignResponse = await apiClient.post("/api/files/presign", { + filename: file.name, + mimeType: file.type || "application/octet-stream", + size: file.size, + folderId: currentFolderId || null, + }); + const { uploadUrl, key, fileId } = presignResponse.data; - const response = await apiClient.post("/api/files", formData, { - headers: { - "Content-Type": "multipart/form-data", - }, - onUploadProgress: (progressEvent) => { - if (progressEvent.total) { + // Step 2: Upload directly to S3 + await new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open("PUT", uploadUrl); + xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream"); + + xhr.upload.onprogress = (event) => { + if (event.lengthComputable) { uploadProgress[file.name] = Math.round( - (progressEvent.loaded / progressEvent.total) * - 100, + (event.loaded / event.total) * 95, // Reserve 5% for confirm ); } - }, + }; + + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + resolve(); + } else { + reject(new Error(`S3 upload failed with status ${xhr.status}`)); + } + }; + xhr.onerror = () => reject(new Error("S3 upload failed")); + xhr.send(file); + }); + + // Step 3: Confirm upload + await apiClient.post("/api/files/confirm", { + key, + fileId, + filename: file.name, + mimeType: file.type || "application/octet-stream", + size: file.size, + folderId: currentFolderId || null, }); fileUploadStatus[file.name] = "success"; diff --git a/platforms/file-manager/client/src/routes/(protected)/files/[id]/+page.svelte b/platforms/file-manager/client/src/routes/(protected)/files/[id]/+page.svelte index cc18681a3..0eae51226 100644 --- a/platforms/file-manager/client/src/routes/(protected)/files/[id]/+page.svelte +++ b/platforms/file-manager/client/src/routes/(protected)/files/[id]/+page.svelte @@ -68,9 +68,13 @@ // Generate preview URL if file can be previewed if (file.canPreview) { - const API_BASE_URL = PUBLIC_FILE_MANAGER_BASE_URL || 'http://localhost:3005'; - const token = localStorage.getItem('file_manager_auth_token'); - previewUrl = `${API_BASE_URL}/api/files/${fileId}/preview?token=${token || ''}`; + if (file.url) { + previewUrl = file.url; + } else { + const API_BASE_URL = PUBLIC_FILE_MANAGER_BASE_URL || 'http://localhost:3005'; + const token = localStorage.getItem('file_manager_auth_token'); + previewUrl = `${API_BASE_URL}/api/files/${fileId}/preview?token=${token || ''}`; + } } // Build breadcrumbs based on file's folder @@ -130,16 +134,26 @@ async function downloadFile() { try { - const response = await apiClient.get(`/api/files/${file.id}/download`, { - responseType: 'blob' - }); - const url = window.URL.createObjectURL(new Blob([response.data])); - const link = document.createElement('a'); - link.href = url; - link.setAttribute('download', file.name); - document.body.appendChild(link); - link.click(); - link.remove(); + if (file.url) { + const link = document.createElement('a'); + link.href = file.url; + link.setAttribute('download', file.name); + link.target = '_blank'; + document.body.appendChild(link); + link.click(); + link.remove(); + } else { + const response = await apiClient.get(`/api/files/${file.id}/download`, { + responseType: 'blob' + }); + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', file.name); + document.body.appendChild(link); + link.click(); + link.remove(); + } toast.success('File downloaded'); } catch (error) { console.error('Download failed:', error); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d6d3291b..5b4e0b82f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2352,6 +2352,12 @@ importers: platforms/esigner/api: dependencies: + '@aws-sdk/client-s3': + specifier: ^3.1009.0 + version: 3.1009.0 + '@aws-sdk/s3-request-presigner': + specifier: ^3.1009.0 + version: 3.1009.0 axios: specifier: ^1.6.7 version: 1.13.6 @@ -2782,6 +2788,12 @@ importers: platforms/file-manager/api: dependencies: + '@aws-sdk/client-s3': + specifier: ^3.1009.0 + version: 3.1009.0 + '@aws-sdk/s3-request-presigner': + specifier: ^3.1009.0 + version: 3.1009.0 archiver: specifier: ^7.0.1 version: 7.0.1 @@ -3871,6 +3883,173 @@ packages: '@auvo/tauri-plugin-crypto-hw-api@0.1.0': resolution: {integrity: sha512-ZvCc1QGaimaLv4p8suRT+inCJE12LCHkciw1cPiIISzdSLLIEg05MMZnanCoefwJL9795KXD64u0vkIZsa+czg==} + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-s3@3.1009.0': + resolution: {integrity: sha512-luy8CxallkoiGWTqU86ca/BbvkWJjs0oala7uIIRN1JtQxMb5i4Yl/PBZVcQFhbK9kQi0PK0GfD8gIpLkI91fw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.973.20': + resolution: {integrity: sha512-i3GuX+lowD892F3IuJf8o6AbyDupMTdyTxQrCJGcn71ni5hTZ82L4nQhcdumxZ7XPJRJJVHS/CR3uYOIIs0PVA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/crc64-nvme@3.972.5': + resolution: {integrity: sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.18': + resolution: {integrity: sha512-X0B8AlQY507i5DwjLByeU2Af4ARsl9Vr84koDcXCbAkplmU+1xBFWxEPrWRAoh56waBne/yJqEloSwvRf4x6XA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.20': + resolution: {integrity: sha512-ey9Lelj001+oOfrbKmS6R2CJAiXX7QKY4Vj9VJv6L2eE6/VjD8DocHIoYqztTm70xDLR4E1jYPTKfIui+eRNDA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.20': + resolution: {integrity: sha512-5flXSnKHMloObNF+9N0cupKegnH1Z37cdVlpETVgx8/rAhCe+VNlkcZH3HDg2SDn9bI765S+rhNPXGDJJPfbtA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.20': + resolution: {integrity: sha512-gEWo54nfqp2jABMu6HNsjVC4hDLpg9HC8IKSJnp0kqWtxIJYHTmiLSsIfI4ScQjxEwpB+jOOH8dOLax1+hy/Hw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.21': + resolution: {integrity: sha512-hah8if3/B/Q+LBYN5FukyQ1Mym6PLPDsBOBsIgNEYD6wLyZg0UmUF/OKIVC3nX9XH8TfTPuITK+7N/jenVACWA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.18': + resolution: {integrity: sha512-Tpl7SRaPoOLT32jbTWchPsn52hYYgJ0kpiFgnwk8pxTANQdUymVSZkzFvv1+oOgZm1CrbQUP9MBeoMZ9IzLZjA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.20': + resolution: {integrity: sha512-p+R+PYR5Z7Gjqf/6pvbCnzEHcqPCpLzR7Yf127HjJ6EAb4hUcD+qsNRnuww1sB/RmSeCLxyay8FMyqREw4p1RA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.20': + resolution: {integrity: sha512-rWCmh8o7QY4CsUj63qopzMzkDq/yPpkrpb+CnjBEFSOg/02T/we7sSTVg4QsDiVS9uwZ8VyONhq98qt+pIh3KA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-bucket-endpoint@3.972.8': + resolution: {integrity: sha512-WR525Rr2QJSETa9a050isktyWi/4yIGcmY3BQ1kpHqb0LqUglQHCS8R27dTJxxWNZvQ0RVGtEZjTCbZJpyF3Aw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-expect-continue@3.972.8': + resolution: {integrity: sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.973.6': + resolution: {integrity: sha512-0nYEgkJH7Yt9k+nZJyllTghnkKaz17TWFcr5Mi0XMVMzYlF4ytDZADQpF2/iJo36cKL5AYSzRsvlykE4M/ErTA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-host-header@3.972.8': + resolution: {integrity: sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-location-constraint@3.972.8': + resolution: {integrity: sha512-KaUoFuoFPziIa98DSQsTPeke1gvGXlc5ZGMhy+b+nLxZ4A7jmJgLzjEF95l8aOQN2T/qlPP3MrAyELm8ExXucw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-logger@3.972.8': + resolution: {integrity: sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.972.8': + resolution: {integrity: sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.20': + resolution: {integrity: sha512-yhva/xL5H4tWQgsBjwV+RRD0ByCzg0TcByDCLp3GXdn/wlyRNfy8zsswDtCvr1WSKQkSQYlyEzPuWkJG0f5HvQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-ssec@3.972.8': + resolution: {integrity: sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-user-agent@3.972.21': + resolution: {integrity: sha512-62XRl1GDYPpkt7cx1AX1SPy9wgNE9Iw/NPuurJu4lmhCWS7sGKO+kS53TQ8eRmIxy3skmvNInnk0ZbWrU5Dpyg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.996.10': + resolution: {integrity: sha512-SlDol5Z+C7Ivnc2rKGqiqfSUmUZzY1qHfVs9myt/nxVwswgfpjdKahyTzLTx802Zfq0NFRs7AejwKzzzl5Co2w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/region-config-resolver@3.972.8': + resolution: {integrity: sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/s3-request-presigner@3.1009.0': + resolution: {integrity: sha512-iLHNt/R35ZWnmP/oxYbcA6NAsOXHn2kUHAii48aPR9A/0+eMXajBaeIQ37CsQPZPTzqGhy0pqBG8CnpcRXP6rg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.8': + resolution: {integrity: sha512-n1qYFD+tbqZuyskVaxUE+t10AUz9g3qzDw3Tp6QZDKmqsjfDmZBd4GIk2EKJJNtcCBtE5YiUjDYA+3djFAFBBg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1009.0': + resolution: {integrity: sha512-KCPLuTqN9u0Rr38Arln78fRG9KXpzsPWmof+PZzfAHMMQq2QED6YjQrkrfiH7PDefLWEposY1o4/eGwrmKA4JA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.6': + resolution: {integrity: sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-arn-parser@3.972.3': + resolution: {integrity: sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-endpoints@3.996.5': + resolution: {integrity: sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-format-url@3.972.8': + resolution: {integrity: sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.5': + resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-user-agent-browser@3.972.8': + resolution: {integrity: sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==} + + '@aws-sdk/util-user-agent-node@3.973.7': + resolution: {integrity: sha512-Hz6EZMUAEzqUd7e+vZ9LE7mn+5gMbxltXy18v+YSFY+9LBJz15wkNZvw5JqfX3z0FS9n3bgUtz3L5rAsfh4YlA==} + engines: {node: '>=20.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.972.11': + resolution: {integrity: sha512-iitV/gZKQMvY9d7ovmyFnFuTHbBAtrmLnvaSb/3X8vOKyevwtpmEtyc8AdhVWZe0pI/1GsHxlEvQeOePFzy7KQ==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -8545,6 +8724,222 @@ packages: '@slorber/remark-comment@1.0.0': resolution: {integrity: sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==} + '@smithy/abort-controller@4.2.12': + resolution: {integrity: sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader-native@4.2.3': + resolution: {integrity: sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader@5.2.2': + resolution: {integrity: sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==} + engines: {node: '>=18.0.0'} + + '@smithy/config-resolver@4.4.11': + resolution: {integrity: sha512-YxFiiG4YDAtX7WMN7RuhHZLeTmRRAOyCbr+zB8e3AQzHPnUhS8zXjB1+cniPVQI3xbWsQPM0X2aaIkO/ME0ymw==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.23.11': + resolution: {integrity: sha512-952rGf7hBRnhUIaeLp6q4MptKW8sPFe5VvkoZ5qIzFAtx6c/QZ/54FS3yootsyUSf9gJX/NBqEBNdNR7jMIlpQ==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.2.12': + resolution: {integrity: sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-codec@4.2.12': + resolution: {integrity: sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-browser@4.2.12': + resolution: {integrity: sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-config-resolver@4.3.12': + resolution: {integrity: sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-node@4.2.12': + resolution: {integrity: sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-universal@4.2.12': + resolution: {integrity: sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.3.15': + resolution: {integrity: sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-blob-browser@4.2.13': + resolution: {integrity: sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.2.12': + resolution: {integrity: sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-stream-node@4.2.12': + resolution: {integrity: sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.2.12': + resolution: {integrity: sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.2.2': + resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==} + engines: {node: '>=18.0.0'} + + '@smithy/md5-js@4.2.12': + resolution: {integrity: sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.2.12': + resolution: {integrity: sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.4.25': + resolution: {integrity: sha512-dqjLwZs2eBxIUG6Qtw8/YZ4DvzHGIf0DA18wrgtfP6a50UIO7e2nY0FPdcbv5tVJKqWCCU5BmGMOUwT7Puan+A==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.4.42': + resolution: {integrity: sha512-vbwyqHRIpIZutNXZpLAozakzamcINaRCpEy1MYmK6xBeW3xN+TyPRA123GjXnuxZIjc9848MRRCugVMTXxC4Eg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.2.14': + resolution: {integrity: sha512-+CcaLoLa5apzSRtloOyG7lQvkUw2ZDml3hRh4QiG9WyEPfW5Ke/3tPOPiPjUneuT59Tpn8+c3RVaUvvkkwqZwg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.2.12': + resolution: {integrity: sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.3.12': + resolution: {integrity: sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.4.16': + resolution: {integrity: sha512-ULC8UCS/HivdCB3jhi+kLFYe4B5gxH2gi9vHBfEIiRrT2jfKiZNiETJSlzRtE6B26XbBHjPtc8iZKSNqMol9bw==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.2.12': + resolution: {integrity: sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.3.12': + resolution: {integrity: sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.2.12': + resolution: {integrity: sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.2.12': + resolution: {integrity: sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.2.12': + resolution: {integrity: sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.4.7': + resolution: {integrity: sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.3.12': + resolution: {integrity: sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.12.5': + resolution: {integrity: sha512-UqwYawyqSr/aog8mnLnfbPurS0gi4G7IYDcD28cUIBhsvWs1+rQcL2IwkUQ+QZ7dibaoRzhNF99fAQ9AUcO00w==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.13.1': + resolution: {integrity: sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.2.12': + resolution: {integrity: sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.3.2': + resolution: {integrity: sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.2.2': + resolution: {integrity: sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.2.3': + resolution: {integrity: sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.2.2': + resolution: {integrity: sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-config-provider@4.2.2': + resolution: {integrity: sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.3.41': + resolution: {integrity: sha512-M1w1Ux0rSVvBOxIIiqbxvZvhnjQ+VUjJrugtORE90BbadSTH+jsQL279KRL3Hv0w69rE7EuYkV/4Lepz/NBW9g==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.44': + resolution: {integrity: sha512-YPze3/lD1KmWuZsl9JlfhcgGLX7AXhSoaCDtiPntUjNW5/YY0lOHjkcgxyE9x/h5vvS1fzDifMGjzqnNlNiqOQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.3.3': + resolution: {integrity: sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.2.2': + resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.2.12': + resolution: {integrity: sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.2.12': + resolution: {integrity: sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.5.19': + resolution: {integrity: sha512-v4sa+3xTweL1CLO2UP0p7tvIMH/Rq1X4KKOxd568mpe6LSLMQCnDHs4uv7m3ukpl3HvcN2JH6jiCS0SNRXKP/w==} + engines: {node: '>=18.0.0'} + + '@smithy/util-uri-escape@4.2.2': + resolution: {integrity: sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.2.2': + resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-waiter@4.2.13': + resolution: {integrity: sha512-2zdZ9DTHngRtcYxJK1GUDxruNr53kv5W2Lupe0LMU+Imr6ohQg8M2T14MNkj1Y0wS3FFwpgpGQyvuaMF7CiTmQ==} + engines: {node: '>=18.0.0'} + + '@smithy/uuid@1.1.2': + resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==} + engines: {node: '>=18.0.0'} + '@sqlite.org/sqlite-wasm@3.48.0-build4': resolution: {integrity: sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==} hasBin: true @@ -10892,6 +11287,9 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + boxen@6.2.1: resolution: {integrity: sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -12952,6 +13350,10 @@ packages: fast-xml-builder@1.1.3: resolution: {integrity: sha512-1o60KoFw2+LWKQu3IdcfcFlGTW4dpqEWmjhYec6H82AYZU2TVBXep6tMl8Z1Y+wM+ZrzCwe3BZ9Vyd9N2rIvmg==} + fast-xml-parser@5.4.1: + resolution: {integrity: sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==} + hasBin: true + fast-xml-parser@5.5.5: resolution: {integrity: sha512-NLY+V5NNbdmiEszx9n14mZBseJTC50bRq1VHsaxOmR72JDuZt+5J1Co+dC/4JPnyq+WrIHNM69r0sqf7BMb3Mg==} hasBin: true @@ -19727,6 +20129,468 @@ snapshots: dependencies: '@tauri-apps/api': 2.10.1 + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.6 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.6 + tslib: 2.8.1 + + '@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.973.6 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@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.973.6 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.6 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.1009.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.20 + '@aws-sdk/credential-provider-node': 3.972.21 + '@aws-sdk/middleware-bucket-endpoint': 3.972.8 + '@aws-sdk/middleware-expect-continue': 3.972.8 + '@aws-sdk/middleware-flexible-checksums': 3.973.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.8 + '@aws-sdk/middleware-sdk-s3': 3.972.20 + '@aws-sdk/middleware-ssec': 3.972.8 + '@aws-sdk/middleware-user-agent': 3.972.21 + '@aws-sdk/region-config-resolver': 3.972.8 + '@aws-sdk/signature-v4-multi-region': 3.996.8 + '@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.7 + '@smithy/config-resolver': 4.4.11 + '@smithy/core': 3.23.11 + '@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.25 + '@smithy/middleware-retry': 4.4.42 + '@smithy/middleware-serde': 4.2.14 + '@smithy/middleware-stack': 4.2.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/node-http-handler': 4.4.16 + '@smithy/protocol-http': 5.3.12 + '@smithy/smithy-client': 4.12.5 + '@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.41 + '@smithy/util-defaults-mode-node': 4.2.44 + '@smithy/util-endpoints': 3.3.3 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-retry': 4.2.12 + '@smithy/util-stream': 4.5.19 + '@smithy/util-utf8': 4.2.2 + '@smithy/util-waiter': 4.2.13 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.973.20': + dependencies: + '@aws-sdk/types': 3.973.6 + '@aws-sdk/xml-builder': 3.972.11 + '@smithy/core': 3.23.11 + '@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.5 + '@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.8.1 + + '@aws-sdk/crc64-nvme@3.972.5': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.18': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.20': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/types': 3.973.6 + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/node-http-handler': 4.4.16 + '@smithy/property-provider': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/smithy-client': 4.12.5 + '@smithy/types': 4.13.1 + '@smithy/util-stream': 4.5.19 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.20': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/credential-provider-env': 3.972.18 + '@aws-sdk/credential-provider-http': 3.972.20 + '@aws-sdk/credential-provider-login': 3.972.20 + '@aws-sdk/credential-provider-process': 3.972.18 + '@aws-sdk/credential-provider-sso': 3.972.20 + '@aws-sdk/credential-provider-web-identity': 3.972.20 + '@aws-sdk/nested-clients': 3.996.10 + '@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.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.972.20': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/nested-clients': 3.996.10 + '@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.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.972.21': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.18 + '@aws-sdk/credential-provider-http': 3.972.20 + '@aws-sdk/credential-provider-ini': 3.972.20 + '@aws-sdk/credential-provider-process': 3.972.18 + '@aws-sdk/credential-provider-sso': 3.972.20 + '@aws-sdk/credential-provider-web-identity': 3.972.20 + '@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.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.972.18': + dependencies: + '@aws-sdk/core': 3.973.20 + '@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.8.1 + + '@aws-sdk/credential-provider-sso@3.972.20': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/nested-clients': 3.996.10 + '@aws-sdk/token-providers': 3.1009.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.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.972.20': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/nested-clients': 3.996.10 + '@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.8.1 + transitivePeerDependencies: + - aws-crt + + '@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.8.1 + + '@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.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.973.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.20 + '@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.19 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@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.8.1 + + '@aws-sdk/middleware-location-constraint@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.20': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-arn-parser': 3.972.3 + '@smithy/core': 3.23.11 + '@smithy/node-config-provider': 4.3.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/signature-v4': 5.3.12 + '@smithy/smithy-client': 4.12.5 + '@smithy/types': 4.13.1 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-stream': 4.5.19 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-ssec@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.972.21': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-endpoints': 3.996.5 + '@smithy/core': 3.23.11 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-retry': 4.2.12 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.996.10': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.20 + '@aws-sdk/middleware-host-header': 3.972.8 + '@aws-sdk/middleware-logger': 3.972.8 + '@aws-sdk/middleware-recursion-detection': 3.972.8 + '@aws-sdk/middleware-user-agent': 3.972.21 + '@aws-sdk/region-config-resolver': 3.972.8 + '@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.7 + '@smithy/config-resolver': 4.4.11 + '@smithy/core': 3.23.11 + '@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.25 + '@smithy/middleware-retry': 4.4.42 + '@smithy/middleware-serde': 4.2.14 + '@smithy/middleware-stack': 4.2.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/node-http-handler': 4.4.16 + '@smithy/protocol-http': 5.3.12 + '@smithy/smithy-client': 4.12.5 + '@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.41 + '@smithy/util-defaults-mode-node': 4.2.44 + '@smithy/util-endpoints': 3.3.3 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-retry': 4.2.12 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/config-resolver': 4.4.11 + '@smithy/node-config-provider': 4.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/s3-request-presigner@3.1009.0': + dependencies: + '@aws-sdk/signature-v4-multi-region': 3.996.8 + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-format-url': 3.972.8 + '@smithy/middleware-endpoint': 4.4.25 + '@smithy/protocol-http': 5.3.12 + '@smithy/smithy-client': 4.12.5 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.8': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.972.20 + '@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.8.1 + + '@aws-sdk/token-providers@3.1009.0': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/nested-clients': 3.996.10 + '@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.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.973.6': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/util-arn-parser@3.972.3': + dependencies: + tslib: 2.8.1 + + '@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.8.1 + + '@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.8.1 + + '@aws-sdk/util-locate-window@3.965.5': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/types': 4.13.1 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.973.7': + dependencies: + '@aws-sdk/middleware-user-agent': 3.972.21 + '@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.8.1 + + '@aws-sdk/xml-builder@3.972.11': + dependencies: + '@smithy/types': 4.13.1 + fast-xml-parser: 5.4.1 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -25763,6 +26627,345 @@ snapshots: micromark-util-character: 1.2.0 micromark-util-symbol: 1.1.0 + '@smithy/abort-controller@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader-native@4.2.3': + dependencies: + '@smithy/util-base64': 4.3.2 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader@5.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/config-resolver@4.4.11': + 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.8.1 + + '@smithy/core@3.23.11': + 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.19 + '@smithy/util-utf8': 4.2.2 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + + '@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.8.1 + + '@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.8.1 + + '@smithy/eventstream-serde-browser@4.2.12': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-config-resolver@4.3.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-node@4.2.12': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-universal@4.2.12': + dependencies: + '@smithy/eventstream-codec': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@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.8.1 + + '@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.8.1 + + '@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.8.1 + + '@smithy/hash-stream-node@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/md5-js@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.2.12': + dependencies: + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.4.25': + dependencies: + '@smithy/core': 3.23.11 + '@smithy/middleware-serde': 4.2.14 + '@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.8.1 + + '@smithy/middleware-retry@4.4.42': + 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.5 + '@smithy/types': 4.13.1 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-retry': 4.2.12 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.2.14': + dependencies: + '@smithy/core': 3.23.11 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@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.8.1 + + '@smithy/node-http-handler@4.4.16': + dependencies: + '@smithy/abort-controller': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/querystring-builder': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/property-provider@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/protocol-http@5.3.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/querystring-builder@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + '@smithy/util-uri-escape': 4.2.2 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/service-error-classification@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + + '@smithy/shared-ini-file-loader@4.4.7': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@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.8.1 + + '@smithy/smithy-client@4.12.5': + dependencies: + '@smithy/core': 3.23.11 + '@smithy/middleware-endpoint': 4.4.25 + '@smithy/middleware-stack': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-stream': 4.5.19 + tslib: 2.8.1 + + '@smithy/types@4.13.1': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@4.2.12': + dependencies: + '@smithy/querystring-parser': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-base64@4.3.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.2.3': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.2.2': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-config-provider@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.3.41': + dependencies: + '@smithy/property-provider': 4.2.12 + '@smithy/smithy-client': 4.12.5 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.2.44': + dependencies: + '@smithy/config-resolver': 4.4.11 + '@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.5 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.3.3': + dependencies: + '@smithy/node-config-provider': 4.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-retry@4.2.12': + dependencies: + '@smithy/service-error-classification': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-stream@4.5.19': + dependencies: + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/node-http-handler': 4.4.16 + '@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.8.1 + + '@smithy/util-uri-escape@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.2.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-waiter@4.2.13': + dependencies: + '@smithy/abort-controller': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/uuid@1.1.2': + dependencies: + tslib: 2.8.1 + '@sqlite.org/sqlite-wasm@3.48.0-build4': {} '@sqltools/formatter@1.2.5': {} @@ -29007,6 +30210,8 @@ snapshots: boolbase@1.0.0: {} + bowser@2.14.1: {} + boxen@6.2.1: dependencies: ansi-align: 3.0.1 @@ -30913,8 +32118,8 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2) eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.39.4(jiti@2.6.1)) @@ -30977,21 +32182,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.3(supports-color@5.5.0) - eslint: 9.39.4(jiti@2.6.1) - get-tsconfig: 4.13.6 - is-bun-module: 2.0.0 - stable-hash: 0.0.5 - tinyglobby: 0.2.15 - unrs-resolver: 1.11.1 - optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) - transitivePeerDependencies: - - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 @@ -31034,17 +32224,6 @@ snapshots: - supports-color eslint-module-utils@2.12.1(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2) - eslint: 9.39.4(jiti@2.6.1) - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.12.1(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: @@ -31084,35 +32263,6 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 9.39.4(jiti@2.6.1) - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) - hasown: 2.0.2 - is-core-module: 2.16.1 - is-glob: 4.0.3 - minimatch: 3.1.5 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 @@ -31124,7 +32274,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -31745,7 +32895,11 @@ snapshots: fast-xml-builder@1.1.3: dependencies: path-expression-matcher: 1.1.3 - optional: true + + fast-xml-parser@5.4.1: + dependencies: + fast-xml-builder: 1.1.3 + strnum: 2.2.0 fast-xml-parser@5.5.5: dependencies: @@ -35859,8 +37013,7 @@ snapshots: path-exists@5.0.0: {} - path-expression-matcher@1.1.3: - optional: true + path-expression-matcher@1.1.3: {} path-is-absolute@1.0.1: {} @@ -38352,8 +39505,7 @@ snapshots: dependencies: js-tokens: 9.0.1 - strnum@2.2.0: - optional: true + strnum@2.2.0: {} stubs@3.0.0: optional: true diff --git a/services/ontology/schemas/file.json b/services/ontology/schemas/file.json index cbfeaabbc..d4962a206 100644 --- a/services/ontology/schemas/file.json +++ b/services/ontology/schemas/file.json @@ -37,7 +37,12 @@ "data": { "type": "string", "format": "base64", - "description": "Base64-encoded file content (binary data)" + "description": "Base64-encoded file content (binary data, legacy)" + }, + "url": { + "type": ["string", "null"], + "format": "uri", + "description": "URL to the file stored in S3-compatible object storage" }, "ownerId": { "type": "string", From 1c4d0616145b1acddb0a607150369ec9cd4bb938 Mon Sep 17 00:00:00 2001 From: coodos Date: Mon, 16 Mar 2026 15:56:08 +0530 Subject: [PATCH 2/7] chore: make env var names consistent --- platforms/esigner/api/src/services/S3Service.ts | 3 --- platforms/file-manager/api/src/services/S3Service.ts | 3 --- 2 files changed, 6 deletions(-) diff --git a/platforms/esigner/api/src/services/S3Service.ts b/platforms/esigner/api/src/services/S3Service.ts index a8a437bec..8a19d730b 100644 --- a/platforms/esigner/api/src/services/S3Service.ts +++ b/platforms/esigner/api/src/services/S3Service.ts @@ -9,9 +9,6 @@ export class S3Service { private originEndpoint: string; constructor() { - // S3_ORIGIN = https://..digitaloceanspaces.com - // S3_CDN = https://..cdn.digitaloceanspaces.com - // S3_ACCESS_KEY, S3_SECRET_KEY this.originEndpoint = process.env.S3_ORIGIN || ""; this.cdnEndpoint = process.env.S3_CDN || ""; diff --git a/platforms/file-manager/api/src/services/S3Service.ts b/platforms/file-manager/api/src/services/S3Service.ts index a8a437bec..8a19d730b 100644 --- a/platforms/file-manager/api/src/services/S3Service.ts +++ b/platforms/file-manager/api/src/services/S3Service.ts @@ -9,9 +9,6 @@ export class S3Service { private originEndpoint: string; constructor() { - // S3_ORIGIN = https://..digitaloceanspaces.com - // S3_CDN = https://..cdn.digitaloceanspaces.com - // S3_ACCESS_KEY, S3_SECRET_KEY this.originEndpoint = process.env.S3_ORIGIN || ""; this.cdnEndpoint = process.env.S3_CDN || ""; From b357ddeb14cf5b71c1a2791fb5c762e7e78eb523 Mon Sep 17 00:00:00 2001 From: coodos Date: Mon, 16 Mar 2026 15:59:16 +0530 Subject: [PATCH 3/7] chore: make s3 loading lazy --- .../esigner/api/src/services/S3Service.ts | 68 ++++++++++++------- .../api/src/services/S3Service.ts | 68 ++++++++++++------- 2 files changed, 88 insertions(+), 48 deletions(-) diff --git a/platforms/esigner/api/src/services/S3Service.ts b/platforms/esigner/api/src/services/S3Service.ts index 8a19d730b..83dc0ab5b 100644 --- a/platforms/esigner/api/src/services/S3Service.ts +++ b/platforms/esigner/api/src/services/S3Service.ts @@ -3,25 +3,33 @@ import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { Readable } from "stream"; export class S3Service { - private client: S3Client; - private bucket: string; - private cdnEndpoint: string; - private originEndpoint: string; - - constructor() { - this.originEndpoint = process.env.S3_ORIGIN || ""; - this.cdnEndpoint = process.env.S3_CDN || ""; - - // Extract bucket name and region from origin URL - // e.g. https://myspace.nyc3.digitaloceanspaces.com → bucket=myspace, region=nyc3 - const originUrl = new URL(this.originEndpoint); + private _client: S3Client | null = null; + private _bucket: string = ""; + private _region: string = ""; + private _cdnEndpoint: string = ""; + private _originEndpoint: string = ""; + private _initialized = false; + + private init() { + if (this._initialized) return; + this._initialized = true; + + this._originEndpoint = process.env.S3_ORIGIN || ""; + this._cdnEndpoint = process.env.S3_CDN || ""; + + if (!this._originEndpoint) { + console.warn("S3_ORIGIN not set — S3 features will not work"); + return; + } + + const originUrl = new URL(this._originEndpoint); const hostParts = originUrl.hostname.split("."); - this.bucket = hostParts[0]; - const region = hostParts[1]; + this._bucket = hostParts[0]; + this._region = hostParts[1]; - this.client = new S3Client({ - endpoint: `https://${region}.digitaloceanspaces.com`, - region, + this._client = new S3Client({ + endpoint: `https://${this._region}.digitaloceanspaces.com`, + region: this._region, credentials: { accessKeyId: process.env.S3_ACCESS_KEY || "", secretAccessKey: process.env.S3_SECRET_KEY || "", @@ -30,6 +38,17 @@ export class S3Service { }); } + private get client(): S3Client { + this.init(); + if (!this._client) throw new Error("S3 not configured — set S3_ORIGIN env var"); + return this._client; + } + + private get bucket(): string { + this.init(); + return this._bucket; + } + generateKey(userId: string, fileId: string, filename: string): string { return `files/${userId}/${fileId}/${filename}`; } @@ -42,21 +61,22 @@ export class S3Service { ACL: "public-read", }); - return getSignedUrl(this.client, command, { expiresIn: 900 }); // 15 minutes + return getSignedUrl(this.client, command, { expiresIn: 900 }); } getPublicUrl(key: string): string { - return `${this.cdnEndpoint}/${key}`; + this.init(); + return `${this._cdnEndpoint}/${key}`; } extractKeyFromUrl(url: string): string { - if (url.startsWith(this.cdnEndpoint)) { - return url.slice(this.cdnEndpoint.length + 1); + this.init(); + if (this._cdnEndpoint && url.startsWith(this._cdnEndpoint)) { + return url.slice(this._cdnEndpoint.length + 1); } - if (url.startsWith(this.originEndpoint)) { - return url.slice(this.originEndpoint.length + 1); + if (this._originEndpoint && url.startsWith(this._originEndpoint)) { + return url.slice(this._originEndpoint.length + 1); } - // Fallback: extract path from URL const urlObj = new URL(url); return urlObj.pathname.slice(1); } diff --git a/platforms/file-manager/api/src/services/S3Service.ts b/platforms/file-manager/api/src/services/S3Service.ts index 8a19d730b..83dc0ab5b 100644 --- a/platforms/file-manager/api/src/services/S3Service.ts +++ b/platforms/file-manager/api/src/services/S3Service.ts @@ -3,25 +3,33 @@ import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { Readable } from "stream"; export class S3Service { - private client: S3Client; - private bucket: string; - private cdnEndpoint: string; - private originEndpoint: string; - - constructor() { - this.originEndpoint = process.env.S3_ORIGIN || ""; - this.cdnEndpoint = process.env.S3_CDN || ""; - - // Extract bucket name and region from origin URL - // e.g. https://myspace.nyc3.digitaloceanspaces.com → bucket=myspace, region=nyc3 - const originUrl = new URL(this.originEndpoint); + private _client: S3Client | null = null; + private _bucket: string = ""; + private _region: string = ""; + private _cdnEndpoint: string = ""; + private _originEndpoint: string = ""; + private _initialized = false; + + private init() { + if (this._initialized) return; + this._initialized = true; + + this._originEndpoint = process.env.S3_ORIGIN || ""; + this._cdnEndpoint = process.env.S3_CDN || ""; + + if (!this._originEndpoint) { + console.warn("S3_ORIGIN not set — S3 features will not work"); + return; + } + + const originUrl = new URL(this._originEndpoint); const hostParts = originUrl.hostname.split("."); - this.bucket = hostParts[0]; - const region = hostParts[1]; + this._bucket = hostParts[0]; + this._region = hostParts[1]; - this.client = new S3Client({ - endpoint: `https://${region}.digitaloceanspaces.com`, - region, + this._client = new S3Client({ + endpoint: `https://${this._region}.digitaloceanspaces.com`, + region: this._region, credentials: { accessKeyId: process.env.S3_ACCESS_KEY || "", secretAccessKey: process.env.S3_SECRET_KEY || "", @@ -30,6 +38,17 @@ export class S3Service { }); } + private get client(): S3Client { + this.init(); + if (!this._client) throw new Error("S3 not configured — set S3_ORIGIN env var"); + return this._client; + } + + private get bucket(): string { + this.init(); + return this._bucket; + } + generateKey(userId: string, fileId: string, filename: string): string { return `files/${userId}/${fileId}/${filename}`; } @@ -42,21 +61,22 @@ export class S3Service { ACL: "public-read", }); - return getSignedUrl(this.client, command, { expiresIn: 900 }); // 15 minutes + return getSignedUrl(this.client, command, { expiresIn: 900 }); } getPublicUrl(key: string): string { - return `${this.cdnEndpoint}/${key}`; + this.init(); + return `${this._cdnEndpoint}/${key}`; } extractKeyFromUrl(url: string): string { - if (url.startsWith(this.cdnEndpoint)) { - return url.slice(this.cdnEndpoint.length + 1); + this.init(); + if (this._cdnEndpoint && url.startsWith(this._cdnEndpoint)) { + return url.slice(this._cdnEndpoint.length + 1); } - if (url.startsWith(this.originEndpoint)) { - return url.slice(this.originEndpoint.length + 1); + if (this._originEndpoint && url.startsWith(this._originEndpoint)) { + return url.slice(this._originEndpoint.length + 1); } - // Fallback: extract path from URL const urlObj = new URL(url); return urlObj.pathname.slice(1); } From 818b5082fb1c7f66eb25be7bfa2c5e44070818d5 Mon Sep 17 00:00:00 2001 From: coodos Date: Mon, 16 Mar 2026 16:01:18 +0530 Subject: [PATCH 4/7] feat: migrations --- platforms/esigner/api/1773657072411-addurl.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 platforms/esigner/api/1773657072411-addurl.ts diff --git a/platforms/esigner/api/1773657072411-addurl.ts b/platforms/esigner/api/1773657072411-addurl.ts new file mode 100644 index 000000000..974ac674d --- /dev/null +++ b/platforms/esigner/api/1773657072411-addurl.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Addurl1773657072411 implements MigrationInterface { + name = 'Addurl1773657072411' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "files" ADD "url" text`); + await queryRunner.query(`ALTER TABLE "files" ALTER COLUMN "data" DROP NOT NULL`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "files" ALTER COLUMN "data" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "files" DROP COLUMN "url"`); + } + +} From af91550f7baf168202972aaa527199a1d34c377f Mon Sep 17 00:00:00 2001 From: coodos Date: Mon, 16 Mar 2026 16:01:51 +0530 Subject: [PATCH 5/7] feat: fm migration --- .../file-manager/api/1773657041144-addurl.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 platforms/file-manager/api/1773657041144-addurl.ts diff --git a/platforms/file-manager/api/1773657041144-addurl.ts b/platforms/file-manager/api/1773657041144-addurl.ts new file mode 100644 index 000000000..1d2288c43 --- /dev/null +++ b/platforms/file-manager/api/1773657041144-addurl.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Addurl1773657041144 implements MigrationInterface { + name = 'Addurl1773657041144' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "files" ADD "url" text`); + await queryRunner.query(`ALTER TABLE "files" ALTER COLUMN "data" DROP NOT NULL`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "files" ALTER COLUMN "data" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "files" DROP COLUMN "url"`); + } + +} From 687e1f32d9d8603958bafa9b6ac725b36867b28d Mon Sep 17 00:00:00 2001 From: coodos Date: Mon, 16 Mar 2026 16:03:32 +0530 Subject: [PATCH 6/7] chore: move migrations --- .../api/{ => src/database/migrations}/1773657072411-addurl.ts | 0 .../api/{ => src/database/migrations}/1773657041144-addurl.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename platforms/esigner/api/{ => src/database/migrations}/1773657072411-addurl.ts (100%) rename platforms/file-manager/api/{ => src/database/migrations}/1773657041144-addurl.ts (100%) diff --git a/platforms/esigner/api/1773657072411-addurl.ts b/platforms/esigner/api/src/database/migrations/1773657072411-addurl.ts similarity index 100% rename from platforms/esigner/api/1773657072411-addurl.ts rename to platforms/esigner/api/src/database/migrations/1773657072411-addurl.ts diff --git a/platforms/file-manager/api/1773657041144-addurl.ts b/platforms/file-manager/api/src/database/migrations/1773657041144-addurl.ts similarity index 100% rename from platforms/file-manager/api/1773657041144-addurl.ts rename to platforms/file-manager/api/src/database/migrations/1773657041144-addurl.ts From 5eb8c473faf45fa1e0a9864c29ef154577551caa Mon Sep 17 00:00:00 2001 From: coodos Date: Mon, 16 Mar 2026 17:15:15 +0530 Subject: [PATCH 7/7] chore: s3 access issue --- platforms/esigner/client/src/lib/stores/files.ts | 1 + .../client/src/routes/(protected)/files/+page.svelte | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/platforms/esigner/client/src/lib/stores/files.ts b/platforms/esigner/client/src/lib/stores/files.ts index 94b8530ce..777817502 100644 --- a/platforms/esigner/client/src/lib/stores/files.ts +++ b/platforms/esigner/client/src/lib/stores/files.ts @@ -114,6 +114,7 @@ export const uploadFile = async ( body: file, headers: { "Content-Type": file.type || "application/octet-stream", + "x-amz-acl": "public-read", }, }); if (!uploadResponse.ok) { diff --git a/platforms/file-manager/client/src/routes/(protected)/files/+page.svelte b/platforms/file-manager/client/src/routes/(protected)/files/+page.svelte index d63aaad4f..0dd5287b7 100644 --- a/platforms/file-manager/client/src/routes/(protected)/files/+page.svelte +++ b/platforms/file-manager/client/src/routes/(protected)/files/+page.svelte @@ -271,11 +271,12 @@ const xhr = new XMLHttpRequest(); xhr.open("PUT", uploadUrl); xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream"); + xhr.setRequestHeader("x-amz-acl", "public-read"); xhr.upload.onprogress = (event) => { if (event.lengthComputable) { uploadProgress[file.name] = Math.round( - (event.loaded / event.total) * 95, // Reserve 5% for confirm + (event.loaded / event.total) * 95, ); } };