diff --git a/Dockerfile b/Dockerfile index f15999b..bab22af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,11 +7,14 @@ COPY package*.json prisma.config.ts ./ COPY prisma ./prisma # Install ALL dependencies (including devDependencies needed for build) -RUN npm ci && npx prisma generate +RUN npm ci # Copy application code (includes .env created in CI from EC2_ENV for NEXT_PUBLIC_* at build time) COPY . . +# Regenerate Prisma Client with final schema before building Next.js +RUN npx prisma generate + # Build the Next.js application (reads .env and embeds NEXT_PUBLIC_* into the bundle) RUN npm run build diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 0a1ad94..545cd29 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -15,8 +15,8 @@ client.connect() sleep 1 done -echo "Syncing database schema with Prisma (db push)..." -npx prisma db push --accept-data-loss +echo "Running Prisma migrations (migrate deploy)..." +npx prisma migrate deploy if [ "$RUN_TEMPLATE_SEED_ON_START" = "true" ]; then echo "Seeding predefined email templates..." diff --git a/next.config.ts b/next.config.ts index ba78b8d..aa21032 100644 --- a/next.config.ts +++ b/next.config.ts @@ -28,6 +28,10 @@ const nextConfig: NextConfig = { protocol: "https", hostname: "*.ngrok-free.dev", }, + { + protocol: "https", + hostname: "junohub-product-images.s3.us-east-1.amazonaws.com", + }, ], }, // Allow ngrok origin for development (fixes cross-origin warnings) diff --git a/package-lock.json b/package-lock.json index 7354978..c08378a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@auth/core": "^0.41.0", "@auth/prisma-adapter": "^2.11.1", "@aws-sdk/client-bedrock-runtime": "^3.989.0", + "@aws-sdk/client-s3": "^3.989.0", "@prisma/adapter-pg": "^7.3.0", "@prisma/client": "^7.3.0", "@tailwindcss/postcss": "^4", @@ -19,6 +20,7 @@ "@upstash/ratelimit": "^2.0.5", "@upstash/redis": "^1.34.3", "aws-cli": "^0.0.2", + "aws-sdk": "^2.1693.0", "bcryptjs": "^3.0.3", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -121,6 +123,64 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "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" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", @@ -293,23 +353,103 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.990.0.tgz", + "integrity": "sha512-XnsM8RgB35Atn2+aYSocitCybDG82x9yYf/s2D23ytpyHCupmuZN3LzK2a0WxmKO6Zf7EtEIYy0mHGY4tLp9YA==", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/credential-provider-node": "^3.972.9", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.3", + "@aws-sdk/middleware-expect-continue": "^3.972.3", + "@aws-sdk/middleware-flexible-checksums": "^3.972.8", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-location-constraint": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-sdk-s3": "^3.972.10", + "@aws-sdk/middleware-ssec": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.10", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/signature-v4-multi-region": "3.990.0", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.990.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.8", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.23.0", + "@smithy/eventstream-serde-browser": "^4.2.8", + "@smithy/eventstream-serde-config-resolver": "^4.3.8", + "@smithy/eventstream-serde-node": "^4.2.8", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-blob-browser": "^4.2.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/hash-stream-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/md5-js": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.14", + "@smithy/middleware-retry": "^4.4.31", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.10", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.3", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@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.30", + "@smithy/util-defaults-mode-node": "^4.2.33", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-stream": "^4.5.12", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-endpoints": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.990.0.tgz", + "integrity": "sha512-kVwtDc9LNI3tQZHEMNbkLIOpeDK8sRSTuT8eMnzGY+O+JImPisfSTjdh+jw9OTznu+MYZjQsv0258sazVKunYg==", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/client-sso": { - "version": "3.989.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.989.0.tgz", - "integrity": "sha512-3sC+J1ru5VFXLgt9KZmXto0M7mnV5RkS6FNGwRMK3XrojSjHso9DLOWjbnXhbNv4motH8vu53L1HK2VC1+Nj5w==", + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.990.0.tgz", + "integrity": "sha512-xTEaPjZwOqVjGbLOP7qzwbdOWJOo1ne2mUhTZwEBBkPvNk4aXB/vcYwWwrjoSWUqtit4+GDbO75ePc/S6TUJYQ==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.9", + "@aws-sdk/core": "^3.973.10", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.9", + "@aws-sdk/middleware-user-agent": "^3.972.10", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.989.0", + "@aws-sdk/util-endpoints": "3.990.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.7", + "@aws-sdk/util-user-agent-node": "^3.972.8", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.0", "@smithy/fetch-http-handler": "^5.3.9", @@ -341,10 +481,25 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-endpoints": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.990.0.tgz", + "integrity": "sha512-kVwtDc9LNI3tQZHEMNbkLIOpeDK8sRSTuT8eMnzGY+O+JImPisfSTjdh+jw9OTznu+MYZjQsv0258sazVKunYg==", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/core": { - "version": "3.973.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.9.tgz", - "integrity": "sha512-cyUOfJSizn8da7XrBEFBf4UMI4A6JQNX6ZFcKtYmh/CrwfzsDcabv3k/z0bNwQ3pX5aeq5sg/8Bs/ASiL0bJaA==", + "version": "3.973.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.10.tgz", + "integrity": "sha512-4u/FbyyT3JqzfsESI70iFg6e2yp87MB5kS2qcxIA66m52VSTN1fvuvbCY1h/LKq1LvuxIrlJ1ItcyjvcKoaPLg==", "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.4", @@ -364,12 +519,24 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.7.tgz", - "integrity": "sha512-r8kBtglvLjGxBT87l6Lqkh9fL8yJJ6O4CYQPjKlj3AkCuL4/4784x3rxxXWw9LTKXOo114VB6mjxAuy5pI7XIg==", + "node_modules/@aws-sdk/crc64-nvme": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.0.tgz", + "integrity": "sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw==", "dependencies": { - "@aws-sdk/core": "^3.973.9", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.8.tgz", + "integrity": "sha512-r91OOPAcHnLCSxaeu/lzZAVRCZ/CtTNuwmJkUwpwSDshUrP7bkX1OmFn2nUMWd9kN53Q4cEo8b7226G4olt2Mg==", + "dependencies": { + "@aws-sdk/core": "^3.973.10", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", @@ -380,11 +547,11 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.9.tgz", - "integrity": "sha512-40caFblEg/TPrp9EpvyMxp4xlJ5TuTI+A8H6g8FhHn2hfH2PObFAPLF9d5AljK/G69E1YtTklkuQeAwPlV3w8Q==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.10.tgz", + "integrity": "sha512-DTtuyXSWB+KetzLcWaSahLJCtTUe/3SXtlGp4ik9PCe9xD6swHEkG8n8/BNsQ9dsihb9nhFvuUB4DpdBGDcvVg==", "dependencies": { - "@aws-sdk/core": "^3.973.9", + "@aws-sdk/core": "^3.973.10", "@aws-sdk/types": "^3.973.1", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.10", @@ -400,18 +567,18 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.7.tgz", - "integrity": "sha512-zeYKrMwM5bCkHFho/x3+1OL0vcZQ0OhTR7k35tLq74+GP5ieV3juHXTZfa2LVE0Bg75cHIIerpX0gomVOhzo/w==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.8.tgz", + "integrity": "sha512-n2dMn21gvbBIEh00E8Nb+j01U/9rSqFIamWRdGm/mE5e+vHQ9g0cBNdrYFlM6AAiryKVHZmShWT9D1JAWJ3ISw==", "dependencies": { - "@aws-sdk/core": "^3.973.9", - "@aws-sdk/credential-provider-env": "^3.972.7", - "@aws-sdk/credential-provider-http": "^3.972.9", - "@aws-sdk/credential-provider-login": "^3.972.7", - "@aws-sdk/credential-provider-process": "^3.972.7", - "@aws-sdk/credential-provider-sso": "^3.972.7", - "@aws-sdk/credential-provider-web-identity": "^3.972.7", - "@aws-sdk/nested-clients": "3.989.0", + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/credential-provider-env": "^3.972.8", + "@aws-sdk/credential-provider-http": "^3.972.10", + "@aws-sdk/credential-provider-login": "^3.972.8", + "@aws-sdk/credential-provider-process": "^3.972.8", + "@aws-sdk/credential-provider-sso": "^3.972.8", + "@aws-sdk/credential-provider-web-identity": "^3.972.8", + "@aws-sdk/nested-clients": "3.990.0", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", @@ -423,13 +590,76 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.7.tgz", - "integrity": "sha512-Q103cLU6OjAllYjX7+V+PKQw654jjvZUkD+lbUUiFbqut6gR5zwl1DrelvJPM5hnzIty7BCaxaRB3KMuz3M/ug==", + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/nested-clients": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.990.0.tgz", + "integrity": "sha512-3NA0s66vsy8g7hPh36ZsUgO4SiMyrhwcYvuuNK1PezO52vX3hXDW4pQrC6OQLGKGJV0o6tbEyQtXb/mPs8zg8w==", "dependencies": { - "@aws-sdk/core": "^3.973.9", - "@aws-sdk/nested-clients": "3.989.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.10", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.990.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.8", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.23.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.14", + "@smithy/middleware-retry": "^4.4.31", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.10", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.3", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@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.30", + "@smithy/util-defaults-mode-node": "^4.2.33", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/util-endpoints": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.990.0.tgz", + "integrity": "sha512-kVwtDc9LNI3tQZHEMNbkLIOpeDK8sRSTuT8eMnzGY+O+JImPisfSTjdh+jw9OTznu+MYZjQsv0258sazVKunYg==", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.8.tgz", + "integrity": "sha512-rMFuVids8ICge/X9DF5pRdGMIvkVhDV9IQFQ8aTYk6iF0rl9jOUa1C3kjepxiXUlpgJQT++sLZkT9n0TMLHhQw==", + "dependencies": { + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/nested-clients": "3.990.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", @@ -441,17 +671,80 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.8.tgz", - "integrity": "sha512-AaDVOT7iNJyLjc3j91VlucPZ4J8Bw+eu9sllRDugJqhHWYyR3Iyp2huBUW8A3+DfHoh70sxGkY92cThAicSzlQ==", + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/nested-clients": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.990.0.tgz", + "integrity": "sha512-3NA0s66vsy8g7hPh36ZsUgO4SiMyrhwcYvuuNK1PezO52vX3hXDW4pQrC6OQLGKGJV0o6tbEyQtXb/mPs8zg8w==", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.7", - "@aws-sdk/credential-provider-http": "^3.972.9", - "@aws-sdk/credential-provider-ini": "^3.972.7", - "@aws-sdk/credential-provider-process": "^3.972.7", - "@aws-sdk/credential-provider-sso": "^3.972.7", - "@aws-sdk/credential-provider-web-identity": "^3.972.7", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.10", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.990.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.8", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.23.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.14", + "@smithy/middleware-retry": "^4.4.31", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.10", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.3", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@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.30", + "@smithy/util-defaults-mode-node": "^4.2.33", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-endpoints": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.990.0.tgz", + "integrity": "sha512-kVwtDc9LNI3tQZHEMNbkLIOpeDK8sRSTuT8eMnzGY+O+JImPisfSTjdh+jw9OTznu+MYZjQsv0258sazVKunYg==", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.9.tgz", + "integrity": "sha512-LfJfO0ClRAq2WsSnA9JuUsNyIicD2eyputxSlSL0EiMrtxOxELLRG6ZVYDf/a1HCepaYPXeakH4y8D5OLCauag==", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.8", + "@aws-sdk/credential-provider-http": "^3.972.10", + "@aws-sdk/credential-provider-ini": "^3.972.8", + "@aws-sdk/credential-provider-process": "^3.972.8", + "@aws-sdk/credential-provider-sso": "^3.972.8", + "@aws-sdk/credential-provider-web-identity": "^3.972.8", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", @@ -464,11 +757,11 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.7.tgz", - "integrity": "sha512-hxMo1V3ujWWrQSONxQJAElnjredkRpB6p8SDjnvRq70IwYY38R/CZSys0IbhRPxdgWZ5j12yDRk2OXhxw4Gj3g==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.8.tgz", + "integrity": "sha512-6cg26ffFltxM51OOS8NH7oE41EccaYiNlbd5VgUYwhiGCySLfHoGuGrLm2rMB4zhy+IO5nWIIG0HiodX8zdvHA==", "dependencies": { - "@aws-sdk/core": "^3.973.9", + "@aws-sdk/core": "^3.973.10", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -480,13 +773,13 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.7.tgz", - "integrity": "sha512-ZGKBOHEj8Ap15jhG2XMncQmKLTqA++2DVU2eZfLu3T/pkwDyhCp5eZv5c/acFxbZcA/6mtxke+vzO/n+aeHs4A==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.8.tgz", + "integrity": "sha512-35kqmFOVU1n26SNv+U37sM8b2TzG8LyqAcd6iM9gprqxyHEh/8IM3gzN4Jzufs3qM6IrH8e43ryZWYdvfVzzKQ==", "dependencies": { - "@aws-sdk/client-sso": "3.989.0", - "@aws-sdk/core": "^3.973.9", - "@aws-sdk/token-providers": "3.989.0", + "@aws-sdk/client-sso": "3.990.0", + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/token-providers": "3.990.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -497,13 +790,61 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.7.tgz", - "integrity": "sha512-AbYupBIoSJoVMlbMqBhNvPhqj+CdGtzW7Uk4ZIMBm2br18pc3rkG1VaKVFV85H87QCvLHEnni1idJjaX1wOmIw==", + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/nested-clients": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.990.0.tgz", + "integrity": "sha512-3NA0s66vsy8g7hPh36ZsUgO4SiMyrhwcYvuuNK1PezO52vX3hXDW4pQrC6OQLGKGJV0o6tbEyQtXb/mPs8zg8w==", "dependencies": { - "@aws-sdk/core": "^3.973.9", - "@aws-sdk/nested-clients": "3.989.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.10", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.990.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.8", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.23.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.14", + "@smithy/middleware-retry": "^4.4.31", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.10", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.3", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@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.30", + "@smithy/util-defaults-mode-node": "^4.2.33", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.990.0.tgz", + "integrity": "sha512-L3BtUb2v9XmYgQdfGBzbBtKMXaP5fV973y3Qdxeevs6oUTVXFmi/mV1+LnScA/1wVPJC9/hlK+1o5vbt7cG7EQ==", + "dependencies": { + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/nested-clients": "3.990.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -514,6 +855,101 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/util-endpoints": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.990.0.tgz", + "integrity": "sha512-kVwtDc9LNI3tQZHEMNbkLIOpeDK8sRSTuT8eMnzGY+O+JImPisfSTjdh+jw9OTznu+MYZjQsv0258sazVKunYg==", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.8.tgz", + "integrity": "sha512-CZhN1bOc1J3ubQPqbmr5b4KaMJBgdDvYsmEIZuX++wFlzmZsKj1bwkaiTEb5U2V7kXuzLlpF5HJSOM9eY/6nGA==", + "dependencies": { + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/nested-clients": "3.990.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@aws-sdk/nested-clients": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.990.0.tgz", + "integrity": "sha512-3NA0s66vsy8g7hPh36ZsUgO4SiMyrhwcYvuuNK1PezO52vX3hXDW4pQrC6OQLGKGJV0o6tbEyQtXb/mPs8zg8w==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.10", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.990.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.8", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.23.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.14", + "@smithy/middleware-retry": "^4.4.31", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.10", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.3", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@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.30", + "@smithy/util-defaults-mode-node": "^4.2.33", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@aws-sdk/util-endpoints": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.990.0.tgz", + "integrity": "sha512-kVwtDc9LNI3tQZHEMNbkLIOpeDK8sRSTuT8eMnzGY+O+JImPisfSTjdh+jw9OTznu+MYZjQsv0258sazVKunYg==", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/eventstream-handler-node": { "version": "3.972.5", "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.5.tgz", @@ -528,6 +964,23 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.3.tgz", + "integrity": "sha512-fmbgWYirF67YF1GfD7cg5N6HHQ96EyRNx/rDIrTF277/zTWVuPI2qS/ZHgofwR1NZPe/NWvoppflQY01LrbVLg==", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-arn-parser": "^3.972.2", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-config-provider": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/middleware-eventstream": { "version": "3.972.3", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.3.tgz", @@ -542,6 +995,44 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.3.tgz", + "integrity": "sha512-4msC33RZsXQpUKR5QR4HnvBSNCPLGHmB55oDiROqqgyOc+TOfVu2xgi5goA7ms6MdZLeEh2905UfWMnMMF4mRg==", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.972.8.tgz", + "integrity": "sha512-Hn6gumcN/3/8Fzo9z7N1pA2PRfE8S+qAqdb4g3MqzXjIOIe+VxD7edO/DKAJ1YH11639EGQIHBz0wdOb5btjtw==", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/crc64-nvme": "3.972.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-stream": "^4.5.12", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/middleware-host-header": { "version": "3.972.3", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.3.tgz", @@ -556,6 +1047,19 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.3.tgz", + "integrity": "sha512-nIg64CVrsXp67vbK0U1/Is8rik3huS3QkRHn2DRDx4NldrEFMgdkZGI/+cZMKD9k4YOS110Dfu21KZLHrFA/1g==", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/middleware-logger": { "version": "3.972.3", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.3.tgz", @@ -584,14 +1088,51 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.9.tgz", - "integrity": "sha512-1g1B7yf7KzessB0mKNiV9gAHEwbM662xgU+VE4LxyGe6kVGZ8LqYsngjhE+Stna09CJ7Pxkjr6Uq1OtbGwJJJg==", + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.10.tgz", + "integrity": "sha512-wLkB4bshbBtsAiC2WwlHzOWXu1fx3ftL63fQl0DxEda48Q6B8bcHydZppE3KjEIpPyiNOllByfSnb07cYpIgmw==", "dependencies": { - "@aws-sdk/core": "^3.973.9", + "@aws-sdk/core": "^3.973.10", "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.989.0", + "@aws-sdk/util-arn-parser": "^3.972.2", + "@smithy/core": "^3.23.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/signature-v4": "^5.3.8", + "@smithy/smithy-client": "^4.11.3", + "@smithy/types": "^4.12.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-stream": "^4.5.12", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.3.tgz", + "integrity": "sha512-dU6kDuULN3o3jEHcjm0c4zWJlY1zWVkjG9NPe9qxYLLpcbdj5kRYBS2DdWYD+1B9f910DezRuws7xDEqKkHQIg==", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.10.tgz", + "integrity": "sha512-bBEL8CAqPQkI91ZM5a9xnFAzedpzH6NYCOtNyLarRAzTUTFN2DKqaC60ugBa7pnU1jSi4mA7WAXBsrod7nJltg==", + "dependencies": { + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.990.0", "@smithy/core": "^3.23.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", @@ -601,6 +1142,21 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/util-endpoints": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.990.0.tgz", + "integrity": "sha512-kVwtDc9LNI3tQZHEMNbkLIOpeDK8sRSTuT8eMnzGY+O+JImPisfSTjdh+jw9OTznu+MYZjQsv0258sazVKunYg==", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/middleware-websocket": { "version": "3.972.6", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.6.tgz", @@ -686,6 +1242,22 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.990.0.tgz", + "integrity": "sha512-O55s1eFmKi+2Ko5T1hbdxL6tFVONGscSVe9VRxS4m91Tlbo9iG2Q2HvKWq1DuKQAuUWSUfMmjrRt07JNzizr2A==", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "^3.972.10", + "@aws-sdk/types": "^3.973.1", + "@smithy/protocol-http": "^5.3.8", + "@smithy/signature-v4": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/token-providers": { "version": "3.989.0", "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.989.0.tgz", @@ -715,6 +1287,17 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.2.tgz", + "integrity": "sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/util-endpoints": { "version": "3.989.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.989.0.tgz", @@ -767,11 +1350,11 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.7.tgz", - "integrity": "sha512-oyhv+FjrgHjP+F16cmsrJzNP4qaRJzkV1n9Lvv4uyh3kLqo3rIe9NSBSBa35f2TedczfG2dD+kaQhHBB47D6Og==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.8.tgz", + "integrity": "sha512-XJZuT0LWsFCW1C8dEpPAXSa7h6Pb3krr2y//1X0Zidpcl0vmgY5nL/X0JuBZlntpBzaN3+U4hvKjuijyiiR8zw==", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.9", + "@aws-sdk/middleware-user-agent": "^3.972.10", "@aws-sdk/types": "^3.973.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", @@ -2862,6 +3445,29 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz", + "integrity": "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.1.tgz", + "integrity": "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==", + "dependencies": { + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/config-resolver": { "version": "4.4.6", "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.6.tgz", @@ -2993,6 +3599,20 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.9.tgz", + "integrity": "sha512-m80d/iicI7DlBDxyQP6Th7BW/ejDGiF0bgI754+tiwK0lgMkcaIBgvwwVc7OFbY4eUzpGtnig52MhPAEJ7iNYg==", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.0", + "@smithy/chunked-blob-reader-native": "^4.2.1", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/hash-node": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.8.tgz", @@ -3007,6 +3627,19 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.8.tgz", + "integrity": "sha512-v0FLTXgHrTeheYZFGhR+ehX5qUm4IQsjAiL9qehad2cyjMWcN2QG6/4mSwbSgEQzI7jwfoXj7z4fxZUx/Mhj2w==", + "dependencies": { + "@smithy/types": "^4.12.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/invalid-dependency": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.8.tgz", @@ -3030,6 +3663,19 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/md5-js": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.8.tgz", + "integrity": "sha512-oGMaLj4tVZzLi3itBa9TCswgMBr7k9b+qKYowQ6x1rTyTuO1IU2YHdHUa+891OsOH+wCsH7aTPRsTJO3RMQmjQ==", + "dependencies": { + "@smithy/types": "^4.12.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/middleware-content-length": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.8.tgz", @@ -3444,6 +4090,19 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.8.tgz", + "integrity": "sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg==", + "dependencies": { + "@smithy/abort-controller": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/uuid": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", @@ -4678,7 +5337,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -4702,7 +5360,7 @@ "aws-cli": "bin/aws.js" } }, - "node_modules/aws-sdk": { + "node_modules/aws-cli/node_modules/aws-sdk": { "version": "2.0.31", "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.0.31.tgz", "integrity": "sha512-rgmExM3/LjhI95PfjwDs1hks/RYrQ/V3qLybFCzBqr8Y3Hz9Qq3jp+WR1BaWhUckgoVJs/ZE4VSvMKZeNwuFoA==", @@ -4715,6 +5373,48 @@ "node": ">= 0.8.0" } }, + "node_modules/aws-cli/node_modules/sax": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.4.2.tgz", + "integrity": "sha512-6Zsl4gnHjiTQfrOzsWdc0bHJepF5KJAVuM5fcyEJrqGyszkx2c55IclWP4D692rJrl1w0tExhbvYKjKNZl5wHg==" + }, + "node_modules/aws-cli/node_modules/xml2js": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.6.tgz", + "integrity": "sha512-h+Nzgo0eLwideldZGqSquNtrsuX5zcDREImBa1GSNw9QljlXiFcM3E1kWjLprRjJe2jRuvnVk0j3WgQo9Deoog==", + "dependencies": { + "sax": "0.4.2" + } + }, + "node_modules/aws-cli/node_modules/xmlbuilder": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.2.tgz", + "integrity": "sha512-h/+ncQQSU/iYycmI2wTN25t7RYN7O2oq9uvI+2+UObi4KcmQh/jUS4N31g5vJttQt7MODsnmBtbcll3YbNyvfw==", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/aws-sdk": { + "version": "2.1693.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1693.0.tgz", + "integrity": "sha512-cJmb8xEnVLT+R6fBS5sn/EFJiX7tUnDaPtOPZ1vFbOJtd0fnZn/Ky2XGgsvvoeliWeH7mL3TWSX5zXXGSQV6gQ==", + "hasInstallScript": true, + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/aws-ssl-profiles": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", @@ -4861,11 +5561,26 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, + "node_modules/buffer/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/c12": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", @@ -4908,7 +5623,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", @@ -4926,7 +5640,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -4939,7 +5652,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" @@ -5348,7 +6060,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -5449,7 +6160,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -5580,7 +6290,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -5589,7 +6298,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -5625,7 +6333,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -6152,6 +6859,14 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==" }, + "node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", @@ -6313,7 +7028,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, "dependencies": { "is-callable": "^1.2.7" }, @@ -6383,7 +7097,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6429,7 +7142,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -6455,7 +7167,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -6484,7 +7195,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -6590,7 +7300,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -6638,7 +7347,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -6665,7 +7373,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -6677,7 +7384,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -6692,7 +7398,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -6763,6 +7468,11 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -6838,6 +7548,21 @@ "node": ">=12" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -6930,7 +7655,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -7022,7 +7746,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "dev": true, "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", @@ -7107,7 +7830,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", @@ -7185,7 +7907,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, "dependencies": { "which-typed-array": "^1.1.16" }, @@ -7275,6 +7996,14 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/jose": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", @@ -7841,7 +8570,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -8593,7 +9321,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -8807,6 +9534,15 @@ "node": ">=10.13.0" } }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -9165,7 +9901,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -9184,9 +9919,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sax": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/sax/-/sax-0.4.2.tgz", - "integrity": "sha512-6Zsl4gnHjiTQfrOzsWdc0bHJepF5KJAVuM5fcyEJrqGyszkx2c55IclWP4D692rJrl1w0tExhbvYKjKNZl5wHg==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" }, "node_modules/scheduler": { "version": "0.27.0", @@ -9216,7 +9951,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -10080,6 +10814,20 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -10088,11 +10836,31 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/valibot": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", @@ -10232,7 +11000,6 @@ "version": "1.1.20", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", - "dev": true, "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", @@ -10272,19 +11039,23 @@ } }, "node_modules/xml2js": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.6.tgz", - "integrity": "sha512-h+Nzgo0eLwideldZGqSquNtrsuX5zcDREImBa1GSNw9QljlXiFcM3E1kWjLprRjJe2jRuvnVk0j3WgQo9Deoog==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", "dependencies": { - "sax": "0.4.2" + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" } }, "node_modules/xmlbuilder": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.2.tgz", - "integrity": "sha512-h/+ncQQSU/iYycmI2wTN25t7RYN7O2oq9uvI+2+UObi4KcmQh/jUS4N31g5vJttQt7MODsnmBtbcll3YbNyvfw==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "engines": { - "node": ">=0.2.0" + "node": ">=4.0" } }, "node_modules/xtend": { diff --git a/package.json b/package.json index 57bc159..76f3ada 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@auth/core": "^0.41.0", "@auth/prisma-adapter": "^2.11.1", "@aws-sdk/client-bedrock-runtime": "^3.989.0", + "@aws-sdk/client-s3": "^3.989.0", "@prisma/adapter-pg": "^7.3.0", "@prisma/client": "^7.3.0", "@tailwindcss/postcss": "^4", @@ -21,6 +22,7 @@ "@upstash/ratelimit": "^2.0.5", "@upstash/redis": "^1.34.3", "aws-cli": "^0.0.2", + "aws-sdk": "^2.1693.0", "bcryptjs": "^3.0.3", "clsx": "^2.1.1", "date-fns": "^4.1.0", diff --git a/prisma/migrations/20260209_init_pgvector/migration.sql b/prisma/migrations/20260209_init_pgvector/migration.sql deleted file mode 100644 index 763c390..0000000 --- a/prisma/migrations/20260209_init_pgvector/migration.sql +++ /dev/null @@ -1,15 +0,0 @@ --- Enable pgvector extension and create index for knowledge chunk embeddings - -CREATE EXTENSION IF NOT EXISTS vector; - --- Adjust the embedding column on KnowledgeChunk to use a concrete vector dimension. --- NOTE: Update 1536 to match the actual embedding size used by your Gemini model. -ALTER TABLE "KnowledgeChunk" - ALTER COLUMN "embedding" TYPE vector(1536); - -CREATE INDEX IF NOT EXISTS "KnowledgeChunk_embedding_storeId_idx" - ON "KnowledgeChunk" - USING ivfflat ("embedding" vector_cosine_ops) - WITH (lists = 100) - WHERE "storeId" IS NOT NULL; - diff --git a/prisma/migrations/20260213120753_bedrock_embeddings/migration.sql b/prisma/migrations/20260213120753_bedrock_embeddings/migration.sql deleted file mode 100644 index e15da5a..0000000 --- a/prisma/migrations/20260213120753_bedrock_embeddings/migration.sql +++ /dev/null @@ -1,22 +0,0 @@ --- Migrate KnowledgeChunk embeddings from 1536 dimensions (Gemini) to 1024 dimensions (Bedrock Titan V2) --- --- NOTE: This migration changes the vector dimension. Existing embeddings will need to be --- re-embedded using the re-embedding script after this migration runs. --- --- Strategy: Drop the index, change column type (will truncate/pad existing vectors), --- then recreate the index with the new dimension. - --- Drop the existing index -DROP INDEX IF EXISTS "KnowledgeChunk_embedding_storeId_idx"; - --- Change the embedding column dimension from 1536 to 1024 --- PostgreSQL will automatically truncate vectors longer than 1024 or pad shorter ones -ALTER TABLE "KnowledgeChunk" - ALTER COLUMN "embedding" TYPE vector(1024); - --- Recreate the index with the new dimension -CREATE INDEX IF NOT EXISTS "KnowledgeChunk_embedding_storeId_idx" - ON "KnowledgeChunk" - USING ivfflat ("embedding" vector_cosine_ops) - WITH (lists = 100) - WHERE "storeId" IS NOT NULL; diff --git a/prisma/migrations/20260216152711_init_schema/migration.sql b/prisma/migrations/20260216152711_init_schema/migration.sql new file mode 100644 index 0000000..336cef5 --- /dev/null +++ b/prisma/migrations/20260216152711_init_schema/migration.sql @@ -0,0 +1,657 @@ +-- Enable pgvector extension for KnowledgeChunk.embedding +CREATE EXTENSION IF NOT EXISTS vector; + +-- CreateEnum +CREATE TYPE "UserRole" AS ENUM ('VENDOR', 'SUPPLIER', 'ADMIN'); + +-- CreateEnum +CREATE TYPE "ConnectionStatus" AS ENUM ('PENDING', 'CONNECTED', 'REJECTED'); + +-- CreateEnum +CREATE TYPE "ProductSyncStatus" AS ENUM ('PENDING', 'ACCEPTED', 'REJECTED'); + +-- CreateEnum +CREATE TYPE "NotificationType" AS ENUM ('INVITATION_ACCEPTED', 'INVITATION_RECEIVED', 'CONNECTION_STATUS_CHANGED', 'ORDER_RECEIVED', 'PRODUCT_UPDATED', 'PRODUCT_SYNC_REQUESTED', 'PRODUCT_SYNC_ACCEPTED', 'PRODUCT_SYNC_REJECTED'); + +-- CreateEnum +CREATE TYPE "FlaggedEmailPriority" AS ENUM ('LOW', 'MEDIUM', 'HIGH'); + +-- CreateEnum +CREATE TYPE "KnowledgeDocumentStatus" AS ENUM ('UPLOADED', 'PROCESSING', 'READY', 'FAILED'); + +-- CreateEnum +CREATE TYPE "KnowledgeChunkSourceType" AS ENUM ('DOCUMENT', 'FAQ'); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "name" TEXT, + "email" TEXT, + "emailVerified" TIMESTAMP(3), + "image" TEXT, + "password" TEXT, + "role" "UserRole" NOT NULL DEFAULT 'VENDOR', + "activeStoreId" TEXT, + "mfaEnabled" BOOLEAN NOT NULL DEFAULT false, + "mfaSecret" TEXT, + "chatPublicKey" TEXT, + "chatPrivateKeyEncrypted" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Account" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "refresh_token" TEXT, + "access_token" TEXT, + "expires_at" INTEGER, + "token_type" TEXT, + "scope" TEXT, + "id_token" TEXT, + "session_state" TEXT, + + CONSTRAINT "Account_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Session" ( + "id" TEXT NOT NULL, + "sessionToken" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Session_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VerificationToken" ( + "identifier" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL +); + +-- CreateTable +CREATE TABLE "Store" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "businessName" TEXT NOT NULL, + "email" TEXT NOT NULL, + "timezone" TEXT NOT NULL, + "country" TEXT NOT NULL, + "shopifyStoreName" TEXT NOT NULL, + "shopifyDomain" TEXT NOT NULL, + "shopifyApiKey" TEXT NOT NULL, + "shopifyApiSecret" TEXT NOT NULL, + "shopifyAccessToken" TEXT NOT NULL, + "onboardingComplete" BOOLEAN NOT NULL DEFAULT false, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Store_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CachedProduct" ( + "id" TEXT NOT NULL, + "shopifyProductId" TEXT NOT NULL, + "storeId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "vendor" TEXT, + "productType" TEXT, + "status" TEXT, + "variants" JSONB NOT NULL, + "images" JSONB NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "lastSyncedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "CachedProduct_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CachedOrder" ( + "id" TEXT NOT NULL, + "shopifyOrderId" TEXT NOT NULL, + "storeId" TEXT NOT NULL, + "orderNumber" TEXT NOT NULL, + "email" TEXT, + "totalPrice" TEXT NOT NULL, + "currency" TEXT, + "financialStatus" TEXT, + "fulfillmentStatus" TEXT, + "lineItems" JSONB NOT NULL, + "customer" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "lastSyncedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "shopifyCreatedAt" TIMESTAMP(3), + + CONSTRAINT "CachedOrder_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CachedCustomer" ( + "id" TEXT NOT NULL, + "shopifyCustomerId" TEXT NOT NULL, + "storeId" TEXT NOT NULL, + "email" TEXT, + "firstName" TEXT, + "lastName" TEXT, + "ordersCount" INTEGER NOT NULL DEFAULT 0, + "totalSpent" TEXT, + "phone" TEXT, + "tags" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "lastSyncedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "shopifyCreatedAt" TIMESTAMP(3), + + CONSTRAINT "CachedCustomer_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SyncMetadata" ( + "id" TEXT NOT NULL, + "storeId" TEXT NOT NULL, + "resourceType" TEXT NOT NULL, + "lastSyncAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "syncStatus" TEXT NOT NULL DEFAULT 'success', + "recordCount" INTEGER NOT NULL DEFAULT 0, + "errorMessage" TEXT, + + CONSTRAINT "SyncMetadata_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SupplierProfile" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "companyName" TEXT NOT NULL, + "description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "SupplierProfile_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Connection" ( + "id" TEXT NOT NULL, + "storeId" TEXT NOT NULL, + "supplierId" TEXT NOT NULL, + "status" "ConnectionStatus" NOT NULL DEFAULT 'PENDING', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Connection_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Invitation" ( + "id" TEXT NOT NULL, + "token" TEXT NOT NULL, + "storeId" TEXT NOT NULL, + "supplierId" TEXT NOT NULL, + "email" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "used" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "usedAt" TIMESTAMP(3), + + CONSTRAINT "Invitation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Product" ( + "id" TEXT NOT NULL, + "supplierId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "price" DOUBLE PRECISION NOT NULL, + "sku" TEXT, + "imageUrl" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Product_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SupplierProductOffer" ( + "id" TEXT NOT NULL, + "productId" TEXT NOT NULL, + "storeId" TEXT NOT NULL, + "connectionId" TEXT NOT NULL, + "supplierId" TEXT NOT NULL, + "price" DOUBLE PRECISION NOT NULL, + "quantity" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "SupplierProductOffer_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ProductSync" ( + "id" TEXT NOT NULL, + "storeId" TEXT NOT NULL, + "supplierId" TEXT NOT NULL, + "shopifyProductId" TEXT NOT NULL, + "shopifyProductTitle" TEXT NOT NULL, + "shopifyProductData" JSONB, + "status" "ProductSyncStatus" NOT NULL DEFAULT 'PENDING', + "requestedBy" TEXT NOT NULL, + "acceptedAt" TIMESTAMP(3), + "rejectedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "ProductSync_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "GmailIntegration" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "accessToken" TEXT NOT NULL, + "refreshToken" TEXT, + "expiresAt" TIMESTAMP(3) NOT NULL, + "email" TEXT NOT NULL, + "scope" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "GmailIntegration_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Notification" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "type" "NotificationType" NOT NULL, + "title" TEXT NOT NULL, + "message" TEXT NOT NULL, + "read" BOOLEAN NOT NULL DEFAULT false, + "readAt" TIMESTAMP(3), + "metadata" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Notification_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "EmailLog" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "storeId" TEXT, + "to" TEXT NOT NULL, + "subject" TEXT NOT NULL, + "bodyPreview" TEXT, + "isAutomated" BOOLEAN NOT NULL DEFAULT false, + "trigger" TEXT, + "status" TEXT NOT NULL, + "providerMessageId" TEXT, + "errorMessage" TEXT, + "sentAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "EmailLog_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "FlaggedEmail" ( + "id" TEXT NOT NULL, + "storeId" TEXT NOT NULL, + "subject" TEXT NOT NULL, + "from" TEXT NOT NULL, + "reason" TEXT NOT NULL, + "priority" "FlaggedEmailPriority" NOT NULL DEFAULT 'MEDIUM', + "messageId" TEXT, + "flaggedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "FlaggedEmail_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KnowledgeDocument" ( + "id" TEXT NOT NULL, + "storeId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "fileName" TEXT NOT NULL, + "mimeType" TEXT NOT NULL, + "sizeBytes" INTEGER NOT NULL, + "status" "KnowledgeDocumentStatus" NOT NULL DEFAULT 'UPLOADED', + "errorMessage" TEXT, + "storagePath" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "KnowledgeDocument_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "FaqItem" ( + "id" TEXT NOT NULL, + "storeId" TEXT NOT NULL, + "question" TEXT NOT NULL, + "answer" TEXT NOT NULL, + "tags" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "FaqItem_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "KnowledgeChunk" ( + "id" TEXT NOT NULL, + "storeId" TEXT NOT NULL, + "documentId" TEXT, + "faqItemId" TEXT, + "sourceType" "KnowledgeChunkSourceType" NOT NULL, + "content" TEXT NOT NULL, + "metadata" JSONB, + "embedding" vector NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "KnowledgeChunk_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Template" ( + "id" TEXT NOT NULL, + "storeId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "intent" TEXT NOT NULL, + "mood" TEXT NOT NULL, + "description" TEXT, + "body" TEXT NOT NULL, + "variables" JSONB, + "isHtml" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Template_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ChatMessage" ( + "id" TEXT NOT NULL, + "senderId" TEXT NOT NULL, + "receiverId" TEXT NOT NULL, + "encryptedContent" TEXT NOT NULL, + "encryptedKeyForSender" TEXT NOT NULL, + "encryptedKeyForReceiver" TEXT NOT NULL, + "iv" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ChatMessage_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PushSubscription" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "endpoint" TEXT NOT NULL, + "p256dh" TEXT NOT NULL, + "auth" TEXT NOT NULL, + "userAgent" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "PushSubscription_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); + +-- CreateIndex +CREATE INDEX "Store_userId_idx" ON "Store"("userId"); + +-- CreateIndex +CREATE INDEX "CachedProduct_storeId_idx" ON "CachedProduct"("storeId"); + +-- CreateIndex +CREATE UNIQUE INDEX "CachedProduct_shopifyProductId_storeId_key" ON "CachedProduct"("shopifyProductId", "storeId"); + +-- CreateIndex +CREATE INDEX "CachedOrder_storeId_idx" ON "CachedOrder"("storeId"); + +-- CreateIndex +CREATE INDEX "CachedOrder_shopifyCreatedAt_idx" ON "CachedOrder"("shopifyCreatedAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "CachedOrder_shopifyOrderId_storeId_key" ON "CachedOrder"("shopifyOrderId", "storeId"); + +-- CreateIndex +CREATE INDEX "CachedCustomer_storeId_idx" ON "CachedCustomer"("storeId"); + +-- CreateIndex +CREATE UNIQUE INDEX "CachedCustomer_shopifyCustomerId_storeId_key" ON "CachedCustomer"("shopifyCustomerId", "storeId"); + +-- CreateIndex +CREATE INDEX "SyncMetadata_storeId_idx" ON "SyncMetadata"("storeId"); + +-- CreateIndex +CREATE UNIQUE INDEX "SyncMetadata_storeId_resourceType_key" ON "SyncMetadata"("storeId", "resourceType"); + +-- CreateIndex +CREATE UNIQUE INDEX "SupplierProfile_userId_key" ON "SupplierProfile"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Connection_storeId_supplierId_key" ON "Connection"("storeId", "supplierId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Invitation_token_key" ON "Invitation"("token"); + +-- CreateIndex +CREATE INDEX "Invitation_token_idx" ON "Invitation"("token"); + +-- CreateIndex +CREATE INDEX "Invitation_supplierId_idx" ON "Invitation"("supplierId"); + +-- CreateIndex +CREATE INDEX "Invitation_storeId_idx" ON "Invitation"("storeId"); + +-- CreateIndex +CREATE INDEX "SupplierProductOffer_storeId_idx" ON "SupplierProductOffer"("storeId"); + +-- CreateIndex +CREATE INDEX "SupplierProductOffer_supplierId_idx" ON "SupplierProductOffer"("supplierId"); + +-- CreateIndex +CREATE INDEX "SupplierProductOffer_connectionId_idx" ON "SupplierProductOffer"("connectionId"); + +-- CreateIndex +CREATE UNIQUE INDEX "SupplierProductOffer_productId_storeId_key" ON "SupplierProductOffer"("productId", "storeId"); + +-- CreateIndex +CREATE INDEX "ProductSync_storeId_idx" ON "ProductSync"("storeId"); + +-- CreateIndex +CREATE INDEX "ProductSync_supplierId_idx" ON "ProductSync"("supplierId"); + +-- CreateIndex +CREATE INDEX "ProductSync_status_idx" ON "ProductSync"("status"); + +-- CreateIndex +CREATE INDEX "ProductSync_shopifyProductId_idx" ON "ProductSync"("shopifyProductId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ProductSync_storeId_supplierId_shopifyProductId_key" ON "ProductSync"("storeId", "supplierId", "shopifyProductId"); + +-- CreateIndex +CREATE UNIQUE INDEX "GmailIntegration_userId_key" ON "GmailIntegration"("userId"); + +-- CreateIndex +CREATE INDEX "GmailIntegration_userId_idx" ON "GmailIntegration"("userId"); + +-- CreateIndex +CREATE INDEX "Notification_userId_idx" ON "Notification"("userId"); + +-- CreateIndex +CREATE INDEX "Notification_userId_read_idx" ON "Notification"("userId", "read"); + +-- CreateIndex +CREATE INDEX "Notification_createdAt_idx" ON "Notification"("createdAt"); + +-- CreateIndex +CREATE INDEX "EmailLog_storeId_sentAt_idx" ON "EmailLog"("storeId", "sentAt"); + +-- CreateIndex +CREATE INDEX "EmailLog_userId_sentAt_idx" ON "EmailLog"("userId", "sentAt"); + +-- CreateIndex +CREATE INDEX "FlaggedEmail_storeId_idx" ON "FlaggedEmail"("storeId"); + +-- CreateIndex +CREATE INDEX "FlaggedEmail_storeId_flaggedAt_idx" ON "FlaggedEmail"("storeId", "flaggedAt"); + +-- CreateIndex +CREATE INDEX "KnowledgeDocument_storeId_idx" ON "KnowledgeDocument"("storeId"); + +-- CreateIndex +CREATE INDEX "FaqItem_storeId_idx" ON "FaqItem"("storeId"); + +-- CreateIndex +CREATE INDEX "KnowledgeChunk_storeId_idx" ON "KnowledgeChunk"("storeId"); + +-- CreateIndex +CREATE INDEX "KnowledgeChunk_documentId_idx" ON "KnowledgeChunk"("documentId"); + +-- CreateIndex +CREATE INDEX "KnowledgeChunk_faqItemId_idx" ON "KnowledgeChunk"("faqItemId"); + +-- CreateIndex +CREATE INDEX "Template_storeId_idx" ON "Template"("storeId"); + +-- CreateIndex +CREATE INDEX "ChatMessage_senderId_receiverId_idx" ON "ChatMessage"("senderId", "receiverId"); + +-- CreateIndex +CREATE INDEX "ChatMessage_receiverId_senderId_idx" ON "ChatMessage"("receiverId", "senderId"); + +-- CreateIndex +CREATE UNIQUE INDEX "PushSubscription_endpoint_key" ON "PushSubscription"("endpoint"); + +-- CreateIndex +CREATE INDEX "PushSubscription_userId_idx" ON "PushSubscription"("userId"); + +-- AddForeignKey +ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Store" ADD CONSTRAINT "Store_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CachedProduct" ADD CONSTRAINT "CachedProduct_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CachedOrder" ADD CONSTRAINT "CachedOrder_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CachedCustomer" ADD CONSTRAINT "CachedCustomer_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SyncMetadata" ADD CONSTRAINT "SyncMetadata_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SupplierProfile" ADD CONSTRAINT "SupplierProfile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Connection" ADD CONSTRAINT "Connection_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Connection" ADD CONSTRAINT "Connection_supplierId_fkey" FOREIGN KEY ("supplierId") REFERENCES "SupplierProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Invitation" ADD CONSTRAINT "Invitation_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Invitation" ADD CONSTRAINT "Invitation_supplierId_fkey" FOREIGN KEY ("supplierId") REFERENCES "SupplierProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Product" ADD CONSTRAINT "Product_supplierId_fkey" FOREIGN KEY ("supplierId") REFERENCES "SupplierProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SupplierProductOffer" ADD CONSTRAINT "SupplierProductOffer_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SupplierProductOffer" ADD CONSTRAINT "SupplierProductOffer_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SupplierProductOffer" ADD CONSTRAINT "SupplierProductOffer_connectionId_fkey" FOREIGN KEY ("connectionId") REFERENCES "Connection"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SupplierProductOffer" ADD CONSTRAINT "SupplierProductOffer_supplierId_fkey" FOREIGN KEY ("supplierId") REFERENCES "SupplierProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProductSync" ADD CONSTRAINT "ProductSync_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProductSync" ADD CONSTRAINT "ProductSync_supplierId_fkey" FOREIGN KEY ("supplierId") REFERENCES "SupplierProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "GmailIntegration" ADD CONSTRAINT "GmailIntegration_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Notification" ADD CONSTRAINT "Notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "EmailLog" ADD CONSTRAINT "EmailLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "EmailLog" ADD CONSTRAINT "EmailLog_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "FlaggedEmail" ADD CONSTRAINT "FlaggedEmail_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "KnowledgeDocument" ADD CONSTRAINT "KnowledgeDocument_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "FaqItem" ADD CONSTRAINT "FaqItem_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "KnowledgeChunk" ADD CONSTRAINT "KnowledgeChunk_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "KnowledgeChunk" ADD CONSTRAINT "KnowledgeChunk_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "KnowledgeDocument"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "KnowledgeChunk" ADD CONSTRAINT "KnowledgeChunk_faqItemId_fkey" FOREIGN KEY ("faqItemId") REFERENCES "FaqItem"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Template" ADD CONSTRAINT "Template_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ChatMessage" ADD CONSTRAINT "ChatMessage_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ChatMessage" ADD CONSTRAINT "ChatMessage_receiverId_fkey" FOREIGN KEY ("receiverId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PushSubscription" ADD CONSTRAINT "PushSubscription_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e8d4324..ab590f2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -27,7 +27,8 @@ model User { mfaEnabled Boolean @default(false) mfaSecret String? // Encrypted TOTP secret - chatPublicKey String? + chatPublicKey String? + chatPrivateKeyEncrypted String? @db.Text // Relations sentMessages ChatMessage[] @relation("SentMessages") @@ -110,6 +111,7 @@ model Store { syncMetadata SyncMetadata[] invitations Invitation[] productSyncs ProductSync[] + supplierProductOffers SupplierProductOffer[] knowledgeDocuments KnowledgeDocument[] faqItems FaqItem[] knowledgeChunks KnowledgeChunk[] @@ -217,6 +219,7 @@ model SupplierProfile { products Product[] invitations Invitation[] productSyncs ProductSync[] + supplierProductOffers SupplierProductOffer[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -235,6 +238,7 @@ model Connection { updatedAt DateTime @updatedAt @@unique([storeId, supplierId]) + supplierProductOffers SupplierProductOffer[] } model Invitation { @@ -276,6 +280,30 @@ model Product { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + offers SupplierProductOffer[] +} + +model SupplierProductOffer { + id String @id @default(cuid()) + productId String + product Product @relation(fields: [productId], references: [id], onDelete: Cascade) + storeId String + store Store @relation(fields: [storeId], references: [id], onDelete: Cascade) + connectionId String + connection Connection @relation(fields: [connectionId], references: [id], onDelete: Cascade) + supplierId String + supplier SupplierProfile @relation(fields: [supplierId], references: [id], onDelete: Cascade) + + price Float + quantity Int + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([productId, storeId]) + @@index([storeId]) + @@index([supplierId]) + @@index([connectionId]) } enum ProductSyncStatus { diff --git a/src/app/api/supplier/vendors/[connectionId]/offers/route.ts b/src/app/api/supplier/vendors/[connectionId]/offers/route.ts new file mode 100644 index 0000000..5521f2a --- /dev/null +++ b/src/app/api/supplier/vendors/[connectionId]/offers/route.ts @@ -0,0 +1,208 @@ +import { NextResponse } from "next/server" +import { auth } from "@/auth" +import { prisma } from "@/lib/prisma" + +/** + * GET /api/supplier/vendors/[connectionId]/offers + * List all products this supplier has offered to a specific vendor connection + */ +export async function GET( + req: Request, + { params }: { params: Promise<{ connectionId: string }> } +) { + try { + const session = await auth() + + if (!session?.user?.id || session.user.role !== "SUPPLIER") { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }) + } + + const { connectionId } = await params + + const supplierProfile = await prisma.supplierProfile.findUnique({ + where: { userId: session.user.id } + }) + + if (!supplierProfile) { + return NextResponse.json({ message: "Supplier profile not found" }, { status: 404 }) + } + + const connection = await prisma.connection.findFirst({ + where: { + id: connectionId, + supplierId: supplierProfile.id + }, + include: { + store: true + } + }) + + if (!connection) { + return NextResponse.json({ message: "Connection not found" }, { status: 404 }) + } + + // Access SupplierProductOffer model (camelCase in Prisma Client) + if (!("supplierProductOffer" in prisma)) { + console.error("Prisma Client missing supplierProductOffer model. Run: npx prisma generate") + return NextResponse.json( + { message: "Database model not available. Please regenerate Prisma Client." }, + { status: 500 } + ) + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const offers = await (prisma as any).supplierProductOffer.findMany({ + where: { + connectionId: connection.id + }, + include: { + product: true, + store: { + select: { + id: true, + businessName: true + } + } + }, + orderBy: { + createdAt: "desc" + } + }) + + return NextResponse.json({ + offers + }) + } catch (error) { + console.error("Error fetching supplier product offers:", error) + return NextResponse.json( + { message: "Failed to fetch supplier product offers" }, + { status: 500 } + ) + } +} + +/** + * POST /api/supplier/vendors/[connectionId]/offers + * Create a new product offer from supplier to this vendor + */ +export async function POST( + req: Request, + { params }: { params: Promise<{ connectionId: string }> } +) { + try { + const session = await auth() + + if (!session?.user?.id || session.user.role !== "SUPPLIER") { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }) + } + + const { connectionId } = await params + const body = await req.json() + const { productId, price, quantity } = body as { + productId?: string + price?: number + quantity?: number + } + + if (!productId || price === undefined || quantity === undefined) { + return NextResponse.json( + { message: "productId, price and quantity are required" }, + { status: 400 } + ) + } + + if (quantity <= 0 || price < 0) { + return NextResponse.json( + { message: "Price must be non-negative and quantity must be greater than zero" }, + { status: 400 } + ) + } + + const supplierProfile = await prisma.supplierProfile.findUnique({ + where: { userId: session.user.id } + }) + + if (!supplierProfile) { + return NextResponse.json({ message: "Supplier profile not found" }, { status: 404 }) + } + + const connection = await prisma.connection.findFirst({ + where: { + id: connectionId, + supplierId: supplierProfile.id + } + }) + + if (!connection) { + return NextResponse.json({ message: "Connection not found" }, { status: 404 }) + } + + if (connection.status !== "CONNECTED") { + return NextResponse.json( + { message: "You can only provide products to connected vendors" }, + { status: 400 } + ) + } + + // Ensure the product belongs to this supplier + const product = await prisma.product.findFirst({ + where: { + id: productId, + supplierId: supplierProfile.id + } + }) + + if (!product) { + return NextResponse.json( + { message: "Product not found or does not belong to this supplier" }, + { status: 404 } + ) + } + + // Check for existing offer for this product + store + if (!("supplierProductOffer" in prisma)) { + console.error("Prisma Client missing supplierProductOffer model. Run: npx prisma generate") + return NextResponse.json( + { message: "Database model not available. Please regenerate Prisma Client." }, + { status: 500 } + ) + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const existingOffer = await (prisma as any).supplierProductOffer.findFirst({ + where: { + productId, + storeId: connection.storeId + } + }) + + if (existingOffer) { + return NextResponse.json( + { message: "This product has already been provided to this vendor" }, + { status: 400 } + ) + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const offer = await (prisma as any).supplierProductOffer.create({ + data: { + productId, + storeId: connection.storeId, + connectionId: connection.id, + supplierId: supplierProfile.id, + price, + quantity + } + }) + + return NextResponse.json({ + message: "Product provided to vendor successfully", + offer + }) + } catch (error) { + console.error("Error creating supplier product offer:", error) + return NextResponse.json( + { message: "Failed to provide product to vendor" }, + { status: 500 } + ) + } +} + diff --git a/src/app/api/supplier/vendors/[connectionId]/route.ts b/src/app/api/supplier/vendors/[connectionId]/route.ts index 166e2a8..88148c7 100644 --- a/src/app/api/supplier/vendors/[connectionId]/route.ts +++ b/src/app/api/supplier/vendors/[connectionId]/route.ts @@ -2,6 +2,69 @@ import { NextResponse } from "next/server" import { auth } from "@/auth" import { prisma } from "@/lib/prisma" +/** + * GET /api/supplier/vendors/[connectionId] + * Get a single vendor connection with store details for the authenticated supplier + */ +export async function GET( + req: Request, + { params }: { params: Promise<{ connectionId: string }> } +) { + try { + const session = await auth() + + if (!session?.user?.id || session.user.role !== "SUPPLIER") { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }) + } + + const { connectionId } = await params + + const supplierProfile = await prisma.supplierProfile.findUnique({ + where: { userId: session.user.id } + }) + + if (!supplierProfile) { + return NextResponse.json({ message: "Supplier profile not found" }, { status: 404 }) + } + + const connection = await prisma.connection.findFirst({ + where: { + id: connectionId, + supplierId: supplierProfile.id + }, + include: { + store: { + select: { + id: true, + businessName: true, + shopifyDomain: true, + shopifyStoreName: true, + email: true, + user: { + select: { + name: true, + email: true + } + } + } + } + } + }) + + if (!connection) { + return NextResponse.json({ message: "Connection not found" }, { status: 404 }) + } + + return NextResponse.json({ connection }) + } catch (error) { + console.error("Error fetching vendor connection:", error) + return NextResponse.json( + { message: "Failed to fetch vendor connection" }, + { status: 500 } + ) + } +} + /** * PUT /api/supplier/vendors/[connectionId] * Update connection status (accept/reject) diff --git a/src/app/api/supplier/vendors/offers/[offerId]/route.ts b/src/app/api/supplier/vendors/offers/[offerId]/route.ts new file mode 100644 index 0000000..23de15b --- /dev/null +++ b/src/app/api/supplier/vendors/offers/[offerId]/route.ts @@ -0,0 +1,158 @@ +import { NextResponse } from "next/server" +import { auth } from "@/auth" +import { prisma } from "@/lib/prisma" + +/** + * PUT /api/supplier/vendors/offers/[offerId] + * Update an existing supplier product offer (price, quantity) + */ +export async function PUT( + req: Request, + { params }: { params: Promise<{ offerId: string }> } +) { + try { + const session = await auth() + + if (!session?.user?.id || session.user.role !== "SUPPLIER") { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }) + } + + const { offerId } = await params + const body = await req.json() + const { price, quantity } = body as { + price?: number + quantity?: number + } + + if (price === undefined && quantity === undefined) { + return NextResponse.json( + { message: "At least one of price or quantity must be provided" }, + { status: 400 } + ) + } + + if (quantity !== undefined && quantity <= 0) { + return NextResponse.json( + { message: "Quantity must be greater than zero" }, + { status: 400 } + ) + } + + if (price !== undefined && price < 0) { + return NextResponse.json( + { message: "Price must be non-negative" }, + { status: 400 } + ) + } + + const supplierProfile = await prisma.supplierProfile.findUnique({ + where: { userId: session.user.id } + }) + + if (!supplierProfile) { + return NextResponse.json({ message: "Supplier profile not found" }, { status: 404 }) + } + + if (!("supplierProductOffer" in prisma)) { + console.error("Prisma Client missing supplierProductOffer model. Run: npx prisma generate") + return NextResponse.json( + { message: "Database model not available. Please regenerate Prisma Client." }, + { status: 500 } + ) + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const existingOffer = await (prisma as any).supplierProductOffer.findFirst({ + where: { + id: offerId, + supplierId: supplierProfile.id + } + }) + + if (!existingOffer) { + return NextResponse.json({ message: "Offer not found" }, { status: 404 }) + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const updatedOffer = await (prisma as any).supplierProductOffer.update({ + where: { id: offerId }, + data: { + ...(price !== undefined ? { price } : {}), + ...(quantity !== undefined ? { quantity } : {}) + } + }) + + return NextResponse.json({ + message: "Offer updated successfully", + offer: updatedOffer + }) + } catch (error) { + console.error("Error updating supplier product offer:", error) + return NextResponse.json( + { message: "Failed to update supplier product offer" }, + { status: 500 } + ) + } +} + +/** + * DELETE /api/supplier/vendors/offers/[offerId] + * Withdraw an existing supplier product offer from a vendor + */ +export async function DELETE( + req: Request, + { params }: { params: Promise<{ offerId: string }> } +) { + try { + const session = await auth() + + if (!session?.user?.id || session.user.role !== "SUPPLIER") { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }) + } + + const { offerId } = await params + + const supplierProfile = await prisma.supplierProfile.findUnique({ + where: { userId: session.user.id } + }) + + if (!supplierProfile) { + return NextResponse.json({ message: "Supplier profile not found" }, { status: 404 }) + } + + // Verify the offer belongs to this supplier + if (!("supplierProductOffer" in prisma)) { + console.error("Prisma Client missing supplierProductOffer model. Run: npx prisma generate") + return NextResponse.json( + { message: "Database model not available. Please regenerate Prisma Client." }, + { status: 500 } + ) + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const existingOffer = await (prisma as any).supplierProductOffer.findFirst({ + where: { + id: offerId, + supplierId: supplierProfile.id + } + }) + + if (!existingOffer) { + return NextResponse.json({ message: "Offer not found" }, { status: 404 }) + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await (prisma as any).supplierProductOffer.delete({ + where: { id: offerId } + }) + + return NextResponse.json({ + message: "Offer withdrawn successfully" + }) + } catch (error) { + console.error("Error deleting supplier product offer:", error) + return NextResponse.json( + { message: "Failed to withdraw supplier product offer" }, + { status: 500 } + ) + } +} + diff --git a/src/app/api/uploads/supplier-product-image/route.ts b/src/app/api/uploads/supplier-product-image/route.ts new file mode 100644 index 0000000..19db75b --- /dev/null +++ b/src/app/api/uploads/supplier-product-image/route.ts @@ -0,0 +1,55 @@ +import { NextResponse } from "next/server" +import { auth } from "@/auth" +import { prisma } from "@/lib/prisma" +import { uploadSupplierProductImage } from "@/lib/s3" + +export async function POST(req: Request) { + try { + const session = await auth() + if (!session?.user?.id || session.user.role !== "SUPPLIER") { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }) + } + + const formData = await req.formData() + const file = formData.get("file") + + if (!(file instanceof File)) { + return NextResponse.json( + { message: "Missing file" }, + { status: 400 } + ) + } + + // Resolve supplier profile so we can scope keys per-supplier + const supplierProfile = await prisma.supplierProfile.findUnique({ + where: { userId: session.user.id }, + select: { id: true } + }) + + if (!supplierProfile) { + return NextResponse.json( + { message: "Supplier profile not found" }, + { status: 404 } + ) + } + + const arrayBuffer = await file.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) + + const { url } = await uploadSupplierProductImage({ + supplierId: supplierProfile.id, + fileBuffer: buffer, + contentType: file.type || undefined, + originalName: file.name + }) + + return NextResponse.json({ url }) + } catch (error) { + console.error("[SUPPLIER_PRODUCT_IMAGE_UPLOAD]", error) + return NextResponse.json( + { message: "Failed to upload image" }, + { status: 500 } + ) + } +} + diff --git a/src/app/api/user/chat-key/route.ts b/src/app/api/user/chat-key/route.ts new file mode 100644 index 0000000..8179f3c --- /dev/null +++ b/src/app/api/user/chat-key/route.ts @@ -0,0 +1,53 @@ +import { NextResponse } from "next/server" +import { auth } from "@/auth" +import { prisma } from "@/lib/prisma" +import { decryptString } from "@/lib/encryption" + +export async function GET() { + try { + const session = await auth() + if (!session?.user?.id) { + return new NextResponse("Unauthorized", { status: 401 }) + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id } + }) as { chatPrivateKeyEncrypted?: string | null } | null + + if (!user?.chatPrivateKeyEncrypted) { + return NextResponse.json( + { hasBackup: false }, + { + headers: { + "Cache-Control": "no-store" + } + } + ) + } + + const decrypted = decryptString(user.chatPrivateKeyEncrypted) + if (!decrypted) { + return NextResponse.json( + { hasBackup: false }, + { + headers: { + "Cache-Control": "no-store" + } + } + ) + } + + return NextResponse.json( + { hasBackup: true, privateKey: decrypted }, + { + headers: { + "Cache-Control": "no-store" + } + } + ) + } catch (error) { + console.error("[CHAT_KEY_GET]", error) + return new NextResponse("Internal Error", { status: 500 }) + } +} + diff --git a/src/app/api/user/chat-setup/route.ts b/src/app/api/user/chat-setup/route.ts index ee2ea02..dfe34ce 100644 --- a/src/app/api/user/chat-setup/route.ts +++ b/src/app/api/user/chat-setup/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from "next/server" import { auth } from "@/auth" import { prisma } from "@/lib/prisma" +import { encryptString } from "@/lib/encryption" export async function POST(req: Request) { try { @@ -9,15 +10,31 @@ export async function POST(req: Request) { return new NextResponse("Unauthorized", { status: 401 }) } - const { publicKey } = await req.json() + const { publicKey, privateKey } = await req.json() as { + publicKey?: string + privateKey?: string + } if (!publicKey) { return new NextResponse("Missing publicKey", { status: 400 }) } + let chatPrivateKeyEncrypted: string | undefined + if (privateKey) { + try { + chatPrivateKeyEncrypted = encryptString(privateKey) + } catch (err) { + console.error("[CHAT_SETUP] Failed to encrypt private key", err) + return new NextResponse("Failed to encrypt private key", { status: 500 }) + } + } + await prisma.user.update({ where: { id: session.user.id }, - data: { chatPublicKey: publicKey } + data: { + chatPublicKey: publicKey, + ...(chatPrivateKeyEncrypted ? { chatPrivateKeyEncrypted } : {}) + } }) return NextResponse.json({ success: true }) diff --git a/src/app/api/vendors/synced-products/[id]/route.ts b/src/app/api/vendors/synced-products/[id]/route.ts new file mode 100644 index 0000000..de19085 --- /dev/null +++ b/src/app/api/vendors/synced-products/[id]/route.ts @@ -0,0 +1,141 @@ +import { NextResponse } from "next/server" +import { auth } from "@/auth" +import { prisma } from "@/lib/prisma" + +/** + * GET /api/vendors/synced-products/[id] + * Get a single product visible on the vendor's New Products page. + * The ID may correspond to either: + * - A ProductSync (vendor-initiated sync), or + * - A SupplierProductOffer (supplier-provided product) + */ +export async function GET( + req: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const session = await auth() + + if (!session?.user?.id || session.user.role !== "VENDOR") { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }) + } + + const { id } = await params + + // Get vendor's active store + const vendorStore = await prisma.store.findFirst({ + where: { + userId: session.user.id, + isActive: true + } + }) + + if (!vendorStore) { + return NextResponse.json({ message: "Not found" }, { status: 404 }) + } + + // First, try to find a ProductSync + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sync = await (prisma as any).productSync.findFirst({ + where: { + id, + storeId: vendorStore.id + }, + include: { + supplier: { + select: { + id: true, + companyName: true + } + } + } + }) + + if (sync) { + const productData = sync.shopifyProductData || {} + const product = { + id: sync.id, + syncId: sync.id, + shopifyProductId: sync.shopifyProductId, + title: sync.shopifyProductTitle, + description: productData.body_html || productData.description || null, + price: productData.variants?.[0]?.price || "0", + sku: productData.variants?.[0]?.sku || null, + imageUrl: productData.image?.src || productData.images?.[0]?.src || null, + supplier: { + id: sync.supplier.id, + companyName: sync.supplier.companyName + }, + syncedAt: sync.acceptedAt || sync.createdAt, + isSynced: true, + source: "synced", + shopifyData: productData + } + + return NextResponse.json({ product }) + } + + // Otherwise, try SupplierProductOffer + if (!("supplierProductOffer" in prisma)) { + return NextResponse.json({ message: "Product not found" }, { status: 404 }) + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const offer = await (prisma as any).supplierProductOffer.findFirst({ + where: { + id, + storeId: vendorStore.id + }, + include: { + product: { + select: { + id: true, + title: true, + description: true, + price: true, + sku: true, + imageUrl: true + } + }, + supplier: { + select: { + id: true, + companyName: true + } + } + } + }) + + if (!offer) { + return NextResponse.json({ message: "Product not found" }, { status: 404 }) + } + + const product = { + id: offer.id, + syncId: null, + shopifyProductId: null, + title: offer.product.title, + description: offer.product.description, + price: offer.price.toFixed(2), + sku: offer.product.sku, + imageUrl: offer.product.imageUrl, + supplier: { + id: offer.supplier.id, + companyName: offer.supplier.companyName + }, + syncedAt: offer.createdAt, + isSynced: false, + source: "supplier_offer", + offerQuantity: offer.quantity, + offerId: offer.id + } + + return NextResponse.json({ product }) + } catch (error) { + console.error("Error fetching synced product:", error) + return NextResponse.json( + { message: "Failed to fetch product" }, + { status: 500 } + ) + } +} + diff --git a/src/app/api/vendors/synced-products/route.ts b/src/app/api/vendors/synced-products/route.ts index 4957faf..f9175b3 100644 --- a/src/app/api/vendors/synced-products/route.ts +++ b/src/app/api/vendors/synced-products/route.ts @@ -4,8 +4,10 @@ import { prisma } from "@/lib/prisma" /** * GET /api/vendors/synced-products - * Get all synced products for the vendor (read-only) - * These are products that vendors have synced with suppliers + * Get all products available to the vendor (read-only) + * Includes: + * - Products that vendors have synced with suppliers (ProductSync ACCEPTED) + * - Products that suppliers have provided directly to this vendor (SupplierProductOffer) */ export async function GET() { try { @@ -27,7 +29,7 @@ export async function GET() { return NextResponse.json({ products: [] }) } - // Get all ACCEPTED product syncs for this store + // Get all ACCEPTED product syncs for this store (vendor-initiated) // eslint-disable-next-line @typescript-eslint/no-explicit-any const acceptedSyncs = await (prisma as any).productSync.findMany({ where: { @@ -65,12 +67,70 @@ export async function GET() { }, syncedAt: sync.acceptedAt || sync.createdAt, isSynced: true, // Flag to indicate this is a synced product + source: "synced", // Include full Shopify product data for reference shopifyData: productData } }) - return NextResponse.json({ products: syncedProducts }) + // Get supplier-provided products for this store + if (!("supplierProductOffer" in prisma)) { + console.error("Prisma Client missing supplierProductOffer model. Run: npx prisma generate") + // Return only synced products if SupplierProductOffer model is missing + return NextResponse.json({ products: syncedProducts }) + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const supplierOffers = await (prisma as any).supplierProductOffer.findMany({ + where: { + storeId: vendorStore.id + }, + include: { + product: { + select: { + id: true, + title: true, + description: true, + price: true, + sku: true, + imageUrl: true + } + }, + supplier: { + select: { + id: true, + companyName: true + } + } + }, + orderBy: { + createdAt: "desc" + } + }) + + const supplierProducts = supplierOffers.map((offer: any) => ({ + id: offer.id, + syncId: null, + shopifyProductId: null, + title: offer.product.title, + description: offer.product.description, + // Store as string for consistency with existing response + price: offer.price.toFixed(2), + sku: offer.product.sku, + imageUrl: offer.product.imageUrl, + supplier: { + id: offer.supplier.id, + companyName: offer.supplier.companyName + }, + syncedAt: offer.createdAt, + isSynced: false, + source: "supplier_offer", + offerQuantity: offer.quantity, + offerId: offer.id + })) + + const allProducts = [...syncedProducts, ...supplierProducts] + + return NextResponse.json({ products: allProducts }) } catch (error) { console.error("Error fetching synced products:", error) return NextResponse.json( diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index e6123d8..eac7022 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -55,9 +55,7 @@ export default async function DashboardLayout({
Products synced with your suppliers
++ Products available from your connected suppliers +
${product.price}
++ ${parseFloat(product.price).toFixed(2)} +
{product.sku && (SKU: {product.sku}
)} @@ -136,17 +170,146 @@ export default function NewProductsPage() {+ {selectedProduct.source === "supplier_offer" + ? "Provided by supplier" + : "Synced product"} +
++ Price +
++ ${parseFloat(selectedProduct.price).toFixed(2)} +
+ {selectedProduct.source === "supplier_offer" && + selectedProduct.offerQuantity !== undefined && ( ++ Quantity available from supplier:{" "} + {selectedProduct.offerQuantity} +
+ )} ++ SKU +
++ {selectedProduct.sku} +
++ Supplier +
++ {selectedProduct.supplier.companyName} +
++ Description +
++ {selectedProduct.description.replace(/<[^>]*>/g, "")} +
++ Optional: you can also paste an external URL instead of uploading. +
+ setFormData({ ...formData, imageUrl: e.target.value })} + className="w-full px-4 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/30 focus:border-indigo-500/50" + placeholder="https://example.com/image.jpg" + /> ++ If you upload a file and also provide a URL, the uploaded image will be used. +
++ View store information and provide products to this vendor. +
++ {connection.store.shopifyStoreName} +
+ )} ++ Connected on{" "} + {new Date(connection.createdAt).toLocaleDateString()} +
+No products have been provided to this vendor yet.
+Click "Provide Product" to send your first product.
++ {offer.product.title} +
+ {offer.product.sku && ( ++ SKU: {offer.product.sku} +
+ )} ++ Provided on{" "} + {new Date(offer.createdAt).toLocaleDateString()} +
++ Price for this vendor +
++ ${offer.price.toFixed(2)} +
++ Quantity +
++ {offer.quantity} +
+