From d74128d3cdc6d04643e12abc29bb2405904fe7cd Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Nov 2025 03:51:31 +0000 Subject: [PATCH] Add Agent 3: File Upload & Storage System Implements cloud-based file storage for plant photos, documents, and certificates: Storage Layer: - Multi-provider support (AWS S3, Cloudflare R2, MinIO, local filesystem) - S3-compatible provider with presigned URL generation - Local storage provider for development with signed URL verification - Configurable via environment variables Image Processing: - Automatic thumbnail generation (150x150, 300x300, 600x600, 1200x1200) - WebP conversion for optimized file sizes - EXIF data extraction for image metadata - Image optimization with Sharp API Endpoints: - POST /api/upload/image - Upload images with automatic processing - POST /api/upload/document - Upload documents (PDF, DOC, DOCX) - POST /api/upload/presigned - Get presigned URLs for direct uploads - GET/DELETE /api/upload/[fileId] - File management UI Components: - ImageUploader - Drag & drop image upload with preview - PhotoGallery - Grid gallery with lightbox view - DocumentUploader - Document upload with file type icons - ProgressBar - Animated upload progress indicator Database: - FileStore service with in-memory storage (Prisma schema ready for Agent 2) - File metadata tracking with soft delete support - Category-based file organization --- bun.lock | 330 +++++++++++++++++++++++++ components/upload/DocumentUploader.tsx | 236 ++++++++++++++++++ components/upload/ImageUploader.tsx | 266 ++++++++++++++++++++ components/upload/PhotoGallery.tsx | 213 ++++++++++++++++ components/upload/ProgressBar.tsx | 63 +++++ components/upload/index.tsx | 11 + lib/storage/config.ts | 87 +++++++ lib/storage/fileStore.ts | 258 +++++++++++++++++++ lib/storage/imageProcessor.ts | 269 ++++++++++++++++++++ lib/storage/index.ts | 34 +++ lib/storage/providers/local.ts | 131 ++++++++++ lib/storage/providers/s3.ts | 115 +++++++++ lib/storage/types.ts | 133 ++++++++++ lib/storage/uploadService.ts | 270 ++++++++++++++++++++ package.json | 6 + pages/api/upload/[fileId].ts | 99 ++++++++ pages/api/upload/document.ts | 160 ++++++++++++ pages/api/upload/image.ts | 153 ++++++++++++ pages/api/upload/presigned.ts | 92 +++++++ 19 files changed, 2926 insertions(+) create mode 100644 components/upload/DocumentUploader.tsx create mode 100644 components/upload/ImageUploader.tsx create mode 100644 components/upload/PhotoGallery.tsx create mode 100644 components/upload/ProgressBar.tsx create mode 100644 components/upload/index.tsx create mode 100644 lib/storage/config.ts create mode 100644 lib/storage/fileStore.ts create mode 100644 lib/storage/imageProcessor.ts create mode 100644 lib/storage/index.ts create mode 100644 lib/storage/providers/local.ts create mode 100644 lib/storage/providers/s3.ts create mode 100644 lib/storage/types.ts create mode 100644 lib/storage/uploadService.ts create mode 100644 pages/api/upload/[fileId].ts create mode 100644 pages/api/upload/document.ts create mode 100644 pages/api/upload/image.ts create mode 100644 pages/api/upload/presigned.ts diff --git a/bun.lock b/bun.lock index 8bee58b..4de55e8 100644 --- a/bun.lock +++ b/bun.lock @@ -5,25 +5,31 @@ "": { "name": "localgreenchain", "dependencies": { + "@aws-sdk/client-s3": "^3.937.0", + "@aws-sdk/s3-request-presigner": "^3.937.0", "@tailwindcss/forms": "^0.4.0", "@tailwindcss/typography": "^0.5.1", "@tanstack/react-query": "^4.0.10", "classnames": "^2.3.1", "drupal-jsonapi-params": "^1.2.2", "html-react-parser": "^1.2.7", + "multer": "^2.0.2", "next": "^12.2.3", "next-drupal": "^1.6.0", "nprogress": "^0.2.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-hook-form": "^7.8.6", + "sharp": "^0.34.5", "socks-proxy-agent": "^8.0.2", }, "devDependencies": { "@babel/core": "^7.12.9", "@types/jest": "^29.5.0", + "@types/multer": "^2.0.0", "@types/node": "^17.0.21", "@types/react": "^17.0.0", + "@types/sharp": "^0.32.0", "autoprefixer": "^10.4.2", "eslint-config-next": "^12.0.10", "jest": "^29.5.0", @@ -37,6 +43,90 @@ "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], + + "@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="], + + "@aws-crypto/sha1-browser": ["@aws-crypto/sha1-browser@5.2.0", "", { "dependencies": { "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg=="], + + "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], + + "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], + + "@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="], + + "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + + "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.937.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.936.0", "@aws-sdk/credential-provider-node": "3.936.0", "@aws-sdk/middleware-bucket-endpoint": "3.936.0", "@aws-sdk/middleware-expect-continue": "3.936.0", "@aws-sdk/middleware-flexible-checksums": "3.936.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-location-constraint": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-sdk-s3": "3.936.0", "@aws-sdk/middleware-ssec": "3.936.0", "@aws-sdk/middleware-user-agent": "3.936.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/signature-v4-multi-region": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.936.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-blob-browser": "^4.2.6", "@smithy/hash-node": "^4.2.5", "@smithy/hash-stream-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/md5-js": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-ioeNe6HSc7PxjsUQY7foSHmgesxM5KwAeUtPhIHgKx99nrM+7xYCfW4FMvHypUzz7ZOvqlCdH7CEAZ8ParBvVg=="], + + "@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.936.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.936.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.936.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.936.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-0G73S2cDqYwJVvqL08eakj79MZG2QRaB56Ul8/Ps9oQxllr7DMI1IQ/N3j3xjxgpq/U36pkoFZ8aK1n7Sbr3IQ=="], + + "@aws-sdk/core": ["@aws-sdk/core@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws-sdk/xml-builder": "3.930.0", "@smithy/core": "^3.18.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-eGJ2ySUMvgtOziHhDRDLCrj473RJoL4J1vPjVM3NrKC/fF3/LoHjkut8AAnKmrW6a2uTzNKubigw8dEnpmpERw=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.936.0", "", { "dependencies": { "@aws-sdk/core": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-dKajFuaugEA5i9gCKzOaVy9uTeZcApE+7Z5wdcZ6j40523fY1a56khDAUYkCfwqa7sHci4ccmxBkAo+fW1RChA=="], + + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.936.0", "", { "dependencies": { "@aws-sdk/core": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/node-http-handler": "^4.4.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" } }, "sha512-5FguODLXG1tWx/x8fBxH+GVrk7Hey2LbXV5h9SFzYCx/2h50URBm0+9hndg0Rd23+xzYe14F6SI9HA9c1sPnjg=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.936.0", "", { "dependencies": { "@aws-sdk/core": "3.936.0", "@aws-sdk/credential-provider-env": "3.936.0", "@aws-sdk/credential-provider-http": "3.936.0", "@aws-sdk/credential-provider-login": "3.936.0", "@aws-sdk/credential-provider-process": "3.936.0", "@aws-sdk/credential-provider-sso": "3.936.0", "@aws-sdk/credential-provider-web-identity": "3.936.0", "@aws-sdk/nested-clients": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-TbUv56ERQQujoHcLMcfL0Q6bVZfYF83gu/TjHkVkdSlHPOIKaG/mhE2XZSQzXv1cud6LlgeBbfzVAxJ+HPpffg=="], + + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.936.0", "", { "dependencies": { "@aws-sdk/core": "3.936.0", "@aws-sdk/nested-clients": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-8DVrdRqPyUU66gfV7VZNToh56ZuO5D6agWrkLQE/xbLJOm2RbeRgh6buz7CqV8ipRd6m+zCl9mM4F3osQLZn8Q=="], + + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.936.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.936.0", "@aws-sdk/credential-provider-http": "3.936.0", "@aws-sdk/credential-provider-ini": "3.936.0", "@aws-sdk/credential-provider-process": "3.936.0", "@aws-sdk/credential-provider-sso": "3.936.0", "@aws-sdk/credential-provider-web-identity": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-rk/2PCtxX9xDsQW8p5Yjoca3StqmQcSfkmD7nQ61AqAHL1YgpSQWqHE+HjfGGiHDYKG7PvE33Ku2GyA7lEIJAw=="], + + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.936.0", "", { "dependencies": { "@aws-sdk/core": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-GpA4AcHb96KQK2PSPUyvChvrsEKiLhQ5NWjeef2IZ3Jc8JoosiedYqp6yhZR+S8cTysuvx56WyJIJc8y8OTrLA=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.936.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.936.0", "@aws-sdk/core": "3.936.0", "@aws-sdk/token-providers": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-wHlEAJJvtnSyxTfNhN98JcU4taA1ED2JvuI2eePgawqBwS/Tzi0mhED1lvNIaWOkjfLd+nHALwszGrtJwEq4yQ=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.936.0", "", { "dependencies": { "@aws-sdk/core": "3.936.0", "@aws-sdk/nested-clients": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-v3qHAuoODkoRXsAF4RG+ZVO6q2P9yYBT4GMpMEfU9wXVNn7AIfwZgTwzSUfnjNiGva5BKleWVpRpJ9DeuLFbUg=="], + + "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws-sdk/util-arn-parser": "3.893.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-XLSVVfAorUxZh6dzF+HTOp4R1B5EQcdpGcPliWr0KUj2jukgjZEcqbBmjyMF/p9bmyQsONX80iURF1HLAlW0qg=="], + + "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Eb4ELAC23bEQLJmUMYnPWcjD3FZIsmz2svDiXEcxRkQU9r7NRID7pM7C5NPH94wOfiCk0b2Y8rVyFXW0lGQwbA=="], + + "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.936.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/is-array-buffer": "^4.2.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-l3GG6CrSQtMCM6fWY7foV3JQv0WJWT+3G6PSP3Ceb/KEE/5Lz5PrYFXTBf+bVoYL1b0bGjGajcgAXpstBmtHtQ=="], + + "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw=="], + + "@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-SCMPenDtQMd9o5da9JzkHz838w3327iqXk3cbNnXWqnNRx6unyW8FL0DZ84gIY12kAyVHz5WEqlWuekc15ehfw=="], + + "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw=="], + + "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws/lambda-invoke-store": "^0.2.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-l4aGbHpXM45YNgXggIux1HgsCVAvvBoqHPkqLnqMl9QVapfuSTjJHfDYDsx1Xxct6/m7qSMUzanBALhiaGO2fA=="], + + "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.936.0", "", { "dependencies": { "@aws-sdk/core": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-arn-parser": "3.893.0", "@smithy/core": "^3.18.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-UQs/pVq4cOygsnKON0pOdSKIWkfgY0dzq4h+fR+xHi/Ng3XzxPJhWeAE6tDsKrcyQc1X8UdSbS70XkfGYr5hng=="], + + "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-/GLC9lZdVp05ozRik5KsuODR/N7j+W+2TbfdFL3iS+7un+gnP6hC8RDOZd6WhpZp7drXQ9guKiTAxkZQwzS8DA=="], + + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.936.0", "", { "dependencies": { "@aws-sdk/core": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@smithy/core": "^3.18.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-YB40IPa7K3iaYX0lSnV9easDOLPLh+fJyUDF3BH8doX4i1AOSsYn86L4lVldmOaSX+DwiaqKHpvk4wPBdcIPWw=="], + + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.936.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.936.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.936.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.936.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-eyj2tz1XmDSLSZQ5xnB7cLTVKkSJnYAEoNDSUNhzWPxrBDYeJzIbatecOKceKCU8NBf8gWWZCK/CSY0mDxMO0A=="], + + "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/config-resolver": "^4.4.3", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw=="], + + "@aws-sdk/s3-request-presigner": ["@aws-sdk/s3-request-presigner@3.937.0", "", { "dependencies": { "@aws-sdk/signature-v4-multi-region": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-format-url": "3.936.0", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-AvsCt6FnnKTpkmzDA1pFzmXPyxbGBdtllOIY0mL1iNSVZ3d7SoJKZH4NaqlcgUtbYG9zVh6QfLWememj1yEAmw=="], + + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.936.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-8qS0GFUqkmwO7JZ0P8tdluBmt1UTfYUah8qJXGzNh9n1Pcb0AIeT117cCSiCUtwk+gDbJvd4hhRIhJCNr5wgjg=="], + + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.936.0", "", { "dependencies": { "@aws-sdk/core": "3.936.0", "@aws-sdk/nested-clients": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-vvw8+VXk0I+IsoxZw0mX9TMJawUJvEsg3EF7zcCSetwhNPAU8Xmlhv7E/sN/FgSmm7b7DsqKoW6rVtQiCs1PWQ=="], + + "@aws-sdk/types": ["@aws-sdk/types@3.936.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg=="], + + "@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.893.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA=="], + + "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-endpoints": "^3.2.5", "tslib": "^2.6.2" } }, "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w=="], + + "@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/querystring-builder": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-MS5eSEtDUFIAMHrJaMERiHAvDPdfxc/T869ZjDNFAIiZhyc037REw0aoTNeimNXDNy2txRNZJaAUn/kE4RwN+g=="], + + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.893.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg=="], + + "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@smithy/types": "^4.9.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw=="], + + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.936.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.936.0", "@aws-sdk/types": "3.936.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-XOEc7PF9Op00pWV2AYCGDSu5iHgYjIO53Py2VUQTIvP7SRCaCsXmA33mjBvC2Ms6FhSyWNa4aK4naUGIz0hQcw=="], + + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.930.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA=="], + + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.1", "", {}, "sha512-sIyFcoPZkTtNu9xFeEoynMef3bPJIAbOfUh+ueYcfhVl6xm2VRtMcMclSxmZCMnHHd4hlYKJeq/aggmBEWynww=="], + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], @@ -107,6 +197,8 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="], + "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], @@ -121,6 +213,56 @@ "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="], + "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@istanbuljs/load-nyc-config": ["@istanbuljs/load-nyc-config@1.1.0", "", { "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" } }, "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ=="], "@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="], @@ -209,6 +351,108 @@ "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], + "@smithy/abort-controller": ["@smithy/abort-controller@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA=="], + + "@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA=="], + + "@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.2.1", "", { "dependencies": { "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ=="], + + "@smithy/config-resolver": ["@smithy/config-resolver@4.4.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw=="], + + "@smithy/core": ["@smithy/core@3.18.5", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.6", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-6gnIz3h+PEPQGDj8MnRSjDvKBah042jEoPgjFGJ4iJLBE78L4lY/n98x14XyPF4u3lN179Ub/ZKFY5za9GeLQw=="], + + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ=="], + + "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="], + + "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.5", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-HohfmCQZjppVnKX2PnXlf47CW3j92Ki6T/vkAT2DhBR47e89pen3s4fIa7otGTtrVxmj7q+IhH0RnC5kpR8wtw=="], + + "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-ibjQjM7wEXtECiT6my1xfiMH9IcEczMOS6xiCQXoUIYSj5b1CpBbJ3VYbdwDy8Vcg5JHN7eFpOCGk8nyZAltNQ=="], + + "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.5", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-+elOuaYx6F2H6x1/5BQP5ugv12nfJl66GhxON8+dWVUEDJ9jah/A0tayVdkLRP0AeSac0inYkDz5qBFKfVp2Gg=="], + + "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.5", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-G9WSqbST45bmIFaeNuP/EnC19Rhp54CcVdX9PDL1zyEB514WsDVXhlyihKlGXnRycmHNmVv88Bvvt4EYxWef/Q=="], + + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.6", "", { "dependencies": { "@smithy/protocol-http": "^5.3.5", "@smithy/querystring-builder": "^4.2.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg=="], + + "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.6", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.0", "@smithy/chunked-blob-reader-native": "^4.2.1", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-8P//tA8DVPk+3XURk2rwcKgYwFvwGwmJH/wJqQiSKwXZtf/LiZK+hbUZmPj/9KzM+OVSwe4o85KTp5x9DUZTjw=="], + + "@smithy/hash-node": ["@smithy/hash-node@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA=="], + + "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6+do24VnEyvWcGdHXomlpd0m8bfZePpUKBy7m311n+JuRwug8J4dCanJdTymx//8mi0nlkflZBvJe+dEO/O12Q=="], + + "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A=="], + + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@smithy/md5-js": ["@smithy/md5-js@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Bt6jpSTMWfjCtC0s79gZ/WZ1w90grfmopVOWqkI2ovhjpD5Q2XRXuecIPB9689L2+cCySMbaXDhBPU56FKNDNg=="], + + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A=="], + + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.3.12", "", { "dependencies": { "@smithy/core": "^3.18.5", "@smithy/middleware-serde": "^4.2.6", "@smithy/node-config-provider": "^4.3.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-middleware": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-9pAX/H+VQPzNbouhDhkW723igBMLgrI8OtX+++M7iKJgg/zY/Ig3i1e6seCcx22FWhE6Q/S61BRdi2wXBORT+A=="], + + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.12", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/service-error-classification": "^4.2.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-S4kWNKFowYd0lID7/DBqWHOQxmxlsf0jBaos9chQZUWTVOjSW1Ogyh8/ib5tM+agFDJ/TCxuCTvrnlc+9cIBcQ=="], + + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.6", "", { "dependencies": { "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ=="], + + "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ=="], + + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.5", "", { "dependencies": { "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg=="], + + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.5", "", { "dependencies": { "@smithy/abort-controller": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/querystring-builder": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw=="], + + "@smithy/property-provider": ["@smithy/property-provider@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg=="], + + "@smithy/protocol-http": ["@smithy/protocol-http@5.3.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ=="], + + "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg=="], + + "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ=="], + + "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0" } }, "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ=="], + + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA=="], + + "@smithy/signature-v4": ["@smithy/signature-v4@5.3.5", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w=="], + + "@smithy/smithy-client": ["@smithy/smithy-client@4.9.8", "", { "dependencies": { "@smithy/core": "^3.18.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-stack": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" } }, "sha512-8xgq3LgKDEFoIrLWBho/oYKyWByw9/corz7vuh1upv7ZBm0ZMjGYBhbn6v643WoIqA9UTcx5A5htEp/YatUwMA=="], + + "@smithy/types": ["@smithy/types@4.9.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA=="], + + "@smithy/url-parser": ["@smithy/url-parser@4.2.5", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ=="], + + "@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], + + "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], + + "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA=="], + + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + + "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="], + + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.11", "", { "dependencies": { "@smithy/property-provider": "^4.2.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-yHv+r6wSQXEXTPVCIQTNmXVWs7ekBTpMVErjqZoWkYN75HIFN5y9+/+sYOejfAuvxWGvgzgxbTHa/oz61YTbKw=="], + + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.14", "", { "dependencies": { "@smithy/config-resolver": "^4.4.3", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-ljZN3iRvaJUgulfvobIuG97q1iUuCMrvXAlkZ4msY+ZuVHQHDIqn7FKZCEj+bx8omz6kF5yQXms/xhzjIO5XiA=="], + + "@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A=="], + + "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@smithy/util-middleware": ["@smithy/util-middleware@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA=="], + + "@smithy/util-retry": ["@smithy/util-retry@4.2.5", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg=="], + + "@smithy/util-stream": ["@smithy/util-stream@4.5.6", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.6", "@smithy/node-http-handler": "^4.4.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ=="], + + "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], + + "@smithy/util-waiter": ["@smithy/util-waiter@4.2.5", "", { "dependencies": { "@smithy/abort-controller": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Dbun99A3InifQdIrsXZ+QLcC0PGBPAdrl4cj1mTgJvyc9N2zf7QSxg8TBkzsCmGJdE3TLbO9ycwpY0EkWahQ/g=="], + + "@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + "@swc/helpers": ["@swc/helpers@0.4.11", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw=="], "@tailwindcss/forms": ["@tailwindcss/forms@0.4.1", "", { "dependencies": { "mini-svg-data-uri": "^1.2.3" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" } }, "sha512-gS9xjCmJjUBz/eP12QlENPLnf0tCx68oYE3mri0GMP5jdtVwLbGUNSRpjsp6NzLAZzZy3ueOwrcqB78Ax6Z84A=="], @@ -227,8 +471,18 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/express": ["@types/express@5.0.5", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^1" } }, "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.0", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA=="], + "@types/graceful-fs": ["@types/graceful-fs@4.1.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ=="], + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], "@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="], @@ -239,14 +493,28 @@ "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + + "@types/multer": ["@types/multer@2.0.0", "", { "dependencies": { "@types/express": "*" } }, "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw=="], + "@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="], "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + "@types/react": ["@types/react@17.0.90", "", { "dependencies": { "@types/prop-types": "*", "@types/scheduler": "^0.16", "csstype": "^3.2.2" } }, "sha512-P9beVR/x06U9rCJzSxtENnOr4BrbJ6VrsrDTc+73TtHv9XHhryXKbjGRB+6oooB2r0G/pQkD/S4dHo/7jUfwFw=="], "@types/scheduler": ["@types/scheduler@0.16.8", "", {}, "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A=="], + "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], + + "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="], + + "@types/sharp": ["@types/sharp@0.32.0", "", { "dependencies": { "sharp": "*" } }, "sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw=="], + "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], "@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="], @@ -283,6 +551,8 @@ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + "append-field": ["append-field@1.0.0", "", {}, "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="], + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], @@ -335,6 +605,8 @@ "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + "bowser": ["bowser@2.12.1", "", {}, "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw=="], + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -347,6 +619,8 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -389,6 +663,8 @@ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "concat-stream": ["concat-stream@2.0.0", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "create-jest": ["create-jest@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-config": "^29.7.0", "jest-util": "^29.7.0", "prompts": "^2.0.1" }, "bin": { "create-jest": "bin/create-jest.js" } }, "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q=="], @@ -419,6 +695,8 @@ "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="], "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], @@ -521,6 +799,8 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="], @@ -813,12 +1093,18 @@ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], "mini-svg-data-uri": ["mini-svg-data-uri@1.4.4", "", { "bin": { "mini-svg-data-uri": "cli.js" } }, "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="], @@ -827,8 +1113,12 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "multer": ["multer@2.0.2", "", { "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", "concat-stream": "^2.0.0", "mkdirp": "^0.5.6", "object-assign": "^4.1.1", "type-is": "^1.6.18", "xtend": "^4.0.2" } }, "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw=="], + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -955,6 +1245,8 @@ "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], @@ -979,6 +1271,8 @@ "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], @@ -993,6 +1287,8 @@ "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], @@ -1029,6 +1325,8 @@ "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], + "string-length": ["string-length@4.0.2", "", { "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" } }, "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1045,6 +1343,8 @@ "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], @@ -1053,6 +1353,8 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], + "style-to-js": ["style-to-js@1.1.1", "", { "dependencies": { "style-to-object": "0.3.0" } }, "sha512-RJ18Z9t2B02sYhZtfWKQq5uplVctgvjTfLWT7+Eb1zjUjIrWzX5SdlkwLGQozrqarTmEzJJ/YmdNJCUNI47elg=="], "style-to-object": ["style-to-object@0.3.0", "", { "dependencies": { "inline-style-parser": "0.1.1" } }, "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA=="], @@ -1097,6 +1399,8 @@ "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], @@ -1105,6 +1409,8 @@ "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + "typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="], + "typescript": ["typescript@4.9.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g=="], "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], @@ -1143,6 +1449,8 @@ "write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="], + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], @@ -1153,6 +1461,12 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], "@istanbuljs/load-nyc-config/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], @@ -1163,6 +1477,8 @@ "@tailwindcss/typography/postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], + "@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], @@ -1217,6 +1533,8 @@ "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], "string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -1233,12 +1551,24 @@ "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@istanbuljs/load-nyc-config/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], "@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], diff --git a/components/upload/DocumentUploader.tsx b/components/upload/DocumentUploader.tsx new file mode 100644 index 0000000..f46f008 --- /dev/null +++ b/components/upload/DocumentUploader.tsx @@ -0,0 +1,236 @@ +/** + * Document Uploader Component + * Agent 3: File Upload & Storage System + * + * Upload interface for documents (PDF, DOC, etc.) + */ + +import React, { useState, useCallback, useRef } from 'react'; +import type { FileCategory } from '../../lib/storage/types'; +import ProgressBar from './ProgressBar'; + +interface UploadedDocument { + id: string; + url: string; + size: number; + mimeType: string; + originalName: string; +} + +interface DocumentUploaderProps { + category?: FileCategory; + plantId?: string; + farmId?: string; + userId?: string; + onUpload?: (file: UploadedDocument) => void; + onError?: (error: string) => void; + accept?: string; + className?: string; +} + +export function DocumentUploader({ + category = 'document', + plantId, + farmId, + userId, + onUpload, + onError, + accept = '.pdf,.doc,.docx', + className = '', +}: DocumentUploaderProps) { + const [isUploading, setIsUploading] = useState(false); + const [progress, setProgress] = useState(0); + const [error, setError] = useState(); + const [uploadedFile, setUploadedFile] = useState(null); + const fileInputRef = useRef(null); + + const uploadFile = async (file: File) => { + setIsUploading(true); + setProgress(10); + setError(undefined); + + const formData = new FormData(); + formData.append('file', file); + formData.append('category', category); + if (plantId) formData.append('plantId', plantId); + if (farmId) formData.append('farmId', farmId); + if (userId) formData.append('userId', userId); + + try { + setProgress(30); + + const response = await fetch('/api/upload/document', { + method: 'POST', + body: formData, + }); + + setProgress(80); + + const data = await response.json(); + + if (!data.success) { + throw new Error(data.error || 'Upload failed'); + } + + setProgress(100); + setUploadedFile(data.file); + onUpload?.(data.file); + } catch (error) { + const message = error instanceof Error ? error.message : 'Upload failed'; + setError(message); + onError?.(message); + } finally { + setIsUploading(false); + } + }; + + const handleFileChange = useCallback( + async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + await uploadFile(file); + } + }, + [] + ); + + const handleClick = () => { + fileInputRef.current?.click(); + }; + + const handleRemove = () => { + setUploadedFile(null); + setProgress(0); + setError(undefined); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + const getFileIcon = (mimeType: string) => { + if (mimeType === 'application/pdf') { + return ( + + + + ); + } + + return ( + + + + ); + }; + + const formatFileSize = (bytes: number): string => { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + }; + + return ( +
+ + + {uploadedFile ? ( +
+ {getFileIcon(uploadedFile.mimeType)} +
+

+ {uploadedFile.originalName} +

+

+ {formatFileSize(uploadedFile.size)} +

+
+
+ + + + + + +
+
+ ) : ( + + )} + + {isUploading && ( +
+ +

Uploading...

+
+ )} + + {error && ( +

{error}

+ )} +
+ ); +} + +export default DocumentUploader; diff --git a/components/upload/ImageUploader.tsx b/components/upload/ImageUploader.tsx new file mode 100644 index 0000000..e77bb0e --- /dev/null +++ b/components/upload/ImageUploader.tsx @@ -0,0 +1,266 @@ +/** + * Image Uploader Component + * Agent 3: File Upload & Storage System + * + * Drag & drop image upload with preview and progress + */ + +import React, { useState, useCallback, useRef } from 'react'; +import type { FileCategory } from '../../lib/storage/types'; + +interface UploadedFile { + id: string; + url: string; + thumbnailUrl?: string; + width?: number; + height?: number; + size: number; +} + +interface ImageUploaderProps { + category?: FileCategory; + plantId?: string; + farmId?: string; + userId?: string; + onUpload?: (file: UploadedFile) => void; + onError?: (error: string) => void; + maxFiles?: number; + accept?: string; + className?: string; +} + +interface UploadState { + isUploading: boolean; + progress: number; + error?: string; + preview?: string; +} + +export function ImageUploader({ + category = 'plant-photo', + plantId, + farmId, + userId, + onUpload, + onError, + maxFiles = 1, + accept = 'image/*', + className = '', +}: ImageUploaderProps) { + const [uploadState, setUploadState] = useState({ + isUploading: false, + progress: 0, + }); + const [isDragging, setIsDragging] = useState(false); + const fileInputRef = useRef(null); + + const handleDragEnter = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + }, []); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + }, []); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }, []); + + const uploadFile = async (file: File) => { + // Create preview + const reader = new FileReader(); + reader.onload = (e) => { + setUploadState((prev) => ({ + ...prev, + preview: e.target?.result as string, + })); + }; + reader.readAsDataURL(file); + + // Start upload + setUploadState((prev) => ({ + ...prev, + isUploading: true, + progress: 0, + error: undefined, + })); + + const formData = new FormData(); + formData.append('file', file); + formData.append('category', category); + if (plantId) formData.append('plantId', plantId); + if (farmId) formData.append('farmId', farmId); + if (userId) formData.append('userId', userId); + + try { + const response = await fetch('/api/upload/image', { + method: 'POST', + body: formData, + }); + + const data = await response.json(); + + if (!data.success) { + throw new Error(data.error || 'Upload failed'); + } + + setUploadState({ + isUploading: false, + progress: 100, + preview: data.file.thumbnailUrl || data.file.url, + }); + + onUpload?.(data.file); + } catch (error) { + const message = error instanceof Error ? error.message : 'Upload failed'; + setUploadState((prev) => ({ + ...prev, + isUploading: false, + error: message, + })); + onError?.(message); + } + }; + + const handleDrop = useCallback( + async (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + + const files = Array.from(e.dataTransfer.files).slice(0, maxFiles); + if (files.length > 0) { + await uploadFile(files[0]); + } + }, + [maxFiles] + ); + + const handleFileChange = useCallback( + async (e: React.ChangeEvent) => { + const files = Array.from(e.target.files || []).slice(0, maxFiles); + if (files.length > 0) { + await uploadFile(files[0]); + } + }, + [maxFiles] + ); + + const handleClick = () => { + fileInputRef.current?.click(); + }; + + const handleRemove = () => { + setUploadState({ + isUploading: false, + progress: 0, + preview: undefined, + error: undefined, + }); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + return ( +
+ + + {uploadState.preview ? ( +
+ Uploaded preview + +
+ ) : ( +
+ + + +

+ Click to upload + {' '}or drag and drop +

+

+ PNG, JPG, GIF, WEBP up to 5MB +

+
+ )} + + {uploadState.isUploading && ( +
+
+
+
+

Uploading...

+
+ )} + + {uploadState.error && ( +

{uploadState.error}

+ )} +
+ ); +} + +export default ImageUploader; diff --git a/components/upload/PhotoGallery.tsx b/components/upload/PhotoGallery.tsx new file mode 100644 index 0000000..a07a5c5 --- /dev/null +++ b/components/upload/PhotoGallery.tsx @@ -0,0 +1,213 @@ +/** + * Photo Gallery Component + * Agent 3: File Upload & Storage System + * + * Displays a grid of plant photos with lightbox view + */ + +import React, { useState } from 'react'; + +interface Photo { + id: string; + url: string; + thumbnailUrl?: string; + width?: number; + height?: number; + caption?: string; + uploadedAt?: string; +} + +interface PhotoGalleryProps { + photos: Photo[]; + onDelete?: (photoId: string) => void; + editable?: boolean; + columns?: 2 | 3 | 4; + className?: string; +} + +export function PhotoGallery({ + photos, + onDelete, + editable = false, + columns = 3, + className = '', +}: PhotoGalleryProps) { + const [selectedPhoto, setSelectedPhoto] = useState(null); + const [isDeleting, setIsDeleting] = useState(null); + + const handleDelete = async (photoId: string) => { + if (!onDelete) return; + + setIsDeleting(photoId); + try { + await onDelete(photoId); + } finally { + setIsDeleting(null); + } + }; + + const gridCols = { + 2: 'grid-cols-2', + 3: 'grid-cols-2 sm:grid-cols-3', + 4: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4', + }; + + if (photos.length === 0) { + return ( +
+ + + +

No photos yet

+
+ ); + } + + return ( + <> +
+ {photos.map((photo) => ( +
+ {photo.caption setSelectedPhoto(photo)} + loading="lazy" + /> + + {/* Overlay on hover */} +
+ + + {editable && onDelete && ( + + )} +
+ + {/* Caption */} + {photo.caption && ( +
+

{photo.caption}

+
+ )} +
+ ))} +
+ + {/* Lightbox */} + {selectedPhoto && ( +
setSelectedPhoto(null)} + > + + + {selectedPhoto.caption e.stopPropagation()} + /> + + {selectedPhoto.caption && ( +
+

{selectedPhoto.caption}

+
+ )} +
+ )} + + ); +} + +export default PhotoGallery; diff --git a/components/upload/ProgressBar.tsx b/components/upload/ProgressBar.tsx new file mode 100644 index 0000000..1c0dfef --- /dev/null +++ b/components/upload/ProgressBar.tsx @@ -0,0 +1,63 @@ +/** + * Progress Bar Component + * Agent 3: File Upload & Storage System + * + * Animated progress bar for uploads + */ + +import React from 'react'; + +interface ProgressBarProps { + progress: number; + showPercentage?: boolean; + color?: 'green' | 'blue' | 'purple' | 'orange'; + size?: 'sm' | 'md' | 'lg'; + animated?: boolean; + className?: string; +} + +const colorClasses = { + green: 'bg-green-500', + blue: 'bg-blue-500', + purple: 'bg-purple-500', + orange: 'bg-orange-500', +}; + +const sizeClasses = { + sm: 'h-1', + md: 'h-2', + lg: 'h-3', +}; + +export function ProgressBar({ + progress, + showPercentage = false, + color = 'green', + size = 'md', + animated = true, + className = '', +}: ProgressBarProps) { + const clampedProgress = Math.min(100, Math.max(0, progress)); + + return ( +
+
+
+
+ {showPercentage && ( +

+ {Math.round(clampedProgress)}% +

+ )} +
+ ); +} + +export default ProgressBar; diff --git a/components/upload/index.tsx b/components/upload/index.tsx new file mode 100644 index 0000000..6f77ae9 --- /dev/null +++ b/components/upload/index.tsx @@ -0,0 +1,11 @@ +/** + * Upload Components Index + * Agent 3: File Upload & Storage System + * + * Export all upload-related components + */ + +export { ImageUploader } from './ImageUploader'; +export { PhotoGallery } from './PhotoGallery'; +export { DocumentUploader } from './DocumentUploader'; +export { ProgressBar } from './ProgressBar'; diff --git a/lib/storage/config.ts b/lib/storage/config.ts new file mode 100644 index 0000000..53b1900 --- /dev/null +++ b/lib/storage/config.ts @@ -0,0 +1,87 @@ +/** + * Storage Configuration for LocalGreenChain + * Agent 3: File Upload & Storage System + * + * Supports multiple storage providers: AWS S3, Cloudflare R2, MinIO, or local filesystem + */ + +import { StorageConfig, StorageProvider } from './types'; + +function getStorageProvider(): StorageProvider { + const provider = process.env.STORAGE_PROVIDER as StorageProvider; + if (provider && ['s3', 'r2', 'minio', 'local'].includes(provider)) { + return provider; + } + return 'local'; // Default to local storage for development +} + +export function getStorageConfig(): StorageConfig { + const provider = getStorageProvider(); + + const baseConfig: StorageConfig = { + provider, + bucket: process.env.STORAGE_BUCKET || 'localgreenchain', + region: process.env.STORAGE_REGION || 'us-east-1', + accessKeyId: process.env.STORAGE_ACCESS_KEY_ID, + secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY, + publicUrl: process.env.STORAGE_PUBLIC_URL, + }; + + switch (provider) { + case 's3': + return { + ...baseConfig, + endpoint: process.env.AWS_S3_ENDPOINT, + }; + + case 'r2': + return { + ...baseConfig, + endpoint: process.env.CLOUDFLARE_R2_ENDPOINT || + `https://${process.env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`, + region: 'auto', + }; + + case 'minio': + return { + ...baseConfig, + endpoint: process.env.MINIO_ENDPOINT || 'http://localhost:9000', + region: 'us-east-1', + }; + + case 'local': + default: + return { + ...baseConfig, + localPath: process.env.LOCAL_STORAGE_PATH || './uploads', + publicUrl: process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3001', + }; + } +} + +export const storageConfig = getStorageConfig(); + +/** + * Environment variable template for storage configuration: + * + * # Storage Provider (s3, r2, minio, local) + * STORAGE_PROVIDER=local + * STORAGE_BUCKET=localgreenchain + * STORAGE_REGION=us-east-1 + * STORAGE_ACCESS_KEY_ID=your-access-key + * STORAGE_SECRET_ACCESS_KEY=your-secret-key + * STORAGE_PUBLIC_URL=https://cdn.yourdomain.com + * + * # For AWS S3 + * AWS_S3_ENDPOINT=https://s3.amazonaws.com + * + * # For Cloudflare R2 + * CLOUDFLARE_ACCOUNT_ID=your-account-id + * CLOUDFLARE_R2_ENDPOINT=https://your-account.r2.cloudflarestorage.com + * + * # For MinIO + * MINIO_ENDPOINT=http://localhost:9000 + * + * # For Local Storage + * LOCAL_STORAGE_PATH=./uploads + */ diff --git a/lib/storage/fileStore.ts b/lib/storage/fileStore.ts new file mode 100644 index 0000000..cfd998d --- /dev/null +++ b/lib/storage/fileStore.ts @@ -0,0 +1,258 @@ +/** + * File Store for LocalGreenChain + * Agent 3: File Upload & Storage System + * + * In-memory file metadata store (to be replaced with Prisma when Agent 2 completes) + * + * Prisma Schema (for Agent 2 to integrate): + * + * model File { + * id String @id @default(cuid()) + * filename String // Storage path/key + * originalName String // Original uploaded filename + * mimeType String + * size Int // File size in bytes + * category String // plant-photo, certificate, document, report, avatar + * url String // Public URL + * thumbnailUrl String? // Thumbnail URL (for images) + * urls Json? // All size variants URLs + * width Int? // Image width + * height Int? // Image height + * exifData Json? // EXIF metadata + * uploadedById String? // User who uploaded + * plantId String? // Associated plant + * farmId String? // Associated farm + * createdAt DateTime @default(now()) + * updatedAt DateTime @updatedAt + * deletedAt DateTime? // Soft delete + * + * uploadedBy User? @relation(fields: [uploadedById], references: [id]) + * plant Plant? @relation(fields: [plantId], references: [id]) + * farm VerticalFarm? @relation(fields: [farmId], references: [id]) + * + * @@index([category]) + * @@index([plantId]) + * @@index([farmId]) + * @@index([uploadedById]) + * } + */ + +import { FileMetadata, FileCategory, ImageSize } from './types'; + +/** + * In-memory file store + * This is a temporary implementation until Prisma is set up by Agent 2 + */ +class FileStore { + private files: Map = new Map(); + + /** + * Save file metadata + */ + async save(metadata: FileMetadata): Promise { + this.files.set(metadata.id, { + ...metadata, + updatedAt: new Date(), + }); + return metadata; + } + + /** + * Get file by ID + */ + async getById(id: string): Promise { + const file = this.files.get(id); + if (!file || file.deletedAt) { + return null; + } + return file; + } + + /** + * Get file by filename/key + */ + async getByFilename(filename: string): Promise { + for (const file of this.files.values()) { + if (file.filename === filename && !file.deletedAt) { + return file; + } + } + return null; + } + + /** + * Get files by plant ID + */ + async getByPlantId(plantId: string): Promise { + const results: FileMetadata[] = []; + for (const file of this.files.values()) { + if (file.plantId === plantId && !file.deletedAt) { + results.push(file); + } + } + return results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + } + + /** + * Get files by farm ID + */ + async getByFarmId(farmId: string): Promise { + const results: FileMetadata[] = []; + for (const file of this.files.values()) { + if (file.farmId === farmId && !file.deletedAt) { + results.push(file); + } + } + return results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + } + + /** + * Get files by user ID + */ + async getByUserId(userId: string): Promise { + const results: FileMetadata[] = []; + for (const file of this.files.values()) { + if (file.uploadedBy === userId && !file.deletedAt) { + results.push(file); + } + } + return results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + } + + /** + * Get files by category + */ + async getByCategory(category: FileCategory): Promise { + const results: FileMetadata[] = []; + for (const file of this.files.values()) { + if (file.category === category && !file.deletedAt) { + results.push(file); + } + } + return results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + } + + /** + * Update file metadata + */ + async update(id: string, updates: Partial): Promise { + const existing = this.files.get(id); + if (!existing) { + return null; + } + + const updated: FileMetadata = { + ...existing, + ...updates, + id, // Ensure ID cannot be changed + updatedAt: new Date(), + }; + + this.files.set(id, updated); + return updated; + } + + /** + * Soft delete a file + */ + async softDelete(id: string): Promise { + const existing = this.files.get(id); + if (!existing) { + return false; + } + + this.files.set(id, { + ...existing, + deletedAt: new Date(), + updatedAt: new Date(), + }); + return true; + } + + /** + * Hard delete a file + */ + async hardDelete(id: string): Promise { + return this.files.delete(id); + } + + /** + * List all files with pagination + */ + async list(options: { + limit?: number; + offset?: number; + includeDeleted?: boolean; + } = {}): Promise<{ files: FileMetadata[]; total: number }> { + const { limit = 50, offset = 0, includeDeleted = false } = options; + + let results = Array.from(this.files.values()); + + if (!includeDeleted) { + results = results.filter((f) => !f.deletedAt); + } + + // Sort by creation date descending + results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + + const total = results.length; + const files = results.slice(offset, offset + limit); + + return { files, total }; + } + + /** + * Get storage statistics + */ + async getStats(): Promise<{ + totalFiles: number; + totalSize: number; + byCategory: Record; + }> { + const stats: { + totalFiles: number; + totalSize: number; + byCategory: Record; + } = { + totalFiles: 0, + totalSize: 0, + byCategory: { + 'plant-photo': { count: 0, size: 0 }, + 'certificate': { count: 0, size: 0 }, + 'document': { count: 0, size: 0 }, + 'report': { count: 0, size: 0 }, + 'avatar': { count: 0, size: 0 }, + }, + }; + + for (const file of this.files.values()) { + if (!file.deletedAt) { + stats.totalFiles++; + stats.totalSize += file.size; + stats.byCategory[file.category].count++; + stats.byCategory[file.category].size += file.size; + } + } + + return stats; + } + + /** + * Clear all files (for testing) + */ + clear(): void { + this.files.clear(); + } +} + +// Singleton instance +let fileStoreInstance: FileStore | null = null; + +export function getFileStore(): FileStore { + if (!fileStoreInstance) { + fileStoreInstance = new FileStore(); + } + return fileStoreInstance; +} + +export { FileStore }; diff --git a/lib/storage/imageProcessor.ts b/lib/storage/imageProcessor.ts new file mode 100644 index 0000000..ffd85ba --- /dev/null +++ b/lib/storage/imageProcessor.ts @@ -0,0 +1,269 @@ +/** + * Image Processing Pipeline for LocalGreenChain + * Agent 3: File Upload & Storage System + * + * Handles image optimization, thumbnail generation, and EXIF extraction + */ + +import sharp from 'sharp'; +import { + ImageSize, + ThumbnailConfig, + THUMBNAIL_CONFIGS, + ExifData, + ImageProcessingOptions, +} from './types'; + +export interface ImageMetadata { + width: number; + height: number; + format: string; + space?: string; + channels?: number; + hasAlpha?: boolean; + orientation?: number; +} + +export class ImageProcessor { + private defaultQuality = 85; + + /** + * Get image metadata + */ + async getMetadata(buffer: Buffer): Promise { + const metadata = await sharp(buffer).metadata(); + + return { + width: metadata.width || 0, + height: metadata.height || 0, + format: metadata.format || 'unknown', + space: metadata.space, + channels: metadata.channels, + hasAlpha: metadata.hasAlpha, + orientation: metadata.orientation, + }; + } + + /** + * Extract EXIF data from image + */ + async extractExif(buffer: Buffer): Promise { + try { + const metadata = await sharp(buffer).metadata(); + + if (!metadata.exif) { + return undefined; + } + + // Parse basic EXIF data + const exifData: ExifData = {}; + + // Sharp provides some EXIF data directly + if (metadata.orientation) { + exifData.orientation = metadata.orientation; + } + + // For more detailed EXIF parsing, we would need an EXIF library + // For now, return basic data + return Object.keys(exifData).length > 0 ? exifData : undefined; + } catch (error) { + console.warn('Error extracting EXIF data:', error); + return undefined; + } + } + + /** + * Optimize an image for web + */ + async optimize( + buffer: Buffer, + options: ImageProcessingOptions = {} + ): Promise { + const { + maxWidth = 2048, + maxHeight = 2048, + quality = this.defaultQuality, + convertToWebP = true, + } = options; + + let pipeline = sharp(buffer) + .rotate() // Auto-rotate based on EXIF orientation + .resize(maxWidth, maxHeight, { + fit: 'inside', + withoutEnlargement: true, + }); + + if (convertToWebP) { + pipeline = pipeline.webp({ quality }); + } else { + // Optimize in original format + const metadata = await sharp(buffer).metadata(); + switch (metadata.format) { + case 'jpeg': + pipeline = pipeline.jpeg({ quality, mozjpeg: true }); + break; + case 'png': + pipeline = pipeline.png({ compressionLevel: 9 }); + break; + case 'gif': + pipeline = pipeline.gif(); + break; + default: + pipeline = pipeline.webp({ quality }); + } + } + + return pipeline.toBuffer(); + } + + /** + * Generate all thumbnail sizes + */ + async generateThumbnails( + buffer: Buffer, + sizes: ImageSize[] = ['thumbnail', 'small', 'medium', 'large'] + ): Promise> { + const thumbnails: Record = {}; + + await Promise.all( + sizes.map(async (size) => { + if (size === 'original') return; + + const config = THUMBNAIL_CONFIGS[size]; + thumbnails[size] = await this.generateThumbnail(buffer, config); + }) + ); + + return thumbnails; + } + + /** + * Generate a single thumbnail + */ + async generateThumbnail(buffer: Buffer, config: ThumbnailConfig): Promise { + return sharp(buffer) + .rotate() // Auto-rotate based on EXIF orientation + .resize(config.width, config.height, { + fit: config.fit, + withoutEnlargement: true, + }) + .webp({ quality: 80 }) + .toBuffer(); + } + + /** + * Convert image to WebP format + */ + async toWebP(buffer: Buffer, quality = 85): Promise { + return sharp(buffer) + .rotate() + .webp({ quality }) + .toBuffer(); + } + + /** + * Crop image to specific dimensions + */ + async crop( + buffer: Buffer, + width: number, + height: number, + options: { left?: number; top?: number } = {} + ): Promise { + const { left = 0, top = 0 } = options; + + return sharp(buffer) + .extract({ left, top, width, height }) + .toBuffer(); + } + + /** + * Smart crop using attention/entropy detection + */ + async smartCrop(buffer: Buffer, width: number, height: number): Promise { + return sharp(buffer) + .resize(width, height, { + fit: 'cover', + position: 'attention', // Focus on the most "interesting" part + }) + .toBuffer(); + } + + /** + * Add a watermark to an image + */ + async addWatermark( + buffer: Buffer, + watermarkBuffer: Buffer, + options: { + position?: 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; + opacity?: number; + } = {} + ): Promise { + const { position = 'bottom-right', opacity = 0.5 } = options; + + const image = sharp(buffer); + const metadata = await image.metadata(); + const watermark = await sharp(watermarkBuffer) + .resize(Math.round((metadata.width || 500) * 0.2), null, { + withoutEnlargement: true, + }) + .ensureAlpha() + .toBuffer(); + + const watermarkMeta = await sharp(watermark).metadata(); + + let gravity: sharp.Gravity; + switch (position) { + case 'top-left': + gravity = 'northwest'; + break; + case 'top-right': + gravity = 'northeast'; + break; + case 'bottom-left': + gravity = 'southwest'; + break; + case 'bottom-right': + gravity = 'southeast'; + break; + case 'center': + default: + gravity = 'center'; + } + + return image + .composite([ + { + input: watermark, + gravity, + blend: 'over', + }, + ]) + .toBuffer(); + } + + /** + * Generate a blur placeholder for progressive loading + */ + async generateBlurPlaceholder(buffer: Buffer, size = 10): Promise { + const blurredBuffer = await sharp(buffer) + .resize(size, size, { fit: 'inside' }) + .webp({ quality: 20 }) + .toBuffer(); + + return `data:image/webp;base64,${blurredBuffer.toString('base64')}`; + } + + /** + * Validate that buffer is a valid image + */ + async isValidImage(buffer: Buffer): Promise { + try { + const metadata = await sharp(buffer).metadata(); + return !!(metadata.width && metadata.height); + } catch { + return false; + } + } +} diff --git a/lib/storage/index.ts b/lib/storage/index.ts new file mode 100644 index 0000000..eb35098 --- /dev/null +++ b/lib/storage/index.ts @@ -0,0 +1,34 @@ +/** + * Storage Module for LocalGreenChain + * Agent 3: File Upload & Storage System + * + * Main entry point for file storage functionality + */ + +// Types +export * from './types'; + +// Configuration +export { getStorageConfig, storageConfig } from './config'; + +// Services +export { getUploadService, UploadService } from './uploadService'; +export { ImageProcessor } from './imageProcessor'; +export { getFileStore, FileStore } from './fileStore'; + +// Providers +export { S3StorageProvider } from './providers/s3'; +export { LocalStorageProvider } from './providers/local'; + +// Re-export commonly used types for convenience +export type { + FileMetadata, + FileCategory, + UploadOptions, + UploadResult, + PresignedUrlRequest, + PresignedUrlResponse, + ImageSize, + StorageProvider, + StorageConfig, +} from './types'; diff --git a/lib/storage/providers/local.ts b/lib/storage/providers/local.ts new file mode 100644 index 0000000..3c3a099 --- /dev/null +++ b/lib/storage/providers/local.ts @@ -0,0 +1,131 @@ +/** + * Local Storage Provider for LocalGreenChain + * Agent 3: File Upload & Storage System + * + * Filesystem-based storage for development and testing + */ + +import { promises as fs } from 'fs'; +import path from 'path'; +import crypto from 'crypto'; +import { StorageConfig, StorageProviderInterface } from '../types'; + +export class LocalStorageProvider implements StorageProviderInterface { + private basePath: string; + private publicUrl: string; + + constructor(config: StorageConfig) { + this.basePath = config.localPath || './uploads'; + this.publicUrl = config.publicUrl || 'http://localhost:3001'; + } + + private getFilePath(key: string): string { + return path.join(this.basePath, key); + } + + private async ensureDirectory(filePath: string): Promise { + const dir = path.dirname(filePath); + await fs.mkdir(dir, { recursive: true }); + } + + async upload(key: string, buffer: Buffer, contentType: string): Promise { + const filePath = this.getFilePath(key); + await this.ensureDirectory(filePath); + + await fs.writeFile(filePath, buffer); + + // Write metadata file + const metadataPath = `${filePath}.meta.json`; + await fs.writeFile( + metadataPath, + JSON.stringify({ + contentType, + size: buffer.length, + uploadedAt: new Date().toISOString(), + }) + ); + + return this.getPublicUrl(key); + } + + async delete(key: string): Promise { + try { + const filePath = this.getFilePath(key); + await fs.unlink(filePath); + + // Also delete metadata file if exists + try { + await fs.unlink(`${filePath}.meta.json`); + } catch { + // Metadata file may not exist + } + + return true; + } catch (error) { + console.error('Error deleting local file:', error); + return false; + } + } + + async getSignedUrl(key: string, expiresIn = 3600): Promise { + // For local storage, we generate a signed URL using HMAC + const expires = Date.now() + expiresIn * 1000; + const secret = process.env.LOCAL_STORAGE_SECRET || 'development-secret'; + const signature = crypto + .createHmac('sha256', secret) + .update(`${key}:${expires}`) + .digest('hex') + .substring(0, 16); + + return `${this.publicUrl}/api/upload/${encodeURIComponent(key)}?expires=${expires}&sig=${signature}`; + } + + async getPresignedUploadUrl(key: string, contentType: string, expiresIn = 3600): Promise { + // For local storage, return an API endpoint for upload + const expires = Date.now() + expiresIn * 1000; + const secret = process.env.LOCAL_STORAGE_SECRET || 'development-secret'; + const signature = crypto + .createHmac('sha256', secret) + .update(`upload:${key}:${contentType}:${expires}`) + .digest('hex') + .substring(0, 16); + + return `${this.publicUrl}/api/upload/presigned?key=${encodeURIComponent(key)}&contentType=${encodeURIComponent(contentType)}&expires=${expires}&sig=${signature}`; + } + + async exists(key: string): Promise { + try { + await fs.access(this.getFilePath(key)); + return true; + } catch { + return false; + } + } + + getPublicUrl(key: string): string { + return `${this.publicUrl}/uploads/${key}`; + } + + /** + * Verify a signed URL signature + */ + static verifySignature( + key: string, + expires: number, + signature: string, + prefix = '' + ): boolean { + if (Date.now() > expires) { + return false; + } + + const secret = process.env.LOCAL_STORAGE_SECRET || 'development-secret'; + const expectedSignature = crypto + .createHmac('sha256', secret) + .update(`${prefix}${key}:${expires}`) + .digest('hex') + .substring(0, 16); + + return signature === expectedSignature; + } +} diff --git a/lib/storage/providers/s3.ts b/lib/storage/providers/s3.ts new file mode 100644 index 0000000..e8dce90 --- /dev/null +++ b/lib/storage/providers/s3.ts @@ -0,0 +1,115 @@ +/** + * S3 Storage Provider for LocalGreenChain + * Agent 3: File Upload & Storage System + * + * Compatible with AWS S3, Cloudflare R2, and MinIO + */ + +import { + S3Client, + PutObjectCommand, + DeleteObjectCommand, + HeadObjectCommand, + GetObjectCommand, +} from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { StorageConfig, StorageProviderInterface } from '../types'; + +export class S3StorageProvider implements StorageProviderInterface { + private client: S3Client; + private bucket: string; + private publicUrl?: string; + + constructor(config: StorageConfig) { + this.bucket = config.bucket; + this.publicUrl = config.publicUrl; + + const clientConfig: ConstructorParameters[0] = { + region: config.region || 'us-east-1', + }; + + // Add endpoint for R2/MinIO + if (config.endpoint) { + clientConfig.endpoint = config.endpoint; + clientConfig.forcePathStyle = config.provider === 'minio'; + } + + // Add credentials if provided + if (config.accessKeyId && config.secretAccessKey) { + clientConfig.credentials = { + accessKeyId: config.accessKeyId, + secretAccessKey: config.secretAccessKey, + }; + } + + this.client = new S3Client(clientConfig); + } + + async upload(key: string, buffer: Buffer, contentType: string): Promise { + const command = new PutObjectCommand({ + Bucket: this.bucket, + Key: key, + Body: buffer, + ContentType: contentType, + CacheControl: 'public, max-age=31536000', // 1 year cache + }); + + await this.client.send(command); + return this.getPublicUrl(key); + } + + async delete(key: string): Promise { + try { + const command = new DeleteObjectCommand({ + Bucket: this.bucket, + Key: key, + }); + + await this.client.send(command); + return true; + } catch (error) { + console.error('Error deleting file from S3:', error); + return false; + } + } + + async getSignedUrl(key: string, expiresIn = 3600): Promise { + const command = new GetObjectCommand({ + Bucket: this.bucket, + Key: key, + }); + + return getSignedUrl(this.client, command, { expiresIn }); + } + + async getPresignedUploadUrl(key: string, contentType: string, expiresIn = 3600): Promise { + const command = new PutObjectCommand({ + Bucket: this.bucket, + Key: key, + ContentType: contentType, + }); + + return getSignedUrl(this.client, command, { expiresIn }); + } + + async exists(key: string): Promise { + try { + const command = new HeadObjectCommand({ + Bucket: this.bucket, + Key: key, + }); + + await this.client.send(command); + return true; + } catch { + return false; + } + } + + getPublicUrl(key: string): string { + if (this.publicUrl) { + return `${this.publicUrl}/${key}`; + } + return `https://${this.bucket}.s3.amazonaws.com/${key}`; + } +} diff --git a/lib/storage/types.ts b/lib/storage/types.ts new file mode 100644 index 0000000..7b6aacb --- /dev/null +++ b/lib/storage/types.ts @@ -0,0 +1,133 @@ +/** + * File Storage Types for LocalGreenChain + * Agent 3: File Upload & Storage System + */ + +export type StorageProvider = 's3' | 'r2' | 'minio' | 'local'; + +export type FileCategory = 'plant-photo' | 'certificate' | 'document' | 'report' | 'avatar'; + +export type ImageSize = 'thumbnail' | 'small' | 'medium' | 'large' | 'original'; + +export interface StorageConfig { + provider: StorageProvider; + bucket: string; + region?: string; + endpoint?: string; + accessKeyId?: string; + secretAccessKey?: string; + publicUrl?: string; + localPath?: string; +} + +export interface FileMetadata { + id: string; + filename: string; + originalName: string; + mimeType: string; + size: number; + category: FileCategory; + uploadedBy?: string; + plantId?: string; + farmId?: string; + url: string; + thumbnailUrl?: string; + urls?: Record; + width?: number; + height?: number; + exifData?: ExifData; + createdAt: Date; + updatedAt: Date; + deletedAt?: Date; +} + +export interface ExifData { + make?: string; + model?: string; + dateTaken?: Date; + gpsLatitude?: number; + gpsLongitude?: number; + orientation?: number; +} + +export interface UploadOptions { + category: FileCategory; + plantId?: string; + farmId?: string; + userId?: string; + generateThumbnails?: boolean; + maxSizeBytes?: number; + allowedMimeTypes?: string[]; +} + +export interface UploadResult { + success: boolean; + file?: FileMetadata; + error?: string; + presignedUrl?: string; +} + +export interface PresignedUrlRequest { + filename: string; + contentType: string; + category: FileCategory; + expiresIn?: number; +} + +export interface PresignedUrlResponse { + uploadUrl: string; + fileKey: string; + publicUrl: string; + expiresAt: Date; +} + +export interface ImageProcessingOptions { + generateThumbnails?: boolean; + convertToWebP?: boolean; + extractExif?: boolean; + maxWidth?: number; + maxHeight?: number; + quality?: number; +} + +export interface ThumbnailConfig { + size: ImageSize; + width: number; + height: number; + fit: 'cover' | 'contain' | 'fill' | 'inside' | 'outside'; +} + +export const THUMBNAIL_CONFIGS: Record = { + thumbnail: { size: 'thumbnail', width: 150, height: 150, fit: 'cover' }, + small: { size: 'small', width: 300, height: 300, fit: 'inside' }, + medium: { size: 'medium', width: 600, height: 600, fit: 'inside' }, + large: { size: 'large', width: 1200, height: 1200, fit: 'inside' }, + original: { size: 'original', width: 0, height: 0, fit: 'inside' }, +}; + +export const ALLOWED_IMAGE_TYPES = [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/webp', + 'image/heic', + 'image/heif', +]; + +export const ALLOWED_DOCUMENT_TYPES = [ + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', +]; + +export const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB +export const DEFAULT_MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB + +export interface StorageProviderInterface { + upload(key: string, buffer: Buffer, contentType: string): Promise; + delete(key: string): Promise; + getSignedUrl(key: string, expiresIn?: number): Promise; + getPresignedUploadUrl(key: string, contentType: string, expiresIn?: number): Promise; + exists(key: string): Promise; + getPublicUrl(key: string): string; +} diff --git a/lib/storage/uploadService.ts b/lib/storage/uploadService.ts new file mode 100644 index 0000000..53b0555 --- /dev/null +++ b/lib/storage/uploadService.ts @@ -0,0 +1,270 @@ +/** + * Upload Service for LocalGreenChain + * Agent 3: File Upload & Storage System + * + * Core upload functionality with validation and processing + */ + +import { v4 as uuidv4 } from 'crypto'; +import { + FileMetadata, + FileCategory, + UploadOptions, + UploadResult, + PresignedUrlRequest, + PresignedUrlResponse, + StorageProviderInterface, + ALLOWED_IMAGE_TYPES, + ALLOWED_DOCUMENT_TYPES, + DEFAULT_MAX_FILE_SIZE, + DEFAULT_MAX_IMAGE_SIZE, + ImageSize, +} from './types'; +import { getStorageConfig, storageConfig } from './config'; +import { S3StorageProvider } from './providers/s3'; +import { LocalStorageProvider } from './providers/local'; +import { ImageProcessor } from './imageProcessor'; + +// Generate a UUID v4 +function generateId(): string { + const bytes = require('crypto').randomBytes(16); + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + return bytes.toString('hex').replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5'); +} + +class UploadService { + private provider: StorageProviderInterface; + private imageProcessor: ImageProcessor; + + constructor() { + const config = getStorageConfig(); + + switch (config.provider) { + case 's3': + case 'r2': + case 'minio': + this.provider = new S3StorageProvider(config); + break; + case 'local': + default: + this.provider = new LocalStorageProvider(config); + break; + } + + this.imageProcessor = new ImageProcessor(); + } + + /** + * Generate a unique file key with path + */ + private generateFileKey(filename: string, category: FileCategory): string { + const id = generateId(); + const ext = filename.split('.').pop()?.toLowerCase() || 'bin'; + const date = new Date(); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + + return `${category}/${year}/${month}/${id}.${ext}`; + } + + /** + * Validate file type and size + */ + private validateFile( + buffer: Buffer, + mimeType: string, + options: UploadOptions + ): { valid: boolean; error?: string } { + const isImage = ALLOWED_IMAGE_TYPES.includes(mimeType); + const isDocument = ALLOWED_DOCUMENT_TYPES.includes(mimeType); + + // Check allowed types based on category + if (options.category === 'plant-photo' || options.category === 'avatar') { + if (!isImage) { + return { valid: false, error: 'Only image files are allowed for this category' }; + } + } + + if (options.category === 'document' || options.category === 'certificate') { + if (!isDocument && !isImage) { + return { valid: false, error: 'Only documents and images are allowed for this category' }; + } + } + + // Check custom allowed types + if (options.allowedMimeTypes && options.allowedMimeTypes.length > 0) { + if (!options.allowedMimeTypes.includes(mimeType)) { + return { valid: false, error: `File type ${mimeType} is not allowed` }; + } + } + + // Check file size + const maxSize = options.maxSizeBytes || + (isImage ? DEFAULT_MAX_IMAGE_SIZE : DEFAULT_MAX_FILE_SIZE); + + if (buffer.length > maxSize) { + const maxSizeMB = (maxSize / (1024 * 1024)).toFixed(1); + return { valid: false, error: `File size exceeds maximum of ${maxSizeMB}MB` }; + } + + return { valid: true }; + } + + /** + * Upload a file with processing + */ + async upload( + buffer: Buffer, + originalName: string, + mimeType: string, + options: UploadOptions + ): Promise { + // Validate file + const validation = this.validateFile(buffer, mimeType, options); + if (!validation.valid) { + return { success: false, error: validation.error }; + } + + const isImage = ALLOWED_IMAGE_TYPES.includes(mimeType); + const fileKey = this.generateFileKey(originalName, options.category); + const fileId = generateId(); + + try { + let processedBuffer = buffer; + let width: number | undefined; + let height: number | undefined; + let exifData: FileMetadata['exifData']; + const urls: Partial> = {}; + + // Process images + if (isImage && options.generateThumbnails !== false) { + // Get image metadata + const metadata = await this.imageProcessor.getMetadata(buffer); + width = metadata.width; + height = metadata.height; + + // Extract EXIF data + exifData = await this.imageProcessor.extractExif(buffer); + + // Optimize original image + const optimized = await this.imageProcessor.optimize(buffer); + processedBuffer = optimized; + + // Generate thumbnails + const thumbnails = await this.imageProcessor.generateThumbnails(buffer); + + // Upload thumbnails + for (const [size, thumbBuffer] of Object.entries(thumbnails)) { + const thumbKey = fileKey.replace(/\.[^.]+$/, `-${size}.webp`); + urls[size as ImageSize] = await this.provider.upload(thumbKey, thumbBuffer, 'image/webp'); + } + } + + // Upload main file + const finalMimeType = isImage ? 'image/webp' : mimeType; + const finalKey = isImage ? fileKey.replace(/\.[^.]+$/, '.webp') : fileKey; + const mainUrl = await this.provider.upload(finalKey, processedBuffer, finalMimeType); + + urls.original = mainUrl; + + // Create file metadata + const fileMetadata: FileMetadata = { + id: fileId, + filename: finalKey, + originalName, + mimeType: finalMimeType, + size: processedBuffer.length, + category: options.category, + uploadedBy: options.userId, + plantId: options.plantId, + farmId: options.farmId, + url: mainUrl, + thumbnailUrl: urls.thumbnail, + urls: urls as Record, + width, + height, + exifData, + createdAt: new Date(), + updatedAt: new Date(), + }; + + return { success: true, file: fileMetadata }; + } catch (error) { + console.error('Upload error:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Upload failed', + }; + } + } + + /** + * Generate a presigned URL for direct upload + */ + async getPresignedUploadUrl(request: PresignedUrlRequest): Promise { + const fileKey = this.generateFileKey(request.filename, request.category); + const expiresIn = request.expiresIn || 3600; // 1 hour default + + const uploadUrl = await this.provider.getPresignedUploadUrl( + fileKey, + request.contentType, + expiresIn + ); + + return { + uploadUrl, + fileKey, + publicUrl: this.provider.getPublicUrl(fileKey), + expiresAt: new Date(Date.now() + expiresIn * 1000), + }; + } + + /** + * Delete a file and its thumbnails + */ + async delete(fileKey: string): Promise { + try { + // Delete main file + await this.provider.delete(fileKey); + + // Delete thumbnails if they exist + const sizes: ImageSize[] = ['thumbnail', 'small', 'medium', 'large']; + for (const size of sizes) { + const thumbKey = fileKey.replace(/\.[^.]+$/, `-${size}.webp`); + await this.provider.delete(thumbKey); + } + + return true; + } catch (error) { + console.error('Delete error:', error); + return false; + } + } + + /** + * Get a signed URL for private file access + */ + async getSignedUrl(fileKey: string, expiresIn = 3600): Promise { + return this.provider.getSignedUrl(fileKey, expiresIn); + } + + /** + * Check if a file exists + */ + async exists(fileKey: string): Promise { + return this.provider.exists(fileKey); + } +} + +// Singleton instance +let uploadServiceInstance: UploadService | null = null; + +export function getUploadService(): UploadService { + if (!uploadServiceInstance) { + uploadServiceInstance = new UploadService(); + } + return uploadServiceInstance; +} + +export { UploadService }; diff --git a/package.json b/package.json index b1350a8..08e015f 100644 --- a/package.json +++ b/package.json @@ -18,25 +18,31 @@ "test:e2e:ci": "start-server-and-test 'bun run preview' http://localhost:3001 cy:run" }, "dependencies": { + "@aws-sdk/client-s3": "^3.937.0", + "@aws-sdk/s3-request-presigner": "^3.937.0", "@tailwindcss/forms": "^0.4.0", "@tailwindcss/typography": "^0.5.1", "@tanstack/react-query": "^4.0.10", "classnames": "^2.3.1", "drupal-jsonapi-params": "^1.2.2", "html-react-parser": "^1.2.7", + "multer": "^2.0.2", "next": "^12.2.3", "next-drupal": "^1.6.0", "nprogress": "^0.2.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-hook-form": "^7.8.6", + "sharp": "^0.34.5", "socks-proxy-agent": "^8.0.2" }, "devDependencies": { "@babel/core": "^7.12.9", "@types/jest": "^29.5.0", + "@types/multer": "^2.0.0", "@types/node": "^17.0.21", "@types/react": "^17.0.0", + "@types/sharp": "^0.32.0", "autoprefixer": "^10.4.2", "eslint-config-next": "^12.0.10", "jest": "^29.5.0", diff --git a/pages/api/upload/[fileId].ts b/pages/api/upload/[fileId].ts new file mode 100644 index 0000000..a669bca --- /dev/null +++ b/pages/api/upload/[fileId].ts @@ -0,0 +1,99 @@ +/** + * File Management API Endpoint + * Agent 3: File Upload & Storage System + * + * GET /api/upload/[fileId] - Get file info or signed URL + * DELETE /api/upload/[fileId] - Delete a file + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getUploadService } from '../../../lib/storage'; + +interface FileResponse { + success: boolean; + signedUrl?: string; + exists?: boolean; + error?: string; +} + +interface DeleteResponse { + success: boolean; + error?: string; +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { fileId } = req.query; + + if (!fileId || typeof fileId !== 'string') { + return res.status(400).json({ success: false, error: 'File ID is required' }); + } + + // Decode the file key (it may be URL encoded) + const fileKey = decodeURIComponent(fileId); + + const uploadService = getUploadService(); + + switch (req.method) { + case 'GET': { + try { + // Check if file exists + const exists = await uploadService.exists(fileKey); + + if (!exists) { + return res.status(404).json({ + success: false, + exists: false, + error: 'File not found', + }); + } + + // Get expiration time from query params (default 1 hour) + const expiresIn = parseInt(req.query.expiresIn as string) || 3600; + + // Get signed URL for private file access + const signedUrl = await uploadService.getSignedUrl(fileKey, expiresIn); + + return res.status(200).json({ + success: true, + exists: true, + signedUrl, + }); + } catch (error) { + console.error('Get file error:', error); + return res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }); + } + } + + case 'DELETE': { + try { + const deleted = await uploadService.delete(fileKey); + + if (!deleted) { + return res.status(404).json({ + success: false, + error: 'File not found or could not be deleted', + }); + } + + return res.status(200).json({ + success: true, + }); + } catch (error) { + console.error('Delete file error:', error); + return res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }); + } + } + + default: + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } +} diff --git a/pages/api/upload/document.ts b/pages/api/upload/document.ts new file mode 100644 index 0000000..95977c3 --- /dev/null +++ b/pages/api/upload/document.ts @@ -0,0 +1,160 @@ +/** + * Document Upload API Endpoint + * Agent 3: File Upload & Storage System + * + * POST /api/upload/document - Upload a document file (PDF, DOC, etc.) + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getUploadService } from '../../../lib/storage'; +import type { FileCategory } from '../../../lib/storage/types'; + +// Disable body parser to handle multipart/form-data +export const config = { + api: { + bodyParser: false, + }, +}; + +interface UploadResponse { + success: boolean; + file?: { + id: string; + url: string; + size: number; + mimeType: string; + originalName: string; + }; + error?: string; +} + +async function parseMultipartForm( + req: NextApiRequest +): Promise<{ buffer: Buffer; filename: string; mimeType: string; fields: Record }> { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + let filename = 'document'; + let mimeType = 'application/octet-stream'; + const fields: Record = {}; + + const contentType = req.headers['content-type'] || ''; + const boundary = contentType.split('boundary=')[1]; + + if (!boundary) { + reject(new Error('Missing multipart boundary')); + return; + } + + req.on('data', (chunk: Buffer) => { + chunks.push(chunk); + }); + + req.on('end', () => { + const buffer = Buffer.concat(chunks); + const content = buffer.toString('binary'); + const parts = content.split(`--${boundary}`); + + for (const part of parts) { + if (part.includes('Content-Disposition: form-data')) { + const nameMatch = part.match(/name="([^"]+)"/); + const filenameMatch = part.match(/filename="([^"]+)"/); + + if (filenameMatch) { + filename = filenameMatch[1]; + const contentTypeMatch = part.match(/Content-Type: ([^\r\n]+)/); + if (contentTypeMatch) { + mimeType = contentTypeMatch[1].trim(); + } + + const contentStart = part.indexOf('\r\n\r\n') + 4; + const contentEnd = part.lastIndexOf('\r\n'); + if (contentStart > 4 && contentEnd > contentStart) { + const fileContent = part.slice(contentStart, contentEnd); + const fileBuffer = Buffer.from(fileContent, 'binary'); + resolve({ buffer: fileBuffer, filename, mimeType, fields }); + return; + } + } else if (nameMatch) { + const fieldName = nameMatch[1]; + const contentStart = part.indexOf('\r\n\r\n') + 4; + const contentEnd = part.lastIndexOf('\r\n'); + if (contentStart > 4 && contentEnd > contentStart) { + fields[fieldName] = part.slice(contentStart, contentEnd).trim(); + } + } + } + } + + reject(new Error('No file found in request')); + }); + + req.on('error', reject); + }); +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { buffer, filename, mimeType, fields } = await parseMultipartForm(req); + + // Validate document type + const allowedTypes = [ + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'image/jpeg', + 'image/png', + ]; + + if (!allowedTypes.includes(mimeType)) { + return res.status(400).json({ + success: false, + error: 'Invalid file type. Only PDF, DOC, DOCX, and images are allowed.', + }); + } + + const category = (fields.category as FileCategory) || 'document'; + const plantId = fields.plantId; + const farmId = fields.farmId; + const userId = fields.userId; + + const uploadService = getUploadService(); + const result = await uploadService.upload(buffer, filename, mimeType, { + category, + plantId, + farmId, + userId, + generateThumbnails: false, // No thumbnails for documents + }); + + if (!result.success || !result.file) { + return res.status(400).json({ + success: false, + error: result.error || 'Upload failed', + }); + } + + return res.status(200).json({ + success: true, + file: { + id: result.file.id, + url: result.file.url, + size: result.file.size, + mimeType: result.file.mimeType, + originalName: result.file.originalName, + }, + }); + } catch (error) { + console.error('Document upload error:', error); + return res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }); + } +} diff --git a/pages/api/upload/image.ts b/pages/api/upload/image.ts new file mode 100644 index 0000000..c049941 --- /dev/null +++ b/pages/api/upload/image.ts @@ -0,0 +1,153 @@ +/** + * Image Upload API Endpoint + * Agent 3: File Upload & Storage System + * + * POST /api/upload/image - Upload an image file + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getUploadService } from '../../../lib/storage'; +import type { FileCategory, UploadResult } from '../../../lib/storage/types'; + +// Disable body parser to handle multipart/form-data +export const config = { + api: { + bodyParser: false, + }, +}; + +interface UploadResponse { + success: boolean; + file?: { + id: string; + url: string; + thumbnailUrl?: string; + urls?: Record; + width?: number; + height?: number; + size: number; + mimeType: string; + }; + error?: string; +} + +async function parseMultipartForm( + req: NextApiRequest +): Promise<{ buffer: Buffer; filename: string; mimeType: string; fields: Record }> { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + let filename = 'upload'; + let mimeType = 'application/octet-stream'; + const fields: Record = {}; + + const contentType = req.headers['content-type'] || ''; + const boundary = contentType.split('boundary=')[1]; + + if (!boundary) { + reject(new Error('Missing multipart boundary')); + return; + } + + req.on('data', (chunk: Buffer) => { + chunks.push(chunk); + }); + + req.on('end', () => { + const buffer = Buffer.concat(chunks); + const content = buffer.toString('binary'); + const parts = content.split(`--${boundary}`); + + for (const part of parts) { + if (part.includes('Content-Disposition: form-data')) { + const nameMatch = part.match(/name="([^"]+)"/); + const filenameMatch = part.match(/filename="([^"]+)"/); + + if (filenameMatch) { + // This is a file + filename = filenameMatch[1]; + const contentTypeMatch = part.match(/Content-Type: ([^\r\n]+)/); + if (contentTypeMatch) { + mimeType = contentTypeMatch[1].trim(); + } + + // Extract file content (after double CRLF) + const contentStart = part.indexOf('\r\n\r\n') + 4; + const contentEnd = part.lastIndexOf('\r\n'); + if (contentStart > 4 && contentEnd > contentStart) { + const fileContent = part.slice(contentStart, contentEnd); + const fileBuffer = Buffer.from(fileContent, 'binary'); + resolve({ buffer: fileBuffer, filename, mimeType, fields }); + return; + } + } else if (nameMatch) { + // This is a regular field + const fieldName = nameMatch[1]; + const contentStart = part.indexOf('\r\n\r\n') + 4; + const contentEnd = part.lastIndexOf('\r\n'); + if (contentStart > 4 && contentEnd > contentStart) { + fields[fieldName] = part.slice(contentStart, contentEnd).trim(); + } + } + } + } + + reject(new Error('No file found in request')); + }); + + req.on('error', reject); + }); +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { buffer, filename, mimeType, fields } = await parseMultipartForm(req); + + const category = (fields.category as FileCategory) || 'plant-photo'; + const plantId = fields.plantId; + const farmId = fields.farmId; + const userId = fields.userId; + + const uploadService = getUploadService(); + const result = await uploadService.upload(buffer, filename, mimeType, { + category, + plantId, + farmId, + userId, + generateThumbnails: true, + }); + + if (!result.success || !result.file) { + return res.status(400).json({ + success: false, + error: result.error || 'Upload failed', + }); + } + + return res.status(200).json({ + success: true, + file: { + id: result.file.id, + url: result.file.url, + thumbnailUrl: result.file.thumbnailUrl, + urls: result.file.urls, + width: result.file.width, + height: result.file.height, + size: result.file.size, + mimeType: result.file.mimeType, + }, + }); + } catch (error) { + console.error('Upload error:', error); + return res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }); + } +} diff --git a/pages/api/upload/presigned.ts b/pages/api/upload/presigned.ts new file mode 100644 index 0000000..9341284 --- /dev/null +++ b/pages/api/upload/presigned.ts @@ -0,0 +1,92 @@ +/** + * Presigned URL API Endpoint + * Agent 3: File Upload & Storage System + * + * POST /api/upload/presigned - Get a presigned URL for direct upload + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getUploadService } from '../../../lib/storage'; +import type { FileCategory, PresignedUrlResponse } from '../../../lib/storage/types'; + +interface RequestBody { + filename: string; + contentType: string; + category: FileCategory; + expiresIn?: number; +} + +interface PresignedResponse { + success: boolean; + data?: PresignedUrlResponse; + error?: string; +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { filename, contentType, category, expiresIn } = req.body as RequestBody; + + // Validate required fields + if (!filename || !contentType || !category) { + return res.status(400).json({ + success: false, + error: 'Missing required fields: filename, contentType, category', + }); + } + + // Validate category + const validCategories: FileCategory[] = ['plant-photo', 'certificate', 'document', 'report', 'avatar']; + if (!validCategories.includes(category)) { + return res.status(400).json({ + success: false, + error: `Invalid category. Must be one of: ${validCategories.join(', ')}`, + }); + } + + // Validate content type + const allowedContentTypes = [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/webp', + 'image/heic', + 'image/heif', + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + ]; + + if (!allowedContentTypes.includes(contentType)) { + return res.status(400).json({ + success: false, + error: `Invalid content type. Must be one of: ${allowedContentTypes.join(', ')}`, + }); + } + + const uploadService = getUploadService(); + const presignedUrl = await uploadService.getPresignedUploadUrl({ + filename, + contentType, + category, + expiresIn: expiresIn || 3600, // Default 1 hour + }); + + return res.status(200).json({ + success: true, + data: presignedUrl, + }); + } catch (error) { + console.error('Presigned URL error:', error); + return res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Internal server error', + }); + } +}