From e16c43ca9f0e8ea025c75ceeb85276f3cb95c741 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 30 Jun 2025 16:36:58 +0100 Subject: [PATCH 01/11] create repo if doesn't exist --- apps/webapp/app/env.server.ts | 2 + .../app/v3/getDeploymentImageRef.server.ts | 198 +++++ .../services/initializeDeployment.server.ts | 34 +- apps/webapp/package.json | 1 + .../webapp/test/getDeploymentImageRef.test.ts | 103 +++ pnpm-lock.yaml | 782 +++++++++++++++++- 6 files changed, 1111 insertions(+), 9 deletions(-) create mode 100644 apps/webapp/app/v3/getDeploymentImageRef.server.ts create mode 100644 apps/webapp/test/getDeploymentImageRef.test.ts diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 6f742faa269..7a09877461e 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -232,6 +232,8 @@ const EnvironmentSchema = z.object({ DEPLOY_REGISTRY_USERNAME: z.string().optional(), DEPLOY_REGISTRY_PASSWORD: z.string().optional(), DEPLOY_REGISTRY_NAMESPACE: z.string().min(1).default("trigger"), + DEPLOY_REGISTRY_ID: z.string().optional(), + DEPLOY_REGISTRY_TAGS: z.string().optional(), // csv, for example: "key1=value1,key2=value2" DEPLOY_IMAGE_PLATFORM: z.string().default("linux/amd64"), DEPLOY_TIMEOUT_MS: z.coerce .number() diff --git a/apps/webapp/app/v3/getDeploymentImageRef.server.ts b/apps/webapp/app/v3/getDeploymentImageRef.server.ts new file mode 100644 index 00000000000..f6a11c43a08 --- /dev/null +++ b/apps/webapp/app/v3/getDeploymentImageRef.server.ts @@ -0,0 +1,198 @@ +import { + ECRClient, + CreateRepositoryCommand, + DescribeRepositoriesCommand, + type Repository, + type Tag, + RepositoryNotFoundException, +} from "@aws-sdk/client-ecr"; +import { tryCatch } from "@trigger.dev/core"; +import { logger } from "~/services/logger.server"; + +export async function getDeploymentImageRef({ + host, + namespace, + projectRef, + nextVersion, + environmentSlug, + registryId, + registryTags, +}: { + host: string; + namespace: string; + projectRef: string; + nextVersion: string; + environmentSlug: string; + registryId?: string; + registryTags?: string; +}): Promise<{ + imageRef: string; + isEcr: boolean; +}> { + const repositoryName = `${namespace}/${projectRef}`; + const imageRef = `${host}/${repositoryName}:${nextVersion}.${environmentSlug}`; + + if (!isEcrRegistry(host)) { + return { + imageRef, + isEcr: false, + }; + } + + const [ecrRepoError] = await tryCatch( + ensureEcrRepositoryExists({ repositoryName, registryHost: host, registryId, registryTags }) + ); + + if (ecrRepoError) { + logger.error("Failed to ensure ECR repository exists", { + repositoryName, + host, + ecrRepoError: ecrRepoError.message, + }); + throw ecrRepoError; + } + + return { + imageRef, + isEcr: true, + }; +} + +function isEcrRegistry(registryHost: string) { + return registryHost.includes("amazonaws.com"); +} + +function parseRegistryTags(tags: string): Tag[] { + return tags.split(",").map((tag) => { + const [key, value] = tag.split("="); + return { Key: key, Value: value }; + }); +} + +async function createEcrRepository({ + repositoryName, + region, + registryId, + registryTags, +}: { + repositoryName: string; + region: string; + registryId?: string; + registryTags?: string; +}): Promise { + const ecr = new ECRClient({ region }); + + const result = await ecr.send( + new CreateRepositoryCommand({ + repositoryName, + imageTagMutability: "IMMUTABLE", + encryptionConfiguration: { + encryptionType: "AES256", + }, + registryId, + tags: registryTags ? parseRegistryTags(registryTags) : undefined, + }) + ); + + if (!result.repository) { + logger.error("Failed to create ECR repository", { repositoryName, result }); + throw new Error(`Failed to create ECR repository: ${repositoryName}`); + } + + return result.repository; +} + +async function getEcrRepository({ + repositoryName, + region, + registryId, +}: { + repositoryName: string; + region: string; + registryId?: string; +}): Promise { + const ecr = new ECRClient({ region }); + + try { + const result = await ecr.send( + new DescribeRepositoriesCommand({ + repositoryNames: [repositoryName], + registryId, + }) + ); + + if (!result.repositories || result.repositories.length === 0) { + logger.debug("ECR repository not found", { repositoryName, region, result }); + return undefined; + } + + return result.repositories[0]; + } catch (error) { + if (error instanceof RepositoryNotFoundException) { + logger.debug("ECR repository not found: RepositoryNotFoundException", { + repositoryName, + region, + }); + return undefined; + } + throw error; + } +} + +export function getEcrRegion(registryHost: string): string | undefined { + const parts = registryHost.split("."); + if (parts.length !== 6 || parts[1] !== "dkr" || parts[2] !== "ecr") { + return undefined; + } + return parts[3]; +} + +async function ensureEcrRepositoryExists({ + repositoryName, + registryHost, + registryId, + registryTags, +}: { + repositoryName: string; + registryHost: string; + registryId?: string; + registryTags?: string; +}): Promise { + const region = getEcrRegion(registryHost); + + if (!region) { + throw new Error(`Invalid ECR registry host: ${registryHost}`); + } + + const [getRepoError, existingRepo] = await tryCatch( + getEcrRepository({ repositoryName, region, registryId }) + ); + + if (getRepoError) { + logger.error("Failed to get ECR repository", { repositoryName, region, getRepoError }); + throw getRepoError; + } + + if (existingRepo) { + logger.debug("ECR repository already exists", { repositoryName, region, existingRepo }); + return existingRepo; + } + + const [createRepoError, newRepo] = await tryCatch( + createEcrRepository({ repositoryName, region, registryId, registryTags }) + ); + + if (createRepoError) { + logger.error("Failed to create ECR repository", { repositoryName, region, createRepoError }); + throw createRepoError; + } + + if (newRepo.repositoryName !== repositoryName) { + logger.error("ECR repository name mismatch", { repositoryName, region, newRepo }); + throw new Error( + `ECR repository name mismatch: ${repositoryName} !== ${newRepo.repositoryName}` + ); + } + + return newRepo; +} diff --git a/apps/webapp/app/v3/services/initializeDeployment.server.ts b/apps/webapp/app/v3/services/initializeDeployment.server.ts index a120fdb27b2..0627ffe75e5 100644 --- a/apps/webapp/app/v3/services/initializeDeployment.server.ts +++ b/apps/webapp/app/v3/services/initializeDeployment.server.ts @@ -1,5 +1,4 @@ import { type InitializeDeploymentRequestBody } from "@trigger.dev/core/v3"; -import { WorkerDeploymentType } from "@trigger.dev/database"; import { customAlphabet } from "nanoid"; import { env } from "~/env.server"; import { type AuthenticatedEnvironment } from "~/services/apiAuth.server"; @@ -9,6 +8,8 @@ import { createRemoteImageBuild, remoteBuildsEnabled } from "../remoteImageBuild import { calculateNextBuildVersion } from "../utils/calculateNextBuildVersion"; import { BaseService, ServiceValidationError } from "./baseService.server"; import { TimeoutDeploymentService } from "./timeoutDeployment.server"; +import { getDeploymentImageRef } from "../getDeploymentImageRef.server"; +import { tryCatch } from "@trigger.dev/core"; const nanoid = customAlphabet("1234567890abcdefghijklmnopqrstuvwxyz", 8); @@ -68,11 +69,31 @@ export class InitializeDeploymentService extends BaseService { }) : undefined; - const imageRef = [ - env.DEPLOY_REGISTRY_HOST, - env.DEPLOY_REGISTRY_NAMESPACE, - `${environment.project.externalRef}:${nextVersion}.${environment.slug}`, - ].join("/"); + const [imageRefError, imageRefResult] = await tryCatch( + getDeploymentImageRef({ + host: env.DEPLOY_REGISTRY_HOST, + namespace: env.DEPLOY_REGISTRY_NAMESPACE, + projectRef: environment.project.externalRef, + nextVersion, + environmentSlug: environment.slug, + registryId: env.DEPLOY_REGISTRY_ID, + registryTags: env.DEPLOY_REGISTRY_TAGS, + }) + ); + + if (imageRefError) { + logger.error("Failed to get deployment image ref", { + environmentId: environment.id, + projectId: environment.projectId, + version: nextVersion, + triggeredById: triggeredBy?.id, + type: payload.type, + cause: imageRefError.message, + }); + throw new ServiceValidationError("Failed to get deployment image ref"); + } + + const { imageRef, isEcr } = imageRefResult; logger.debug("Creating deployment", { environmentId: environment.id, @@ -81,6 +102,7 @@ export class InitializeDeploymentService extends BaseService { triggeredById: triggeredBy?.id, type: payload.type, imageRef, + isEcr, }); const deployment = await this._prisma.workerDeployment.create({ diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 892bc8ecec0..eeca24a135b 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -33,6 +33,7 @@ "dependencies": { "@ariakit/react": "^0.4.6", "@ariakit/react-core": "^0.4.6", + "@aws-sdk/client-ecr": "^3.839.0", "@aws-sdk/client-sqs": "^3.445.0", "@codemirror/autocomplete": "^6.3.1", "@codemirror/commands": "^6.1.2", diff --git a/apps/webapp/test/getDeploymentImageRef.test.ts b/apps/webapp/test/getDeploymentImageRef.test.ts new file mode 100644 index 00000000000..86f25ce462a --- /dev/null +++ b/apps/webapp/test/getDeploymentImageRef.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it } from "vitest"; +import { getDeploymentImageRef, getEcrRegion } from "../app/v3/getDeploymentImageRef.server"; +import { ECRClient, DeleteRepositoryCommand } from "@aws-sdk/client-ecr"; + +describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef", () => { + const testHost = "123456789012.dkr.ecr.us-east-1.amazonaws.com"; + const testNamespace = "test-namespace"; + const testProjectRef = "test-project-" + Math.random().toString(36).substring(7); + + const registryId = process.env.DEPLOY_REGISTRY_ID; + const registryTags = "test=test,test2=test2"; + + // Clean up test repository after tests + afterAll(async () => { + if (!registryId) { + return; + } + + if (process.env.KEEP_TEST_REPO === "1") { + return; + } + + try { + const region = getEcrRegion(testHost); + const ecr = new ECRClient({ region }); + await ecr.send( + new DeleteRepositoryCommand({ + repositoryName: `${testNamespace}/${testProjectRef}`, + registryId, + force: true, + }) + ); + } catch (error) { + console.warn("Failed to delete test repository:", error); + } + }); + + it("should return the correct image ref for non-ECR registry", async () => { + const imageRef = await getDeploymentImageRef({ + host: "registry.digitalocean.com", + namespace: testNamespace, + projectRef: testProjectRef, + nextVersion: "20250630.1", + environmentSlug: "test", + registryId, + registryTags, + }); + + expect(imageRef.imageRef).toBe( + `registry.digitalocean.com/${testNamespace}/${testProjectRef}:20250630.1.test` + ); + expect(imageRef.isEcr).toBe(false); + }); + + it("should create ECR repository and return correct image ref", async () => { + const imageRef = await getDeploymentImageRef({ + host: testHost, + namespace: testNamespace, + projectRef: testProjectRef, + nextVersion: "20250630.1", + environmentSlug: "test", + registryId, + registryTags, + }); + + expect(imageRef.imageRef).toBe( + `${testHost}/${testNamespace}/${testProjectRef}:20250630.1.test` + ); + expect(imageRef.isEcr).toBe(true); + }); + + it("should reuse existing ECR repository", async () => { + // This should use the repository created in the previous test + const imageRef = await getDeploymentImageRef({ + host: testHost, + namespace: testNamespace, + projectRef: testProjectRef, + nextVersion: "20250630.2", + environmentSlug: "prod", + registryId, + registryTags, + }); + + expect(imageRef.imageRef).toBe( + `${testHost}/${testNamespace}/${testProjectRef}:20250630.2.prod` + ); + expect(imageRef.isEcr).toBe(true); + }); + + it("should throw error for invalid ECR host", async () => { + await expect( + getDeploymentImageRef({ + host: "invalid.ecr.amazonaws.com", + namespace: testNamespace, + projectRef: testProjectRef, + nextVersion: "20250630.1", + environmentSlug: "test", + registryId, + registryTags, + }) + ).rejects.toThrow("Invalid ECR registry host: invalid.ecr.amazonaws.com"); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be39db8422b..8276f543dc2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -194,6 +194,9 @@ importers: '@ariakit/react-core': specifier: ^0.4.6 version: 0.4.6(react-dom@18.2.0)(react@18.2.0) + '@aws-sdk/client-ecr': + specifier: ^3.839.0 + version: 3.839.0 '@aws-sdk/client-sqs': specifier: ^3.445.0 version: 3.454.0 @@ -2947,7 +2950,7 @@ packages: '@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.714.0 + '@aws-sdk/types': 3.821.0 '@aws-sdk/util-locate-window': 3.310.0 '@smithy/util-utf8': 2.0.2 tslib: 2.8.1 @@ -2966,7 +2969,7 @@ packages: engines: {node: '>=16.0.0'} dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.714.0 + '@aws-sdk/types': 3.821.0 tslib: 2.8.1 dev: false @@ -2998,6 +3001,54 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/client-ecr@3.839.0: + resolution: {integrity: sha512-aaSPaqfWvz9OAGfcl97Ps/bQHulRLt8tg8tB8zc4gyGo4OTn6gnKMd+FEhPBszRKP7xeb4pzjjqcXvHsFt7HEQ==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.839.0 + '@aws-sdk/credential-provider-node': 3.839.0 + '@aws-sdk/middleware-host-header': 3.821.0 + '@aws-sdk/middleware-logger': 3.821.0 + '@aws-sdk/middleware-recursion-detection': 3.821.0 + '@aws-sdk/middleware-user-agent': 3.839.0 + '@aws-sdk/region-config-resolver': 3.821.0 + '@aws-sdk/types': 3.821.0 + '@aws-sdk/util-endpoints': 3.828.0 + '@aws-sdk/util-user-agent-browser': 3.821.0 + '@aws-sdk/util-user-agent-node': 3.839.0 + '@smithy/config-resolver': 4.1.4 + '@smithy/core': 3.6.0 + '@smithy/fetch-http-handler': 5.0.4 + '@smithy/hash-node': 4.0.4 + '@smithy/invalid-dependency': 4.0.4 + '@smithy/middleware-content-length': 4.0.4 + '@smithy/middleware-endpoint': 4.1.13 + '@smithy/middleware-retry': 4.1.14 + '@smithy/middleware-serde': 4.0.8 + '@smithy/middleware-stack': 4.0.4 + '@smithy/node-config-provider': 4.1.3 + '@smithy/node-http-handler': 4.0.6 + '@smithy/protocol-http': 5.1.2 + '@smithy/smithy-client': 4.4.5 + '@smithy/types': 4.3.1 + '@smithy/url-parser': 4.0.4 + '@smithy/util-base64': 4.0.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-body-length-node': 4.0.0 + '@smithy/util-defaults-mode-browser': 4.0.21 + '@smithy/util-defaults-mode-node': 4.0.21 + '@smithy/util-endpoints': 3.0.6 + '@smithy/util-middleware': 4.0.4 + '@smithy/util-retry': 4.0.6 + '@smithy/util-utf8': 4.0.0 + '@smithy/util-waiter': 4.0.6 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/client-ses@3.716.0: resolution: {integrity: sha512-lYsg2x3Z6R5ngBX1EqFKR6jf77ewbGg+aZV6V4ucVCghaGGcGnGisRP4FAep3IgkrZuByEYeJaA6cTli98qaOQ==} engines: {node: '>=16.0.0'} @@ -3237,6 +3288,52 @@ packages: - aws-crt dev: false + /@aws-sdk/client-sso@3.839.0: + resolution: {integrity: sha512-AZABysUhbfcwXVlMo97/vwHgsfJNF81wypCAowpqAJkSjP2KrqsqHpb71/RoR2w8JGmEnBBXRD4wIxDhnmifWg==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.839.0 + '@aws-sdk/middleware-host-header': 3.821.0 + '@aws-sdk/middleware-logger': 3.821.0 + '@aws-sdk/middleware-recursion-detection': 3.821.0 + '@aws-sdk/middleware-user-agent': 3.839.0 + '@aws-sdk/region-config-resolver': 3.821.0 + '@aws-sdk/types': 3.821.0 + '@aws-sdk/util-endpoints': 3.828.0 + '@aws-sdk/util-user-agent-browser': 3.821.0 + '@aws-sdk/util-user-agent-node': 3.839.0 + '@smithy/config-resolver': 4.1.4 + '@smithy/core': 3.6.0 + '@smithy/fetch-http-handler': 5.0.4 + '@smithy/hash-node': 4.0.4 + '@smithy/invalid-dependency': 4.0.4 + '@smithy/middleware-content-length': 4.0.4 + '@smithy/middleware-endpoint': 4.1.13 + '@smithy/middleware-retry': 4.1.14 + '@smithy/middleware-serde': 4.0.8 + '@smithy/middleware-stack': 4.0.4 + '@smithy/node-config-provider': 4.1.3 + '@smithy/node-http-handler': 4.0.6 + '@smithy/protocol-http': 5.1.2 + '@smithy/smithy-client': 4.4.5 + '@smithy/types': 4.3.1 + '@smithy/url-parser': 4.0.4 + '@smithy/util-base64': 4.0.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-body-length-node': 4.0.0 + '@smithy/util-defaults-mode-browser': 4.0.21 + '@smithy/util-defaults-mode-node': 4.0.21 + '@smithy/util-endpoints': 3.0.6 + '@smithy/util-middleware': 4.0.4 + '@smithy/util-retry': 4.0.6 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/client-sts@3.454.0: resolution: {integrity: sha512-0fDvr8WeB6IYO8BUCzcivWmahgGl/zDbaYfakzGnt4mrl5ztYaXE875WI6b7+oFcKMRvN+KLvwu5TtyFuNY+GQ==} engines: {node: '>=14.0.0'} @@ -3358,6 +3455,27 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/core@3.839.0: + resolution: {integrity: sha512-KdwL5RaK7eUIlOpdOoZ5u+2t4X1rdX/MTZgz3IV/aBzjVUoGsp+uUnbyqXomLQSUitPHp72EE/NHDsvWW/IHvQ==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/types': 3.821.0 + '@aws-sdk/xml-builder': 3.821.0 + '@smithy/core': 3.6.0 + '@smithy/node-config-provider': 4.1.3 + '@smithy/property-provider': 4.0.4 + '@smithy/protocol-http': 5.1.2 + '@smithy/signature-v4': 5.1.2 + '@smithy/smithy-client': 4.4.5 + '@smithy/types': 4.3.1 + '@smithy/util-base64': 4.0.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-middleware': 4.0.4 + '@smithy/util-utf8': 4.0.0 + fast-xml-parser: 4.4.1 + tslib: 2.8.1 + dev: false + /@aws-sdk/credential-provider-env@3.451.0: resolution: {integrity: sha512-9dAav7DcRgaF7xCJEQR5ER9ErXxnu/tdnVJ+UPmb1NPeIZdESv1A3lxFDEq1Fs8c4/lzAj9BpshGyJVIZwZDKg==} engines: {node: '>=14.0.0'} @@ -3379,6 +3497,17 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/credential-provider-env@3.839.0: + resolution: {integrity: sha512-cWTadewPPz1OvObZJB+olrgh8VwcgIVcT293ZUT9V0CMF0UU7QaPwJP7uNXcNxltTh+sk1yhjH4UlcnJigZZbA==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/core': 3.839.0 + '@aws-sdk/types': 3.821.0 + '@smithy/property-provider': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@aws-sdk/credential-provider-http@3.716.0: resolution: {integrity: sha512-CZ04pl2z7igQPysQyH2xKZHM3fLwkemxQbKOlje3TmiS1NwXvcKvERhp9PE/H23kOL7beTM19NMRog/Fka/rlw==} engines: {node: '>=16.0.0'} @@ -3395,6 +3524,22 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/credential-provider-http@3.839.0: + resolution: {integrity: sha512-fv0BZwrDhWDju4D1MCLT4I2aPjr0dVQ6P+MpqvcGNOA41Oa9UdRhYTV5iuy5NLXzIzoCmnS+XfSq5Kbsf6//xw==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/core': 3.839.0 + '@aws-sdk/types': 3.821.0 + '@smithy/fetch-http-handler': 5.0.4 + '@smithy/node-http-handler': 4.0.6 + '@smithy/property-provider': 4.0.4 + '@smithy/protocol-http': 5.1.2 + '@smithy/smithy-client': 4.4.5 + '@smithy/types': 4.3.1 + '@smithy/util-stream': 4.2.2 + tslib: 2.8.1 + dev: false + /@aws-sdk/credential-provider-ini@3.451.0: resolution: {integrity: sha512-TySt64Ci5/ZbqFw1F9Z0FIGvYx5JSC9e6gqDnizIYd8eMnn8wFRUscRrD7pIHKfrhvVKN5h0GdYovmMO/FMCBw==} engines: {node: '>=14.0.0'} @@ -3437,6 +3582,27 @@ packages: - aws-crt dev: false + /@aws-sdk/credential-provider-ini@3.839.0: + resolution: {integrity: sha512-GHm0hF4CiDxIDR7TauMaA6iI55uuSqRxMBcqTAHaTPm6+h1A+MS+ysQMxZ+Jvwtoy8WmfTIGrJVxSCw0sK2hvA==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/core': 3.839.0 + '@aws-sdk/credential-provider-env': 3.839.0 + '@aws-sdk/credential-provider-http': 3.839.0 + '@aws-sdk/credential-provider-process': 3.839.0 + '@aws-sdk/credential-provider-sso': 3.839.0 + '@aws-sdk/credential-provider-web-identity': 3.839.0 + '@aws-sdk/nested-clients': 3.839.0 + '@aws-sdk/types': 3.821.0 + '@smithy/credential-provider-imds': 4.0.6 + '@smithy/property-provider': 4.0.4 + '@smithy/shared-ini-file-loader': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/credential-provider-node@3.451.0: resolution: {integrity: sha512-AEwM1WPyxUdKrKyUsKyFqqRFGU70e4qlDyrtBxJnSU9NRLZI8tfEZ67bN7fHSxBUBODgDXpMSlSvJiBLh5/3pw==} engines: {node: '>=14.0.0'} @@ -3478,6 +3644,26 @@ packages: - aws-crt dev: false + /@aws-sdk/credential-provider-node@3.839.0: + resolution: {integrity: sha512-7bR+U2h+ft0V8chyeu9Bh/pvau4ZkQMeRt5f0dAULoepZQ77QQVRP4H04yJPTg9DCtqbVULQ3uf5YOp1/08vQw==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.839.0 + '@aws-sdk/credential-provider-http': 3.839.0 + '@aws-sdk/credential-provider-ini': 3.839.0 + '@aws-sdk/credential-provider-process': 3.839.0 + '@aws-sdk/credential-provider-sso': 3.839.0 + '@aws-sdk/credential-provider-web-identity': 3.839.0 + '@aws-sdk/types': 3.821.0 + '@smithy/credential-provider-imds': 4.0.6 + '@smithy/property-provider': 4.0.4 + '@smithy/shared-ini-file-loader': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/credential-provider-process@3.451.0: resolution: {integrity: sha512-HQywSdKeD5PErcLLnZfSyCJO+6T+ZyzF+Lm/QgscSC+CbSUSIPi//s15qhBRVely/3KBV6AywxwNH+5eYgt4lQ==} engines: {node: '>=14.0.0'} @@ -3501,6 +3687,18 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/credential-provider-process@3.839.0: + resolution: {integrity: sha512-qShpekjociUZ+isyQNa0P7jo+0q3N2+0eJDg8SGyP6K6hHTcGfiqxTDps+IKl6NreCPhZCBzyI9mWkP0xSDR6g==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/core': 3.839.0 + '@aws-sdk/types': 3.821.0 + '@smithy/property-provider': 4.0.4 + '@smithy/shared-ini-file-loader': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@aws-sdk/credential-provider-sso@3.451.0: resolution: {integrity: sha512-Usm/N51+unOt8ID4HnQzxIjUJDrkAQ1vyTOC0gSEEJ7h64NSSPGD5yhN7il5WcErtRd3EEtT1a8/GTC5TdBctg==} engines: {node: '>=14.0.0'} @@ -3533,6 +3731,22 @@ packages: - aws-crt dev: false + /@aws-sdk/credential-provider-sso@3.839.0: + resolution: {integrity: sha512-w10zBLHhU8SBQcdrSPMI02haLoRGZg+gP7mH/Er8VhIXfHefbr7o4NirmB0hwdw/YAH8MLlC9jj7c2SJlsNhYA==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/client-sso': 3.839.0 + '@aws-sdk/core': 3.839.0 + '@aws-sdk/token-providers': 3.839.0 + '@aws-sdk/types': 3.821.0 + '@smithy/property-provider': 4.0.4 + '@smithy/shared-ini-file-loader': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/credential-provider-web-identity@3.451.0: resolution: {integrity: sha512-Xtg3Qw65EfDjWNG7o2xD6sEmumPfsy3WDGjk2phEzVg8s7hcZGxf5wYwe6UY7RJvlEKrU0rFA+AMn6Hfj5oOzg==} engines: {node: '>=14.0.0'} @@ -3557,6 +3771,20 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/credential-provider-web-identity@3.839.0: + resolution: {integrity: sha512-EvqTc7J1kgmiuxknpCp1S60hyMQvmKxsI5uXzQtcogl/N55rxiXEqnCLI5q6p33q91PJegrcMCM5Q17Afhm5qA==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/core': 3.839.0 + '@aws-sdk/nested-clients': 3.839.0 + '@aws-sdk/types': 3.821.0 + '@smithy/property-provider': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/middleware-host-header@3.451.0: resolution: {integrity: sha512-j8a5jAfhWmsK99i2k8oR8zzQgXrsJtgrLxc3js6U+525mcZytoiDndkWTmD5fjJ1byU1U2E5TaPq+QJeDip05Q==} engines: {node: '>=14.0.0'} @@ -3577,6 +3805,16 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/middleware-host-header@3.821.0: + resolution: {integrity: sha512-xSMR+sopSeWGx5/4pAGhhfMvGBHioVBbqGvDs6pG64xfNwM5vq5s5v6D04e2i+uSTj4qGa71dLUs5I0UzAK3sw==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/types': 3.821.0 + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@aws-sdk/middleware-logger@3.451.0: resolution: {integrity: sha512-0kHrYEyVeB2QBfP6TfbI240aRtatLZtcErJbhpiNUb+CQPgEL3crIjgVE8yYiJumZ7f0jyjo8HLPkwD1/2APaw==} engines: {node: '>=14.0.0'} @@ -3595,6 +3833,15 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/middleware-logger@3.821.0: + resolution: {integrity: sha512-0cvI0ipf2tGx7fXYEEN5fBeZDz2RnHyb9xftSgUsEq7NBxjV0yTZfLJw6Za5rjE6snC80dRN8+bTNR1tuG89zA==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/types': 3.821.0 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@aws-sdk/middleware-recursion-detection@3.451.0: resolution: {integrity: sha512-J6jL6gJ7orjHGM70KDRcCP7so/J2SnkN4vZ9YRLTeeZY6zvBuHDjX8GCIgSqPn/nXFXckZO8XSnA7u6+3TAT0w==} engines: {node: '>=14.0.0'} @@ -3615,6 +3862,16 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/middleware-recursion-detection@3.821.0: + resolution: {integrity: sha512-efmaifbhBoqKG3bAoEfDdcM8hn1psF+4qa7ykWuYmfmah59JBeqHLfz5W9m9JoTwoKPkFcVLWZxnyZzAnVBOIg==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/types': 3.821.0 + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@aws-sdk/middleware-sdk-sqs@3.451.0: resolution: {integrity: sha512-GXpFSc9Ji4IAT/OaTkmnDrxzZrrAsJctUAC9vihpgGDof79A1Oz4R+r2uBMTKGxCIFkqyYW5Se7y2ijjTNa3ZA==} engines: {node: '>=14.0.0'} @@ -3673,6 +3930,65 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/middleware-user-agent@3.839.0: + resolution: {integrity: sha512-2u74uRM1JWq6Sf7+3YpjejPM9YkomGt4kWhrmooIBEq1k5r2GTbkH7pNCxBQwBueXM21jAGVDxxeClpTx+5hig==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/core': 3.839.0 + '@aws-sdk/types': 3.821.0 + '@aws-sdk/util-endpoints': 3.828.0 + '@smithy/core': 3.6.0 + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + + /@aws-sdk/nested-clients@3.839.0: + resolution: {integrity: sha512-Glic0pg2THYP3aRhJORwJJBe1JLtJoEdWV/MFZNyzCklfMwEzpWtZAyxy+tQyFmMeW50uBAnh2R0jhMMcf257w==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.839.0 + '@aws-sdk/middleware-host-header': 3.821.0 + '@aws-sdk/middleware-logger': 3.821.0 + '@aws-sdk/middleware-recursion-detection': 3.821.0 + '@aws-sdk/middleware-user-agent': 3.839.0 + '@aws-sdk/region-config-resolver': 3.821.0 + '@aws-sdk/types': 3.821.0 + '@aws-sdk/util-endpoints': 3.828.0 + '@aws-sdk/util-user-agent-browser': 3.821.0 + '@aws-sdk/util-user-agent-node': 3.839.0 + '@smithy/config-resolver': 4.1.4 + '@smithy/core': 3.6.0 + '@smithy/fetch-http-handler': 5.0.4 + '@smithy/hash-node': 4.0.4 + '@smithy/invalid-dependency': 4.0.4 + '@smithy/middleware-content-length': 4.0.4 + '@smithy/middleware-endpoint': 4.1.13 + '@smithy/middleware-retry': 4.1.14 + '@smithy/middleware-serde': 4.0.8 + '@smithy/middleware-stack': 4.0.4 + '@smithy/node-config-provider': 4.1.3 + '@smithy/node-http-handler': 4.0.6 + '@smithy/protocol-http': 5.1.2 + '@smithy/smithy-client': 4.4.5 + '@smithy/types': 4.3.1 + '@smithy/url-parser': 4.0.4 + '@smithy/util-base64': 4.0.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-body-length-node': 4.0.0 + '@smithy/util-defaults-mode-browser': 4.0.21 + '@smithy/util-defaults-mode-node': 4.0.21 + '@smithy/util-endpoints': 3.0.6 + '@smithy/util-middleware': 4.0.4 + '@smithy/util-retry': 4.0.6 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/region-config-resolver@3.451.0: resolution: {integrity: sha512-3iMf4OwzrFb4tAAmoROXaiORUk2FvSejnHIw/XHvf/jjR4EqGGF95NZP/n/MeFZMizJWVssrwS412GmoEyoqhg==} engines: {node: '>=14.0.0'} @@ -3696,6 +4012,18 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/region-config-resolver@3.821.0: + resolution: {integrity: sha512-t8og+lRCIIy5nlId0bScNpCkif8sc0LhmtaKsbm0ZPm3sCa/WhCbSZibjbZ28FNjVCV+p0D9RYZx0VDDbtWyjw==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/types': 3.821.0 + '@smithy/node-config-provider': 4.1.3 + '@smithy/types': 4.3.1 + '@smithy/util-config-provider': 4.0.0 + '@smithy/util-middleware': 4.0.4 + tslib: 2.8.1 + dev: false + /@aws-sdk/token-providers@3.451.0: resolution: {integrity: sha512-ij1L5iUbn6CwxVOT1PG4NFjsrsKN9c4N1YEM0lkl6DwmaNOscjLKGSNyj9M118vSWsOs1ZDbTwtj++h0O/BWrQ==} engines: {node: '>=14.0.0'} @@ -3755,6 +4083,21 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/token-providers@3.839.0: + resolution: {integrity: sha512-2nlafqdSbet/2WtYIoZ7KEGFowFonPBDYlTjrUvwU2yooE10VhvzhLSCTB2aKIVzo2Z2wL5WGFQsqAY5QwK6Bw==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/core': 3.839.0 + '@aws-sdk/nested-clients': 3.839.0 + '@aws-sdk/types': 3.821.0 + '@smithy/property-provider': 4.0.4 + '@smithy/shared-ini-file-loader': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/types@3.451.0: resolution: {integrity: sha512-rhK+qeYwCIs+laJfWCcrYEjay2FR/9VABZJ2NRM89jV/fKqGVQR52E5DQqrI+oEIL5JHMhhnr4N4fyECMS35lw==} engines: {node: '>=14.0.0'} @@ -3771,6 +4114,14 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/types@3.821.0: + resolution: {integrity: sha512-Znroqdai1a90TlxGaJ+FK1lwC0fHpo97Xjsp5UKGR5JODYm7f9+/fF17ebO1KdoBr/Rm0UIFiF5VmI8ts9F1eA==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@aws-sdk/util-endpoints@3.451.0: resolution: {integrity: sha512-giqLGBTnRIcKkDqwU7+GQhKbtJ5Ku35cjGQIfMyOga6pwTBUbaK0xW1Sdd8sBQ1GhApscnChzI9o/R9x0368vw==} engines: {node: '>=14.0.0'} @@ -3790,6 +4141,16 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/util-endpoints@3.828.0: + resolution: {integrity: sha512-RvKch111SblqdkPzg3oCIdlGxlQs+k+P7Etory9FmxPHyPDvsP1j1c74PmgYqtzzMWmoXTjd+c9naUHh9xG8xg==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/types': 3.821.0 + '@smithy/types': 4.3.1 + '@smithy/util-endpoints': 3.0.6 + tslib: 2.8.1 + dev: false + /@aws-sdk/util-locate-window@3.310.0: resolution: {integrity: sha512-qo2t/vBTnoXpjKxlsC2e1gBrRm80M3bId27r0BRB2VniSSe7bL1mmzM+/HFtujm0iAxtPM+aLEflLJlJeDPg0w==} engines: {node: '>=14.0.0'} @@ -3815,6 +4176,15 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/util-user-agent-browser@3.821.0: + resolution: {integrity: sha512-irWZHyM0Jr1xhC+38OuZ7JB6OXMLPZlj48thElpsO1ZSLRkLZx5+I7VV6k3sp2yZ7BYbKz/G2ojSv4wdm7XTLw==} + dependencies: + '@aws-sdk/types': 3.821.0 + '@smithy/types': 4.3.1 + bowser: 2.11.0 + tslib: 2.8.1 + dev: false + /@aws-sdk/util-user-agent-node@3.451.0: resolution: {integrity: sha512-TBzm6P+ql4mkGFAjPlO1CI+w3yUT+NulaiALjl/jNX/nnUp6HsJsVxJf4nVFQTG5KRV0iqMypcs7I3KIhH+LmA==} engines: {node: '>=14.0.0'} @@ -3846,12 +4216,36 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/util-user-agent-node@3.839.0: + resolution: {integrity: sha512-MuunkIG1bJVMtTH7MbjXOrhHleU5wjHz5eCAUc6vj7M9rwol71nqjj9b8RLnkO5gsJcKc29Qk8iV6xQuzKWNMw==} + engines: {node: '>=18.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + dependencies: + '@aws-sdk/middleware-user-agent': 3.839.0 + '@aws-sdk/types': 3.821.0 + '@smithy/node-config-provider': 4.1.3 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@aws-sdk/util-utf8-browser@3.259.0: resolution: {integrity: sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==} dependencies: tslib: 2.8.1 dev: false + /@aws-sdk/xml-builder@3.821.0: + resolution: {integrity: sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@babel/code-frame@7.22.13: resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} engines: {node: '>=6.9.0'} @@ -16177,6 +16571,14 @@ packages: tslib: 2.8.1 dev: false + /@smithy/abort-controller@4.0.4: + resolution: {integrity: sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@smithy/config-resolver@2.0.19: resolution: {integrity: sha512-JsghnQ5zjWmjEVY8TFOulLdEOCj09SjRLugrHlkPZTIBBm7PQitCFVLThbsKPZQOP7N3ME1DU1nKUc1UaVnBog==} engines: {node: '>=14.0.0'} @@ -16199,6 +16601,17 @@ packages: tslib: 2.8.1 dev: false + /@smithy/config-resolver@4.1.4: + resolution: {integrity: sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/node-config-provider': 4.1.3 + '@smithy/types': 4.3.1 + '@smithy/util-config-provider': 4.0.0 + '@smithy/util-middleware': 4.0.4 + tslib: 2.8.1 + dev: false + /@smithy/core@2.5.5: resolution: {integrity: sha512-G8G/sDDhXA7o0bOvkc7bgai6POuSld/+XhNnWAbpQTpLv2OZPvyqQ58tLPPlz0bSNsXktldDDREIv1LczFeNEw==} engines: {node: '>=16.0.0'} @@ -16213,6 +16626,21 @@ packages: tslib: 2.8.1 dev: false + /@smithy/core@3.6.0: + resolution: {integrity: sha512-Pgvfb+TQ4wUNLyHzvgCP4aYZMh16y7GcfF59oirRHcgGgkH1e/s9C0nv/v3WP+Quymyr5je71HeFQCwh+44XLg==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/middleware-serde': 4.0.8 + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + '@smithy/util-base64': 4.0.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-middleware': 4.0.4 + '@smithy/util-stream': 4.2.2 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + dev: false + /@smithy/credential-provider-imds@2.1.2: resolution: {integrity: sha512-Y62jBWdoLPSYjr9fFvJf+KwTa1EunjVr6NryTEWCnwIY93OJxwV4t0qxjwdPl/XMsUkq79ppNJSEQN6Ohnhxjw==} engines: {node: '>=14.0.0'} @@ -16235,6 +16663,17 @@ packages: tslib: 2.8.1 dev: false + /@smithy/credential-provider-imds@4.0.6: + resolution: {integrity: sha512-hKMWcANhUiNbCJouYkZ9V3+/Qf9pteR1dnwgdyzR09R4ODEYx8BbUysHwRSyex4rZ9zapddZhLFTnT4ZijR4pw==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/node-config-provider': 4.1.3 + '@smithy/property-provider': 4.0.4 + '@smithy/types': 4.3.1 + '@smithy/url-parser': 4.0.4 + tslib: 2.8.1 + dev: false + /@smithy/eventstream-codec@2.0.14: resolution: {integrity: sha512-g/OU/MeWGfHDygoXgMWfG/Xb0QqDnAGcM9t2FRrVAhleXYRddGOEnfanR5cmHgB9ue52MJsyorqFjckzXsylaA==} dependencies: @@ -16264,6 +16703,17 @@ packages: tslib: 2.8.1 dev: false + /@smithy/fetch-http-handler@5.0.4: + resolution: {integrity: sha512-AMtBR5pHppYMVD7z7G+OlHHAcgAN7v0kVKEpHuTO4Gb199Gowh0taYi9oDStFeUhetkeP55JLSVlTW1n9rFtUw==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/protocol-http': 5.1.2 + '@smithy/querystring-builder': 4.0.4 + '@smithy/types': 4.3.1 + '@smithy/util-base64': 4.0.0 + tslib: 2.8.1 + dev: false + /@smithy/hash-node@2.0.16: resolution: {integrity: sha512-Wbi9A0PacMYUOwjAulQP90Wl3mQ6NDwnyrZQzFjDz+UzjXOSyQMgBrTkUBz+pVoYVlX3DUu24gWMZBcit+wOGg==} engines: {node: '>=14.0.0'} @@ -16284,6 +16734,16 @@ packages: tslib: 2.8.1 dev: false + /@smithy/hash-node@4.0.4: + resolution: {integrity: sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/types': 4.3.1 + '@smithy/util-buffer-from': 4.0.0 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + dev: false + /@smithy/invalid-dependency@2.0.14: resolution: {integrity: sha512-d8ohpwZo9RzTpGlAfsWtfm1SHBSU7+N4iuZ6MzR10xDTujJJWtmXYHK1uzcr7rggbpUTaWyHpPFgnf91q0EFqQ==} dependencies: @@ -16298,6 +16758,14 @@ packages: tslib: 2.8.1 dev: false + /@smithy/invalid-dependency@4.0.4: + resolution: {integrity: sha512-bNYMi7WKTJHu0gn26wg8OscncTt1t2b8KcsZxvOv56XA6cyXtOAAAaNP7+m45xfppXfOatXF3Sb1MNsLUgVLTw==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@smithy/is-array-buffer@2.0.0: resolution: {integrity: sha512-z3PjFjMyZNI98JFRJi/U0nGoLWMSJlDjAW4QUX2WNZLas5C0CmVV6LJ01JI0k90l7FvpmixjWxPFmENSClQ7ug==} engines: {node: '>=14.0.0'} @@ -16312,6 +16780,13 @@ packages: tslib: 2.8.1 dev: false + /@smithy/is-array-buffer@4.0.0: + resolution: {integrity: sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==} + engines: {node: '>=18.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + /@smithy/md5-js@2.0.16: resolution: {integrity: sha512-YhWt9aKl+EMSNXyUTUo7I01WHf3HcCkPu/Hl2QmTNwrHT49eWaY7hptAMaERZuHFH0V5xHgPKgKZo2I93DFtgQ==} dependencies: @@ -16338,6 +16813,15 @@ packages: tslib: 2.8.1 dev: false + /@smithy/middleware-content-length@4.0.4: + resolution: {integrity: sha512-F7gDyfI2BB1Kc+4M6rpuOLne5LOcEknH1n6UQB69qv+HucXBR1rkzXBnQTB2q46sFy1PM/zuSJOB532yc8bg3w==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@smithy/middleware-endpoint@2.2.1: resolution: {integrity: sha512-dVDS7HNJl/wb0lpByXor6whqDbb1YlLoaoWYoelyYzLHioXOE7y/0iDwJWtDcN36/tVCw9EPBFZ3aans84jLpg==} engines: {node: '>=14.0.0'} @@ -16365,6 +16849,20 @@ packages: tslib: 2.8.1 dev: false + /@smithy/middleware-endpoint@4.1.13: + resolution: {integrity: sha512-xg3EHV/Q5ZdAO5b0UiIMj3RIOCobuS40pBBODguUDVdko6YK6QIzCVRrHTogVuEKglBWqWenRnZ71iZnLL3ZAQ==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/core': 3.6.0 + '@smithy/middleware-serde': 4.0.8 + '@smithy/node-config-provider': 4.1.3 + '@smithy/shared-ini-file-loader': 4.0.4 + '@smithy/types': 4.3.1 + '@smithy/url-parser': 4.0.4 + '@smithy/util-middleware': 4.0.4 + tslib: 2.8.1 + dev: false + /@smithy/middleware-retry@2.0.21: resolution: {integrity: sha512-EZS1EXv1k6IJX6hyu/0yNQuPcPaXwG8SWljQHYueyRbOxmqYgoWMWPtfZj0xRRQ4YtLawQSpBgAeiJltq8/MPw==} engines: {node: '>=14.0.0'} @@ -16394,6 +16892,21 @@ packages: uuid: 9.0.1 dev: false + /@smithy/middleware-retry@4.1.14: + resolution: {integrity: sha512-eoXaLlDGpKvdmvt+YBfRXE7HmIEtFF+DJCbTPwuLunP0YUnrydl+C4tS+vEM0+nyxXrX3PSUFqC+lP1+EHB1Tw==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/node-config-provider': 4.1.3 + '@smithy/protocol-http': 5.1.2 + '@smithy/service-error-classification': 4.0.6 + '@smithy/smithy-client': 4.4.5 + '@smithy/types': 4.3.1 + '@smithy/util-middleware': 4.0.4 + '@smithy/util-retry': 4.0.6 + tslib: 2.8.1 + uuid: 9.0.1 + dev: false + /@smithy/middleware-serde@2.0.14: resolution: {integrity: sha512-hFi3FqoYWDntCYA2IGY6gJ6FKjq2gye+1tfxF2HnIJB5uW8y2DhpRNBSUMoqP+qvYzRqZ6ntv4kgbG+o3pX57g==} engines: {node: '>=14.0.0'} @@ -16410,6 +16923,15 @@ packages: tslib: 2.8.1 dev: false + /@smithy/middleware-serde@4.0.8: + resolution: {integrity: sha512-iSSl7HJoJaGyMIoNn2B7czghOVwJ9nD7TMvLhMWeSB5vt0TnEYyRRqPJu/TqW76WScaNvYYB8nRoiBHR9S1Ddw==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@smithy/middleware-stack@2.0.8: resolution: {integrity: sha512-7/N59j0zWqVEKExJcA14MrLDZ/IeN+d6nbkN8ucs+eURyaDUXWYlZrQmMOd/TyptcQv0+RDlgag/zSTTV62y/Q==} engines: {node: '>=14.0.0'} @@ -16426,6 +16948,14 @@ packages: tslib: 2.8.1 dev: false + /@smithy/middleware-stack@4.0.4: + resolution: {integrity: sha512-kagK5ggDrBUCCzI93ft6DjteNSfY8Ulr83UtySog/h09lTIOAJ/xUSObutanlPT0nhoHAkpmW9V5K8oPyLh+QA==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@smithy/node-config-provider@2.1.6: resolution: {integrity: sha512-HLqTs6O78m3M3z1cPLFxddxhEPv5MkVatfPuxoVO3A+cHZanNd/H5I6btcdHy6N2CB1MJ/lihJC92h30SESsBA==} engines: {node: '>=14.0.0'} @@ -16446,6 +16976,16 @@ packages: tslib: 2.8.1 dev: false + /@smithy/node-config-provider@4.1.3: + resolution: {integrity: sha512-HGHQr2s59qaU1lrVH6MbLlmOBxadtzTsoO4c+bF5asdgVik3I8o7JIOzoeqWc5MjVa+vD36/LWE0iXKpNqooRw==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/property-provider': 4.0.4 + '@smithy/shared-ini-file-loader': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@smithy/node-http-handler@2.1.10: resolution: {integrity: sha512-lkALAwtN6odygIM4nB8aHDahINM6WXXjNrZmWQAh0RSossySRT2qa31cFv0ZBuAYVWeprskRk13AFvvLmf1WLw==} engines: {node: '>=14.0.0'} @@ -16468,6 +17008,17 @@ packages: tslib: 2.8.1 dev: false + /@smithy/node-http-handler@4.0.6: + resolution: {integrity: sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/abort-controller': 4.0.4 + '@smithy/protocol-http': 5.1.2 + '@smithy/querystring-builder': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@smithy/property-provider@2.0.15: resolution: {integrity: sha512-YbRFBn8oiiC3o1Kn3a4KjGa6k47rCM9++5W9cWqYn9WnkyH+hBWgfJAckuxpyA2Hq6Ys4eFrWzXq6fqHEw7iew==} engines: {node: '>=14.0.0'} @@ -16484,6 +17035,14 @@ packages: tslib: 2.8.1 dev: false + /@smithy/property-provider@4.0.4: + resolution: {integrity: sha512-qHJ2sSgu4FqF4U/5UUp4DhXNmdTrgmoAai6oQiM+c5RZ/sbDwJ12qxB1M6FnP+Tn/ggkPZf9ccn4jqKSINaquw==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@smithy/protocol-http@3.0.10: resolution: {integrity: sha512-6+tjNk7rXW7YTeGo9qwxXj/2BFpJTe37kTj3EnZCoX/nH+NP/WLA7O83fz8XhkGqsaAhLUPo/bB12vvd47nsmg==} engines: {node: '>=14.0.0'} @@ -16500,6 +17059,14 @@ packages: tslib: 2.8.1 dev: false + /@smithy/protocol-http@5.1.2: + resolution: {integrity: sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@smithy/querystring-builder@2.0.14: resolution: {integrity: sha512-lQ4pm9vTv9nIhl5jt6uVMPludr6syE2FyJmHsIJJuOD7QPIJnrf9HhUGf1iHh9KJ4CUv21tpOU3X6s0rB6uJ0g==} engines: {node: '>=14.0.0'} @@ -16518,6 +17085,15 @@ packages: tslib: 2.8.1 dev: false + /@smithy/querystring-builder@4.0.4: + resolution: {integrity: sha512-SwREZcDnEYoh9tLNgMbpop+UTGq44Hl9tdj3rf+yeLcfH7+J8OXEBaMc2kDxtyRHu8BhSg9ADEx0gFHvpJgU8w==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/types': 4.3.1 + '@smithy/util-uri-escape': 4.0.0 + tslib: 2.8.1 + dev: false + /@smithy/querystring-parser@2.0.14: resolution: {integrity: sha512-+cbtXWI9tNtQjlgQg3CA+pvL3zKTAxPnG3Pj6MP89CR3vi3QMmD0SOWoq84tqZDnJCxlsusbgIXk1ngMReXo+A==} engines: {node: '>=14.0.0'} @@ -16534,6 +17110,14 @@ packages: tslib: 2.8.1 dev: false + /@smithy/querystring-parser@4.0.4: + resolution: {integrity: sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@smithy/service-error-classification@2.0.7: resolution: {integrity: sha512-LLxgW12qGz8doYto15kZ4x1rHjtXl0BnCG6T6Wb8z2DI4PT9cJfOSvzbuLzy7+5I24PAepKgFeWHRd9GYy3Z9w==} engines: {node: '>=14.0.0'} @@ -16548,6 +17132,13 @@ packages: '@smithy/types': 3.7.2 dev: false + /@smithy/service-error-classification@4.0.6: + resolution: {integrity: sha512-RRoTDL//7xi4tn5FrN2NzH17jbgmnKidUqd4KvquT0954/i6CXXkh1884jBiunq24g9cGtPBEXlU40W6EpNOOg==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/types': 4.3.1 + dev: false + /@smithy/shared-ini-file-loader@2.2.5: resolution: {integrity: sha512-LHA68Iu7SmNwfAVe8egmjDCy648/7iJR/fK1UnVw+iAOUJoEYhX2DLgVd5pWllqdDiRbQQzgaHLcRokM+UFR1w==} engines: {node: '>=14.0.0'} @@ -16564,6 +17155,14 @@ packages: tslib: 2.8.1 dev: false + /@smithy/shared-ini-file-loader@4.0.4: + resolution: {integrity: sha512-63X0260LoFBjrHifPDs+nM9tV0VMkOTl4JRMYNuKh/f5PauSjowTfvF3LogfkWdcPoxsA9UjqEOgjeYIbhb7Nw==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@smithy/signature-v4@2.0.16: resolution: {integrity: sha512-ilLY85xS2kZZzTb83diQKYLIYALvart0KnBaKnIRnMBHAGEio5aHSlANQoxVn0VsonwmQ3CnWhnCT0sERD8uTg==} engines: {node: '>=14.0.0'} @@ -16592,6 +17191,20 @@ packages: tslib: 2.8.1 dev: false + /@smithy/signature-v4@5.1.2: + resolution: {integrity: sha512-d3+U/VpX7a60seHziWnVZOHuEgJlclufjkS6zhXvxcJgkJq4UWdH5eOBLzHRMx6gXjsdT9h6lfpmLzbrdupHgQ==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/is-array-buffer': 4.0.0 + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + '@smithy/util-hex-encoding': 4.0.0 + '@smithy/util-middleware': 4.0.4 + '@smithy/util-uri-escape': 4.0.0 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + dev: false + /@smithy/smithy-client@2.1.16: resolution: {integrity: sha512-Lw67+yQSpLl4YkDLUzI2KgS8TXclXmbzSeOJUmRFS4ueT56B4pw3RZRF/SRzvgyxM/HxgkUan8oSHXCujPDafQ==} engines: {node: '>=14.0.0'} @@ -16615,6 +17228,19 @@ packages: tslib: 2.8.1 dev: false + /@smithy/smithy-client@4.4.5: + resolution: {integrity: sha512-+lynZjGuUFJaMdDYSTMnP/uPBBXXukVfrJlP+1U/Dp5SFTEI++w6NMga8DjOENxecOF71V9Z2DllaVDYRnGlkg==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/core': 3.6.0 + '@smithy/middleware-endpoint': 4.1.13 + '@smithy/middleware-stack': 4.0.4 + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + '@smithy/util-stream': 4.2.2 + tslib: 2.8.1 + dev: false + /@smithy/types@2.6.0: resolution: {integrity: sha512-PgqxJq2IcdMF9iAasxcqZqqoOXBHufEfmbEUdN1pmJrJltT42b0Sc8UiYSWWzKkciIp9/mZDpzYi4qYG1qqg6g==} engines: {node: '>=14.0.0'} @@ -16629,6 +17255,13 @@ packages: tslib: 2.8.1 dev: false + /@smithy/types@4.3.1: + resolution: {integrity: sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==} + engines: {node: '>=18.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + /@smithy/url-parser@2.0.14: resolution: {integrity: sha512-kbu17Y1AFXi5lNlySdDj7ZzmvupyWKCX/0jNZ8ffquRyGdbDZb+eBh0QnWqsSmnZa/ctyWaTf7n4l/pXLExrnw==} dependencies: @@ -16645,6 +17278,15 @@ packages: tslib: 2.8.1 dev: false + /@smithy/url-parser@4.0.4: + resolution: {integrity: sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/querystring-parser': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@smithy/util-base64@2.0.1: resolution: {integrity: sha512-DlI6XFYDMsIVN+GH9JtcRp3j02JEVuWIn/QOZisVzpIAprdsxGveFed0bjbMRCqmIFe8uetn5rxzNrBtIGrPIQ==} engines: {node: '>=14.0.0'} @@ -16662,6 +17304,15 @@ packages: tslib: 2.8.1 dev: false + /@smithy/util-base64@4.0.0: + resolution: {integrity: sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/util-buffer-from': 4.0.0 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + dev: false + /@smithy/util-body-length-browser@2.0.0: resolution: {integrity: sha512-JdDuS4ircJt+FDnaQj88TzZY3+njZ6O+D3uakS32f2VNnDo3vyEuNdBOh/oFd8Df1zSZOuH1HEChk2AOYDezZg==} dependencies: @@ -16674,6 +17325,13 @@ packages: tslib: 2.8.1 dev: false + /@smithy/util-body-length-browser@4.0.0: + resolution: {integrity: sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==} + engines: {node: '>=18.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + /@smithy/util-body-length-node@2.1.0: resolution: {integrity: sha512-/li0/kj/y3fQ3vyzn36NTLGmUwAICb7Jbe/CsWCktW363gh1MOcpEcSO3mJ344Gv2dqz8YJCLQpb6hju/0qOWw==} engines: {node: '>=14.0.0'} @@ -16688,6 +17346,13 @@ packages: tslib: 2.8.1 dev: false + /@smithy/util-body-length-node@4.0.0: + resolution: {integrity: sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==} + engines: {node: '>=18.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + /@smithy/util-buffer-from@2.0.0: resolution: {integrity: sha512-/YNnLoHsR+4W4Vf2wL5lGv0ksg8Bmk3GEGxn2vEQt52AQaPSCuaO5PM5VM7lP1K9qHRKHwrPGktqVoAHKWHxzw==} engines: {node: '>=14.0.0'} @@ -16704,6 +17369,14 @@ packages: tslib: 2.8.1 dev: false + /@smithy/util-buffer-from@4.0.0: + resolution: {integrity: sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/is-array-buffer': 4.0.0 + tslib: 2.8.1 + dev: false + /@smithy/util-config-provider@2.0.0: resolution: {integrity: sha512-xCQ6UapcIWKxXHEU4Mcs2s7LcFQRiU3XEluM2WcCjjBtQkUN71Tb+ydGmJFPxMUrW/GWMgQEEGipLym4XG0jZg==} engines: {node: '>=14.0.0'} @@ -16718,6 +17391,13 @@ packages: tslib: 2.8.1 dev: false + /@smithy/util-config-provider@4.0.0: + resolution: {integrity: sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==} + engines: {node: '>=18.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + /@smithy/util-defaults-mode-browser@2.0.20: resolution: {integrity: sha512-QJtnbTIl0/BbEASkx1MUFf6EaoWqWW1/IM90N++8NNscePvPf77GheYfpoPis6CBQawUWq8QepTP2QUSAdrVkw==} engines: {node: '>= 10.0.0'} @@ -16740,6 +17420,17 @@ packages: tslib: 2.8.1 dev: false + /@smithy/util-defaults-mode-browser@4.0.21: + resolution: {integrity: sha512-wM0jhTytgXu3wzJoIqpbBAG5U6BwiubZ6QKzSbP7/VbmF1v96xlAbX2Am/mz0Zep0NLvLh84JT0tuZnk3wmYQA==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/property-provider': 4.0.4 + '@smithy/smithy-client': 4.4.5 + '@smithy/types': 4.3.1 + bowser: 2.11.0 + tslib: 2.8.1 + dev: false + /@smithy/util-defaults-mode-node@2.0.26: resolution: {integrity: sha512-lGFPOFCHv1ql019oegYqa54BZH7HREw6EBqjDLbAr0wquMX0BDi2sg8TJ6Eq+JGLijkZbJB73m4+aK8OFAapMg==} engines: {node: '>= 10.0.0'} @@ -16766,6 +17457,19 @@ packages: tslib: 2.8.1 dev: false + /@smithy/util-defaults-mode-node@4.0.21: + resolution: {integrity: sha512-/F34zkoU0GzpUgLJydHY8Rxu9lBn8xQC/s/0M0U9lLBkYbA1htaAFjWYJzpzsbXPuri5D1H8gjp2jBum05qBrA==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/config-resolver': 4.1.4 + '@smithy/credential-provider-imds': 4.0.6 + '@smithy/node-config-provider': 4.1.3 + '@smithy/property-provider': 4.0.4 + '@smithy/smithy-client': 4.4.5 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@smithy/util-endpoints@1.0.5: resolution: {integrity: sha512-K7qNuCOD5K/90MjHvHm9kJldrfm40UxWYQxNEShMFxV/lCCCRIg8R4uu1PFAxRvPxNpIdcrh1uK6I1ISjDXZJw==} engines: {node: '>= 14.0.0'} @@ -16784,6 +17488,15 @@ packages: tslib: 2.8.1 dev: false + /@smithy/util-endpoints@3.0.6: + resolution: {integrity: sha512-YARl3tFL3WgPuLzljRUnrS2ngLiUtkwhQtj8PAL13XZSyUiNLQxwG3fBBq3QXFqGFUXepIN73pINp3y8c2nBmA==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/node-config-provider': 4.1.3 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@smithy/util-hex-encoding@2.0.0: resolution: {integrity: sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==} engines: {node: '>=14.0.0'} @@ -16798,6 +17511,13 @@ packages: tslib: 2.8.1 dev: false + /@smithy/util-hex-encoding@4.0.0: + resolution: {integrity: sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==} + engines: {node: '>=18.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + /@smithy/util-middleware@2.0.7: resolution: {integrity: sha512-tRINOTlf1G9B0ECarFQAtTgMhpnrMPSa+5j4ZEwEawCLfTFTavk6757sxhE4RY5RMlD/I3x+DCS8ZUiR8ho9Pw==} engines: {node: '>=14.0.0'} @@ -16814,6 +17534,14 @@ packages: tslib: 2.8.1 dev: false + /@smithy/util-middleware@4.0.4: + resolution: {integrity: sha512-9MLKmkBmf4PRb0ONJikCbCwORACcil6gUWojwARCClT7RmLzF04hUR4WdRprIXal7XVyrddadYNfp2eF3nrvtQ==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@smithy/util-retry@2.0.7: resolution: {integrity: sha512-fIe5yARaF0+xVT1XKcrdnHKTJ1Vc4+3e3tLDjCuIcE9b6fkBzzGFY7AFiX4M+vj6yM98DrwkuZeHf7/hmtVp0Q==} engines: {node: '>= 14.0.0'} @@ -16832,6 +17560,15 @@ packages: tslib: 2.8.1 dev: false + /@smithy/util-retry@4.0.6: + resolution: {integrity: sha512-+YekoF2CaSMv6zKrA6iI/N9yva3Gzn4L6n35Luydweu5MMPYpiGZlWqehPHDHyNbnyaYlz/WJyYAZnC+loBDZg==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/service-error-classification': 4.0.6 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@smithy/util-stream@2.0.21: resolution: {integrity: sha512-0BUE16d7n1x7pi1YluXJdB33jOTyBChT0j/BlOkFa9uxfg6YqXieHxjHNuCdJRARa7AZEj32LLLEPJ1fSa4inA==} engines: {node: '>=14.0.0'} @@ -16860,6 +17597,20 @@ packages: tslib: 2.8.1 dev: false + /@smithy/util-stream@4.2.2: + resolution: {integrity: sha512-aI+GLi7MJoVxg24/3J1ipwLoYzgkB4kUfogZfnslcYlynj3xsQ0e7vk4TnTro9hhsS5PvX1mwmkRqqHQjwcU7w==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/fetch-http-handler': 5.0.4 + '@smithy/node-http-handler': 4.0.6 + '@smithy/types': 4.3.1 + '@smithy/util-base64': 4.0.0 + '@smithy/util-buffer-from': 4.0.0 + '@smithy/util-hex-encoding': 4.0.0 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + dev: false + /@smithy/util-uri-escape@2.0.0: resolution: {integrity: sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw==} engines: {node: '>=14.0.0'} @@ -16874,6 +17625,13 @@ packages: tslib: 2.8.1 dev: false + /@smithy/util-uri-escape@4.0.0: + resolution: {integrity: sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==} + engines: {node: '>=18.0.0'} + dependencies: + tslib: 2.8.1 + dev: false + /@smithy/util-utf8@2.0.2: resolution: {integrity: sha512-qOiVORSPm6Ce4/Yu6hbSgNHABLP2VMv8QOC3tTDNHHlWY19pPyc++fBTbZPtx6egPXi4HQxKDnMxVxpbtX2GoA==} engines: {node: '>=14.0.0'} @@ -16890,6 +17648,14 @@ packages: tslib: 2.8.1 dev: false + /@smithy/util-utf8@4.0.0: + resolution: {integrity: sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/util-buffer-from': 4.0.0 + tslib: 2.8.1 + dev: false + /@smithy/util-waiter@3.2.0: resolution: {integrity: sha512-PpjSboaDUE6yl+1qlg3Si57++e84oXdWGbuFUSAciXsVfEZJJJupR2Nb0QuXHiunt2vGR+1PTizOMvnUPaG2Qg==} engines: {node: '>=16.0.0'} @@ -16899,6 +17665,15 @@ packages: tslib: 2.8.1 dev: false + /@smithy/util-waiter@4.0.6: + resolution: {integrity: sha512-slcr1wdRbX7NFphXZOxtxRNA7hXAAtJAXJDE/wdoMAos27SIquVCKiSqfB6/28YzQ8FCsB5NKkhdM5gMADbqxg==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/abort-controller': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@socket.io/component-emitter@3.1.0: resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} dev: false @@ -31360,7 +32135,7 @@ packages: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} requiresBuild: true dependencies: - tslib: 2.6.2 + tslib: 2.8.1 dev: true optional: true @@ -33553,6 +34328,7 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: false /tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} From eb9954572964d6031dcc52287ff9915573535008 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 30 Jun 2025 17:25:04 +0100 Subject: [PATCH 02/11] fresh auth token for each deploy --- .../app/v3/getDeploymentImageRef.server.ts | 39 ++++++++++++++++++- .../services/finalizeDeploymentV2.server.ts | 38 ++++++++++++++---- .../webapp/test/getDeploymentImageRef.test.ts | 38 +++++++++++++++++- 3 files changed, 106 insertions(+), 9 deletions(-) diff --git a/apps/webapp/app/v3/getDeploymentImageRef.server.ts b/apps/webapp/app/v3/getDeploymentImageRef.server.ts index f6a11c43a08..18a2fc80889 100644 --- a/apps/webapp/app/v3/getDeploymentImageRef.server.ts +++ b/apps/webapp/app/v3/getDeploymentImageRef.server.ts @@ -5,6 +5,7 @@ import { type Repository, type Tag, RepositoryNotFoundException, + GetAuthorizationTokenCommand, } from "@aws-sdk/client-ecr"; import { tryCatch } from "@trigger.dev/core"; import { logger } from "~/services/logger.server"; @@ -58,7 +59,7 @@ export async function getDeploymentImageRef({ }; } -function isEcrRegistry(registryHost: string) { +export function isEcrRegistry(registryHost: string) { return registryHost.includes("amazonaws.com"); } @@ -196,3 +197,39 @@ async function ensureEcrRepositoryExists({ return newRepo; } + +export async function getEcrAuthToken({ + registryHost, + registryId, +}: { + registryHost: string; + registryId?: string; +}): Promise<{ username: string; password: string }> { + const region = getEcrRegion(registryHost); + if (!region) { + logger.error("Invalid ECR registry host", { registryHost }); + throw new Error("Invalid ECR registry host"); + } + + const ecr = new ECRClient({ region }); + const response = await ecr.send( + new GetAuthorizationTokenCommand({ + registryIds: registryId ? [registryId] : undefined, + }) + ); + + if (!response.authorizationData) { + throw new Error("Failed to get ECR authorization token"); + } + + const authData = response.authorizationData[0]; + + if (!authData.authorizationToken) { + throw new Error("No authorization token returned from ECR"); + } + + const authToken = Buffer.from(authData.authorizationToken, "base64").toString(); + const [username, password] = authToken.split(":"); + + return { username, password }; +} diff --git a/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts b/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts index 810d0516804..3edc90bf4f1 100644 --- a/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts +++ b/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts @@ -9,6 +9,8 @@ import { env } from "~/env.server"; import { depot as execDepot } from "@depot/cli"; import { FinalizeDeploymentService } from "./finalizeDeployment.server"; import { remoteBuildsEnabled } from "../remoteImageBuilder.server"; +import { getEcrAuthToken, isEcrRegistry } from "../getDeploymentImageRef.server"; +import { tryCatch } from "@trigger.dev/core"; export class FinalizeDeploymentV2Service extends BaseService { public async call( @@ -172,11 +174,27 @@ async function executePushToRegistry( { depot, registry, deployment }: ExecutePushToRegistryOptions, writer?: WritableStreamDefaultWriter ): Promise { - // Step 1: We need to "login" to the digital ocean registry - const configDir = await ensureLoggedIntoDockerRegistry(registry.host, { - username: registry.username, - password: registry.password, - }); + // Step 1: We need to "login" to the registry + const [loginError, configDir] = await tryCatch( + ensureLoggedIntoDockerRegistry(registry.host, { + username: registry.username, + password: registry.password, + }) + ); + + if (loginError) { + logger.error("Failed to login to registry", { + deployment, + registryHost: registry.host, + error: loginError.message, + }); + + return { + ok: false as const, + error: "Failed to login to registry", + logs: "", + }; + } const imageTag = deployment.imageReference; @@ -244,12 +262,18 @@ async function executePushToRegistry( async function ensureLoggedIntoDockerRegistry( registryHost: string, - auth: { username: string; password: string } + auth: { username: string; password: string } | undefined = undefined ) { const tmpDir = await createTempDir(); - // Read the current docker config const dockerConfigPath = join(tmpDir, "config.json"); + // If this is an ECR registry, get fresh credentials + if (isEcrRegistry(registryHost)) { + auth = await getEcrAuthToken({ registryHost, registryId: env.DEPLOY_REGISTRY_ID }); + } else if (!auth) { + throw new Error("Authentication required for non-ECR registry"); + } + await writeJSONFile(dockerConfigPath, { auths: { [registryHost]: { diff --git a/apps/webapp/test/getDeploymentImageRef.test.ts b/apps/webapp/test/getDeploymentImageRef.test.ts index 86f25ce462a..0f45c36cf73 100644 --- a/apps/webapp/test/getDeploymentImageRef.test.ts +++ b/apps/webapp/test/getDeploymentImageRef.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "vitest"; -import { getDeploymentImageRef, getEcrRegion } from "../app/v3/getDeploymentImageRef.server"; +import { + getDeploymentImageRef, + getEcrAuthToken, + getEcrRegion, +} from "../app/v3/getDeploymentImageRef.server"; import { ECRClient, DeleteRepositoryCommand } from "@aws-sdk/client-ecr"; describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef", () => { @@ -101,3 +105,35 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef", ).rejects.toThrow("Invalid ECR registry host: invalid.ecr.amazonaws.com"); }); }); + +describe.skipIf(process.env.RUN_REGISTRY_AUTH_TESTS !== "1")("getEcrAuthToken", () => { + const registryId = process.env.DEPLOY_REGISTRY_ID; + const testHost = "123456789012.dkr.ecr.us-east-1.amazonaws.com"; + + it("should return valid ECR credentials", async () => { + const auth = await getEcrAuthToken({ + registryHost: testHost, + registryId, + }); + + // Check the structure and basic validation of the returned credentials + expect(auth).toHaveProperty("username"); + expect(auth).toHaveProperty("password"); + expect(auth.username).toBe("AWS"); + expect(typeof auth.password).toBe("string"); + expect(auth.password.length).toBeGreaterThan(0); + + // Verify the token format (should be a base64-encoded string) + const base64Regex = /^[A-Za-z0-9+/=]+$/; + expect(base64Regex.test(auth.password)).toBe(true); + }); + + it("should throw error for invalid region", async () => { + await expect( + getEcrAuthToken({ + registryHost: "invalid.ecr.amazonaws.com", + registryId, + }) + ).rejects.toThrow(); + }); +}); From 14cbc22c19486f04a69841aee3c47253ba3f23a9 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:26:21 +0100 Subject: [PATCH 03/11] optional assume role --- .../app/v3/getDeploymentImageRef.server.ts | 110 ++++- apps/webapp/package.json | 1 + .../webapp/test/getDeploymentImageRef.test.ts | 9 + pnpm-lock.yaml | 389 +++++++++++++++++- 4 files changed, 501 insertions(+), 8 deletions(-) diff --git a/apps/webapp/app/v3/getDeploymentImageRef.server.ts b/apps/webapp/app/v3/getDeploymentImageRef.server.ts index 18a2fc80889..cbfa41aeba8 100644 --- a/apps/webapp/app/v3/getDeploymentImageRef.server.ts +++ b/apps/webapp/app/v3/getDeploymentImageRef.server.ts @@ -7,9 +7,91 @@ import { RepositoryNotFoundException, GetAuthorizationTokenCommand, } from "@aws-sdk/client-ecr"; +import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts"; import { tryCatch } from "@trigger.dev/core"; import { logger } from "~/services/logger.server"; +// Optional configuration for cross-account access +export type CrossAccountConfig = { + assumeRole: boolean; + roleName: string; +}; + +const DEFAULT_CROSS_ACCOUNT_CONFIG: CrossAccountConfig = { + assumeRole: false, + roleName: "OrganizationAccountAccessRole", +}; + +async function getAssumedRoleCredentials( + region: string, + accountId: string, + config: CrossAccountConfig +): Promise<{ + accessKeyId: string; + secretAccessKey: string; + sessionToken: string; +}> { + const sts = new STSClient({ region }); + const roleArn = `arn:aws:iam::${accountId}:role/${config.roleName}`; + + // Generate a unique session name using timestamp and random string + // This helps with debugging but doesn't affect concurrent sessions + const timestamp = Date.now(); + const randomSuffix = Math.random().toString(36).substring(2, 8); + const sessionName = `TriggerWebappECRAccess_${timestamp}_${randomSuffix}`; + + try { + const response = await sts.send( + new AssumeRoleCommand({ + RoleArn: roleArn, + RoleSessionName: sessionName, + // Sessions automatically expire after 1 hour + // AWS allows 5000 concurrent sessions by default + DurationSeconds: 3600, + }) + ); + + if (!response.Credentials) { + throw new Error("STS: No credentials returned from assumed role"); + } + + if ( + !response.Credentials.AccessKeyId || + !response.Credentials.SecretAccessKey || + !response.Credentials.SessionToken + ) { + throw new Error("STS: Invalid credentials returned from assumed role"); + } + + return { + accessKeyId: response.Credentials.AccessKeyId, + secretAccessKey: response.Credentials.SecretAccessKey, + sessionToken: response.Credentials.SessionToken, + }; + } catch (error) { + logger.error("Failed to assume role", { roleArn, sessionName, error }); + throw error; + } +} + +async function createEcrClient( + region: string, + registryId?: string, + crossAccountConfig: CrossAccountConfig = DEFAULT_CROSS_ACCOUNT_CONFIG +) { + // If no registryId or role assumption is disabled, use default credentials + if (!registryId || !crossAccountConfig.assumeRole) { + return new ECRClient({ region }); + } + + // Get credentials for cross-account access + const credentials = await getAssumedRoleCredentials(region, registryId, crossAccountConfig); + return new ECRClient({ + region, + credentials, + }); +} + export async function getDeploymentImageRef({ host, namespace, @@ -18,6 +100,7 @@ export async function getDeploymentImageRef({ environmentSlug, registryId, registryTags, + crossAccountConfig, }: { host: string; namespace: string; @@ -26,6 +109,7 @@ export async function getDeploymentImageRef({ environmentSlug: string; registryId?: string; registryTags?: string; + crossAccountConfig?: CrossAccountConfig; }): Promise<{ imageRef: string; isEcr: boolean; @@ -41,7 +125,13 @@ export async function getDeploymentImageRef({ } const [ecrRepoError] = await tryCatch( - ensureEcrRepositoryExists({ repositoryName, registryHost: host, registryId, registryTags }) + ensureEcrRepositoryExists({ + repositoryName, + registryHost: host, + registryId, + registryTags, + crossAccountConfig, + }) ); if (ecrRepoError) { @@ -75,13 +165,15 @@ async function createEcrRepository({ region, registryId, registryTags, + crossAccountConfig, }: { repositoryName: string; region: string; registryId?: string; registryTags?: string; + crossAccountConfig?: CrossAccountConfig; }): Promise { - const ecr = new ECRClient({ region }); + const ecr = await createEcrClient(region, registryId, crossAccountConfig); const result = await ecr.send( new CreateRepositoryCommand({ @@ -107,12 +199,14 @@ async function getEcrRepository({ repositoryName, region, registryId, + crossAccountConfig, }: { repositoryName: string; region: string; registryId?: string; + crossAccountConfig?: CrossAccountConfig; }): Promise { - const ecr = new ECRClient({ region }); + const ecr = await createEcrClient(region, registryId, crossAccountConfig); try { const result = await ecr.send( @@ -153,11 +247,13 @@ async function ensureEcrRepositoryExists({ registryHost, registryId, registryTags, + crossAccountConfig, }: { repositoryName: string; registryHost: string; registryId?: string; registryTags?: string; + crossAccountConfig?: CrossAccountConfig; }): Promise { const region = getEcrRegion(registryHost); @@ -166,7 +262,7 @@ async function ensureEcrRepositoryExists({ } const [getRepoError, existingRepo] = await tryCatch( - getEcrRepository({ repositoryName, region, registryId }) + getEcrRepository({ repositoryName, region, registryId, crossAccountConfig }) ); if (getRepoError) { @@ -180,7 +276,7 @@ async function ensureEcrRepositoryExists({ } const [createRepoError, newRepo] = await tryCatch( - createEcrRepository({ repositoryName, region, registryId, registryTags }) + createEcrRepository({ repositoryName, region, registryId, registryTags, crossAccountConfig }) ); if (createRepoError) { @@ -201,9 +297,11 @@ async function ensureEcrRepositoryExists({ export async function getEcrAuthToken({ registryHost, registryId, + crossAccountConfig, }: { registryHost: string; registryId?: string; + crossAccountConfig?: CrossAccountConfig; }): Promise<{ username: string; password: string }> { const region = getEcrRegion(registryHost); if (!region) { @@ -211,7 +309,7 @@ export async function getEcrAuthToken({ throw new Error("Invalid ECR registry host"); } - const ecr = new ECRClient({ region }); + const ecr = await createEcrClient(region, registryId, crossAccountConfig); const response = await ecr.send( new GetAuthorizationTokenCommand({ registryIds: registryId ? [registryId] : undefined, diff --git a/apps/webapp/package.json b/apps/webapp/package.json index eeca24a135b..6d0e2b24075 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -35,6 +35,7 @@ "@ariakit/react-core": "^0.4.6", "@aws-sdk/client-ecr": "^3.839.0", "@aws-sdk/client-sqs": "^3.445.0", + "@aws-sdk/client-sts": "^3.840.0", "@codemirror/autocomplete": "^6.3.1", "@codemirror/commands": "^6.1.2", "@codemirror/lang-javascript": "^6.1.1", diff --git a/apps/webapp/test/getDeploymentImageRef.test.ts b/apps/webapp/test/getDeploymentImageRef.test.ts index 0f45c36cf73..c11b19aa909 100644 --- a/apps/webapp/test/getDeploymentImageRef.test.ts +++ b/apps/webapp/test/getDeploymentImageRef.test.ts @@ -14,6 +14,13 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef", const registryId = process.env.DEPLOY_REGISTRY_ID; const registryTags = "test=test,test2=test2"; + const assumeRole = process.env.ASSUME_ROLE === "1"; + + const crossAccountConfig = { + assumeRole, + roleName: "OrganizationAccountAccessRole", + }; + // Clean up test repository after tests afterAll(async () => { if (!registryId) { @@ -65,6 +72,7 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef", environmentSlug: "test", registryId, registryTags, + crossAccountConfig, }); expect(imageRef.imageRef).toBe( @@ -83,6 +91,7 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef", environmentSlug: "prod", registryId, registryTags, + crossAccountConfig, }); expect(imageRef.imageRef).toBe( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a123d626cc..bbe53940ca6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -200,6 +200,9 @@ importers: '@aws-sdk/client-sqs': specifier: ^3.445.0 version: 3.454.0 + '@aws-sdk/client-sts': + specifier: ^3.840.0 + version: 3.840.0 '@codemirror/autocomplete': specifier: ^6.3.1 version: 6.4.0(@codemirror/language@6.3.2)(@codemirror/state@6.2.0)(@codemirror/view@6.7.2)(@lezer/common@1.2.3) @@ -2953,7 +2956,7 @@ packages: '@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.821.0 + '@aws-sdk/types': 3.840.0 '@aws-sdk/util-locate-window': 3.310.0 '@smithy/util-utf8': 2.0.2 tslib: 2.8.1 @@ -2972,7 +2975,7 @@ packages: engines: {node: '>=16.0.0'} dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.821.0 + '@aws-sdk/types': 3.840.0 tslib: 2.8.1 dev: false @@ -3337,6 +3340,52 @@ packages: - aws-crt dev: false + /@aws-sdk/client-sso@3.840.0: + resolution: {integrity: sha512-3Zp+FWN2hhmKdpS0Ragi5V2ZPsZNScE3jlbgoJjzjI/roHZqO+e3/+XFN4TlM0DsPKYJNp+1TAjmhxN6rOnfYA==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.840.0 + '@aws-sdk/middleware-host-header': 3.840.0 + '@aws-sdk/middleware-logger': 3.840.0 + '@aws-sdk/middleware-recursion-detection': 3.840.0 + '@aws-sdk/middleware-user-agent': 3.840.0 + '@aws-sdk/region-config-resolver': 3.840.0 + '@aws-sdk/types': 3.840.0 + '@aws-sdk/util-endpoints': 3.840.0 + '@aws-sdk/util-user-agent-browser': 3.840.0 + '@aws-sdk/util-user-agent-node': 3.840.0 + '@smithy/config-resolver': 4.1.4 + '@smithy/core': 3.6.0 + '@smithy/fetch-http-handler': 5.0.4 + '@smithy/hash-node': 4.0.4 + '@smithy/invalid-dependency': 4.0.4 + '@smithy/middleware-content-length': 4.0.4 + '@smithy/middleware-endpoint': 4.1.13 + '@smithy/middleware-retry': 4.1.14 + '@smithy/middleware-serde': 4.0.8 + '@smithy/middleware-stack': 4.0.4 + '@smithy/node-config-provider': 4.1.3 + '@smithy/node-http-handler': 4.0.6 + '@smithy/protocol-http': 5.1.2 + '@smithy/smithy-client': 4.4.5 + '@smithy/types': 4.3.1 + '@smithy/url-parser': 4.0.4 + '@smithy/util-base64': 4.0.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-body-length-node': 4.0.0 + '@smithy/util-defaults-mode-browser': 4.0.21 + '@smithy/util-defaults-mode-node': 4.0.21 + '@smithy/util-endpoints': 3.0.6 + '@smithy/util-middleware': 4.0.4 + '@smithy/util-retry': 4.0.6 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/client-sts@3.454.0: resolution: {integrity: sha512-0fDvr8WeB6IYO8BUCzcivWmahgGl/zDbaYfakzGnt4mrl5ztYaXE875WI6b7+oFcKMRvN+KLvwu5TtyFuNY+GQ==} engines: {node: '>=14.0.0'} @@ -3433,6 +3482,53 @@ packages: - aws-crt dev: false + /@aws-sdk/client-sts@3.840.0: + resolution: {integrity: sha512-h+mu89Wk81Ne+B624GT/pBM5VjuAZueSeQNixhgtQ1QHi6bZzrpz8+lvMSibKO+kXFyQsTLzkyibbxnhLpWQZA==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.840.0 + '@aws-sdk/credential-provider-node': 3.840.0 + '@aws-sdk/middleware-host-header': 3.840.0 + '@aws-sdk/middleware-logger': 3.840.0 + '@aws-sdk/middleware-recursion-detection': 3.840.0 + '@aws-sdk/middleware-user-agent': 3.840.0 + '@aws-sdk/region-config-resolver': 3.840.0 + '@aws-sdk/types': 3.840.0 + '@aws-sdk/util-endpoints': 3.840.0 + '@aws-sdk/util-user-agent-browser': 3.840.0 + '@aws-sdk/util-user-agent-node': 3.840.0 + '@smithy/config-resolver': 4.1.4 + '@smithy/core': 3.6.0 + '@smithy/fetch-http-handler': 5.0.4 + '@smithy/hash-node': 4.0.4 + '@smithy/invalid-dependency': 4.0.4 + '@smithy/middleware-content-length': 4.0.4 + '@smithy/middleware-endpoint': 4.1.13 + '@smithy/middleware-retry': 4.1.14 + '@smithy/middleware-serde': 4.0.8 + '@smithy/middleware-stack': 4.0.4 + '@smithy/node-config-provider': 4.1.3 + '@smithy/node-http-handler': 4.0.6 + '@smithy/protocol-http': 5.1.2 + '@smithy/smithy-client': 4.4.5 + '@smithy/types': 4.3.1 + '@smithy/url-parser': 4.0.4 + '@smithy/util-base64': 4.0.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-body-length-node': 4.0.0 + '@smithy/util-defaults-mode-browser': 4.0.21 + '@smithy/util-defaults-mode-node': 4.0.21 + '@smithy/util-endpoints': 3.0.6 + '@smithy/util-middleware': 4.0.4 + '@smithy/util-retry': 4.0.6 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/core@3.451.0: resolution: {integrity: sha512-SamWW2zHEf1ZKe3j1w0Piauryl8BQIlej0TBS18A4ACzhjhWXhCs13bO1S88LvPR5mBFXok3XOT6zPOnKDFktw==} engines: {node: '>=14.0.0'} @@ -3479,6 +3575,27 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/core@3.840.0: + resolution: {integrity: sha512-x3Zgb39tF1h2XpU+yA4OAAQlW6LVEfXNlSedSYJ7HGKXqA/E9h3rWQVpYfhXXVVsLdYXdNw5KBUkoAoruoZSZA==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/types': 3.840.0 + '@aws-sdk/xml-builder': 3.821.0 + '@smithy/core': 3.6.0 + '@smithy/node-config-provider': 4.1.3 + '@smithy/property-provider': 4.0.4 + '@smithy/protocol-http': 5.1.2 + '@smithy/signature-v4': 5.1.2 + '@smithy/smithy-client': 4.4.5 + '@smithy/types': 4.3.1 + '@smithy/util-base64': 4.0.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-middleware': 4.0.4 + '@smithy/util-utf8': 4.0.0 + fast-xml-parser: 4.4.1 + tslib: 2.8.1 + dev: false + /@aws-sdk/credential-provider-env@3.451.0: resolution: {integrity: sha512-9dAav7DcRgaF7xCJEQR5ER9ErXxnu/tdnVJ+UPmb1NPeIZdESv1A3lxFDEq1Fs8c4/lzAj9BpshGyJVIZwZDKg==} engines: {node: '>=14.0.0'} @@ -3511,6 +3628,17 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/credential-provider-env@3.840.0: + resolution: {integrity: sha512-EzF6VcJK7XvQ/G15AVEfJzN2mNXU8fcVpXo4bRyr1S6t2q5zx6UPH/XjDbn18xyUmOq01t+r8gG+TmHEVo18fA==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/core': 3.840.0 + '@aws-sdk/types': 3.840.0 + '@smithy/property-provider': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@aws-sdk/credential-provider-http@3.716.0: resolution: {integrity: sha512-CZ04pl2z7igQPysQyH2xKZHM3fLwkemxQbKOlje3TmiS1NwXvcKvERhp9PE/H23kOL7beTM19NMRog/Fka/rlw==} engines: {node: '>=16.0.0'} @@ -3543,6 +3671,22 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/credential-provider-http@3.840.0: + resolution: {integrity: sha512-wbnUiPGLVea6mXbUh04fu+VJmGkQvmToPeTYdHE8eRZq3NRDi3t3WltT+jArLBKD/4NppRpMjf2ju4coMCz91g==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/core': 3.840.0 + '@aws-sdk/types': 3.840.0 + '@smithy/fetch-http-handler': 5.0.4 + '@smithy/node-http-handler': 4.0.6 + '@smithy/property-provider': 4.0.4 + '@smithy/protocol-http': 5.1.2 + '@smithy/smithy-client': 4.4.5 + '@smithy/types': 4.3.1 + '@smithy/util-stream': 4.2.2 + tslib: 2.8.1 + dev: false + /@aws-sdk/credential-provider-ini@3.451.0: resolution: {integrity: sha512-TySt64Ci5/ZbqFw1F9Z0FIGvYx5JSC9e6gqDnizIYd8eMnn8wFRUscRrD7pIHKfrhvVKN5h0GdYovmMO/FMCBw==} engines: {node: '>=14.0.0'} @@ -3606,6 +3750,27 @@ packages: - aws-crt dev: false + /@aws-sdk/credential-provider-ini@3.840.0: + resolution: {integrity: sha512-7F290BsWydShHb+7InXd+IjJc3mlEIm9I0R57F/Pjl1xZB69MdkhVGCnuETWoBt4g53ktJd6NEjzm/iAhFXFmw==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/core': 3.840.0 + '@aws-sdk/credential-provider-env': 3.840.0 + '@aws-sdk/credential-provider-http': 3.840.0 + '@aws-sdk/credential-provider-process': 3.840.0 + '@aws-sdk/credential-provider-sso': 3.840.0 + '@aws-sdk/credential-provider-web-identity': 3.840.0 + '@aws-sdk/nested-clients': 3.840.0 + '@aws-sdk/types': 3.840.0 + '@smithy/credential-provider-imds': 4.0.6 + '@smithy/property-provider': 4.0.4 + '@smithy/shared-ini-file-loader': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/credential-provider-node@3.451.0: resolution: {integrity: sha512-AEwM1WPyxUdKrKyUsKyFqqRFGU70e4qlDyrtBxJnSU9NRLZI8tfEZ67bN7fHSxBUBODgDXpMSlSvJiBLh5/3pw==} engines: {node: '>=14.0.0'} @@ -3667,6 +3832,26 @@ packages: - aws-crt dev: false + /@aws-sdk/credential-provider-node@3.840.0: + resolution: {integrity: sha512-KufP8JnxA31wxklLm63evUPSFApGcH8X86z3mv9SRbpCm5ycgWIGVCTXpTOdgq6rPZrwT9pftzv2/b4mV/9clg==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.840.0 + '@aws-sdk/credential-provider-http': 3.840.0 + '@aws-sdk/credential-provider-ini': 3.840.0 + '@aws-sdk/credential-provider-process': 3.840.0 + '@aws-sdk/credential-provider-sso': 3.840.0 + '@aws-sdk/credential-provider-web-identity': 3.840.0 + '@aws-sdk/types': 3.840.0 + '@smithy/credential-provider-imds': 4.0.6 + '@smithy/property-provider': 4.0.4 + '@smithy/shared-ini-file-loader': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/credential-provider-process@3.451.0: resolution: {integrity: sha512-HQywSdKeD5PErcLLnZfSyCJO+6T+ZyzF+Lm/QgscSC+CbSUSIPi//s15qhBRVely/3KBV6AywxwNH+5eYgt4lQ==} engines: {node: '>=14.0.0'} @@ -3702,6 +3887,18 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/credential-provider-process@3.840.0: + resolution: {integrity: sha512-HkDQWHy8tCI4A0Ps2NVtuVYMv9cB4y/IuD/TdOsqeRIAT12h8jDb98BwQPNLAImAOwOWzZJ8Cu0xtSpX7CQhMw==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/core': 3.840.0 + '@aws-sdk/types': 3.840.0 + '@smithy/property-provider': 4.0.4 + '@smithy/shared-ini-file-loader': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@aws-sdk/credential-provider-sso@3.451.0: resolution: {integrity: sha512-Usm/N51+unOt8ID4HnQzxIjUJDrkAQ1vyTOC0gSEEJ7h64NSSPGD5yhN7il5WcErtRd3EEtT1a8/GTC5TdBctg==} engines: {node: '>=14.0.0'} @@ -3750,6 +3947,22 @@ packages: - aws-crt dev: false + /@aws-sdk/credential-provider-sso@3.840.0: + resolution: {integrity: sha512-2qgdtdd6R0Z1y0KL8gzzwFUGmhBHSUx4zy85L2XV1CXhpRNwV71SVWJqLDVV5RVWVf9mg50Pm3AWrUC0xb0pcA==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/client-sso': 3.840.0 + '@aws-sdk/core': 3.840.0 + '@aws-sdk/token-providers': 3.840.0 + '@aws-sdk/types': 3.840.0 + '@smithy/property-provider': 4.0.4 + '@smithy/shared-ini-file-loader': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/credential-provider-web-identity@3.451.0: resolution: {integrity: sha512-Xtg3Qw65EfDjWNG7o2xD6sEmumPfsy3WDGjk2phEzVg8s7hcZGxf5wYwe6UY7RJvlEKrU0rFA+AMn6Hfj5oOzg==} engines: {node: '>=14.0.0'} @@ -3788,6 +4001,20 @@ packages: - aws-crt dev: false + /@aws-sdk/credential-provider-web-identity@3.840.0: + resolution: {integrity: sha512-dpEeVXG8uNZSmVXReE4WP0lwoioX2gstk4RnUgrdUE3YaPq8A+hJiVAyc3h+cjDeIqfbsQbZm9qFetKC2LF9dQ==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/core': 3.840.0 + '@aws-sdk/nested-clients': 3.840.0 + '@aws-sdk/types': 3.840.0 + '@smithy/property-provider': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/middleware-host-header@3.451.0: resolution: {integrity: sha512-j8a5jAfhWmsK99i2k8oR8zzQgXrsJtgrLxc3js6U+525mcZytoiDndkWTmD5fjJ1byU1U2E5TaPq+QJeDip05Q==} engines: {node: '>=14.0.0'} @@ -3818,6 +4045,16 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/middleware-host-header@3.840.0: + resolution: {integrity: sha512-ub+hXJAbAje94+Ya6c6eL7sYujoE8D4Bumu1NUI8TXjUhVVn0HzVWQjpRLshdLsUp1AW7XyeJaxyajRaJQ8+Xg==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/types': 3.840.0 + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@aws-sdk/middleware-logger@3.451.0: resolution: {integrity: sha512-0kHrYEyVeB2QBfP6TfbI240aRtatLZtcErJbhpiNUb+CQPgEL3crIjgVE8yYiJumZ7f0jyjo8HLPkwD1/2APaw==} engines: {node: '>=14.0.0'} @@ -3845,6 +4082,15 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/middleware-logger@3.840.0: + resolution: {integrity: sha512-lSV8FvjpdllpGaRspywss4CtXV8M7NNNH+2/j86vMH+YCOZ6fu2T/TyFd/tHwZ92vDfHctWkRbQxg0bagqwovA==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/types': 3.840.0 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@aws-sdk/middleware-recursion-detection@3.451.0: resolution: {integrity: sha512-J6jL6gJ7orjHGM70KDRcCP7so/J2SnkN4vZ9YRLTeeZY6zvBuHDjX8GCIgSqPn/nXFXckZO8XSnA7u6+3TAT0w==} engines: {node: '>=14.0.0'} @@ -3875,6 +4121,16 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/middleware-recursion-detection@3.840.0: + resolution: {integrity: sha512-Gu7lGDyfddyhIkj1Z1JtrY5NHb5+x/CRiB87GjaSrKxkDaydtX2CU977JIABtt69l9wLbcGDIQ+W0uJ5xPof7g==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/types': 3.840.0 + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@aws-sdk/middleware-sdk-sqs@3.451.0: resolution: {integrity: sha512-GXpFSc9Ji4IAT/OaTkmnDrxzZrrAsJctUAC9vihpgGDof79A1Oz4R+r2uBMTKGxCIFkqyYW5Se7y2ijjTNa3ZA==} engines: {node: '>=14.0.0'} @@ -3946,6 +4202,19 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/middleware-user-agent@3.840.0: + resolution: {integrity: sha512-hiiMf7BP5ZkAFAvWRcK67Mw/g55ar7OCrvrynC92hunx/xhMkrgSLM0EXIZ1oTn3uql9kH/qqGF0nqsK6K555A==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/core': 3.840.0 + '@aws-sdk/types': 3.840.0 + '@aws-sdk/util-endpoints': 3.840.0 + '@smithy/core': 3.6.0 + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@aws-sdk/nested-clients@3.839.0: resolution: {integrity: sha512-Glic0pg2THYP3aRhJORwJJBe1JLtJoEdWV/MFZNyzCklfMwEzpWtZAyxy+tQyFmMeW50uBAnh2R0jhMMcf257w==} engines: {node: '>=18.0.0'} @@ -3992,6 +4261,52 @@ packages: - aws-crt dev: false + /@aws-sdk/nested-clients@3.840.0: + resolution: {integrity: sha512-LXYYo9+n4hRqnRSIMXLBb+BLz+cEmjMtTudwK1BF6Bn2RfdDv29KuyeDRrPCS3TwKl7ZKmXUmE9n5UuHAPfBpA==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.840.0 + '@aws-sdk/middleware-host-header': 3.840.0 + '@aws-sdk/middleware-logger': 3.840.0 + '@aws-sdk/middleware-recursion-detection': 3.840.0 + '@aws-sdk/middleware-user-agent': 3.840.0 + '@aws-sdk/region-config-resolver': 3.840.0 + '@aws-sdk/types': 3.840.0 + '@aws-sdk/util-endpoints': 3.840.0 + '@aws-sdk/util-user-agent-browser': 3.840.0 + '@aws-sdk/util-user-agent-node': 3.840.0 + '@smithy/config-resolver': 4.1.4 + '@smithy/core': 3.6.0 + '@smithy/fetch-http-handler': 5.0.4 + '@smithy/hash-node': 4.0.4 + '@smithy/invalid-dependency': 4.0.4 + '@smithy/middleware-content-length': 4.0.4 + '@smithy/middleware-endpoint': 4.1.13 + '@smithy/middleware-retry': 4.1.14 + '@smithy/middleware-serde': 4.0.8 + '@smithy/middleware-stack': 4.0.4 + '@smithy/node-config-provider': 4.1.3 + '@smithy/node-http-handler': 4.0.6 + '@smithy/protocol-http': 5.1.2 + '@smithy/smithy-client': 4.4.5 + '@smithy/types': 4.3.1 + '@smithy/url-parser': 4.0.4 + '@smithy/util-base64': 4.0.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-body-length-node': 4.0.0 + '@smithy/util-defaults-mode-browser': 4.0.21 + '@smithy/util-defaults-mode-node': 4.0.21 + '@smithy/util-endpoints': 3.0.6 + '@smithy/util-middleware': 4.0.4 + '@smithy/util-retry': 4.0.6 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/region-config-resolver@3.451.0: resolution: {integrity: sha512-3iMf4OwzrFb4tAAmoROXaiORUk2FvSejnHIw/XHvf/jjR4EqGGF95NZP/n/MeFZMizJWVssrwS412GmoEyoqhg==} engines: {node: '>=14.0.0'} @@ -4027,6 +4342,18 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/region-config-resolver@3.840.0: + resolution: {integrity: sha512-Qjnxd/yDv9KpIMWr90ZDPtRj0v75AqGC92Lm9+oHXZ8p1MjG5JE2CW0HL8JRgK9iKzgKBL7pPQRXI8FkvEVfrA==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/types': 3.840.0 + '@smithy/node-config-provider': 4.1.3 + '@smithy/types': 4.3.1 + '@smithy/util-config-provider': 4.0.0 + '@smithy/util-middleware': 4.0.4 + tslib: 2.8.1 + dev: false + /@aws-sdk/token-providers@3.451.0: resolution: {integrity: sha512-ij1L5iUbn6CwxVOT1PG4NFjsrsKN9c4N1YEM0lkl6DwmaNOscjLKGSNyj9M118vSWsOs1ZDbTwtj++h0O/BWrQ==} engines: {node: '>=14.0.0'} @@ -4101,6 +4428,21 @@ packages: - aws-crt dev: false + /@aws-sdk/token-providers@3.840.0: + resolution: {integrity: sha512-6BuTOLTXvmgwjK7ve7aTg9JaWFdM5UoMolLVPMyh3wTv9Ufalh8oklxYHUBIxsKkBGO2WiHXytveuxH6tAgTYg==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/core': 3.840.0 + '@aws-sdk/nested-clients': 3.840.0 + '@aws-sdk/types': 3.840.0 + '@smithy/property-provider': 4.0.4 + '@smithy/shared-ini-file-loader': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + dev: false + /@aws-sdk/types@3.451.0: resolution: {integrity: sha512-rhK+qeYwCIs+laJfWCcrYEjay2FR/9VABZJ2NRM89jV/fKqGVQR52E5DQqrI+oEIL5JHMhhnr4N4fyECMS35lw==} engines: {node: '>=14.0.0'} @@ -4125,6 +4467,14 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/types@3.840.0: + resolution: {integrity: sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA==} + engines: {node: '>=18.0.0'} + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@aws-sdk/util-endpoints@3.451.0: resolution: {integrity: sha512-giqLGBTnRIcKkDqwU7+GQhKbtJ5Ku35cjGQIfMyOga6pwTBUbaK0xW1Sdd8sBQ1GhApscnChzI9o/R9x0368vw==} engines: {node: '>=14.0.0'} @@ -4154,6 +4504,16 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/util-endpoints@3.840.0: + resolution: {integrity: sha512-eqE9ROdg/Kk0rj3poutyRCFauPDXIf/WSvCqFiRDDVi6QOnCv/M0g2XW8/jSvkJlOyaXkNCptapIp6BeeFFGYw==} + engines: {node: '>=18.0.0'} + dependencies: + '@aws-sdk/types': 3.840.0 + '@smithy/types': 4.3.1 + '@smithy/util-endpoints': 3.0.6 + tslib: 2.8.1 + dev: false + /@aws-sdk/util-locate-window@3.310.0: resolution: {integrity: sha512-qo2t/vBTnoXpjKxlsC2e1gBrRm80M3bId27r0BRB2VniSSe7bL1mmzM+/HFtujm0iAxtPM+aLEflLJlJeDPg0w==} engines: {node: '>=14.0.0'} @@ -4188,6 +4548,15 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/util-user-agent-browser@3.840.0: + resolution: {integrity: sha512-JdyZM3EhhL4PqwFpttZu1afDpPJCCc3eyZOLi+srpX11LsGj6sThf47TYQN75HT1CarZ7cCdQHGzP2uy3/xHfQ==} + dependencies: + '@aws-sdk/types': 3.840.0 + '@smithy/types': 4.3.1 + bowser: 2.11.0 + tslib: 2.8.1 + dev: false + /@aws-sdk/util-user-agent-node@3.451.0: resolution: {integrity: sha512-TBzm6P+ql4mkGFAjPlO1CI+w3yUT+NulaiALjl/jNX/nnUp6HsJsVxJf4nVFQTG5KRV0iqMypcs7I3KIhH+LmA==} engines: {node: '>=14.0.0'} @@ -4235,6 +4604,22 @@ packages: tslib: 2.8.1 dev: false + /@aws-sdk/util-user-agent-node@3.840.0: + resolution: {integrity: sha512-Fy5JUEDQU1tPm2Yw/YqRYYc27W5+QD/J4mYvQvdWjUGZLB5q3eLFMGD35Uc28ZFoGMufPr4OCxK/bRfWROBRHQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + dependencies: + '@aws-sdk/middleware-user-agent': 3.840.0 + '@aws-sdk/types': 3.840.0 + '@smithy/node-config-provider': 4.1.3 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + dev: false + /@aws-sdk/util-utf8-browser@3.259.0: resolution: {integrity: sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==} dependencies: From ef87110b0ebc3cad7c5d8a8486649ea614a6d27e Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 2 Jul 2025 13:22:38 +0100 Subject: [PATCH 04/11] log when machine overrides enabled --- apps/webapp/app/services/platform.v3.server.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/webapp/app/services/platform.v3.server.ts b/apps/webapp/app/services/platform.v3.server.ts index dbc9acc2650..928ace65920 100644 --- a/apps/webapp/app/services/platform.v3.server.ts +++ b/apps/webapp/app/services/platform.v3.server.ts @@ -102,6 +102,8 @@ function initializeMachinePresets(): { }; } + logger.info("🎛️ Overriding machine presets", { overrides }); + return { defaultMachine: overrideDefaultMachine(defaultMachineFromPlatform, overrides.defaultMachine), machines: overrideMachines(machinesFromPlatform, overrides.machines), From 372c7ce1e60a7a51c377040df2a2d422c308bc28 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 2 Jul 2025 13:22:53 +0100 Subject: [PATCH 05/11] make test repo namespace configurable --- apps/webapp/test/getDeploymentImageRef.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/test/getDeploymentImageRef.test.ts b/apps/webapp/test/getDeploymentImageRef.test.ts index c11b19aa909..83882f83f4e 100644 --- a/apps/webapp/test/getDeploymentImageRef.test.ts +++ b/apps/webapp/test/getDeploymentImageRef.test.ts @@ -8,8 +8,8 @@ import { ECRClient, DeleteRepositoryCommand } from "@aws-sdk/client-ecr"; describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef", () => { const testHost = "123456789012.dkr.ecr.us-east-1.amazonaws.com"; - const testNamespace = "test-namespace"; - const testProjectRef = "test-project-" + Math.random().toString(36).substring(7); + const testNamespace = process.env.DEPLOY_REGISTRY_NAMESPACE || "test-namespace"; + const testProjectRef = "proj_test_" + Math.random().toString(36).substring(7); const registryId = process.env.DEPLOY_REGISTRY_ID; const registryTags = "test=test,test2=test2"; From 512d6827f9f5b47e5295d4a2b3a29aaff6e400ab Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:31:17 +0100 Subject: [PATCH 06/11] assume role fix and env var changes --- apps/webapp/app/env.server.ts | 8 +- .../app/v3/getDeploymentImageRef.server.ts | 136 +++++++++--------- .../services/finalizeDeploymentV2.server.ts | 8 +- .../services/initializeDeployment.server.ts | 7 +- .../webapp/test/getDeploymentImageRef.test.ts | 80 +++++++---- 5 files changed, 142 insertions(+), 97 deletions(-) diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 56fd6cddacf..4ece590f6c2 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -228,17 +228,21 @@ const EnvironmentSchema = z.object({ DEPOT_TOKEN: z.string().optional(), DEPOT_ORG_ID: z.string().optional(), DEPOT_REGION: z.string().default("us-east-1"), + + // Deployment registry DEPLOY_REGISTRY_HOST: z.string().min(1), DEPLOY_REGISTRY_USERNAME: z.string().optional(), DEPLOY_REGISTRY_PASSWORD: z.string().optional(), DEPLOY_REGISTRY_NAMESPACE: z.string().min(1).default("trigger"), - DEPLOY_REGISTRY_ID: z.string().optional(), - DEPLOY_REGISTRY_TAGS: z.string().optional(), // csv, for example: "key1=value1,key2=value2" + DEPLOY_REGISTRY_ECR_TAGS: z.string().optional(), // csv, for example: "key1=value1,key2=value2" + DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN: z.string().optional(), + DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID: z.string().optional(), DEPLOY_IMAGE_PLATFORM: z.string().default("linux/amd64"), DEPLOY_TIMEOUT_MS: z.coerce .number() .int() .default(60 * 1000 * 8), // 8 minutes + OBJECT_STORE_BASE_URL: z.string().optional(), OBJECT_STORE_ACCESS_KEY_ID: z.string().optional(), OBJECT_STORE_SECRET_ACCESS_KEY: z.string().optional(), diff --git a/apps/webapp/app/v3/getDeploymentImageRef.server.ts b/apps/webapp/app/v3/getDeploymentImageRef.server.ts index cbfa41aeba8..f403ba242cd 100644 --- a/apps/webapp/app/v3/getDeploymentImageRef.server.ts +++ b/apps/webapp/app/v3/getDeploymentImageRef.server.ts @@ -12,27 +12,23 @@ import { tryCatch } from "@trigger.dev/core"; import { logger } from "~/services/logger.server"; // Optional configuration for cross-account access -export type CrossAccountConfig = { - assumeRole: boolean; - roleName: string; +export type AssumeRoleConfig = { + roleArn?: string; + externalId?: string; }; -const DEFAULT_CROSS_ACCOUNT_CONFIG: CrossAccountConfig = { - assumeRole: false, - roleName: "OrganizationAccountAccessRole", -}; - -async function getAssumedRoleCredentials( - region: string, - accountId: string, - config: CrossAccountConfig -): Promise<{ +async function getAssumedRoleCredentials({ + region, + assumeRole, +}: { + region: string; + assumeRole?: AssumeRoleConfig; +}): Promise<{ accessKeyId: string; secretAccessKey: string; sessionToken: string; }> { const sts = new STSClient({ region }); - const roleArn = `arn:aws:iam::${accountId}:role/${config.roleName}`; // Generate a unique session name using timestamp and random string // This helps with debugging but doesn't affect concurrent sessions @@ -43,11 +39,12 @@ async function getAssumedRoleCredentials( try { const response = await sts.send( new AssumeRoleCommand({ - RoleArn: roleArn, + RoleArn: assumeRole?.roleArn, RoleSessionName: sessionName, // Sessions automatically expire after 1 hour // AWS allows 5000 concurrent sessions by default DurationSeconds: 3600, + ExternalId: assumeRole?.externalId, }) ); @@ -69,23 +66,28 @@ async function getAssumedRoleCredentials( sessionToken: response.Credentials.SessionToken, }; } catch (error) { - logger.error("Failed to assume role", { roleArn, sessionName, error }); + logger.error("Failed to assume role", { + assumeRole, + sessionName, + error, + }); throw error; } } -async function createEcrClient( - region: string, - registryId?: string, - crossAccountConfig: CrossAccountConfig = DEFAULT_CROSS_ACCOUNT_CONFIG -) { - // If no registryId or role assumption is disabled, use default credentials - if (!registryId || !crossAccountConfig.assumeRole) { +export async function createEcrClient({ + region, + assumeRole, +}: { + region: string; + assumeRole?: AssumeRoleConfig; +}) { + if (!assumeRole) { return new ECRClient({ region }); } // Get credentials for cross-account access - const credentials = await getAssumedRoleCredentials(region, registryId, crossAccountConfig); + const credentials = await getAssumedRoleCredentials({ region, assumeRole }); return new ECRClient({ region, credentials, @@ -98,18 +100,16 @@ export async function getDeploymentImageRef({ projectRef, nextVersion, environmentSlug, - registryId, registryTags, - crossAccountConfig, + assumeRole, }: { host: string; namespace: string; projectRef: string; nextVersion: string; environmentSlug: string; - registryId?: string; registryTags?: string; - crossAccountConfig?: CrossAccountConfig; + assumeRole?: AssumeRoleConfig; }): Promise<{ imageRef: string; isEcr: boolean; @@ -128,9 +128,8 @@ export async function getDeploymentImageRef({ ensureEcrRepositoryExists({ repositoryName, registryHost: host, - registryId, registryTags, - crossAccountConfig, + assumeRole, }) ); @@ -163,17 +162,17 @@ function parseRegistryTags(tags: string): Tag[] { async function createEcrRepository({ repositoryName, region, - registryId, + accountId, registryTags, - crossAccountConfig, + assumeRole, }: { repositoryName: string; region: string; - registryId?: string; + accountId?: string; registryTags?: string; - crossAccountConfig?: CrossAccountConfig; + assumeRole?: AssumeRoleConfig; }): Promise { - const ecr = await createEcrClient(region, registryId, crossAccountConfig); + const ecr = await createEcrClient({ region, assumeRole }); const result = await ecr.send( new CreateRepositoryCommand({ @@ -182,7 +181,7 @@ async function createEcrRepository({ encryptionConfiguration: { encryptionType: "AES256", }, - registryId, + registryId: accountId, tags: registryTags ? parseRegistryTags(registryTags) : undefined, }) ); @@ -198,21 +197,21 @@ async function createEcrRepository({ async function getEcrRepository({ repositoryName, region, - registryId, - crossAccountConfig, + accountId, + assumeRole, }: { repositoryName: string; region: string; - registryId?: string; - crossAccountConfig?: CrossAccountConfig; + accountId?: string; + assumeRole?: AssumeRoleConfig; }): Promise { - const ecr = await createEcrClient(region, registryId, crossAccountConfig); + const ecr = await createEcrClient({ region, assumeRole }); try { const result = await ecr.send( new DescribeRepositoriesCommand({ repositoryNames: [repositoryName], - registryId, + registryId: accountId, }) ); @@ -234,35 +233,46 @@ async function getEcrRepository({ } } -export function getEcrRegion(registryHost: string): string | undefined { +export type EcrRegistryComponents = { + accountId: string; + region: string; +}; + +export function parseEcrRegistryDomain(registryHost: string): EcrRegistryComponents { const parts = registryHost.split("."); - if (parts.length !== 6 || parts[1] !== "dkr" || parts[2] !== "ecr") { - return undefined; + + const isValid = + parts.length === 6 && + parts[1] === "dkr" && + parts[2] === "ecr" && + parts[4] === "amazonaws" && + parts[5] === "com"; + + if (!isValid) { + throw new Error(`Invalid ECR registry host: ${registryHost}`); } - return parts[3]; + + return { + accountId: parts[0], + region: parts[3], + }; } async function ensureEcrRepositoryExists({ repositoryName, registryHost, - registryId, registryTags, - crossAccountConfig, + assumeRole, }: { repositoryName: string; registryHost: string; - registryId?: string; registryTags?: string; - crossAccountConfig?: CrossAccountConfig; + assumeRole?: AssumeRoleConfig; }): Promise { - const region = getEcrRegion(registryHost); - - if (!region) { - throw new Error(`Invalid ECR registry host: ${registryHost}`); - } + const { region, accountId } = parseEcrRegistryDomain(registryHost); const [getRepoError, existingRepo] = await tryCatch( - getEcrRepository({ repositoryName, region, registryId, crossAccountConfig }) + getEcrRepository({ repositoryName, region, accountId, assumeRole }) ); if (getRepoError) { @@ -276,7 +286,7 @@ async function ensureEcrRepositoryExists({ } const [createRepoError, newRepo] = await tryCatch( - createEcrRepository({ repositoryName, region, registryId, registryTags, crossAccountConfig }) + createEcrRepository({ repositoryName, region, accountId, registryTags, assumeRole }) ); if (createRepoError) { @@ -296,23 +306,21 @@ async function ensureEcrRepositoryExists({ export async function getEcrAuthToken({ registryHost, - registryId, - crossAccountConfig, + assumeRole, }: { registryHost: string; - registryId?: string; - crossAccountConfig?: CrossAccountConfig; + assumeRole?: AssumeRoleConfig; }): Promise<{ username: string; password: string }> { - const region = getEcrRegion(registryHost); + const { region, accountId } = parseEcrRegistryDomain(registryHost); if (!region) { logger.error("Invalid ECR registry host", { registryHost }); throw new Error("Invalid ECR registry host"); } - const ecr = await createEcrClient(region, registryId, crossAccountConfig); + const ecr = await createEcrClient({ region, assumeRole }); const response = await ecr.send( new GetAuthorizationTokenCommand({ - registryIds: registryId ? [registryId] : undefined, + registryIds: accountId ? [accountId] : undefined, }) ); diff --git a/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts b/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts index 3edc90bf4f1..e15c3d2c453 100644 --- a/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts +++ b/apps/webapp/app/v3/services/finalizeDeploymentV2.server.ts @@ -269,7 +269,13 @@ async function ensureLoggedIntoDockerRegistry( // If this is an ECR registry, get fresh credentials if (isEcrRegistry(registryHost)) { - auth = await getEcrAuthToken({ registryHost, registryId: env.DEPLOY_REGISTRY_ID }); + auth = await getEcrAuthToken({ + registryHost, + assumeRole: { + roleArn: env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN, + externalId: env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID, + }, + }); } else if (!auth) { throw new Error("Authentication required for non-ECR registry"); } diff --git a/apps/webapp/app/v3/services/initializeDeployment.server.ts b/apps/webapp/app/v3/services/initializeDeployment.server.ts index 0627ffe75e5..73965e5b57d 100644 --- a/apps/webapp/app/v3/services/initializeDeployment.server.ts +++ b/apps/webapp/app/v3/services/initializeDeployment.server.ts @@ -76,8 +76,11 @@ export class InitializeDeploymentService extends BaseService { projectRef: environment.project.externalRef, nextVersion, environmentSlug: environment.slug, - registryId: env.DEPLOY_REGISTRY_ID, - registryTags: env.DEPLOY_REGISTRY_TAGS, + registryTags: env.DEPLOY_REGISTRY_ECR_TAGS, + assumeRole: { + roleArn: env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN, + externalId: env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID, + }, }) ); diff --git a/apps/webapp/test/getDeploymentImageRef.test.ts b/apps/webapp/test/getDeploymentImageRef.test.ts index 83882f83f4e..e3f9e2a7a31 100644 --- a/apps/webapp/test/getDeploymentImageRef.test.ts +++ b/apps/webapp/test/getDeploymentImageRef.test.ts @@ -1,43 +1,39 @@ import { describe, expect, it } from "vitest"; import { + createEcrClient, getDeploymentImageRef, getEcrAuthToken, - getEcrRegion, + parseEcrRegistryDomain, } from "../app/v3/getDeploymentImageRef.server"; -import { ECRClient, DeleteRepositoryCommand } from "@aws-sdk/client-ecr"; +import { DeleteRepositoryCommand } from "@aws-sdk/client-ecr"; describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef", () => { - const testHost = "123456789012.dkr.ecr.us-east-1.amazonaws.com"; + const testHost = + process.env.DEPLOY_REGISTRY_HOST || "123456789012.dkr.ecr.us-east-1.amazonaws.com"; const testNamespace = process.env.DEPLOY_REGISTRY_NAMESPACE || "test-namespace"; const testProjectRef = "proj_test_" + Math.random().toString(36).substring(7); - const registryId = process.env.DEPLOY_REGISTRY_ID; - const registryTags = "test=test,test2=test2"; - - const assumeRole = process.env.ASSUME_ROLE === "1"; - - const crossAccountConfig = { - assumeRole, - roleName: "OrganizationAccountAccessRole", + const registryTags = process.env.DEPLOY_REGISTRY_ECR_TAGS || "test=test,test2=test2"; + const roleArn = process.env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN; + const externalId = process.env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID; + const assumeRole = { + roleArn, + externalId, }; // Clean up test repository after tests afterAll(async () => { - if (!registryId) { - return; - } - if (process.env.KEEP_TEST_REPO === "1") { return; } try { - const region = getEcrRegion(testHost); - const ecr = new ECRClient({ region }); + const { region, accountId } = parseEcrRegistryDomain(testHost); + const ecr = await createEcrClient({ region, assumeRole }); await ecr.send( new DeleteRepositoryCommand({ repositoryName: `${testNamespace}/${testProjectRef}`, - registryId, + registryId: accountId, force: true, }) ); @@ -53,8 +49,8 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef", projectRef: testProjectRef, nextVersion: "20250630.1", environmentSlug: "test", - registryId, registryTags, + assumeRole, }); expect(imageRef.imageRef).toBe( @@ -70,9 +66,8 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef", projectRef: testProjectRef, nextVersion: "20250630.1", environmentSlug: "test", - registryId, registryTags, - crossAccountConfig, + assumeRole, }); expect(imageRef.imageRef).toBe( @@ -89,9 +84,8 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef", projectRef: testProjectRef, nextVersion: "20250630.2", environmentSlug: "prod", - registryId, registryTags, - crossAccountConfig, + assumeRole, }); expect(imageRef.imageRef).toBe( @@ -108,21 +102,28 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef", projectRef: testProjectRef, nextVersion: "20250630.1", environmentSlug: "test", - registryId, registryTags, + assumeRole, }) ).rejects.toThrow("Invalid ECR registry host: invalid.ecr.amazonaws.com"); }); }); describe.skipIf(process.env.RUN_REGISTRY_AUTH_TESTS !== "1")("getEcrAuthToken", () => { - const registryId = process.env.DEPLOY_REGISTRY_ID; - const testHost = "123456789012.dkr.ecr.us-east-1.amazonaws.com"; + const testHost = + process.env.DEPLOY_REGISTRY_HOST || "123456789012.dkr.ecr.us-east-1.amazonaws.com"; + + const roleArn = process.env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN; + const externalId = process.env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID; + const assumeRole = { + roleArn, + externalId, + }; it("should return valid ECR credentials", async () => { const auth = await getEcrAuthToken({ registryHost: testHost, - registryId, + assumeRole, }); // Check the structure and basic validation of the returned credentials @@ -141,8 +142,31 @@ describe.skipIf(process.env.RUN_REGISTRY_AUTH_TESTS !== "1")("getEcrAuthToken", await expect( getEcrAuthToken({ registryHost: "invalid.ecr.amazonaws.com", - registryId, + assumeRole, }) ).rejects.toThrow(); }); }); + +describe("parseEcrRegistry", () => { + it("should correctly parse a valid ECR registry host", () => { + const result = parseEcrRegistryDomain("123456789012.dkr.ecr.us-east-1.amazonaws.com"); + expect(result).toEqual({ + accountId: "123456789012", + region: "us-east-1", + }); + }); + + it("should handle invalid ECR registry hosts", () => { + const invalidHosts = [ + "invalid.ecr.amazonaws.com", + "registry.hub.docker.com", + "123456789012.dkr.ecr.us-east-1.not-amazon.com", + "123456789012.wrong.ecr.us-east-1.amazonaws.com", + ]; + + for (const host of invalidHosts) { + expect(() => parseEcrRegistryDomain(host)).toThrow("Invalid ECR registry host"); + } + }); +}); From 2eae336f6102a41cffa8766b3dc5f9ec43805564 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:50:20 +0100 Subject: [PATCH 07/11] improve ecr check --- apps/webapp/app/v3/getDeploymentImageRef.server.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/v3/getDeploymentImageRef.server.ts b/apps/webapp/app/v3/getDeploymentImageRef.server.ts index f403ba242cd..720630ad031 100644 --- a/apps/webapp/app/v3/getDeploymentImageRef.server.ts +++ b/apps/webapp/app/v3/getDeploymentImageRef.server.ts @@ -149,7 +149,12 @@ export async function getDeploymentImageRef({ } export function isEcrRegistry(registryHost: string) { - return registryHost.includes("amazonaws.com"); + try { + parseEcrRegistryDomain(registryHost); + return true; + } catch { + return false; + } } function parseRegistryTags(tags: string): Tag[] { From 1c82f7244288315e7f617d6847e6dcf165df61d5 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:58:59 +0100 Subject: [PATCH 08/11] improve tag parsing --- .../app/v3/getDeploymentImageRef.server.ts | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/v3/getDeploymentImageRef.server.ts b/apps/webapp/app/v3/getDeploymentImageRef.server.ts index 720630ad031..2a3fd00accd 100644 --- a/apps/webapp/app/v3/getDeploymentImageRef.server.ts +++ b/apps/webapp/app/v3/getDeploymentImageRef.server.ts @@ -158,10 +158,34 @@ export function isEcrRegistry(registryHost: string) { } function parseRegistryTags(tags: string): Tag[] { - return tags.split(",").map((tag) => { - const [key, value] = tag.split("="); - return { Key: key, Value: value }; - }); + if (!tags) { + return []; + } + + return tags + .split(",") + .map((t) => { + const tag = t.trim(); + if (tag.length === 0) { + return null; + } + + // If there's no '=' in the tag, treat the whole tag as the key with an empty value + const equalIndex = tag.indexOf("="); + const key = equalIndex === -1 ? tag : tag.slice(0, equalIndex); + const value = equalIndex === -1 ? "" : tag.slice(equalIndex + 1); + + if (key.trim().length === 0) { + logger.warn("Invalid ECR tag format (empty key), skipping tag", { tag: t }); + return null; + } + + return { + Key: key.trim(), + Value: value.trim(), + } as Tag; + }) + .filter((tag): tag is Tag => tag !== null); } async function createEcrRepository({ From e65875d126b0ead58dba01449a7553819227f2e3 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 2 Jul 2025 17:05:30 +0100 Subject: [PATCH 09/11] tag parsing tests --- .../webapp/test/getDeploymentImageRef.test.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/apps/webapp/test/getDeploymentImageRef.test.ts b/apps/webapp/test/getDeploymentImageRef.test.ts index e3f9e2a7a31..b3b9ca57128 100644 --- a/apps/webapp/test/getDeploymentImageRef.test.ts +++ b/apps/webapp/test/getDeploymentImageRef.test.ts @@ -170,3 +170,46 @@ describe("parseEcrRegistry", () => { } }); }); + +describe("parseRegistryTags", () => { + it("should handle empty or null input", () => { + expect(parseRegistryTags("")).toEqual([]); + expect(parseRegistryTags(",,,")).toEqual([]); + }); + + it("should parse key-only tags", () => { + expect(parseRegistryTags("key1,key2")).toEqual([ + { Key: "key1", Value: "" }, + { Key: "key2", Value: "" }, + ]); + }); + + it("should parse key-value tags", () => { + expect(parseRegistryTags("key1=value1,key2=value2")).toEqual([ + { Key: "key1", Value: "value1" }, + { Key: "key2", Value: "value2" }, + ]); + }); + + it("should handle mixed key-only and key-value tags", () => { + expect(parseRegistryTags("key1,key2=value2,key3")).toEqual([ + { Key: "key1", Value: "" }, + { Key: "key2", Value: "value2" }, + { Key: "key3", Value: "" }, + ]); + }); + + it("should handle whitespace", () => { + expect(parseRegistryTags(" key1 , key2 = value2 ")).toEqual([ + { Key: "key1", Value: "" }, + { Key: "key2", Value: "value2" }, + ]); + }); + + it("should skip invalid tags", () => { + expect(parseRegistryTags("=value,key1,=,key2=value2")).toEqual([ + { Key: "key1", Value: "" }, + { Key: "key2", Value: "value2" }, + ]); + }); +}); From ba68d19a74377b6c08345575dd450372aa2cc81b Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 2 Jul 2025 17:10:28 +0100 Subject: [PATCH 10/11] track if repo created and fix test --- .../app/v3/getDeploymentImageRef.server.ts | 19 +++++-- .../services/initializeDeployment.server.ts | 3 +- .../webapp/test/getDeploymentImageRef.test.ts | 53 ++++++++++++++----- 3 files changed, 57 insertions(+), 18 deletions(-) diff --git a/apps/webapp/app/v3/getDeploymentImageRef.server.ts b/apps/webapp/app/v3/getDeploymentImageRef.server.ts index 2a3fd00accd..9a770cf15d6 100644 --- a/apps/webapp/app/v3/getDeploymentImageRef.server.ts +++ b/apps/webapp/app/v3/getDeploymentImageRef.server.ts @@ -113,6 +113,7 @@ export async function getDeploymentImageRef({ }): Promise<{ imageRef: string; isEcr: boolean; + repoCreated: boolean; }> { const repositoryName = `${namespace}/${projectRef}`; const imageRef = `${host}/${repositoryName}:${nextVersion}.${environmentSlug}`; @@ -121,10 +122,11 @@ export async function getDeploymentImageRef({ return { imageRef, isEcr: false, + repoCreated: false, }; } - const [ecrRepoError] = await tryCatch( + const [ecrRepoError, ecrData] = await tryCatch( ensureEcrRepositoryExists({ repositoryName, registryHost: host, @@ -145,6 +147,7 @@ export async function getDeploymentImageRef({ return { imageRef, isEcr: true, + repoCreated: ecrData.repoCreated, }; } @@ -157,7 +160,7 @@ export function isEcrRegistry(registryHost: string) { } } -function parseRegistryTags(tags: string): Tag[] { +export function parseRegistryTags(tags: string): Tag[] { if (!tags) { return []; } @@ -297,7 +300,7 @@ async function ensureEcrRepositoryExists({ registryHost: string; registryTags?: string; assumeRole?: AssumeRoleConfig; -}): Promise { +}): Promise<{ repo: Repository; repoCreated: boolean }> { const { region, accountId } = parseEcrRegistryDomain(registryHost); const [getRepoError, existingRepo] = await tryCatch( @@ -311,7 +314,10 @@ async function ensureEcrRepositoryExists({ if (existingRepo) { logger.debug("ECR repository already exists", { repositoryName, region, existingRepo }); - return existingRepo; + return { + repo: existingRepo, + repoCreated: false, + }; } const [createRepoError, newRepo] = await tryCatch( @@ -330,7 +336,10 @@ async function ensureEcrRepositoryExists({ ); } - return newRepo; + return { + repo: newRepo, + repoCreated: true, + }; } export async function getEcrAuthToken({ diff --git a/apps/webapp/app/v3/services/initializeDeployment.server.ts b/apps/webapp/app/v3/services/initializeDeployment.server.ts index 73965e5b57d..6462c14c244 100644 --- a/apps/webapp/app/v3/services/initializeDeployment.server.ts +++ b/apps/webapp/app/v3/services/initializeDeployment.server.ts @@ -96,7 +96,7 @@ export class InitializeDeploymentService extends BaseService { throw new ServiceValidationError("Failed to get deployment image ref"); } - const { imageRef, isEcr } = imageRefResult; + const { imageRef, isEcr, repoCreated } = imageRefResult; logger.debug("Creating deployment", { environmentId: environment.id, @@ -106,6 +106,7 @@ export class InitializeDeploymentService extends BaseService { type: payload.type, imageRef, isEcr, + repoCreated, }); const deployment = await this._prisma.workerDeployment.create({ diff --git a/apps/webapp/test/getDeploymentImageRef.test.ts b/apps/webapp/test/getDeploymentImageRef.test.ts index b3b9ca57128..01a04c53a0d 100644 --- a/apps/webapp/test/getDeploymentImageRef.test.ts +++ b/apps/webapp/test/getDeploymentImageRef.test.ts @@ -4,6 +4,7 @@ import { getDeploymentImageRef, getEcrAuthToken, parseEcrRegistryDomain, + parseRegistryTags, } from "../app/v3/getDeploymentImageRef.server"; import { DeleteRepositoryCommand } from "@aws-sdk/client-ecr"; @@ -12,6 +13,7 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef", process.env.DEPLOY_REGISTRY_HOST || "123456789012.dkr.ecr.us-east-1.amazonaws.com"; const testNamespace = process.env.DEPLOY_REGISTRY_NAMESPACE || "test-namespace"; const testProjectRef = "proj_test_" + Math.random().toString(36).substring(7); + const testProjectRef2 = testProjectRef + "_2"; const registryTags = process.env.DEPLOY_REGISTRY_ECR_TAGS || "test=test,test2=test2"; const roleArn = process.env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN; @@ -30,13 +32,23 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef", try { const { region, accountId } = parseEcrRegistryDomain(testHost); const ecr = await createEcrClient({ region, assumeRole }); - await ecr.send( - new DeleteRepositoryCommand({ - repositoryName: `${testNamespace}/${testProjectRef}`, - registryId: accountId, - force: true, - }) - ); + + await Promise.all([ + ecr.send( + new DeleteRepositoryCommand({ + repositoryName: `${testNamespace}/${testProjectRef}`, + registryId: accountId, + force: true, + }) + ), + ecr.send( + new DeleteRepositoryCommand({ + repositoryName: `${testNamespace}/${testProjectRef2}`, + registryId: accountId, + force: true, + }) + ), + ]); } catch (error) { console.warn("Failed to delete test repository:", error); } @@ -60,20 +72,37 @@ describe.skipIf(process.env.RUN_REGISTRY_TESTS !== "1")("getDeploymentImageRef", }); it("should create ECR repository and return correct image ref", async () => { - const imageRef = await getDeploymentImageRef({ + const imageRef1 = await getDeploymentImageRef({ host: testHost, namespace: testNamespace, - projectRef: testProjectRef, + projectRef: testProjectRef2, nextVersion: "20250630.1", environmentSlug: "test", registryTags, assumeRole, }); - expect(imageRef.imageRef).toBe( - `${testHost}/${testNamespace}/${testProjectRef}:20250630.1.test` + expect(imageRef1.imageRef).toBe( + `${testHost}/${testNamespace}/${testProjectRef2}:20250630.1.test` ); - expect(imageRef.isEcr).toBe(true); + expect(imageRef1.isEcr).toBe(true); + expect(imageRef1.repoCreated).toBe(true); + + const imageRef2 = await getDeploymentImageRef({ + host: testHost, + namespace: testNamespace, + projectRef: testProjectRef2, + nextVersion: "20250630.2", + environmentSlug: "test", + registryTags, + assumeRole, + }); + + expect(imageRef2.imageRef).toBe( + `${testHost}/${testNamespace}/${testProjectRef2}:20250630.2.test` + ); + expect(imageRef2.isEcr).toBe(true); + expect(imageRef2.repoCreated).toBe(false); }); it("should reuse existing ECR repository", async () => { From 210984a9eadd2d348555439295943d8a82ae7710 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Thu, 3 Jul 2025 14:12:56 +0100 Subject: [PATCH 11/11] missing tryCatch for sts call --- .../app/v3/getDeploymentImageRef.server.ts | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/apps/webapp/app/v3/getDeploymentImageRef.server.ts b/apps/webapp/app/v3/getDeploymentImageRef.server.ts index 9a770cf15d6..c6610146ea2 100644 --- a/apps/webapp/app/v3/getDeploymentImageRef.server.ts +++ b/apps/webapp/app/v3/getDeploymentImageRef.server.ts @@ -36,8 +36,8 @@ async function getAssumedRoleCredentials({ const randomSuffix = Math.random().toString(36).substring(2, 8); const sessionName = `TriggerWebappECRAccess_${timestamp}_${randomSuffix}`; - try { - const response = await sts.send( + const [error, response] = await tryCatch( + sts.send( new AssumeRoleCommand({ RoleArn: assumeRole?.roleArn, RoleSessionName: sessionName, @@ -46,33 +46,35 @@ async function getAssumedRoleCredentials({ DurationSeconds: 3600, ExternalId: assumeRole?.externalId, }) - ); - - if (!response.Credentials) { - throw new Error("STS: No credentials returned from assumed role"); - } - - if ( - !response.Credentials.AccessKeyId || - !response.Credentials.SecretAccessKey || - !response.Credentials.SessionToken - ) { - throw new Error("STS: Invalid credentials returned from assumed role"); - } + ) + ); - return { - accessKeyId: response.Credentials.AccessKeyId, - secretAccessKey: response.Credentials.SecretAccessKey, - sessionToken: response.Credentials.SessionToken, - }; - } catch (error) { + if (error) { logger.error("Failed to assume role", { assumeRole, sessionName, - error, + error: error.message, }); throw error; } + + if (!response.Credentials) { + throw new Error("STS: No credentials returned from assumed role"); + } + + if ( + !response.Credentials.AccessKeyId || + !response.Credentials.SecretAccessKey || + !response.Credentials.SessionToken + ) { + throw new Error("STS: Invalid credentials returned from assumed role"); + } + + return { + accessKeyId: response.Credentials.AccessKeyId, + secretAccessKey: response.Credentials.SecretAccessKey, + sessionToken: response.Credentials.SessionToken, + }; } export async function createEcrClient({