From 270eaaf671cd097e09e5fb19be24f1d54b3be810 Mon Sep 17 00:00:00 2001 From: Jo Humphrey <31373245+jamdelion@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:19:51 +0000 Subject: [PATCH 1/9] wip [skip-pizza] --- .../modules/file/service/deleteFile.ts | 5 +- api.planx.uk/modules/file/service/getFile.ts | 6 +- .../modules/file/service/uploadFile.ts | 10 +- api.planx.uk/modules/file/service/utils.ts | 13 +- api.planx.uk/package.json | 1 + api.planx.uk/pnpm-lock.yaml | 1131 +++++++++++++++++ 6 files changed, 1152 insertions(+), 14 deletions(-) diff --git a/api.planx.uk/modules/file/service/deleteFile.ts b/api.planx.uk/modules/file/service/deleteFile.ts index e8b8f7e0b4..e49cf5e8ae 100644 --- a/api.planx.uk/modules/file/service/deleteFile.ts +++ b/api.planx.uk/modules/file/service/deleteFile.ts @@ -5,15 +5,14 @@ export const deleteFilesByURL = async ( fileURLs: string[], ): Promise => { const keys = fileURLs.map(getS3KeyFromURL); - const result = await deleteFilesByKey(keys); - return result; + return await deleteFilesByKey(keys); }; export const deleteFilesByKey = async (keys: string[]): Promise => { const s3 = s3Factory(); const params = getDeleteFilesParams(keys); try { - await s3.deleteObjects(params).promise(); + s3.deleteObjects(params); return keys; } catch (error) { throw Error(`Failed to delete S3 files: ${error}`); diff --git a/api.planx.uk/modules/file/service/getFile.ts b/api.planx.uk/modules/file/service/getFile.ts index 400c5151ca..7d2720ee28 100644 --- a/api.planx.uk/modules/file/service/getFile.ts +++ b/api.planx.uk/modules/file/service/getFile.ts @@ -1,4 +1,4 @@ -import type S3 from "aws-sdk/clients/s3.js"; +import type { PutObjectCommandInput } from "@aws-sdk/client-s3"; import { s3Factory } from "./utils.js"; export const getFileFromS3 = async (fileId: string) => { @@ -6,9 +6,9 @@ export const getFileFromS3 = async (fileId: string) => { const params = { Key: fileId, - } as S3.PutObjectRequest; + } as PutObjectCommandInput; - const file = await s3.getObject(params).promise(); + const file = await s3.getObject(params); return { body: file.Body as Buffer, diff --git a/api.planx.uk/modules/file/service/uploadFile.ts b/api.planx.uk/modules/file/service/uploadFile.ts index 0ae471af8b..3d02dbfdef 100644 --- a/api.planx.uk/modules/file/service/uploadFile.ts +++ b/api.planx.uk/modules/file/service/uploadFile.ts @@ -1,4 +1,4 @@ -import type S3 from "aws-sdk/clients/s3.js"; +import type { PutObjectCommandInput } from "@aws-sdk/client-s3"; import { customAlphabet } from "nanoid"; import mime from "mime"; import { s3Factory } from "./utils.js"; @@ -14,7 +14,7 @@ export const uploadPublicFile = async ( const { params, key, fileType } = generateFileParams(file, filename, filekey); - await s3.putObject(params).promise(); + await s3.putObject(params); const fileUrl = buildFileUrl(key, "public"); return { @@ -36,7 +36,7 @@ export const uploadPrivateFile = async ( is_private: "true", }; - await s3.putObject(params).promise(); + await s3.putObject(params); const fileUrl = buildFileUrl(key, "private"); return { @@ -63,7 +63,7 @@ export function generateFileParams( filename: string, filekey?: string, ): { - params: S3.PutObjectRequest; + params: PutObjectCommandInput; fileType: string | null; key: string; } { @@ -76,7 +76,7 @@ export function generateFileParams( Body: file?.buffer || JSON.stringify(file), ContentDisposition: `inline;filename="${filename}"`, ContentType: file?.mimetype || "application/json", - } as S3.PutObjectRequest; + } as PutObjectCommandInput; return { fileType, diff --git a/api.planx.uk/modules/file/service/utils.ts b/api.planx.uk/modules/file/service/utils.ts index 8d7da9c2ea..c1846eee95 100644 --- a/api.planx.uk/modules/file/service/utils.ts +++ b/api.planx.uk/modules/file/service/utils.ts @@ -1,12 +1,19 @@ -import S3 from "aws-sdk/clients/s3.js"; +import { S3 } from "@aws-sdk/client-s3"; import { isLiveEnv } from "../../../helpers.js"; export function s3Factory() { return new S3({ + // The key params is no longer supported in v3, and can be removed. + // @deprecated The object needs to be passed to individual operations where it's intended. params: { Bucket: process.env.AWS_S3_BUCKET }, + region: process.env.AWS_S3_REGION, - accessKeyId: process.env.AWS_ACCESS_KEY, - secretAccessKey: process.env.AWS_SECRET_KEY, + + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY, + secretAccessKey: process.env.AWS_SECRET_KEY, + }, + ...useMinio(), }); } diff --git a/api.planx.uk/package.json b/api.planx.uk/package.json index 574db7b104..bbe7bcee19 100644 --- a/api.planx.uk/package.json +++ b/api.planx.uk/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@airbrake/node": "^2.1.8", + "@aws-sdk/client-s3": "^3.696.0", "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#ccf9ac3", "@types/isomorphic-fetch": "^0.0.36", "adm-zip": "^0.5.10", diff --git a/api.planx.uk/pnpm-lock.yaml b/api.planx.uk/pnpm-lock.yaml index c08a7e1cbf..5858c6f4c3 100644 --- a/api.planx.uk/pnpm-lock.yaml +++ b/api.planx.uk/pnpm-lock.yaml @@ -14,6 +14,9 @@ dependencies: '@airbrake/node': specifier: ^2.1.8 version: 2.1.8 + '@aws-sdk/client-s3': + specifier: ^3.696.0 + version: 3.696.0 '@opensystemslab/planx-core': specifier: git+https://github.com/theopensystemslab/planx-core#ccf9ac3 version: github.com/theopensystemslab/planx-core/ccf9ac3 @@ -348,6 +351,637 @@ packages: z-schema: 5.0.5 dev: false + /@aws-crypto/crc32@5.2.0: + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.696.0 + tslib: 2.8.1 + dev: false + + /@aws-crypto/crc32c@5.2.0: + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.696.0 + tslib: 2.8.1 + dev: false + + /@aws-crypto/sha1-browser@5.2.0: + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.696.0 + '@aws-sdk/util-locate-window': 3.693.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + dev: false + + /@aws-crypto/sha256-browser@5.2.0: + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.696.0 + '@aws-sdk/util-locate-window': 3.693.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + dev: false + + /@aws-crypto/sha256-js@5.2.0: + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.696.0 + tslib: 2.8.1 + dev: false + + /@aws-crypto/supports-web-crypto@5.2.0: + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + dependencies: + tslib: 2.8.1 + dev: false + + /@aws-crypto/util@5.2.0: + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + dependencies: + '@aws-sdk/types': 3.696.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + dev: false + + /@aws-sdk/client-s3@3.696.0: + resolution: {integrity: sha512-kJw6J8QqxKLd2yNACPt2Y9J0vzi7Q25DHOrBkz6SX3nPLp5Qtzxm4rjKbHs9gKhX7ijUoVkOpzZ/4ufz6/RJmA==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sso-oidc': 3.696.0(@aws-sdk/client-sts@3.696.0) + '@aws-sdk/client-sts': 3.696.0 + '@aws-sdk/core': 3.696.0 + '@aws-sdk/credential-provider-node': 3.696.0(@aws-sdk/client-sso-oidc@3.696.0)(@aws-sdk/client-sts@3.696.0) + '@aws-sdk/middleware-bucket-endpoint': 3.696.0 + '@aws-sdk/middleware-expect-continue': 3.696.0 + '@aws-sdk/middleware-flexible-checksums': 3.696.0 + '@aws-sdk/middleware-host-header': 3.696.0 + '@aws-sdk/middleware-location-constraint': 3.696.0 + '@aws-sdk/middleware-logger': 3.696.0 + '@aws-sdk/middleware-recursion-detection': 3.696.0 + '@aws-sdk/middleware-sdk-s3': 3.696.0 + '@aws-sdk/middleware-ssec': 3.696.0 + '@aws-sdk/middleware-user-agent': 3.696.0 + '@aws-sdk/region-config-resolver': 3.696.0 + '@aws-sdk/signature-v4-multi-region': 3.696.0 + '@aws-sdk/types': 3.696.0 + '@aws-sdk/util-endpoints': 3.696.0 + '@aws-sdk/util-user-agent-browser': 3.696.0 + '@aws-sdk/util-user-agent-node': 3.696.0 + '@aws-sdk/xml-builder': 3.696.0 + '@smithy/config-resolver': 3.0.12 + '@smithy/core': 2.5.3 + '@smithy/eventstream-serde-browser': 3.0.13 + '@smithy/eventstream-serde-config-resolver': 3.0.10 + '@smithy/eventstream-serde-node': 3.0.12 + '@smithy/fetch-http-handler': 4.1.1 + '@smithy/hash-blob-browser': 3.1.9 + '@smithy/hash-node': 3.0.10 + '@smithy/hash-stream-node': 3.1.9 + '@smithy/invalid-dependency': 3.0.10 + '@smithy/md5-js': 3.0.10 + '@smithy/middleware-content-length': 3.0.12 + '@smithy/middleware-endpoint': 3.2.3 + '@smithy/middleware-retry': 3.0.27 + '@smithy/middleware-serde': 3.0.10 + '@smithy/middleware-stack': 3.0.10 + '@smithy/node-config-provider': 3.1.11 + '@smithy/node-http-handler': 3.3.1 + '@smithy/protocol-http': 4.1.7 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + '@smithy/url-parser': 3.0.10 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.27 + '@smithy/util-defaults-mode-node': 3.0.27 + '@smithy/util-endpoints': 2.1.6 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-retry': 3.0.10 + '@smithy/util-stream': 3.3.1 + '@smithy/util-utf8': 3.0.0 + '@smithy/util-waiter': 3.1.9 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sso-oidc@3.696.0(@aws-sdk/client-sts@3.696.0): + resolution: {integrity: sha512-ikxQ3mo86d1mAq5zTaQAh8rLBERwL+I4MUYu/IVYW2hhl9J2SDsl0SgnKeXQG6S8zWuHcBO587zsZaRta1MQ/g==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-sts': ^3.696.0 + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sts': 3.696.0 + '@aws-sdk/core': 3.696.0 + '@aws-sdk/credential-provider-node': 3.696.0(@aws-sdk/client-sso-oidc@3.696.0)(@aws-sdk/client-sts@3.696.0) + '@aws-sdk/middleware-host-header': 3.696.0 + '@aws-sdk/middleware-logger': 3.696.0 + '@aws-sdk/middleware-recursion-detection': 3.696.0 + '@aws-sdk/middleware-user-agent': 3.696.0 + '@aws-sdk/region-config-resolver': 3.696.0 + '@aws-sdk/types': 3.696.0 + '@aws-sdk/util-endpoints': 3.696.0 + '@aws-sdk/util-user-agent-browser': 3.696.0 + '@aws-sdk/util-user-agent-node': 3.696.0 + '@smithy/config-resolver': 3.0.12 + '@smithy/core': 2.5.3 + '@smithy/fetch-http-handler': 4.1.1 + '@smithy/hash-node': 3.0.10 + '@smithy/invalid-dependency': 3.0.10 + '@smithy/middleware-content-length': 3.0.12 + '@smithy/middleware-endpoint': 3.2.3 + '@smithy/middleware-retry': 3.0.27 + '@smithy/middleware-serde': 3.0.10 + '@smithy/middleware-stack': 3.0.10 + '@smithy/node-config-provider': 3.1.11 + '@smithy/node-http-handler': 3.3.1 + '@smithy/protocol-http': 4.1.7 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + '@smithy/url-parser': 3.0.10 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.27 + '@smithy/util-defaults-mode-node': 3.0.27 + '@smithy/util-endpoints': 2.1.6 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-retry': 3.0.10 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sso@3.696.0: + resolution: {integrity: sha512-q5TTkd08JS0DOkHfUL853tuArf7NrPeqoS5UOvqJho8ibV9Ak/a/HO4kNvy9Nj3cib/toHYHsQIEtecUPSUUrQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.696.0 + '@aws-sdk/middleware-host-header': 3.696.0 + '@aws-sdk/middleware-logger': 3.696.0 + '@aws-sdk/middleware-recursion-detection': 3.696.0 + '@aws-sdk/middleware-user-agent': 3.696.0 + '@aws-sdk/region-config-resolver': 3.696.0 + '@aws-sdk/types': 3.696.0 + '@aws-sdk/util-endpoints': 3.696.0 + '@aws-sdk/util-user-agent-browser': 3.696.0 + '@aws-sdk/util-user-agent-node': 3.696.0 + '@smithy/config-resolver': 3.0.12 + '@smithy/core': 2.5.3 + '@smithy/fetch-http-handler': 4.1.1 + '@smithy/hash-node': 3.0.10 + '@smithy/invalid-dependency': 3.0.10 + '@smithy/middleware-content-length': 3.0.12 + '@smithy/middleware-endpoint': 3.2.3 + '@smithy/middleware-retry': 3.0.27 + '@smithy/middleware-serde': 3.0.10 + '@smithy/middleware-stack': 3.0.10 + '@smithy/node-config-provider': 3.1.11 + '@smithy/node-http-handler': 3.3.1 + '@smithy/protocol-http': 4.1.7 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + '@smithy/url-parser': 3.0.10 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.27 + '@smithy/util-defaults-mode-node': 3.0.27 + '@smithy/util-endpoints': 2.1.6 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-retry': 3.0.10 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sts@3.696.0: + resolution: {integrity: sha512-eJOxR8/UyI7kGSRyE751Ea7MKEzllQs7eNveDJy9OP4t/jsN/P19HJ1YHeA1np40JRTUBfqa6WLAAiIXsk8rkg==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sso-oidc': 3.696.0(@aws-sdk/client-sts@3.696.0) + '@aws-sdk/core': 3.696.0 + '@aws-sdk/credential-provider-node': 3.696.0(@aws-sdk/client-sso-oidc@3.696.0)(@aws-sdk/client-sts@3.696.0) + '@aws-sdk/middleware-host-header': 3.696.0 + '@aws-sdk/middleware-logger': 3.696.0 + '@aws-sdk/middleware-recursion-detection': 3.696.0 + '@aws-sdk/middleware-user-agent': 3.696.0 + '@aws-sdk/region-config-resolver': 3.696.0 + '@aws-sdk/types': 3.696.0 + '@aws-sdk/util-endpoints': 3.696.0 + '@aws-sdk/util-user-agent-browser': 3.696.0 + '@aws-sdk/util-user-agent-node': 3.696.0 + '@smithy/config-resolver': 3.0.12 + '@smithy/core': 2.5.3 + '@smithy/fetch-http-handler': 4.1.1 + '@smithy/hash-node': 3.0.10 + '@smithy/invalid-dependency': 3.0.10 + '@smithy/middleware-content-length': 3.0.12 + '@smithy/middleware-endpoint': 3.2.3 + '@smithy/middleware-retry': 3.0.27 + '@smithy/middleware-serde': 3.0.10 + '@smithy/middleware-stack': 3.0.10 + '@smithy/node-config-provider': 3.1.11 + '@smithy/node-http-handler': 3.3.1 + '@smithy/protocol-http': 4.1.7 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + '@smithy/url-parser': 3.0.10 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.27 + '@smithy/util-defaults-mode-node': 3.0.27 + '@smithy/util-endpoints': 2.1.6 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-retry': 3.0.10 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/core@3.696.0: + resolution: {integrity: sha512-3c9III1k03DgvRZWg8vhVmfIXPG6hAciN9MzQTzqGngzWAELZF/WONRTRQuDFixVtarQatmLHYVw/atGeA2Byw==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.696.0 + '@smithy/core': 2.5.3 + '@smithy/node-config-provider': 3.1.11 + '@smithy/property-provider': 3.1.10 + '@smithy/protocol-http': 4.1.7 + '@smithy/signature-v4': 4.2.3 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + '@smithy/util-middleware': 3.0.10 + fast-xml-parser: 4.4.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/credential-provider-env@3.696.0: + resolution: {integrity: sha512-T9iMFnJL7YTlESLpVFT3fg1Lkb1lD+oiaIC8KMpepb01gDUBIpj9+Y+pA/cgRWW0yRxmkDXNazAE2qQTVFGJzA==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/core': 3.696.0 + '@aws-sdk/types': 3.696.0 + '@smithy/property-provider': 3.1.10 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/credential-provider-http@3.696.0: + resolution: {integrity: sha512-GV6EbvPi2eq1+WgY/o2RFA3P7HGmnkIzCNmhwtALFlqMroLYWKE7PSeHw66Uh1dFQeVESn0/+hiUNhu1mB0emA==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/core': 3.696.0 + '@aws-sdk/types': 3.696.0 + '@smithy/fetch-http-handler': 4.1.1 + '@smithy/node-http-handler': 3.3.1 + '@smithy/property-provider': 3.1.10 + '@smithy/protocol-http': 4.1.7 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + '@smithy/util-stream': 3.3.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/credential-provider-ini@3.696.0(@aws-sdk/client-sso-oidc@3.696.0)(@aws-sdk/client-sts@3.696.0): + resolution: {integrity: sha512-9WsZZofjPjNAAZhIh7c7FOhLK8CR3RnGgUm1tdZzV6ZSM1BuS2m6rdwIilRxAh3fxxKDkmW/r/aYmmCYwA+AYA==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-sts': ^3.696.0 + dependencies: + '@aws-sdk/client-sts': 3.696.0 + '@aws-sdk/core': 3.696.0 + '@aws-sdk/credential-provider-env': 3.696.0 + '@aws-sdk/credential-provider-http': 3.696.0 + '@aws-sdk/credential-provider-process': 3.696.0 + '@aws-sdk/credential-provider-sso': 3.696.0(@aws-sdk/client-sso-oidc@3.696.0) + '@aws-sdk/credential-provider-web-identity': 3.696.0(@aws-sdk/client-sts@3.696.0) + '@aws-sdk/types': 3.696.0 + '@smithy/credential-provider-imds': 3.2.7 + '@smithy/property-provider': 3.1.10 + '@smithy/shared-ini-file-loader': 3.1.11 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' + - aws-crt + dev: false + + /@aws-sdk/credential-provider-node@3.696.0(@aws-sdk/client-sso-oidc@3.696.0)(@aws-sdk/client-sts@3.696.0): + resolution: {integrity: sha512-8F6y5FcfRuMJouC5s207Ko1mcVvOXReBOlJmhIwE4QH1CnO/CliIyepnAZrRQ659mo5wIuquz6gXnpYbitEVMg==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.696.0 + '@aws-sdk/credential-provider-http': 3.696.0 + '@aws-sdk/credential-provider-ini': 3.696.0(@aws-sdk/client-sso-oidc@3.696.0)(@aws-sdk/client-sts@3.696.0) + '@aws-sdk/credential-provider-process': 3.696.0 + '@aws-sdk/credential-provider-sso': 3.696.0(@aws-sdk/client-sso-oidc@3.696.0) + '@aws-sdk/credential-provider-web-identity': 3.696.0(@aws-sdk/client-sts@3.696.0) + '@aws-sdk/types': 3.696.0 + '@smithy/credential-provider-imds': 3.2.7 + '@smithy/property-provider': 3.1.10 + '@smithy/shared-ini-file-loader': 3.1.11 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' + - '@aws-sdk/client-sts' + - aws-crt + dev: false + + /@aws-sdk/credential-provider-process@3.696.0: + resolution: {integrity: sha512-mL1RcFDe9sfmyU5K1nuFkO8UiJXXxLX4JO1gVaDIOvPqwStpUAwi3A1BoeZhWZZNQsiKI810RnYGo0E0WB/hUA==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/core': 3.696.0 + '@aws-sdk/types': 3.696.0 + '@smithy/property-provider': 3.1.10 + '@smithy/shared-ini-file-loader': 3.1.11 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/credential-provider-sso@3.696.0(@aws-sdk/client-sso-oidc@3.696.0): + resolution: {integrity: sha512-4SSZ9Nk08JSu4/rX1a+dEac/Ims1HCXfV7YLUe5LGdtRLSKRoQQUy+hkFaGYoSugP/p1UfUPl3BuTO9Vv8z1pA==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/client-sso': 3.696.0 + '@aws-sdk/core': 3.696.0 + '@aws-sdk/token-providers': 3.696.0(@aws-sdk/client-sso-oidc@3.696.0) + '@aws-sdk/types': 3.696.0 + '@smithy/property-provider': 3.1.10 + '@smithy/shared-ini-file-loader': 3.1.11 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' + - aws-crt + dev: false + + /@aws-sdk/credential-provider-web-identity@3.696.0(@aws-sdk/client-sts@3.696.0): + resolution: {integrity: sha512-XJ/CVlWChM0VCoc259vWguFUjJDn/QwDqHwbx+K9cg3v6yrqXfK5ai+p/6lx0nQpnk4JzPVeYYxWRpaTsGC9rg==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-sts': ^3.696.0 + dependencies: + '@aws-sdk/client-sts': 3.696.0 + '@aws-sdk/core': 3.696.0 + '@aws-sdk/types': 3.696.0 + '@smithy/property-provider': 3.1.10 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/middleware-bucket-endpoint@3.696.0: + resolution: {integrity: sha512-V07jishKHUS5heRNGFpCWCSTjRJyQLynS/ncUeE8ZYtG66StOOQWftTwDfFOSoXlIqrXgb4oT9atryzXq7Z4LQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.696.0 + '@aws-sdk/util-arn-parser': 3.693.0 + '@smithy/node-config-provider': 3.1.11 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + '@smithy/util-config-provider': 3.0.0 + tslib: 2.8.1 + dev: false + + /@aws-sdk/middleware-expect-continue@3.696.0: + resolution: {integrity: sha512-vpVukqY3U2pb+ULeX0shs6L0aadNep6kKzjme/MyulPjtUDJpD3AekHsXRrCCGLmOqSKqRgQn5zhV9pQhHsb6Q==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.696.0 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/middleware-flexible-checksums@3.696.0: + resolution: {integrity: sha512-EF+iHcLluuRPashlrRa2Sjf2fjUU6h+FB9CpALv9hq2XgDziseWbs+fJcP3QUnJ1tSdW+3WXtMQFvyMyVYpQKw==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.696.0 + '@aws-sdk/types': 3.696.0 + '@smithy/is-array-buffer': 3.0.0 + '@smithy/node-config-provider': 3.1.11 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-stream': 3.3.1 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + dev: false + + /@aws-sdk/middleware-host-header@3.696.0: + resolution: {integrity: sha512-zELJp9Ta2zkX7ELggMN9qMCgekqZhFC5V2rOr4hJDEb/Tte7gpfKSObAnw/3AYiVqt36sjHKfdkoTsuwGdEoDg==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.696.0 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/middleware-location-constraint@3.696.0: + resolution: {integrity: sha512-FgH12OB0q+DtTrP2aiDBddDKwL4BPOrm7w3VV9BJrSdkqQCNBPz8S1lb0y5eVH4tBG+2j7gKPlOv1wde4jF/iw==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.696.0 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/middleware-logger@3.696.0: + resolution: {integrity: sha512-KhkHt+8AjCxcR/5Zp3++YPJPpFQzxpr+jmONiT/Jw2yqnSngZ0Yspm5wGoRx2hS1HJbyZNuaOWEGuJoxLeBKfA==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.696.0 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/middleware-recursion-detection@3.696.0: + resolution: {integrity: sha512-si/maV3Z0hH7qa99f9ru2xpS5HlfSVcasRlNUXKSDm611i7jFMWwGNLUOXFAOLhXotPX5G3Z6BLwL34oDeBMug==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.696.0 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/middleware-sdk-s3@3.696.0: + resolution: {integrity: sha512-M7fEiAiN7DBMHflzOFzh1I2MNSlLpbiH2ubs87bdRc2wZsDPSbs4l3v6h3WLhxoQK0bq6vcfroudrLBgvCuX3Q==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/core': 3.696.0 + '@aws-sdk/types': 3.696.0 + '@aws-sdk/util-arn-parser': 3.693.0 + '@smithy/core': 2.5.3 + '@smithy/node-config-provider': 3.1.11 + '@smithy/protocol-http': 4.1.7 + '@smithy/signature-v4': 4.2.3 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + '@smithy/util-config-provider': 3.0.0 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-stream': 3.3.1 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + dev: false + + /@aws-sdk/middleware-ssec@3.696.0: + resolution: {integrity: sha512-w/d6O7AOZ7Pg3w2d3BxnX5RmGNWb5X4RNxF19rJqcgu/xqxxE/QwZTNd5a7eTsqLXAUIfbbR8hh0czVfC1pJLA==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.696.0 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/middleware-user-agent@3.696.0: + resolution: {integrity: sha512-Lvyj8CTyxrHI6GHd2YVZKIRI5Fmnugt3cpJo0VrKKEgK5zMySwEZ1n4dqPK6czYRWKd5+WnYHYAuU+Wdk6Jsjw==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/core': 3.696.0 + '@aws-sdk/types': 3.696.0 + '@aws-sdk/util-endpoints': 3.696.0 + '@smithy/core': 2.5.3 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/region-config-resolver@3.696.0: + resolution: {integrity: sha512-7EuH142lBXjI8yH6dVS/CZeiK/WZsmb/8zP6bQbVYpMrppSTgB3MzZZdxVZGzL5r8zPQOU10wLC4kIMy0qdBVQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.696.0 + '@smithy/node-config-provider': 3.1.11 + '@smithy/types': 3.7.1 + '@smithy/util-config-provider': 3.0.0 + '@smithy/util-middleware': 3.0.10 + tslib: 2.8.1 + dev: false + + /@aws-sdk/signature-v4-multi-region@3.696.0: + resolution: {integrity: sha512-ijPkoLjXuPtgxAYlDoYls8UaG/VKigROn9ebbvPL/orEY5umedd3iZTcS9T+uAf4Ur3GELLxMQiERZpfDKaz3g==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.696.0 + '@aws-sdk/types': 3.696.0 + '@smithy/protocol-http': 4.1.7 + '@smithy/signature-v4': 4.2.3 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/token-providers@3.696.0(@aws-sdk/client-sso-oidc@3.696.0): + resolution: {integrity: sha512-fvTcMADrkwRdNwVmJXi2pSPf1iizmUqczrR1KusH4XehI/KybS4U6ViskRT0v07vpxwL7x+iaD/8fR0PUu5L/g==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-sso-oidc': ^3.696.0 + dependencies: + '@aws-sdk/client-sso-oidc': 3.696.0(@aws-sdk/client-sts@3.696.0) + '@aws-sdk/types': 3.696.0 + '@smithy/property-provider': 3.1.10 + '@smithy/shared-ini-file-loader': 3.1.11 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/types@3.696.0: + resolution: {integrity: sha512-9rTvUJIAj5d3//U5FDPWGJ1nFJLuWb30vugGOrWk7aNZ6y9tuA3PI7Cc9dP8WEXKVyK1vuuk8rSFP2iqXnlgrw==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/util-arn-parser@3.693.0: + resolution: {integrity: sha512-WC8x6ca+NRrtpAH64rWu+ryDZI3HuLwlEr8EU6/dbC/pt+r/zC0PBoC15VEygUaBA+isppCikQpGyEDu0Yj7gQ==} + engines: {node: '>=16.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + + /@aws-sdk/util-endpoints@3.696.0: + resolution: {integrity: sha512-T5s0IlBVX+gkb9g/I6CLt4yAZVzMSiGnbUqWihWsHvQR1WOoIcndQy/Oz/IJXT9T2ipoy7a80gzV6a5mglrioA==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.696.0 + '@smithy/types': 3.7.1 + '@smithy/util-endpoints': 2.1.6 + tslib: 2.8.1 + dev: false + + /@aws-sdk/util-locate-window@3.693.0: + resolution: {integrity: sha512-ttrag6haJLWABhLqtg1Uf+4LgHWIMOVSYL+VYZmAp2v4PUGOwWmWQH0Zk8RM7YuQcLfH/EoR72/Yxz6A4FKcuw==} + engines: {node: '>=16.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + + /@aws-sdk/util-user-agent-browser@3.696.0: + resolution: {integrity: sha512-Z5rVNDdmPOe6ELoM5AhF/ja5tSjbe6ctSctDPb0JdDf4dT0v2MfwhJKzXju2RzX8Es/77Glh7MlaXLE0kCB9+Q==} + dependencies: + '@aws-sdk/types': 3.696.0 + '@smithy/types': 3.7.1 + bowser: 2.11.0 + tslib: 2.8.1 + dev: false + + /@aws-sdk/util-user-agent-node@3.696.0: + resolution: {integrity: sha512-KhKqcfyXIB0SCCt+qsu4eJjsfiOrNzK5dCV7RAW2YIpp+msxGUUX0NdRE9rkzjiv+3EMktgJm3eEIS+yxtlVdQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + dependencies: + '@aws-sdk/middleware-user-agent': 3.696.0 + '@aws-sdk/types': 3.696.0 + '@smithy/node-config-provider': 3.1.11 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/xml-builder@3.696.0: + resolution: {integrity: sha512-dn1mX+EeqivoLYnY7p2qLrir0waPnCgS/0YdRCAVU2x14FgfUYCH6Im3w3oi2dMwhxfKY5lYVB5NKvZu7uI9lQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + /@babel/code-frame@7.26.2: resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -1545,6 +2179,487 @@ packages: requiresBuild: true dev: false + /@smithy/abort-controller@3.1.8: + resolution: {integrity: sha512-+3DOBcUn5/rVjlxGvUPKc416SExarAQ+Qe0bqk30YSUjbepwpS7QN0cyKUSifvLJhdMZ0WPzPP5ymut0oonrpQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/chunked-blob-reader-native@3.0.1: + resolution: {integrity: sha512-VEYtPvh5rs/xlyqpm5NRnfYLZn+q0SRPELbvBV+C/G7IQ+ouTuo+NKKa3ShG5OaFR8NYVMXls9hPYLTvIKKDrQ==} + dependencies: + '@smithy/util-base64': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/chunked-blob-reader@4.0.0: + resolution: {integrity: sha512-jSqRnZvkT4egkq/7b6/QRCNXmmYVcHwnJldqJ3IhVpQE2atObVJ137xmGeuGFhjFUr8gCEVAOKwSY79OvpbDaQ==} + dependencies: + tslib: 2.8.1 + dev: false + + /@smithy/config-resolver@3.0.12: + resolution: {integrity: sha512-YAJP9UJFZRZ8N+UruTeq78zkdjUHmzsY62J4qKWZ4SXB4QXJ/+680EfXXgkYA2xj77ooMqtUY9m406zGNqwivQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/node-config-provider': 3.1.11 + '@smithy/types': 3.7.1 + '@smithy/util-config-provider': 3.0.0 + '@smithy/util-middleware': 3.0.10 + tslib: 2.8.1 + dev: false + + /@smithy/core@2.5.3: + resolution: {integrity: sha512-96uW8maifUSmehaeW7uydWn7wBc98NEeNI3zN8vqakGpyCQgzyJaA64Z4FCOUmAdCJkhppd/7SZ798Fo4Xx37g==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/middleware-serde': 3.0.10 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-stream': 3.3.1 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/credential-provider-imds@3.2.7: + resolution: {integrity: sha512-cEfbau+rrWF8ylkmmVAObOmjbTIzKyUC5TkBL58SbLywD0RCBC4JAUKbmtSm2w5KUJNRPGgpGFMvE2FKnuNlWQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/node-config-provider': 3.1.11 + '@smithy/property-provider': 3.1.10 + '@smithy/types': 3.7.1 + '@smithy/url-parser': 3.0.10 + tslib: 2.8.1 + dev: false + + /@smithy/eventstream-codec@3.1.9: + resolution: {integrity: sha512-F574nX0hhlNOjBnP+noLtsPFqXnWh2L0+nZKCwcu7P7J8k+k+rdIDs+RMnrMwrzhUE4mwMgyN0cYnEn0G8yrnQ==} + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 3.7.1 + '@smithy/util-hex-encoding': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/eventstream-serde-browser@3.0.13: + resolution: {integrity: sha512-Nee9m+97o9Qj6/XeLz2g2vANS2SZgAxV4rDBMKGHvFJHU/xz88x2RwCkwsvEwYjSX4BV1NG1JXmxEaDUzZTAtw==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/eventstream-serde-universal': 3.0.12 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/eventstream-serde-config-resolver@3.0.10: + resolution: {integrity: sha512-K1M0x7P7qbBUKB0UWIL5KOcyi6zqV5mPJoL0/o01HPJr0CSq3A9FYuJC6e11EX6hR8QTIR++DBiGrYveOu6trw==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/eventstream-serde-node@3.0.12: + resolution: {integrity: sha512-kiZymxXvZ4tnuYsPSMUHe+MMfc4FTeFWJIc0Q5wygJoUQM4rVHNghvd48y7ppuulNMbuYt95ah71pYc2+o4JOA==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/eventstream-serde-universal': 3.0.12 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/eventstream-serde-universal@3.0.12: + resolution: {integrity: sha512-1i8ifhLJrOZ+pEifTlF0EfZzMLUGQggYQ6WmZ4d5g77zEKf7oZ0kvh1yKWHPjofvOwqrkwRDVuxuYC8wVd662A==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/eventstream-codec': 3.1.9 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/fetch-http-handler@4.1.1: + resolution: {integrity: sha512-bH7QW0+JdX0bPBadXt8GwMof/jz0H28I84hU1Uet9ISpzUqXqRQ3fEZJ+ANPOhzSEczYvANNl3uDQDYArSFDtA==} + dependencies: + '@smithy/protocol-http': 4.1.7 + '@smithy/querystring-builder': 3.0.10 + '@smithy/types': 3.7.1 + '@smithy/util-base64': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/hash-blob-browser@3.1.9: + resolution: {integrity: sha512-wOu78omaUuW5DE+PVWXiRKWRZLecARyP3xcq5SmkXUw9+utgN8HnSnBfrjL2B/4ZxgqPjaAJQkC/+JHf1ITVaQ==} + dependencies: + '@smithy/chunked-blob-reader': 4.0.0 + '@smithy/chunked-blob-reader-native': 3.0.1 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/hash-node@3.0.10: + resolution: {integrity: sha512-3zWGWCHI+FlJ5WJwx73Mw2llYR8aflVyZN5JhoqLxbdPZi6UyKSdCeXAWJw9ja22m6S6Tzz1KZ+kAaSwvydi0g==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + '@smithy/util-buffer-from': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/hash-stream-node@3.1.9: + resolution: {integrity: sha512-3XfHBjSP3oDWxLmlxnt+F+FqXpL3WlXs+XXaB6bV9Wo8BBu87fK1dSEsyH7Z4ZHRmwZ4g9lFMdf08m9hoX1iRA==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/invalid-dependency@3.0.10: + resolution: {integrity: sha512-Lp2L65vFi+cj0vFMu2obpPW69DU+6O5g3086lmI4XcnRCG8PxvpWC7XyaVwJCxsZFzueHjXnrOH/E0pl0zikfA==} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/is-array-buffer@2.2.0: + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + + /@smithy/is-array-buffer@3.0.0: + resolution: {integrity: sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==} + engines: {node: '>=16.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + + /@smithy/md5-js@3.0.10: + resolution: {integrity: sha512-m3bv6dApflt3fS2Y1PyWPUtRP7iuBlvikEOGwu0HsCZ0vE7zcIX+dBoh3e+31/rddagw8nj92j0kJg2TfV+SJA==} + dependencies: + '@smithy/types': 3.7.1 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/middleware-content-length@3.0.12: + resolution: {integrity: sha512-1mDEXqzM20yywaMDuf5o9ue8OkJ373lSPbaSjyEvkWdqELhFMyNNgKGWL/rCSf4KME8B+HlHKuR8u9kRj8HzEQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/middleware-endpoint@3.2.3: + resolution: {integrity: sha512-Hdl9296i/EMptaX7agrSzJZDiz5Y8XPUeBbctTmMtnCguGpqfU3jVsTUan0VLaOhsnquqWLL8Bl5HrlbVGT1og==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/core': 2.5.3 + '@smithy/middleware-serde': 3.0.10 + '@smithy/node-config-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.11 + '@smithy/types': 3.7.1 + '@smithy/url-parser': 3.0.10 + '@smithy/util-middleware': 3.0.10 + tslib: 2.8.1 + dev: false + + /@smithy/middleware-retry@3.0.27: + resolution: {integrity: sha512-H3J/PjJpLL7Tt+fxDKiOD25sMc94YetlQhCnYeNmina2LZscAdu0ZEZPas/kwePHABaEtqp7hqa5S4UJgMs1Tg==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/node-config-provider': 3.1.11 + '@smithy/protocol-http': 4.1.7 + '@smithy/service-error-classification': 3.0.10 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-retry': 3.0.10 + tslib: 2.8.1 + uuid: 9.0.1 + dev: false + + /@smithy/middleware-serde@3.0.10: + resolution: {integrity: sha512-MnAuhh+dD14F428ubSJuRnmRsfOpxSzvRhaGVTvd/lrUDE3kxzCCmH8lnVTvoNQnV2BbJ4c15QwZ3UdQBtFNZA==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/middleware-stack@3.0.10: + resolution: {integrity: sha512-grCHyoiARDBBGPyw2BeicpjgpsDFWZZxptbVKb3CRd/ZA15F/T6rZjCCuBUjJwdck1nwUuIxYtsS4H9DDpbP5w==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/node-config-provider@3.1.11: + resolution: {integrity: sha512-URq3gT3RpDikh/8MBJUB+QGZzfS7Bm6TQTqoh4CqE8NBuyPkWa5eUXj0XFcFfeZVgg3WMh1u19iaXn8FvvXxZw==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/property-provider': 3.1.10 + '@smithy/shared-ini-file-loader': 3.1.11 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/node-http-handler@3.3.1: + resolution: {integrity: sha512-fr+UAOMGWh6bn4YSEezBCpJn9Ukp9oR4D32sCjCo7U81evE11YePOQ58ogzyfgmjIO79YeOdfXXqr0jyhPQeMg==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/abort-controller': 3.1.8 + '@smithy/protocol-http': 4.1.7 + '@smithy/querystring-builder': 3.0.10 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/property-provider@3.1.10: + resolution: {integrity: sha512-n1MJZGTorTH2DvyTVj+3wXnd4CzjJxyXeOgnTlgNVFxaaMeT4OteEp4QrzF8p9ee2yg42nvyVK6R/awLCakjeQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/protocol-http@4.1.7: + resolution: {integrity: sha512-FP2LepWD0eJeOTm0SjssPcgqAlDFzOmRXqXmGhfIM52G7Lrox/pcpQf6RP4F21k0+O12zaqQt5fCDOeBtqY6Cg==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/querystring-builder@3.0.10: + resolution: {integrity: sha512-nT9CQF3EIJtIUepXQuBFb8dxJi3WVZS3XfuDksxSCSn+/CzZowRLdhDn+2acbBv8R6eaJqPupoI/aRFIImNVPQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + '@smithy/util-uri-escape': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/querystring-parser@3.0.10: + resolution: {integrity: sha512-Oa0XDcpo9SmjhiDD9ua2UyM3uU01ZTuIrNdZvzwUTykW1PM8o2yJvMh1Do1rY5sUQg4NDV70dMi0JhDx4GyxuQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/service-error-classification@3.0.10: + resolution: {integrity: sha512-zHe642KCqDxXLuhs6xmHVgRwy078RfqxP2wRDpIyiF8EmsWXptMwnMwbVa50lw+WOGNrYm9zbaEg0oDe3PTtvQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + dev: false + + /@smithy/shared-ini-file-loader@3.1.11: + resolution: {integrity: sha512-AUdrIZHFtUgmfSN4Gq9nHu3IkHMa1YDcN+s061Nfm+6pQ0mJy85YQDB0tZBCmls0Vuj22pLwDPmL92+Hvfwwlg==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/signature-v4@4.2.3: + resolution: {integrity: sha512-pPSQQ2v2vu9vc8iew7sszLd0O09I5TRc5zhY71KA+Ao0xYazIG+uLeHbTJfIWGO3BGVLiXjUr3EEeCcEQLjpWQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/is-array-buffer': 3.0.0 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + '@smithy/util-hex-encoding': 3.0.0 + '@smithy/util-middleware': 3.0.10 + '@smithy/util-uri-escape': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/smithy-client@3.4.4: + resolution: {integrity: sha512-dPGoJuSZqvirBq+yROapBcHHvFjChoAQT8YPWJ820aPHHiowBlB3RL1Q4kPT1hx0qKgJuf+HhyzKi5Gbof4fNA==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/core': 2.5.3 + '@smithy/middleware-endpoint': 3.2.3 + '@smithy/middleware-stack': 3.0.10 + '@smithy/protocol-http': 4.1.7 + '@smithy/types': 3.7.1 + '@smithy/util-stream': 3.3.1 + tslib: 2.8.1 + dev: false + + /@smithy/types@3.7.1: + resolution: {integrity: sha512-XKLcLXZY7sUQgvvWyeaL/qwNPp6V3dWcUjqrQKjSb+tzYiCy340R/c64LV5j+Tnb2GhmunEX0eou+L+m2hJNYA==} + engines: {node: '>=16.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + + /@smithy/url-parser@3.0.10: + resolution: {integrity: sha512-j90NUalTSBR2NaZTuruEgavSdh8MLirf58LoGSk4AtQfyIymogIhgnGUU2Mga2bkMkpSoC9gxb74xBXL5afKAQ==} + dependencies: + '@smithy/querystring-parser': 3.0.10 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/util-base64@3.0.0: + resolution: {integrity: sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/util-buffer-from': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/util-body-length-browser@3.0.0: + resolution: {integrity: sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==} + dependencies: + tslib: 2.8.1 + dev: false + + /@smithy/util-body-length-node@3.0.0: + resolution: {integrity: sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==} + engines: {node: '>=16.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + + /@smithy/util-buffer-from@2.2.0: + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + dev: false + + /@smithy/util-buffer-from@3.0.0: + resolution: {integrity: sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/is-array-buffer': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/util-config-provider@3.0.0: + resolution: {integrity: sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==} + engines: {node: '>=16.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + + /@smithy/util-defaults-mode-browser@3.0.27: + resolution: {integrity: sha512-GV8NvPy1vAGp7u5iD/xNKUxCorE4nQzlyl057qRac+KwpH5zq8wVq6rE3lPPeuFLyQXofPN6JwxL1N9ojGapiQ==} + engines: {node: '>= 10.0.0'} + dependencies: + '@smithy/property-provider': 3.1.10 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + bowser: 2.11.0 + tslib: 2.8.1 + dev: false + + /@smithy/util-defaults-mode-node@3.0.27: + resolution: {integrity: sha512-7+4wjWfZqZxZVJvDutO+i1GvL6bgOajEkop4FuR6wudFlqBiqwxw3HoH6M9NgeCd37km8ga8NPp2JacQEtAMPg==} + engines: {node: '>= 10.0.0'} + dependencies: + '@smithy/config-resolver': 3.0.12 + '@smithy/credential-provider-imds': 3.2.7 + '@smithy/node-config-provider': 3.1.11 + '@smithy/property-provider': 3.1.10 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/util-endpoints@2.1.6: + resolution: {integrity: sha512-mFV1t3ndBh0yZOJgWxO9J/4cHZVn5UG1D8DeCc6/echfNkeEJWu9LD7mgGH5fHrEdR7LDoWw7PQO6QiGpHXhgA==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/node-config-provider': 3.1.11 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/util-hex-encoding@3.0.0: + resolution: {integrity: sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==} + engines: {node: '>=16.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + + /@smithy/util-middleware@3.0.10: + resolution: {integrity: sha512-eJO+/+RsrG2RpmY68jZdwQtnfsxjmPxzMlQpnHKjFPwrYqvlcT+fHdT+ZVwcjlWSrByOhGr9Ff2GG17efc192A==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/util-retry@3.0.10: + resolution: {integrity: sha512-1l4qatFp4PiU6j7UsbasUHL2VU023NRB/gfaa1M0rDqVrRN4g3mCArLRyH3OuktApA4ye+yjWQHjdziunw2eWA==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/service-error-classification': 3.0.10 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + + /@smithy/util-stream@3.3.1: + resolution: {integrity: sha512-Ff68R5lJh2zj+AUTvbAU/4yx+6QPRzg7+pI7M1FbtQHcRIp7xvguxVsQBKyB3fwiOwhAKu0lnNyYBaQfSW6TNw==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/fetch-http-handler': 4.1.1 + '@smithy/node-http-handler': 3.3.1 + '@smithy/types': 3.7.1 + '@smithy/util-base64': 3.0.0 + '@smithy/util-buffer-from': 3.0.0 + '@smithy/util-hex-encoding': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/util-uri-escape@3.0.0: + resolution: {integrity: sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==} + engines: {node: '>=16.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + + /@smithy/util-utf8@2.3.0: + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + dev: false + + /@smithy/util-utf8@3.0.0: + resolution: {integrity: sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/util-buffer-from': 3.0.0 + tslib: 2.8.1 + dev: false + + /@smithy/util-waiter@3.1.9: + resolution: {integrity: sha512-/aMXPANhMOlMPjfPtSrDfPeVP8l56SJlz93xeiLmhLe5xvlXA5T3abZ2ilEsDEPeY9T/wnN/vNGn9wa1SbufWA==} + engines: {node: '>=16.0.0'} + dependencies: + '@smithy/abort-controller': 3.1.8 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + /@types/adm-zip@0.5.0: resolution: {integrity: sha512-FCJBJq9ODsQZUNURo5ILAQueuA8WJhRvuihS3ke2iI25mJlfV2LK8jG2Qj2z2AWg8U0FtWWqBHVRetceLskSaw==} dependencies: @@ -2313,6 +3428,10 @@ packages: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} dev: false + /bowser@2.11.0: + resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} + dev: false + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -3356,6 +4475,13 @@ packages: punycode: 1.4.1 dev: false + /fast-xml-parser@4.4.1: + resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==} + hasBin: true + dependencies: + strnum: 1.0.5 + dev: false + /fast-xml-parser@4.5.0: resolution: {integrity: sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==} hasBin: true @@ -5948,6 +7074,11 @@ packages: hasBin: true dev: false + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + dev: false + /validator@13.12.0: resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} engines: {node: '>= 0.10'} From 3fa7a5a2623536bd2cb0934ceec794b13ed29373 Mon Sep 17 00:00:00 2001 From: Jo Humphrey <31373245+jamdelion@users.noreply.github.com> Date: Wed, 27 Nov 2024 12:21:59 +0000 Subject: [PATCH 2/9] Try more stuff --- .../modules/file/service/deleteFile.ts | 3 ++- api.planx.uk/modules/file/service/getFile.ts | 4 +-- .../modules/file/service/uploadFile.ts | 23 +++++++++------- api.planx.uk/package.json | 1 + api.planx.uk/pnpm-lock.yaml | 27 +++++++++++++++++++ 5 files changed, 46 insertions(+), 12 deletions(-) diff --git a/api.planx.uk/modules/file/service/deleteFile.ts b/api.planx.uk/modules/file/service/deleteFile.ts index e49cf5e8ae..8ea143fbca 100644 --- a/api.planx.uk/modules/file/service/deleteFile.ts +++ b/api.planx.uk/modules/file/service/deleteFile.ts @@ -1,3 +1,4 @@ +import type { DeleteObjectsCommandInput } from "@aws-sdk/client-s3"; import type { DeleteObjectsRequest } from "aws-sdk/clients/s3.js"; import { getS3KeyFromURL, s3Factory } from "./utils.js"; @@ -10,7 +11,7 @@ export const deleteFilesByURL = async ( export const deleteFilesByKey = async (keys: string[]): Promise => { const s3 = s3Factory(); - const params = getDeleteFilesParams(keys); + const params = getDeleteFilesParams(keys) as DeleteObjectsCommandInput; try { s3.deleteObjects(params); return keys; diff --git a/api.planx.uk/modules/file/service/getFile.ts b/api.planx.uk/modules/file/service/getFile.ts index 7d2720ee28..dae32f703f 100644 --- a/api.planx.uk/modules/file/service/getFile.ts +++ b/api.planx.uk/modules/file/service/getFile.ts @@ -11,7 +11,7 @@ export const getFileFromS3 = async (fileId: string) => { const file = await s3.getObject(params); return { - body: file.Body as Buffer, + body: file.Body, isPrivate: file.Metadata?.is_private === "true", headers: { "Content-Type": file.ContentType, @@ -19,7 +19,7 @@ export const getFileFromS3 = async (fileId: string) => { "Content-Disposition": file.ContentDisposition, "Content-Encoding": file.ContentEncoding, "Cache-Control": file.CacheControl, - Expires: file.Expires, + Expires: file.ExpiresString, "Last-Modified": file.LastModified, ETag: file.ETag, "cross-origin-resource-policy": "cross-site", diff --git a/api.planx.uk/modules/file/service/uploadFile.ts b/api.planx.uk/modules/file/service/uploadFile.ts index 3d02dbfdef..caa8a004be 100644 --- a/api.planx.uk/modules/file/service/uploadFile.ts +++ b/api.planx.uk/modules/file/service/uploadFile.ts @@ -1,8 +1,12 @@ -import type { PutObjectCommandInput } from "@aws-sdk/client-s3"; -import { customAlphabet } from "nanoid"; +import { + GetObjectCommand, + type PutObjectCommandInput, +} from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import mime from "mime"; -import { s3Factory } from "./utils.js"; +import { customAlphabet } from "nanoid"; import { isLiveEnv } from "../../../helpers.js"; +import { s3Factory } from "./utils.js"; const nanoid = customAlphabet("1234567890abcdefghijklmnopqrstuvwxyz", 8); export const uploadPublicFile = async ( @@ -46,16 +50,17 @@ export const uploadPrivateFile = async ( }; // Construct an API URL for the uploaded file -const buildFileUrl = (key: string, path: "public" | "private") => { +const buildFileUrl = async (key: string, path: "public" | "private") => { const s3 = s3Factory(); - const s3Url = new URL(s3.getSignedUrl("getObject", { Key: key })); - let s3Pathname = s3Url.pathname; + const s3Url = await getSignedUrl( + s3, + new GetObjectCommand({ Key: key, Bucket: process.env.AWS_S3_BUCKET }), + ); + let s3Pathname = s3Url; // Minio returns a pathname with bucket name prepended, remove this if (!isLiveEnv()) s3Pathname = s3Pathname.replace(`/${process.env.AWS_S3_BUCKET}`, ""); - // URL.pathname has a leading "/" - const fileUrl = `${process.env.API_URL_EXT}/file/${path}${s3Pathname}`; - return fileUrl; + return `${process.env.API_URL_EXT}/file/${path}${s3Pathname}`; }; export function generateFileParams( diff --git a/api.planx.uk/package.json b/api.planx.uk/package.json index bbe7bcee19..734d908cdd 100644 --- a/api.planx.uk/package.json +++ b/api.planx.uk/package.json @@ -12,6 +12,7 @@ "dependencies": { "@airbrake/node": "^2.1.8", "@aws-sdk/client-s3": "^3.696.0", + "@aws-sdk/s3-request-presigner": "^3.701.0", "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#ccf9ac3", "@types/isomorphic-fetch": "^0.0.36", "adm-zip": "^0.5.10", diff --git a/api.planx.uk/pnpm-lock.yaml b/api.planx.uk/pnpm-lock.yaml index 5858c6f4c3..15059a9fbe 100644 --- a/api.planx.uk/pnpm-lock.yaml +++ b/api.planx.uk/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: '@aws-sdk/client-s3': specifier: ^3.696.0 version: 3.696.0 + '@aws-sdk/s3-request-presigner': + specifier: ^3.701.0 + version: 3.701.0 '@opensystemslab/planx-core': specifier: git+https://github.com/theopensystemslab/planx-core#ccf9ac3 version: github.com/theopensystemslab/planx-core/ccf9ac3 @@ -891,6 +894,20 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/s3-request-presigner@3.701.0: + resolution: {integrity: sha512-S4eKSZxhDcVmUoHv9N4dCxGde7V4v60R/+qFz/LgHxU++XOZ2npM/jqX5I9vT4uOkHLwQD6DgkL0j37vZpsqxA==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/signature-v4-multi-region': 3.696.0 + '@aws-sdk/types': 3.696.0 + '@aws-sdk/util-format-url': 3.696.0 + '@smithy/middleware-endpoint': 3.2.3 + '@smithy/protocol-http': 4.1.7 + '@smithy/smithy-client': 3.4.4 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + /@aws-sdk/signature-v4-multi-region@3.696.0: resolution: {integrity: sha512-ijPkoLjXuPtgxAYlDoYls8UaG/VKigROn9ebbvPL/orEY5umedd3iZTcS9T+uAf4Ur3GELLxMQiERZpfDKaz3g==} engines: {node: '>=16.0.0'} @@ -942,6 +959,16 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/util-format-url@3.696.0: + resolution: {integrity: sha512-R6yK1LozUD1GdAZRPhNsIow6VNFJUTyyoIar1OCWaknlucBMcq7musF3DN3TlORBwfFMj5buHc2ET9OtMtzvuA==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-sdk/types': 3.696.0 + '@smithy/querystring-builder': 3.0.10 + '@smithy/types': 3.7.1 + tslib: 2.8.1 + dev: false + /@aws-sdk/util-locate-window@3.693.0: resolution: {integrity: sha512-ttrag6haJLWABhLqtg1Uf+4LgHWIMOVSYL+VYZmAp2v4PUGOwWmWQH0Zk8RM7YuQcLfH/EoR72/Yxz6A4FKcuw==} engines: {node: '>=16.0.0'} From b51fd63fe1bf1681be2088892aed509d90ed2a03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Thu, 28 Nov 2024 16:50:49 +0000 Subject: [PATCH 3/9] chore: First pass at updating types --- .env.example | 1 - api.planx.uk/modules/file/service/deleteFile.ts | 10 +++++----- api.planx.uk/modules/file/service/getFile.ts | 12 +++++++++--- api.planx.uk/modules/file/service/uploadFile.ts | 11 ++++++----- api.planx.uk/modules/file/service/utils.ts | 10 +++------- api.planx.uk/modules/send/utils/exportZip.ts | 2 +- docker-compose.yml | 1 - infrastructure/application/index.ts | 1 - 8 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.env.example b/.env.example index e226500261..fbdd5ecd11 100644 --- a/.env.example +++ b/.env.example @@ -22,7 +22,6 @@ MICROSOFT_CLIENT_SECRET=👻 # AWS credentials for uploading user files from local and pull request environments to a staging S3 bucket AWS_S3_REGION=eu-west-2 -AWS_S3_ACL=public-read AWS_S3_BUCKET=👻 AWS_ACCESS_KEY=👻 AWS_SECRET_KEY=👻 diff --git a/api.planx.uk/modules/file/service/deleteFile.ts b/api.planx.uk/modules/file/service/deleteFile.ts index 8ea143fbca..87557b31ea 100644 --- a/api.planx.uk/modules/file/service/deleteFile.ts +++ b/api.planx.uk/modules/file/service/deleteFile.ts @@ -1,26 +1,26 @@ import type { DeleteObjectsCommandInput } from "@aws-sdk/client-s3"; -import type { DeleteObjectsRequest } from "aws-sdk/clients/s3.js"; import { getS3KeyFromURL, s3Factory } from "./utils.js"; export const deleteFilesByURL = async ( fileURLs: string[], ): Promise => { const keys = fileURLs.map(getS3KeyFromURL); - return await deleteFilesByKey(keys); + const result = await deleteFilesByKey(keys); + return result; }; export const deleteFilesByKey = async (keys: string[]): Promise => { const s3 = s3Factory(); - const params = getDeleteFilesParams(keys) as DeleteObjectsCommandInput; + const params = getDeleteFilesParams(keys); try { - s3.deleteObjects(params); + await s3.deleteObjects(params); return keys; } catch (error) { throw Error(`Failed to delete S3 files: ${error}`); } }; -const getDeleteFilesParams = (keys: string[]): DeleteObjectsRequest => ({ +const getDeleteFilesParams = (keys: string[]): DeleteObjectsCommandInput => ({ Bucket: process.env.AWS_S3_BUCKET!, Delete: { Objects: keys.map((key) => ({ Key: key })), diff --git a/api.planx.uk/modules/file/service/getFile.ts b/api.planx.uk/modules/file/service/getFile.ts index dae32f703f..db40874ba1 100644 --- a/api.planx.uk/modules/file/service/getFile.ts +++ b/api.planx.uk/modules/file/service/getFile.ts @@ -4,14 +4,20 @@ import { s3Factory } from "./utils.js"; export const getFileFromS3 = async (fileId: string) => { const s3 = s3Factory(); - const params = { + const params: PutObjectCommandInput = { Key: fileId, - } as PutObjectCommandInput; + Bucket: process.env.AWS_S3_BUCKET, + }; const file = await s3.getObject(params); + // TODO: test this + if (!file.Body) throw Error(`Missing body from S3 file ${fileId}`); + + const body = Buffer.from(await file.Body.transformToByteArray()); + return { - body: file.Body, + body, isPrivate: file.Metadata?.is_private === "true", headers: { "Content-Type": file.ContentType, diff --git a/api.planx.uk/modules/file/service/uploadFile.ts b/api.planx.uk/modules/file/service/uploadFile.ts index caa8a004be..5d4974b0a0 100644 --- a/api.planx.uk/modules/file/service/uploadFile.ts +++ b/api.planx.uk/modules/file/service/uploadFile.ts @@ -19,7 +19,7 @@ export const uploadPublicFile = async ( const { params, key, fileType } = generateFileParams(file, filename, filekey); await s3.putObject(params); - const fileUrl = buildFileUrl(key, "public"); + const fileUrl = await buildFileUrl(key, "public"); return { fileType, @@ -41,7 +41,7 @@ export const uploadPrivateFile = async ( }; await s3.putObject(params); - const fileUrl = buildFileUrl(key, "private"); + const fileUrl = await buildFileUrl(key, "private"); return { fileType, @@ -75,13 +75,14 @@ export function generateFileParams( const fileType = mime.getType(filename); const key = `${filekey || nanoid()}/${filename}`; - const params = { - ACL: process.env.AWS_S3_ACL, + const params: PutObjectCommandInput = { + ACL: "public-read", + Bucket: process.env.AWS_S3_BUCKET, Key: key, Body: file?.buffer || JSON.stringify(file), ContentDisposition: `inline;filename="${filename}"`, ContentType: file?.mimetype || "application/json", - } as PutObjectCommandInput; + }; return { fileType, diff --git a/api.planx.uk/modules/file/service/utils.ts b/api.planx.uk/modules/file/service/utils.ts index c1846eee95..0f95e170e5 100644 --- a/api.planx.uk/modules/file/service/utils.ts +++ b/api.planx.uk/modules/file/service/utils.ts @@ -3,15 +3,11 @@ import { isLiveEnv } from "../../../helpers.js"; export function s3Factory() { return new S3({ - // The key params is no longer supported in v3, and can be removed. - // @deprecated The object needs to be passed to individual operations where it's intended. - params: { Bucket: process.env.AWS_S3_BUCKET }, - - region: process.env.AWS_S3_REGION, + region: process.env.AWS_S3_REGION!, credentials: { - accessKeyId: process.env.AWS_ACCESS_KEY, - secretAccessKey: process.env.AWS_SECRET_KEY, + accessKeyId: process.env.AWS_ACCESS_KEY!, + secretAccessKey: process.env.AWS_SECRET_KEY!, }, ...useMinio(), diff --git a/api.planx.uk/modules/send/utils/exportZip.ts b/api.planx.uk/modules/send/utils/exportZip.ts index e67a32ce7c..da068d0aef 100644 --- a/api.planx.uk/modules/send/utils/exportZip.ts +++ b/api.planx.uk/modules/send/utils/exportZip.ts @@ -245,7 +245,7 @@ export class ExportZip { const { body } = await getFileFromS3(decodedS3Key); if (!body) throw new Error("file not found"); - this.zip.addFile(name, body as Buffer); + this.zip.addFile(name, body); } toBuffer(): Buffer { diff --git a/docker-compose.yml b/docker-compose.yml index 8569cc75dd..7243ae7c86 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -117,7 +117,6 @@ services: API_URL_EXT: ${API_URL_EXT} APP_ENVIRONMENT: ${APP_ENVIRONMENT} AWS_ACCESS_KEY: ${AWS_ACCESS_KEY} - AWS_S3_ACL: ${AWS_S3_ACL} AWS_S3_BUCKET: ${AWS_S3_BUCKET} AWS_S3_REGION: ${AWS_S3_REGION} AWS_SECRET_KEY: ${AWS_SECRET_KEY} diff --git a/infrastructure/application/index.ts b/infrastructure/application/index.ts index 5e599d415d..764e3913ca 100644 --- a/infrastructure/application/index.ts +++ b/infrastructure/application/index.ts @@ -339,7 +339,6 @@ export = async () => { name: "AWS_S3_BUCKET", value: pulumi.interpolate`${apiBucket.bucket}`, }, - { name: "AWS_S3_ACL", value: "public-read" }, { name: "FILE_API_KEY", value: config.requireSecret("file-api-key"), From f6637fe85e25d34301696cc1ef75d55dd99f4588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Thu, 28 Nov 2024 17:17:24 +0000 Subject: [PATCH 4/9] test: Update utils tests --- .../modules/file/service/utils.test.ts | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/api.planx.uk/modules/file/service/utils.test.ts b/api.planx.uk/modules/file/service/utils.test.ts index 95f300868b..7bc37559b3 100644 --- a/api.planx.uk/modules/file/service/utils.test.ts +++ b/api.planx.uk/modules/file/service/utils.test.ts @@ -1,27 +1,27 @@ import { getS3KeyFromURL, s3Factory } from "./utils.js"; describe("s3 Factory", () => { - const OLD_ENV = process.env; + afterEach(() => vi.unstubAllEnvs()); - beforeEach(() => { - process.env = { ...OLD_ENV }; - }); + it("returns Minio config for local development", async () => { + const s3 = s3Factory(); - afterAll(() => { - process.env = OLD_ENV; - }); + // Minio should be set up as a custom endpoint + if (!s3.config.endpoint) expect.fail(); + + const endpoint = await s3.config.endpoint(); - it("returns Minio config for local development", () => { - expect(s3Factory()).toHaveProperty("endpoint.host", "minio:1234"); + expect(endpoint.hostname).toBe("minio"); }); ["pizza", "staging", "production"].forEach((env) => { - it(`does not use Minio config on ${env} environment`, () => { - process.env.NODE_ENV = env; - expect(s3Factory()).toHaveProperty( - "endpoint.host", - "s3.eu-west-2.amazonaws.com", - ); + it(`does not use Minio config on ${env} environment`, async () => { + vi.stubEnv("NODE_ENV", env); + + const s3 = s3Factory(); + + // No custom endpoint used + expect(s3.config.endpoint).toBeUndefined(); }); }); }); From 76f98ff5afae0827fcdc365e1e5c69bf7f8c2e8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Thu, 28 Nov 2024 18:16:27 +0000 Subject: [PATCH 5/9] test: File tests and s3 mocks --- api.planx.uk/modules/file/file.test.ts | 76 +++++++++---------- .../modules/file/service/uploadFile.ts | 2 +- 2 files changed, 35 insertions(+), 43 deletions(-) diff --git a/api.planx.uk/modules/file/file.test.ts b/api.planx.uk/modules/file/file.test.ts index 165248b2c0..819b9c310b 100644 --- a/api.planx.uk/modules/file/file.test.ts +++ b/api.planx.uk/modules/file/file.test.ts @@ -5,41 +5,45 @@ import app from "../../server.js"; import { deleteFilesByURL } from "./service/deleteFile.js"; import { authHeader } from "../../tests/mockJWT.js"; +import type * as s3Client from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; + let mockPutObject: Mocked<() => void>; let mockGetObject: Mocked<() => void>; let mockDeleteObjects: Mocked<() => void>; let getObjectResponse = {}; -const mockGetSignedUrl = vi.fn(() => { - const randomFolderName = "nanoid"; - const modifiedKey = "modified%20key"; - return ` - https://test-bucket.s3.eu-west-2.amazonaws.com/${randomFolderName}/${modifiedKey}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-SignedHeaders=host - `; -}); +vi.mock("@aws-sdk/s3-request-presigner", () => ({ + getSignedUrl: vi.fn(() => { + const randomFolderName = "nanoid"; + const modifiedKey = "modified%20key"; + return `https://test-bucket.s3.eu-west-2.amazonaws.com/${randomFolderName}/${modifiedKey}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-SignedHeaders=host`; + }), +})); const s3Mock = () => { return { putObject: mockPutObject, getObject: mockGetObject, - getSignedUrl: mockGetSignedUrl, deleteObjects: mockDeleteObjects, }; }; -vi.mock("aws-sdk/clients/s3", () => ({ - default: vi.fn().mockImplementation(() => { - return s3Mock(); - }), -})); +vi.mock("@aws-sdk/client-s3", async (importOriginal) => { + const actualS3Client = await importOriginal(); + return { + ...actualS3Client, + S3: vi.fn().mockImplementation(() => { + return s3Mock(); + }), + }; +}); describe("File upload", () => { beforeEach(() => { vi.clearAllMocks(); - mockPutObject = vi.fn(() => ({ - promise: () => Promise.resolve(), - })); + mockPutObject = vi.fn(() => Promise.resolve()); }); describe("Private", () => { @@ -83,13 +87,11 @@ describe("File upload", () => { }); }); expect(mockPutObject).toHaveBeenCalledTimes(1); - expect(mockGetSignedUrl).toHaveBeenCalledTimes(1); + expect(getSignedUrl).toHaveBeenCalledTimes(1); }); it("should handle S3 error", async () => { - mockPutObject = vi.fn(() => ({ - promise: () => Promise.reject(new Error("S3 error!")), - })); + mockPutObject = vi.fn(() => Promise.reject(new Error("S3 error!"))); await supertest(app) .post("/file/private/upload") @@ -160,13 +162,11 @@ describe("File upload", () => { }); }); expect(mockPutObject).toHaveBeenCalledTimes(1); - expect(mockGetSignedUrl).toHaveBeenCalledTimes(1); + expect(getSignedUrl).toHaveBeenCalledTimes(1); }); it("should handle S3 error", async () => { - mockPutObject = vi.fn(() => ({ - promise: () => Promise.reject(new Error("S3 error!")), - })); + mockPutObject = vi.fn(() => Promise.reject(new Error("S3 error!"))); await supertest(app) .post(ENDPOINT) @@ -185,7 +185,7 @@ describe("File upload", () => { describe("File download", () => { beforeEach(() => { getObjectResponse = { - Body: Buffer.from("some data"), + Body: { transformToByteArray: () => new ArrayBuffer(24) }, ContentLength: "633", ContentDisposition: "inline;filename='some_file.txt'", ContentEncoding: "undefined", @@ -197,9 +197,7 @@ describe("File download", () => { }; vi.clearAllMocks(); - mockGetObject = vi.fn(() => ({ - promise: () => Promise.resolve(getObjectResponse), - })); + mockGetObject = vi.fn(() => Promise.resolve(getObjectResponse)); }); describe("Public", () => { @@ -235,9 +233,7 @@ describe("File download", () => { }); it("should handle S3 error", async () => { - mockGetObject = vi.fn(() => ({ - promise: () => Promise.reject(new Error("S3 error!")), - })); + mockGetObject = vi.fn(() => Promise.reject(new Error("S3 error!"))); await supertest(app) .get("/file/public/someKey/someFile.txt") @@ -313,9 +309,7 @@ describe("File download", () => { }); it("should handle S3 error", async () => { - mockGetObject = vi.fn(() => ({ - promise: () => Promise.reject(new Error("S3 error!")), - })); + mockGetObject = vi.fn(() => Promise.reject(new Error("S3 error!"))); await supertest(app) .get("/file/private/someKey/someFile.txt") @@ -337,9 +331,8 @@ describe("File delete", () => { }); it("deletes files by URL", async () => { - mockDeleteObjects = vi.fn(() => ({ - promise: () => Promise.resolve(), - })); + mockDeleteObjects = vi.fn(() => Promise.resolve()); + const fileURLs = [ "https://api.planx.dev/file/private/abc/123", "https://api.planx.dev/file/private/def/456", @@ -361,11 +354,10 @@ describe("File delete", () => { }); it("throw an error if S3 fails to delete the file", async () => { - mockDeleteObjects = vi.fn(() => ({ - promise: () => { - throw Error(); - }, - })); + mockDeleteObjects = vi.fn(() => { + throw Error(); + }); + const fileURLs = [ "https://api.planx.dev/file/private/abc/123", "https://api.planx.dev/file/private/def/456", diff --git a/api.planx.uk/modules/file/service/uploadFile.ts b/api.planx.uk/modules/file/service/uploadFile.ts index 5d4974b0a0..fb046d21f9 100644 --- a/api.planx.uk/modules/file/service/uploadFile.ts +++ b/api.planx.uk/modules/file/service/uploadFile.ts @@ -56,7 +56,7 @@ const buildFileUrl = async (key: string, path: "public" | "private") => { s3, new GetObjectCommand({ Key: key, Bucket: process.env.AWS_S3_BUCKET }), ); - let s3Pathname = s3Url; + let s3Pathname = new URL(s3Url).pathname; // Minio returns a pathname with bucket name prepended, remove this if (!isLiveEnv()) s3Pathname = s3Pathname.replace(`/${process.env.AWS_S3_BUCKET}`, ""); From e9875eeda3a4f1407ab3c7adeee5fc9a3184e26a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Thu, 28 Nov 2024 18:36:52 +0000 Subject: [PATCH 6/9] test: Configure mocks in operations --- .../operations.test.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.test.ts b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.test.ts index 13ad138a43..0aca2e88eb 100644 --- a/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.test.ts +++ b/api.planx.uk/modules/webhooks/service/sanitiseApplicationData/operations.test.ts @@ -29,6 +29,8 @@ import { } from "./operations.js"; import type { MockedFunction } from "vitest"; +import type * as s3Client from "@aws-sdk/client-s3"; + vi.mock("../../../../lib/hasura/schema"); const mockRunSQL = runSQL as MockedFunction; @@ -52,17 +54,19 @@ vi.mock("@opensystemslab/planx-core", async (importOriginal) => { const s3Mock = () => { return { - deleteObjects: vi.fn(() => ({ - promise: () => Promise.resolve(), - })), + deleteObjects: vi.fn(() => Promise.resolve()), }; }; -vi.mock("aws-sdk/clients/s3", () => ({ - default: vi.fn().mockImplementation(() => { - return s3Mock(); - }), -})); +vi.mock("@aws-sdk/client-s3", async (importOriginal) => { + const actualS3Client = await importOriginal(); + return { + ...actualS3Client, + S3: vi.fn().mockImplementation(() => { + return s3Mock(); + }), + }; +}); describe("'operationHandler' helper function", () => { it("returns a success result when an operation succeeds", async () => { From 541aa4a12e58c3a4729845ba83934ac0a928b02c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Thu, 28 Nov 2024 19:30:30 +0000 Subject: [PATCH 7/9] chore: Drop aws-sdk v2 --- api.planx.uk/package.json | 1 - api.planx.uk/pnpm-lock.yaml | 144 +----------------------------------- 2 files changed, 2 insertions(+), 143 deletions(-) diff --git a/api.planx.uk/package.json b/api.planx.uk/package.json index 734d908cdd..3f17c1f479 100644 --- a/api.planx.uk/package.json +++ b/api.planx.uk/package.json @@ -16,7 +16,6 @@ "@opensystemslab/planx-core": "git+https://github.com/theopensystemslab/planx-core#ccf9ac3", "@types/isomorphic-fetch": "^0.0.36", "adm-zip": "^0.5.10", - "aws-sdk": "^2.1467.0", "axios": "^1.7.4", "body-parser": "^1.20.3", "cookie-parser": "^1.4.7", diff --git a/api.planx.uk/pnpm-lock.yaml b/api.planx.uk/pnpm-lock.yaml index 15059a9fbe..e1beae8474 100644 --- a/api.planx.uk/pnpm-lock.yaml +++ b/api.planx.uk/pnpm-lock.yaml @@ -29,9 +29,6 @@ dependencies: adm-zip: specifier: ^0.5.10 version: 0.5.10 - aws-sdk: - specifier: ^2.1467.0 - version: 2.1467.0 axios: specifier: ^1.7.4 version: 1.7.4 @@ -3373,29 +3370,6 @@ packages: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} - /available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - dependencies: - possible-typed-array-names: 1.0.0 - dev: false - - /aws-sdk@2.1467.0: - resolution: {integrity: sha512-XJNbV2ORQB6XanyBLPJBjvm6axWlblFdyFHa+ZaUK4Kp3cZBggy53+0PMxS3bAGJKcP6h2pZQio9xZMCoebAKQ==} - engines: {node: '>= 10.0.0'} - 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.5 - uuid: 8.0.0 - xml2js: 0.5.0 - dev: false - /axios@1.7.4: resolution: {integrity: sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==} dependencies: @@ -3418,10 +3392,6 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - /base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: false - /base64url@3.0.1: resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} engines: {node: '>=6.0.0'} @@ -3496,14 +3466,6 @@ packages: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: false - /buffer@4.9.2: - resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} - dependencies: - base64-js: 1.5.1 - ieee754: 1.1.13 - isarray: 1.0.0 - dev: false - /busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -4358,11 +4320,6 @@ packages: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} dev: true - /events@1.1.1: - resolution: {integrity: sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==} - engines: {node: '>=0.4.x'} - dev: false - /execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} @@ -4594,12 +4551,6 @@ packages: optional: true dev: false - /for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} - dependencies: - is-callable: 1.2.7 - dev: false - /foreground-child@3.3.0: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} @@ -4856,6 +4807,7 @@ packages: engines: {node: '>= 0.4'} dependencies: has-symbols: 1.0.3 + dev: true /hash.js@1.1.7: resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} @@ -4997,10 +4949,6 @@ packages: dependencies: safer-buffer: 2.1.2 - /ieee754@1.1.13: - resolution: {integrity: sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==} - dev: false - /ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -5041,16 +4989,12 @@ packages: dependencies: call-bind: 1.0.7 has-tostringtag: 1.0.2 + dev: true /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} dev: false - /is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - dev: false - /is-core-module@2.15.1: resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} engines: {node: '>= 0.4'} @@ -5078,13 +5022,6 @@ packages: engines: {node: '>=12'} dev: true - /is-generator-function@1.0.10: - resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} - engines: {node: '>= 0.4'} - dependencies: - has-tostringtag: 1.0.2 - dev: false - /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -5120,13 +5057,6 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: true - /is-typed-array@1.1.13: - resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} - engines: {node: '>= 0.4'} - dependencies: - which-typed-array: 1.1.15 - dev: false - /isarray@0.0.1: resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} dev: false @@ -5218,11 +5148,6 @@ packages: resolution: {integrity: sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==} dev: true - /jmespath@0.16.0: - resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} - engines: {node: '>= 0.6.0'} - dev: false - /jose@4.15.9: resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} dev: false @@ -6115,11 +6040,6 @@ packages: thread-stream: 0.15.2 dev: false - /possible-typed-array-names@1.0.0: - resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} - engines: {node: '>= 0.4'} - dev: false - /postcss@8.4.49: resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} engines: {node: ^10 || ^12 || >=14} @@ -6201,10 +6121,6 @@ packages: once: 1.4.0 dev: true - /punycode@1.3.2: - resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} - dev: false - /punycode@1.4.1: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} dev: false @@ -6227,12 +6143,6 @@ packages: side-channel: 1.0.6 dev: true - /querystring@0.2.0: - resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} - engines: {node: '>=0.4.x'} - deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. - dev: false - /querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} @@ -6467,10 +6377,6 @@ packages: /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - /sax@1.2.1: - resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==} - dev: false - /sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} dev: false @@ -7060,27 +6966,10 @@ packages: querystringify: 2.2.0 requires-port: 1.0.0 - /url@0.10.3: - resolution: {integrity: sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==} - dependencies: - punycode: 1.3.2 - querystring: 0.2.0 - dev: false - /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: false - /util@0.12.5: - resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} - dependencies: - inherits: 2.0.4 - is-arguments: 1.1.1 - is-generator-function: 1.0.10 - is-typed-array: 1.1.13 - which-typed-array: 1.1.15 - dev: false - /utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -7096,11 +6985,6 @@ packages: hasBin: true dev: false - /uuid@8.0.0: - resolution: {integrity: sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==} - hasBin: true - dev: false - /uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true @@ -7277,17 +7161,6 @@ packages: webidl-conversions: 3.0.1 dev: false - /which-typed-array@1.1.15: - resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} - engines: {node: '>= 0.4'} - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.7 - for-each: 0.3.3 - gopd: 1.0.1 - has-tostringtag: 1.0.2 - dev: false - /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -7351,23 +7224,10 @@ packages: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} - /xml2js@0.5.0: - resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} - engines: {node: '>=4.0.0'} - dependencies: - sax: 1.2.1 - xmlbuilder: 11.0.1 - dev: false - /xml@1.0.1: resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} dev: false - /xmlbuilder@11.0.1: - resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} - engines: {node: '>=4.0'} - dev: false - /xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} From 888091ef063e7a7c95b456c08298239ec4475722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Thu, 28 Nov 2024 21:43:55 +0000 Subject: [PATCH 8/9] test: Bring file module coverage back up to 100\% --- api.planx.uk/modules/file/file.test.ts | 48 +++++++++++++++++-- api.planx.uk/modules/file/service/getFile.ts | 1 - .../modules/file/service/uploadFile.ts | 4 +- api.planx.uk/vitest.config.ts | 8 ++-- 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/api.planx.uk/modules/file/file.test.ts b/api.planx.uk/modules/file/file.test.ts index 819b9c310b..bf883dd49e 100644 --- a/api.planx.uk/modules/file/file.test.ts +++ b/api.planx.uk/modules/file/file.test.ts @@ -74,6 +74,9 @@ describe("File upload", () => { }); it("should upload file", async () => { + vi.stubEnv("API_URL_EXT", "https://api.editor.planx.dev"); + vi.stubEnv("AWS_S3_BUCKET", "myBucketName"); + await supertest(app) .post(ENDPOINT) .field("filename", "some_file.txt") @@ -81,9 +84,9 @@ describe("File upload", () => { .then((res) => { expect(res.body).toEqual({ fileType: "text/plain", - fileUrl: expect.stringContaining( - "/file/private/nanoid/modified%20key", - ), + // Bucket name stripped from URL + fileUrl: + "https://api.editor.planx.dev/file/private/nanoid/modified%20key", }); }); expect(mockPutObject).toHaveBeenCalledTimes(1); @@ -103,6 +106,26 @@ describe("File upload", () => { }); expect(mockPutObject).toHaveBeenCalledTimes(1); }); + + it("should generate a correct URL on production", async () => { + vi.stubEnv("API_URL_EXT", "https://api.editor.planx.dev"); + vi.stubEnv("NODE_ENV", "production"); + + await supertest(app) + .post(ENDPOINT) + .field("filename", "some_file.txt") + .attach("file", Buffer.from("some data"), "some_file.txt") + .then((res) => { + expect(res.body).toEqual({ + fileType: "text/plain", + fileUrl: expect.stringContaining( + "/file/private/nanoid/modified%20key", + ), + }); + }); + expect(mockPutObject).toHaveBeenCalledTimes(1); + expect(getSignedUrl).toHaveBeenCalledTimes(1); + }); }); describe("Public", () => { @@ -237,14 +260,29 @@ describe("File download", () => { await supertest(app) .get("/file/public/someKey/someFile.txt") - .field("filename", "some_file.txt") - .attach("file", Buffer.from("some data"), "some_file.txt") .expect(500) .then((res) => { expect(res.body.error).toMatch(/S3 error!/); }); expect(mockGetObject).toHaveBeenCalledTimes(1); }); + + it("should handle an empty file body", async () => { + mockGetObject = vi.fn(() => + Promise.resolve({ + ...getObjectResponse, + Body: undefined, + }), + ); + + await supertest(app) + .get("/file/public/someKey/someFile.txt") + .expect(500) + .then((res) => { + expect(res.body.error).toMatch(/Missing body from S3 file/); + }); + expect(mockGetObject).toHaveBeenCalledTimes(1); + }); }); describe("Private", () => { diff --git a/api.planx.uk/modules/file/service/getFile.ts b/api.planx.uk/modules/file/service/getFile.ts index db40874ba1..0ce7cd8d75 100644 --- a/api.planx.uk/modules/file/service/getFile.ts +++ b/api.planx.uk/modules/file/service/getFile.ts @@ -11,7 +11,6 @@ export const getFileFromS3 = async (fileId: string) => { const file = await s3.getObject(params); - // TODO: test this if (!file.Body) throw Error(`Missing body from S3 file ${fileId}`); const body = Buffer.from(await file.Body.transformToByteArray()); diff --git a/api.planx.uk/modules/file/service/uploadFile.ts b/api.planx.uk/modules/file/service/uploadFile.ts index fb046d21f9..ead3cee9af 100644 --- a/api.planx.uk/modules/file/service/uploadFile.ts +++ b/api.planx.uk/modules/file/service/uploadFile.ts @@ -79,9 +79,9 @@ export function generateFileParams( ACL: "public-read", Bucket: process.env.AWS_S3_BUCKET, Key: key, - Body: file?.buffer || JSON.stringify(file), + Body: file.buffer, ContentDisposition: `inline;filename="${filename}"`, - ContentType: file?.mimetype || "application/json", + ContentType: file.mimetype, }; return { diff --git a/api.planx.uk/vitest.config.ts b/api.planx.uk/vitest.config.ts index a01ff940c0..77fcbeff21 100644 --- a/api.planx.uk/vitest.config.ts +++ b/api.planx.uk/vitest.config.ts @@ -13,10 +13,10 @@ export default defineConfig({ // html reporter required to inspect coverage in Vitest UI dashboard reporter: ["lcov", "html", "text-summary"], thresholds: { - statements: 72.03, - branches: 54.92, - functions: 67.62, - lines: 71.84, + statements: 73.28, + branches: 55.27, + functions: 73.59, + lines: 73.42, autoUpdate: true, }, }, From 8f1ff37873dfcc3e8007c1d9478c3bb0fdc956cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Mon, 2 Dec 2024 11:13:25 +0000 Subject: [PATCH 9/9] test: Test full prod url is generated --- api.planx.uk/modules/file/file.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/api.planx.uk/modules/file/file.test.ts b/api.planx.uk/modules/file/file.test.ts index bf883dd49e..321b400d96 100644 --- a/api.planx.uk/modules/file/file.test.ts +++ b/api.planx.uk/modules/file/file.test.ts @@ -108,7 +108,7 @@ describe("File upload", () => { }); it("should generate a correct URL on production", async () => { - vi.stubEnv("API_URL_EXT", "https://api.editor.planx.dev"); + vi.stubEnv("API_URL_EXT", "https://api.editor.planx.uk"); vi.stubEnv("NODE_ENV", "production"); await supertest(app) @@ -118,9 +118,8 @@ describe("File upload", () => { .then((res) => { expect(res.body).toEqual({ fileType: "text/plain", - fileUrl: expect.stringContaining( - "/file/private/nanoid/modified%20key", - ), + fileUrl: + "https://api.editor.planx.uk/file/private/nanoid/modified%20key", }); }); expect(mockPutObject).toHaveBeenCalledTimes(1);