diff --git a/agent/src/browser.py b/agent/src/browser.py new file mode 100644 index 0000000..714eefe --- /dev/null +++ b/agent/src/browser.py @@ -0,0 +1,63 @@ +"""Browser screenshot functions for AgentCore BrowserCustom. + +Best-effort (fail-open): all operations are wrapped in try/except +so a Browser API outage never blocks the agent pipeline. +""" + +import json +import os + +_lambda_client = None + + +def _get_lambda_client(): + """Lazy-init and cache the Lambda client.""" + global _lambda_client + if _lambda_client is not None: + return _lambda_client + import boto3 + + region = os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION") + if not region: + raise ValueError("AWS_REGION or AWS_DEFAULT_REGION must be set") + _lambda_client = boto3.client("lambda", region_name=region) + return _lambda_client + + +def capture_screenshot(url: str, task_id: str = "") -> str | None: + """Invoke browser-tool Lambda to capture a screenshot. Returns pre-signed URL or None.""" + function_name = os.environ.get("BROWSER_TOOL_FUNCTION_NAME") + if not function_name: + return None + try: + client = _get_lambda_client() + except ValueError as e: + print(f"[browser] [ERROR] Configuration error: {e}", flush=True) + return None + try: + payload = json.dumps({"action": "screenshot", "url": url, "taskId": task_id}) + response = client.invoke( + FunctionName=function_name, + InvocationType="RequestResponse", + Payload=payload, + ) + if "FunctionError" in response: + error_payload = json.loads(response["Payload"].read()) + print( + f"[browser] [ERROR] Lambda function crashed: " + f"{response['FunctionError']} — {error_payload}", + flush=True, + ) + return None + result = json.loads(response["Payload"].read()) + if result.get("status") == "success": + print(f"[browser] Screenshot captured: {result.get('screenshotS3Key')}", flush=True) + return result.get("presignedUrl") + print(f"[browser] Screenshot failed: {result.get('error', 'unknown')}", flush=True) + return None + except Exception as e: + print( + f"[browser] [WARN] capture_screenshot failed (transient): {type(e).__name__}: {e}", + flush=True, + ) + return None diff --git a/agent/src/models.py b/agent/src/models.py index 0e4f8f1..69ce097 100644 --- a/agent/src/models.py +++ b/agent/src/models.py @@ -167,3 +167,4 @@ class TaskResult(BaseModel): output_tokens: int | None = None cache_read_input_tokens: int | None = None cache_creation_input_tokens: int | None = None + screenshot_urls: list[str] = Field(default_factory=list) diff --git a/agent/src/pipeline.py b/agent/src/pipeline.py index b1dcafc..332421f 100644 --- a/agent/src/pipeline.py +++ b/agent/src/pipeline.py @@ -328,6 +328,21 @@ def run_task( pr_url = ensure_pr( config, setup, build_passed, lint_passed, agent_result=agent_result ) + # Screenshot capture (fail-open) + screenshot_urls: list[str] = [] + if pr_url: + from post_hooks import _append_screenshots_to_pr, capture_pr_screenshots + + try: + screenshot_urls = capture_pr_screenshots(pr_url, config.task_id) + if screenshot_urls: + _append_screenshots_to_pr(config, setup, screenshot_urls) + except Exception as exc: + log( + "WARN", + f"Screenshot capture failed (non-fatal): {type(exc).__name__}: {exc}", + ) + post_span.set_attribute("build.passed", build_passed) post_span.set_attribute("lint.passed", lint_passed) post_span.set_attribute("pr.url", pr_url or "") @@ -398,6 +413,7 @@ def run_task( output_tokens=usage.output_tokens if usage else None, cache_read_input_tokens=usage.cache_read_input_tokens if usage else None, cache_creation_input_tokens=usage.cache_creation_input_tokens if usage else None, + screenshot_urls=screenshot_urls, ) result_dict = result.model_dump() diff --git a/agent/src/post_hooks.py b/agent/src/post_hooks.py index e66ae9b..8cf3de6 100644 --- a/agent/src/post_hooks.py +++ b/agent/src/post_hooks.py @@ -327,6 +327,86 @@ def ensure_pr( return None +def capture_pr_screenshots(pr_url: str, task_id: str = "") -> list[str]: + """Capture screenshot of PR page. Returns list of pre-signed URLs (fail-open).""" + from browser import capture_screenshot + + if not pr_url or not pr_url.startswith("https://github.com/"): + return [] + try: + url = capture_screenshot(pr_url, task_id) + return [url] if url else [] + except Exception as e: + log("WARN", f"PR screenshot capture failed (non-fatal): {type(e).__name__}: {e}") + return [] + + +def _append_screenshots_to_pr( + config: TaskConfig, + setup: RepoSetup, + screenshot_urls: list[str], +) -> None: + """Append ## Screenshots section to PR body via gh pr edit.""" + if not screenshot_urls: + return + try: + result = subprocess.run( + [ + "gh", + "pr", + "view", + setup.branch, + "--repo", + config.repo_url, + "--json", + "body", + "-q", + ".body", + ], + cwd=setup.repo_dir, + capture_output=True, + text=True, + timeout=30, + ) + if result.returncode != 0: + log("WARN", "Could not read PR body for screenshot append") + return + + current_body = result.stdout.strip() + images_md = "\n".join( + f"![Screenshot {i + 1}]({url})" for i, url in enumerate(screenshot_urls) + ) + screenshots_section = f"## Screenshots\n\n{images_md}" + + if re.search(r"## Screenshots", current_body): + updated_body = re.sub( + r"## Screenshots\n.*?(?=\n## |\Z)", + screenshots_section, + current_body, + flags=re.DOTALL, + ) + else: + updated_body = f"{current_body}\n\n{screenshots_section}" + + edit_result = subprocess.run( + ["gh", "pr", "edit", setup.branch, "--repo", config.repo_url, "--body", updated_body], + cwd=setup.repo_dir, + capture_output=True, + text=True, + timeout=30, + ) + if edit_result.returncode == 0: + log("POST", f"Appended {len(screenshot_urls)} screenshot(s) to PR body") + else: + log( + "WARN", + f"gh pr edit failed (rc={edit_result.returncode}): " + f"{edit_result.stderr.strip()[:200]}", + ) + except Exception as e: + log("WARN", f"Failed to append screenshots to PR: {type(e).__name__}: {e}") + + def _extract_agent_notes(repo_dir: str, branch: str, config: TaskConfig) -> str | None: """Extract the "## Agent notes" section from the PR body. diff --git a/agent/tests/test_browser.py b/agent/tests/test_browser.py new file mode 100644 index 0000000..9d8b8be --- /dev/null +++ b/agent/tests/test_browser.py @@ -0,0 +1,82 @@ +"""Unit tests for browser.py screenshot functions.""" + +import json +from io import BytesIO +from unittest.mock import MagicMock, patch + +import pytest + +import browser + + +@pytest.fixture(autouse=True) +def _reset_client(): + """Reset the cached Lambda client between tests.""" + browser._lambda_client = None + yield + browser._lambda_client = None + + +class TestCaptureScreenshot: + def test_success_returns_presigned_url(self, monkeypatch): + monkeypatch.setenv("BROWSER_TOOL_FUNCTION_NAME", "my-browser-fn") + monkeypatch.setenv("AWS_REGION", "us-east-1") + + response_payload = json.dumps( + { + "status": "success", + "screenshotS3Key": "screenshots/abc123.png", + "presignedUrl": "https://s3.amazonaws.com/bucket/screenshots/abc123.png", + } + ).encode() + + mock_client = MagicMock() + mock_client.invoke.return_value = { + "Payload": BytesIO(response_payload), + } + + with patch("boto3.client", return_value=mock_client): + url = browser.capture_screenshot("https://github.com/owner/repo/pull/1", "task-123") + + assert url == "https://s3.amazonaws.com/bucket/screenshots/abc123.png" + mock_client.invoke.assert_called_once() + + def test_error_response_returns_none(self, monkeypatch): + monkeypatch.setenv("BROWSER_TOOL_FUNCTION_NAME", "my-browser-fn") + monkeypatch.setenv("AWS_REGION", "us-east-1") + + response_payload = json.dumps( + { + "status": "error", + "error": "page not found", + } + ).encode() + + mock_client = MagicMock() + mock_client.invoke.return_value = { + "Payload": BytesIO(response_payload), + } + + with patch("boto3.client", return_value=mock_client): + url = browser.capture_screenshot("https://example.com", "task-123") + + assert url is None + + def test_missing_env_var_returns_none(self, monkeypatch): + monkeypatch.delenv("BROWSER_TOOL_FUNCTION_NAME", raising=False) + + url = browser.capture_screenshot("https://example.com", "task-123") + + assert url is None + + def test_lambda_invocation_exception_returns_none(self, monkeypatch): + monkeypatch.setenv("BROWSER_TOOL_FUNCTION_NAME", "my-browser-fn") + monkeypatch.setenv("AWS_REGION", "us-east-1") + + mock_client = MagicMock() + mock_client.invoke.side_effect = Exception("Lambda timeout") + + with patch("boto3.client", return_value=mock_client): + url = browser.capture_screenshot("https://example.com", "task-123") + + assert url is None diff --git a/agent/tests/test_post_hooks.py b/agent/tests/test_post_hooks.py new file mode 100644 index 0000000..a5d462b --- /dev/null +++ b/agent/tests/test_post_hooks.py @@ -0,0 +1,81 @@ +"""Unit tests for post_hooks screenshot functions.""" + +from unittest.mock import MagicMock, patch + +from post_hooks import _append_screenshots_to_pr, capture_pr_screenshots + + +class TestCapturePrScreenshots: + def test_returns_urls_on_success(self): + with patch("browser.capture_screenshot", return_value="https://s3/img.png"): + result = capture_pr_screenshots("https://github.com/owner/repo/pull/1", "task-1") + assert result == ["https://s3/img.png"] + + def test_returns_empty_list_when_pr_url_empty(self): + result = capture_pr_screenshots("", "task-1") + assert result == [] + + def test_returns_empty_list_when_pr_url_not_github(self): + result = capture_pr_screenshots("https://gitlab.com/owner/repo/pull/1", "task-1") + assert result == [] + + def test_returns_empty_list_on_exception(self): + with patch("browser.capture_screenshot", side_effect=RuntimeError("boom")): + result = capture_pr_screenshots("https://github.com/owner/repo/pull/1", "task-1") + assert result == [] + + +class TestAppendScreenshotsToPr: + def _make_mocks(self): + config = MagicMock() + config.repo_url = "https://github.com/owner/repo" + setup = MagicMock() + setup.branch = "bgagent/task-1" + setup.repo_dir = "/tmp/repo" + return config, setup + + def test_appends_screenshots_section(self): + config, setup = self._make_mocks() + view_result = MagicMock(returncode=0, stdout="## Summary\n\nSome PR body") + edit_result = MagicMock(returncode=0, stderr="") + with patch("post_hooks.subprocess.run", side_effect=[view_result, edit_result]) as mock_run: + _append_screenshots_to_pr(config, setup, ["https://s3/img1.png"]) + edit_call = mock_run.call_args_list[1] + body_arg = edit_call[0][0][edit_call[0][0].index("--body") + 1] + assert "## Screenshots" in body_arg + assert "![Screenshot 1](https://s3/img1.png)" in body_arg + + def test_replaces_existing_screenshots_section(self): + config, setup = self._make_mocks() + existing_body = "## Summary\n\nBody\n\n## Screenshots\n\n![Screenshot 1](https://old.png)" + view_result = MagicMock(returncode=0, stdout=existing_body) + edit_result = MagicMock(returncode=0, stderr="") + with patch("post_hooks.subprocess.run", side_effect=[view_result, edit_result]) as mock_run: + _append_screenshots_to_pr(config, setup, ["https://s3/new.png"]) + edit_call = mock_run.call_args_list[1] + body_arg = edit_call[0][0][edit_call[0][0].index("--body") + 1] + assert "![Screenshot 1](https://s3/new.png)" in body_arg + assert "https://old.png" not in body_arg + # Should only have one ## Screenshots section + assert body_arg.count("## Screenshots") == 1 + + def test_handles_gh_pr_view_failure(self): + config, setup = self._make_mocks() + view_result = MagicMock(returncode=1, stdout="", stderr="not found") + with patch("post_hooks.subprocess.run", return_value=view_result): + # Should not raise + _append_screenshots_to_pr(config, setup, ["https://s3/img.png"]) + + def test_handles_gh_pr_edit_failure(self): + config, setup = self._make_mocks() + view_result = MagicMock(returncode=0, stdout="## Summary\n\nBody") + edit_result = MagicMock(returncode=1, stderr="permission denied") + with patch("post_hooks.subprocess.run", side_effect=[view_result, edit_result]): + # Should not raise + _append_screenshots_to_pr(config, setup, ["https://s3/img.png"]) + + def test_does_nothing_when_urls_empty(self): + config, setup = self._make_mocks() + with patch("post_hooks.subprocess.run") as mock_run: + _append_screenshots_to_pr(config, setup, []) + mock_run.assert_not_called() diff --git a/agent/tests/test_server.py b/agent/tests/test_server.py index fdc951f..547ef5d 100644 --- a/agent/tests/test_server.py +++ b/agent/tests/test_server.py @@ -63,6 +63,13 @@ def boom(**_kwargs): assert body["status"] == "unhealthy" assert body["reason"] == "background_pipeline_failed" + # Wait for the background thread to finish so write_terminal has been called + # (the 503 flag is set before write_terminal in the except block) + with server._threads_lock: + threads = list(server._active_threads) + for t in threads: + t.join(timeout=5) + mock_write.assert_called() call_kw = mock_write.call_args assert call_kw[0][0] == "task-crash-1" diff --git a/cdk/package.json b/cdk/package.json index 0feaf5b..edc5b1b 100644 --- a/cdk/package.json +++ b/cdk/package.json @@ -19,6 +19,8 @@ "@aws-sdk/client-bedrock-agentcore": "^3.1021.0", "@aws-sdk/client-bedrock-runtime": "^3.1021.0", "@aws-sdk/client-ecs": "^3.1021.0", + "@aws-sdk/client-s3": "^3.1021.0", + "@aws-sdk/s3-request-presigner": "^3.1021.0", "@aws-sdk/client-dynamodb": "^3.1021.0", "@aws-sdk/client-lambda": "^3.1021.0", "@aws-sdk/client-secrets-manager": "^3.1021.0", @@ -27,7 +29,8 @@ "aws-cdk-lib": "^2.238.0", "cdk-nag": "^2.37.55", "constructs": "^10.3.0", - "ulid": "^3.0.2" + "ulid": "^3.0.2", + "ws": "^8.18.0" }, "devDependencies": { "@cdklabs/eslint-plugin": "^1.5.10", @@ -35,6 +38,7 @@ "@types/aws-lambda": "^8.10.161", "@types/jest": "^30.0.0", "@types/node": "^20", + "@types/ws": "^8.18.0", "@typescript-eslint/eslint-plugin": "^8", "@typescript-eslint/parser": "^8", "aws-cdk": "^2", diff --git a/cdk/src/constructs/agent-browser.ts b/cdk/src/constructs/agent-browser.ts new file mode 100644 index 0000000..71d458d --- /dev/null +++ b/cdk/src/constructs/agent-browser.ts @@ -0,0 +1,125 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import * as path from 'path'; +import * as agentcore from '@aws-cdk/aws-bedrock-agentcore-alpha'; +import { Duration, RemovalPolicy } from 'aws-cdk-lib'; +import type * as iam from 'aws-cdk-lib/aws-iam'; +import { Architecture, Runtime } from 'aws-cdk-lib/aws-lambda'; +import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs'; +import * as s3 from 'aws-cdk-lib/aws-s3'; +import { NagSuppressions } from 'cdk-nag'; +import { Construct } from 'constructs'; + +export interface AgentBrowserProps { + readonly browserName?: string; + readonly screenshotRetentionDays?: number; +} + +export class AgentBrowser extends Construct { + public readonly browser: agentcore.BrowserCustom; + public readonly browserToolFn: lambda.NodejsFunction; + public readonly screenshotBucket: s3.Bucket; + + constructor(scope: Construct, id: string, props?: AgentBrowserProps) { + super(scope, id); + + // --- Screenshot S3 bucket --- + this.screenshotBucket = new s3.Bucket(this, 'ScreenshotBucket', { + encryption: s3.BucketEncryption.S3_MANAGED, + blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, + enforceSSL: true, + removalPolicy: RemovalPolicy.DESTROY, + autoDeleteObjects: true, + lifecycleRules: [ + { + expiration: Duration.days(props?.screenshotRetentionDays ?? 30), + }, + ], + }); + + NagSuppressions.addResourceSuppressions(this.screenshotBucket, [ + { + id: 'AwsSolutions-S1', + reason: 'Screenshot bucket does not require server access logging — short-lived artifacts with lifecycle expiration', + }, + ]); + + // --- BrowserCustom resource --- + this.browser = new agentcore.BrowserCustom(this, 'BrowserCustom', { + browserCustomName: props?.browserName ?? 'bgagent_browser', + recordingConfig: { + enabled: true, + s3Location: { + bucketName: this.screenshotBucket.bucketName, + objectKey: 'recordings/', + }, + }, + browserSigning: agentcore.BrowserSigning.ENABLED, + }); + + // --- Lambda function for browser tool --- + const handlersDir = path.join(__dirname, '..', 'handlers'); + + this.browserToolFn = new lambda.NodejsFunction(this, 'BrowserToolFn', { + entry: path.join(handlersDir, 'browser-tool.ts'), + handler: 'handler', + runtime: Runtime.NODEJS_24_X, + architecture: Architecture.ARM_64, + timeout: Duration.minutes(2), + memorySize: 256, + environment: { + BROWSER_ID: this.browser.browserId, + SCREENSHOT_BUCKET_NAME: this.screenshotBucket.bucketName, + }, + bundling: { + externalModules: ['@aws-sdk/*'], + }, + }); + + this.browser.grantUse(this.browserToolFn); + this.screenshotBucket.grantReadWrite(this.browserToolFn); + + NagSuppressions.addResourceSuppressions(this.browserToolFn, [ + { + id: 'AwsSolutions-IAM4', + reason: 'Lambda basic execution role uses AWS managed AWSLambdaBasicExecutionRole', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'Browser grantUse and S3 grantReadWrite generate wildcard permissions — required by L2 construct grants', + }, + ], true); + + NagSuppressions.addResourceSuppressions(this.browser, [ + { + id: 'AwsSolutions-IAM5', + reason: 'BrowserCustom execution role requires wildcard permissions for Bedrock browser operations — generated by CDK L2 construct', + }, + ], true); + } + + grantInvokeBrowserTool(grantee: iam.IGrantable): void { + this.browserToolFn.grantInvoke(grantee); + } + + grantReadScreenshots(grantee: iam.IGrantable): void { + this.screenshotBucket.grantRead(grantee); + } +} diff --git a/cdk/src/handlers/browser-tool.ts b/cdk/src/handlers/browser-tool.ts new file mode 100644 index 0000000..94ea74e --- /dev/null +++ b/cdk/src/handlers/browser-tool.ts @@ -0,0 +1,256 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { BedrockAgentCoreClient, StartBrowserSessionCommand, StopBrowserSessionCommand } from '@aws-sdk/client-bedrock-agentcore'; +import { PutObjectCommand, S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import WebSocket from 'ws'; + +const agentCoreClient = new BedrockAgentCoreClient({}); +const s3Client = new S3Client({}); + +const BROWSER_ID = process.env.BROWSER_ID!; +const SCREENSHOT_BUCKET_NAME = process.env.SCREENSHOT_BUCKET_NAME!; + +const PAGE_LOAD_TIMEOUT_MS = 30_000; +// Actual validity is bounded by the signing credential lifetime (IAM role session). +const PRESIGNED_URL_EXPIRES_IN = 604_800; // 7 days + +interface BrowserToolEvent { + action: 'screenshot'; + url: string; + taskId?: string; +} + +type BrowserToolResponse = + | { status: 'success'; screenshotS3Key: string; presignedUrl: string } + | { status: 'error'; error: string }; + +interface CdpResponse { + id: number; + result?: Record; + error?: { code: number; message: string }; +} + +const ALLOWED_DOMAIN = 'github.com'; + +const RFC1918_PATTERNS = [ + /^10\.\d{1,3}\.\d{1,3}\.\d{1,3}/, + /^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}/, + /^192\.168\.\d{1,3}\.\d{1,3}/, + /^169\.254\.\d{1,3}\.\d{1,3}/, + /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}/, +]; + +export function validateUrl(url: string): string | null { + let parsed: URL; + try { + parsed = new URL(url); + } catch { + return 'Invalid URL'; + } + + if (parsed.protocol !== 'https:') { + return 'Only HTTPS URLs are allowed'; + } + + const hostname = parsed.hostname.toLowerCase(); + + if (hostname === 'localhost' || hostname === '[::1]') { + return 'Localhost URLs are not allowed'; + } + + for (const pattern of RFC1918_PATTERNS) { + if (pattern.test(hostname)) { + return 'Private/internal IP addresses are not allowed'; + } + } + + if (hostname !== ALLOWED_DOMAIN && !hostname.endsWith(`.${ALLOWED_DOMAIN}`)) { + return `Only ${ALLOWED_DOMAIN} URLs are allowed`; + } + + return null; +} + +function sendCdpCommand(ws: WebSocket, id: number, method: string, params?: Record): void { + ws.send(JSON.stringify({ id, method, params })); +} + +function waitForCdpResponse(ws: WebSocket, expectedId: number, timeoutMs: number): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + reject(new Error(`CDP response timeout for id ${expectedId}`)); + }, timeoutMs); + + const onMessage = (data: WebSocket.Data) => { + let msg: CdpResponse & { method?: string }; + try { + msg = JSON.parse(String(data)) as CdpResponse & { method?: string }; + } catch { + return; // Skip unparseable messages + } + if (msg.id === expectedId) { + clearTimeout(timer); + ws.off('message', onMessage); + if (msg.error) { + reject(new Error(`CDP error: ${msg.error.message}`)); + } else { + resolve(msg); + } + } + }; + ws.on('message', onMessage); + }); +} + +function waitForCdpEvent(ws: WebSocket, eventName: string, timeoutMs: number): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + reject(new Error(`CDP event timeout waiting for ${eventName}`)); + }, timeoutMs); + + const onMessage = (data: WebSocket.Data) => { + let msg: { method?: string }; + try { + msg = JSON.parse(String(data)) as { method?: string }; + } catch { + return; // Skip unparseable messages + } + if (msg.method === eventName) { + clearTimeout(timer); + ws.off('message', onMessage); + resolve(); + } + }; + ws.on('message', onMessage); + }); +} + +export async function handler(event: BrowserToolEvent): Promise { + if (event.action !== 'screenshot') { + return { status: 'error', error: `Unsupported action: ${event.action}` }; + } + + const urlError = validateUrl(event.url); + if (urlError) { + return { status: 'error', error: `URL rejected: ${urlError}` }; + } + + let sessionId: string | undefined; + + try { + // Step 1: Start browser session + const startResponse = await agentCoreClient.send(new StartBrowserSessionCommand({ + browserIdentifier: BROWSER_ID, + name: `screenshot-${Date.now()}`, + })); + + if (!startResponse.sessionId) { + throw new Error('StartBrowserSession did not return a sessionId'); + } + sessionId = startResponse.sessionId; + + if (!startResponse.streams?.automationStream?.streamEndpoint) { + throw new Error('StartBrowserSession did not return a stream endpoint'); + } + const streamEndpoint = startResponse.streams.automationStream.streamEndpoint; + + // Step 2: Connect WebSocket to CDP endpoint + const ws = new WebSocket(streamEndpoint); + + ws.on('error', (err) => { + // eslint-disable-next-line no-console + console.error('WebSocket error:', err); + }); + + await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('WebSocket connection timeout')), 10_000); + ws.on('open', () => { clearTimeout(timer); resolve(); }); + ws.on('error', (err) => { clearTimeout(timer); reject(err); }); + }); + + try { + // Step 3: Enable Page domain + sendCdpCommand(ws, 1, 'Page.enable'); + await waitForCdpResponse(ws, 1, 10_000); + + // Step 4: Navigate to URL + const loadPromise = waitForCdpEvent(ws, 'Page.loadEventFired', PAGE_LOAD_TIMEOUT_MS); + sendCdpCommand(ws, 2, 'Page.navigate', { url: event.url }); + await waitForCdpResponse(ws, 2, 10_000); + + // Step 5: Wait for page load + await loadPromise; + + // Step 6: Capture screenshot + sendCdpCommand(ws, 3, 'Page.captureScreenshot', { format: 'png' }); + const screenshotResponse = await waitForCdpResponse(ws, 3, 15_000); + if (!screenshotResponse.result?.data) { + throw new Error('Screenshot response did not contain image data'); + } + const base64Data = screenshotResponse.result.data as string; + + // Step 7: Upload to S3 + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const sanitizedTaskId = (event.taskId ?? 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_'); + const s3Key = `screenshots/${sanitizedTaskId}/${timestamp}.png`; + + await s3Client.send(new PutObjectCommand({ + Bucket: SCREENSHOT_BUCKET_NAME, + Key: s3Key, + Body: Buffer.from(base64Data, 'base64'), + ContentType: 'image/png', + })); + + // Step 8: Generate presigned URL + const presignedUrl = await getSignedUrl( + s3Client, + new GetObjectCommand({ Bucket: SCREENSHOT_BUCKET_NAME, Key: s3Key }), + { expiresIn: PRESIGNED_URL_EXPIRES_IN }, + ); + + return { + status: 'success', + screenshotS3Key: s3Key, + presignedUrl, + }; + } finally { + ws.close(); + } + } catch (err) { + return { + status: 'error', + error: err instanceof Error ? err.message : String(err), + }; + } finally { + // Step 9: Stop browser session + if (sessionId) { + try { + await agentCoreClient.send(new StopBrowserSessionCommand({ + browserIdentifier: BROWSER_ID, + sessionId, + })); + } catch (cleanupErr) { + // eslint-disable-next-line no-console + console.error('Failed to stop browser session:', sessionId, cleanupErr); + } + } + } +} diff --git a/cdk/src/stacks/agent.ts b/cdk/src/stacks/agent.ts index da99401..b637f8c 100644 --- a/cdk/src/stacks/agent.ts +++ b/cdk/src/stacks/agent.ts @@ -31,6 +31,7 @@ import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; import * as cr from 'aws-cdk-lib/custom-resources'; import { NagSuppressions } from 'cdk-nag'; import { Construct } from 'constructs'; +import { AgentBrowser } from '../constructs/agent-browser'; import { AgentMemory } from '../constructs/agent-memory'; import { AgentVpc } from '../constructs/agent-vpc'; import { Blueprint } from '../constructs/blueprint'; @@ -126,6 +127,9 @@ export class AgentStack extends Stack { // --- AgentCore Memory (cross-task learning) --- const agentMemory = new AgentMemory(this, 'AgentMemory'); + // --- AgentCore Browser (web screenshot tool) --- + const agentBrowser = new AgentBrowser(this, 'AgentBrowser'); + const runtime = new agentcore.Runtime(this, 'Runtime', { runtimeName, agentRuntimeArtifact: artifact, @@ -145,6 +149,8 @@ export class AgentStack extends Stack { USER_CONCURRENCY_TABLE_NAME: userConcurrencyTable.table.tableName, LOG_GROUP_NAME: applicationLogGroup.logGroupName, MEMORY_ID: agentMemory.memory.memoryId, + BROWSER_TOOL_FUNCTION_NAME: agentBrowser.browserToolFn.functionName, + SCREENSHOT_BUCKET_NAME: agentBrowser.screenshotBucket.bucketName, MAX_TURNS: '100', // Session storage: the S3-backed FUSE mount at /mnt/workspace does NOT // support flock(). Only caches whose tools never call flock() go there. @@ -181,6 +187,8 @@ export class AgentStack extends Stack { githubTokenSecret.grantRead(runtime); applicationLogGroup.grantWrite(runtime); agentMemory.grantReadWrite(runtime); + agentBrowser.grantInvokeBrowserTool(runtime); + agentBrowser.grantReadScreenshots(runtime); const model = new bedrock.BedrockFoundationModel('anthropic.claude-sonnet-4-6', { supportsAgents: true, @@ -275,6 +283,18 @@ export class AgentStack extends Stack { description: 'ARN of the Secrets Manager secret for the GitHub token', }); + new CfnOutput(this, 'BrowserId', { + value: agentBrowser.browser.browserId, + description: 'ID of the AgentCore Browser', + }); + + NagSuppressions.addResourceSuppressions(agentBrowser, [ + { + id: 'AwsSolutions-IAM5', + reason: 'Browser tool Lambda grants and S3 grantRead generate wildcard permissions — required by L2 construct grants', + }, + ], true); + // --- Bedrock Guardrail for prompt injection detection --- const inputGuardrail = new bedrock.Guardrail(this, 'InputGuardrail', { guardrailName: 'task-input-guardrail', diff --git a/cdk/test/constructs/agent-browser.test.ts b/cdk/test/constructs/agent-browser.test.ts new file mode 100644 index 0000000..fcad265 --- /dev/null +++ b/cdk/test/constructs/agent-browser.test.ts @@ -0,0 +1,157 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { App, Stack } from 'aws-cdk-lib'; +import { Template, Match } from 'aws-cdk-lib/assertions'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import { AgentBrowser } from '../../src/constructs/agent-browser'; + +function createStack(props?: { browserName?: string; screenshotRetentionDays?: number }): { + stack: Stack; + template: Template; + agentBrowser: AgentBrowser; +} { + const app = new App(); + const stack = new Stack(app, 'TestStack'); + const agentBrowser = new AgentBrowser(stack, 'AgentBrowser', props); + const template = Template.fromStack(stack); + return { stack, template, agentBrowser }; +} + +describe('AgentBrowser construct', () => { + test('creates a BrowserCustom resource', () => { + const { template } = createStack(); + template.resourceCountIs('AWS::BedrockAgentCore::BrowserCustom', 1); + }); + + test('creates an S3 bucket with encryption and lifecycle', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::S3::Bucket', { + BucketEncryption: { + ServerSideEncryptionConfiguration: [ + { + ServerSideEncryptionByDefault: { + SSEAlgorithm: 'AES256', + }, + }, + ], + }, + LifecycleConfiguration: { + Rules: Match.arrayWith([ + Match.objectLike({ + ExpirationInDays: 30, + Status: 'Enabled', + }), + ]), + }, + PublicAccessBlockConfiguration: { + BlockPublicAcls: true, + BlockPublicPolicy: true, + IgnorePublicAcls: true, + RestrictPublicBuckets: true, + }, + }); + }); + + test('creates a Lambda function with correct env vars', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::Lambda::Function', { + Environment: { + Variables: Match.objectLike({ + BROWSER_ID: Match.anyValue(), + SCREENSHOT_BUCKET_NAME: Match.anyValue(), + }), + }, + }); + }); + + test('uses default browser name', () => { + const { template } = createStack(); + template.hasResourceProperties('AWS::BedrockAgentCore::BrowserCustom', { + Name: 'bgagent_browser', + }); + }); + + test('accepts custom browser name', () => { + const { template } = createStack({ browserName: 'custom_browser' }); + template.hasResourceProperties('AWS::BedrockAgentCore::BrowserCustom', { + Name: 'custom_browser', + }); + }); + + test('exposes browser, browserToolFn, screenshotBucket', () => { + const { agentBrowser } = createStack(); + expect(agentBrowser.browser).toBeDefined(); + expect(agentBrowser.browserToolFn).toBeDefined(); + expect(agentBrowser.screenshotBucket).toBeDefined(); + }); + + test('grantInvokeBrowserTool grants lambda:InvokeFunction', () => { + const app = new App(); + const stack = new Stack(app, 'TestStack'); + const agentBrowser = new AgentBrowser(stack, 'AgentBrowser'); + + const role = new iam.Role(stack, 'TestRole', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + + agentBrowser.grantInvokeBrowserTool(role); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: Match.arrayWith([ + Match.objectLike({ + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + }), + ]), + }, + }); + }); + + test('grantReadScreenshots grants S3 read', () => { + const app = new App(); + const stack = new Stack(app, 'TestStack'); + const agentBrowser = new AgentBrowser(stack, 'AgentBrowser'); + + const role = new iam.Role(stack, 'TestRole', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + + agentBrowser.grantReadScreenshots(role); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: Match.arrayWith([ + Match.objectLike({ + Action: Match.arrayWith([ + 's3:GetObject*', + ]), + Effect: 'Allow', + }), + ]), + }, + Roles: Match.arrayWith([ + { Ref: Match.stringLikeRegexp('TestRole') }, + ]), + }); + }); +}); diff --git a/cdk/test/handlers/browser-tool.test.ts b/cdk/test/handlers/browser-tool.test.ts new file mode 100644 index 0000000..28551a9 --- /dev/null +++ b/cdk/test/handlers/browser-tool.test.ts @@ -0,0 +1,262 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// --- Mocks --- +const mockSend = jest.fn(); +jest.mock('@aws-sdk/client-bedrock-agentcore', () => ({ + BedrockAgentCoreClient: jest.fn().mockImplementation(() => ({ send: mockSend })), + StartBrowserSessionCommand: jest.fn().mockImplementation((input) => ({ input })), + StopBrowserSessionCommand: jest.fn().mockImplementation((input) => ({ input })), +})); + +const mockS3Send = jest.fn(); +jest.mock('@aws-sdk/client-s3', () => ({ + S3Client: jest.fn().mockImplementation(() => ({ send: mockS3Send })), + PutObjectCommand: jest.fn().mockImplementation((input) => ({ input })), + GetObjectCommand: jest.fn().mockImplementation((input) => ({ input })), +})); + +jest.mock('@aws-sdk/s3-request-presigner', () => ({ + getSignedUrl: jest.fn().mockResolvedValue('https://s3.example.com/presigned'), +})); + +jest.mock('ws', () => { + return jest.fn().mockImplementation(() => { + const handlers: Record = {}; + const ws = { + on: jest.fn((event: string, cb: Function) => { + if (!handlers[event]) handlers[event] = []; + handlers[event].push(cb); + // Auto-fire 'open' event on registration + if (event === 'open') { + setTimeout(() => cb(), 0); + } + }), + off: jest.fn((event: string, cb: Function) => { + if (handlers[event]) { + handlers[event] = handlers[event].filter(h => h !== cb); + } + }), + send: jest.fn((data: string) => { + const msg = JSON.parse(data); + setTimeout(() => { + if (msg.method === 'Page.enable') { + handlers.message?.forEach(h => h(JSON.stringify({ id: msg.id, result: {} }))); + } else if (msg.method === 'Page.navigate') { + handlers.message?.forEach(h => h(JSON.stringify({ id: msg.id, result: { frameId: '1' } }))); + handlers.message?.forEach(h => h(JSON.stringify({ method: 'Page.loadEventFired' }))); + } else if (msg.method === 'Page.captureScreenshot') { + handlers.message?.forEach(h => h(JSON.stringify({ id: msg.id, result: { data: 'base64png' } }))); + } + }, 0); + }), + close: jest.fn(), + readyState: 1, + }; + return ws; + }); +}); + +import { StopBrowserSessionCommand } from '@aws-sdk/client-bedrock-agentcore'; +import { handler, validateUrl } from '../../src/handlers/browser-tool'; + +describe('browser-tool handler', () => { + beforeEach(() => { + jest.clearAllMocks(); + process.env.BROWSER_ID = 'test-browser-id'; + process.env.SCREENSHOT_BUCKET_NAME = 'test-screenshot-bucket'; + process.env.AWS_REGION = 'us-east-1'; + }); + + afterEach(() => { + delete process.env.BROWSER_ID; + delete process.env.SCREENSHOT_BUCKET_NAME; + delete process.env.AWS_REGION; + }); + + test('returns screenshot on success', async () => { + mockSend + .mockResolvedValueOnce({ + sessionId: 'session-123', + streams: { + automationStream: { + streamEndpoint: 'wss://example.com/stream', + streamStatus: 'ENABLED', + }, + }, + }) + .mockResolvedValueOnce({}); + + const result = await handler({ + action: 'screenshot', + url: 'https://github.com/owner/repo/pull/1', + }); + + expect(result).toEqual( + expect.objectContaining({ + status: 'success', + presignedUrl: 'https://s3.example.com/presigned', + }), + ); + + // Verify StopBrowserSession was called + expect(StopBrowserSessionCommand).toHaveBeenCalledWith( + expect.objectContaining({ sessionId: 'session-123' }), + ); + }); + + test('returns error on session failure', async () => { + mockSend.mockRejectedValueOnce(new Error('Session start failed')); + + const result = await handler({ + action: 'screenshot', + url: 'https://github.com/owner/repo/pull/1', + }); + + expect(result).toEqual( + expect.objectContaining({ + error: expect.stringContaining('Session start failed'), + }), + ); + }); + + test('stops session in finally block even on error', async () => { + mockSend + .mockResolvedValueOnce({ + sessionId: 'session-456', + streams: { + automationStream: { + streamEndpoint: 'wss://example.com/stream', + streamStatus: 'ENABLED', + }, + }, + }) + .mockResolvedValueOnce({}); + + // Override S3 upload to fail + mockS3Send.mockRejectedValueOnce(new Error('S3 upload failed')); + + const result = await handler({ + action: 'screenshot', + url: 'https://github.com/owner/repo/pull/1', + }); + + // Session should still be stopped even though S3 failed + expect(StopBrowserSessionCommand).toHaveBeenCalledWith( + expect.objectContaining({ sessionId: 'session-456' }), + ); + + expect(result).toEqual( + expect.objectContaining({ + error: expect.any(String), + }), + ); + }); + + test('returns error for unknown action', async () => { + const result = await handler({ + action: 'unknown' as any, + url: 'https://github.com/owner/repo', + }); + + expect(result).toEqual( + expect.objectContaining({ + error: expect.stringContaining('unknown'), + }), + ); + }); + + test('rejects non-HTTPS URLs', async () => { + const result = await handler({ + action: 'screenshot', + url: 'http://github.com/owner/repo', + }); + + expect(result).toEqual({ + status: 'error', + error: 'URL rejected: Only HTTPS URLs are allowed', + }); + }); + + test('rejects IMDS metadata endpoint', async () => { + const result = await handler({ + action: 'screenshot', + url: 'https://169.254.169.254/latest/meta-data/', + }); + + expect(result).toEqual({ + status: 'error', + error: 'URL rejected: Private/internal IP addresses are not allowed', + }); + }); + + test('rejects non-github.com domains', async () => { + const result = await handler({ + action: 'screenshot', + url: 'https://evil.com/something', + }); + + expect(result).toEqual({ + status: 'error', + error: 'URL rejected: Only github.com URLs are allowed', + }); + }); +}); + +describe('validateUrl', () => { + test('allows github.com HTTPS URLs', () => { + expect(validateUrl('https://github.com/owner/repo/pull/1')).toBeNull(); + }); + + test('rejects http URLs', () => { + expect(validateUrl('http://github.com')).toBe('Only HTTPS URLs are allowed'); + }); + + test('rejects file URLs', () => { + expect(validateUrl('file:///etc/passwd')).toBe('Only HTTPS URLs are allowed'); + }); + + test('rejects localhost', () => { + expect(validateUrl('https://localhost/foo')).toBe('Localhost URLs are not allowed'); + }); + + test('rejects RFC1918 10.x', () => { + expect(validateUrl('https://10.0.0.1/')).toBe('Private/internal IP addresses are not allowed'); + }); + + test('rejects RFC1918 172.16.x', () => { + expect(validateUrl('https://172.16.0.1/')).toBe('Private/internal IP addresses are not allowed'); + }); + + test('rejects RFC1918 192.168.x', () => { + expect(validateUrl('https://192.168.1.1/')).toBe('Private/internal IP addresses are not allowed'); + }); + + test('rejects link-local 169.254.x', () => { + expect(validateUrl('https://169.254.169.254/')).toBe('Private/internal IP addresses are not allowed'); + }); + + test('rejects non-allowed domains', () => { + expect(validateUrl('https://example.com')).toBe('Only github.com URLs are allowed'); + }); + + test('rejects invalid URLs', () => { + expect(validateUrl('not-a-url')).toBe('Invalid URL'); + }); +}); diff --git a/yarn.lock b/yarn.lock index 86a7e7e..0bb8159 100644 --- a/yarn.lock +++ b/yarn.lock @@ -221,6 +221,27 @@ "@aws-sdk/types" "^3.222.0" tslib "^2.6.2" +"@aws-crypto/crc32c@5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz#4e34aab7f419307821509a98b9b08e84e0c1917e" + integrity sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag== + dependencies: + "@aws-crypto/util" "^5.2.0" + "@aws-sdk/types" "^3.222.0" + tslib "^2.6.2" + +"@aws-crypto/sha1-browser@5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz#b0ee2d2821d3861f017e965ef3b4cb38e3b6a0f4" + integrity sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg== + dependencies: + "@aws-crypto/supports-web-crypto" "^5.2.0" + "@aws-crypto/util" "^5.2.0" + "@aws-sdk/types" "^3.222.0" + "@aws-sdk/util-locate-window" "^3.0.0" + "@smithy/util-utf8" "^2.0.0" + tslib "^2.6.2" + "@aws-crypto/sha256-browser@5.2.0": version "5.2.0" resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz#153895ef1dba6f9fce38af550e0ef58988eb649e" @@ -250,7 +271,7 @@ dependencies: tslib "^2.6.2" -"@aws-crypto/util@^5.2.0": +"@aws-crypto/util@5.2.0", "@aws-crypto/util@^5.2.0": version "5.2.0" resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-5.2.0.tgz#71284c9cffe7927ddadac793c14f14886d3876da" integrity sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ== @@ -550,6 +571,67 @@ "@smithy/util-waiter" "^4.2.14" tslib "^2.6.2" +"@aws-sdk/client-s3@^3.1021.0": + version "3.1030.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.1030.0.tgz#f5c593deb0e32fbd0a174d00feae9c69c0e7cccf" + integrity sha512-sgGb4ub0JXnHaXnok5td7A1KGwENFPwOrwgzvpkeWq9w16Sl7x2KhYtVl+Fdd/7LAvaEtm3HqrYtNmm2d0OXmQ== + dependencies: + "@aws-crypto/sha1-browser" "5.2.0" + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/core" "^3.973.27" + "@aws-sdk/credential-provider-node" "^3.972.30" + "@aws-sdk/middleware-bucket-endpoint" "^3.972.9" + "@aws-sdk/middleware-expect-continue" "^3.972.9" + "@aws-sdk/middleware-flexible-checksums" "^3.974.7" + "@aws-sdk/middleware-host-header" "^3.972.9" + "@aws-sdk/middleware-location-constraint" "^3.972.9" + "@aws-sdk/middleware-logger" "^3.972.9" + "@aws-sdk/middleware-recursion-detection" "^3.972.10" + "@aws-sdk/middleware-sdk-s3" "^3.972.28" + "@aws-sdk/middleware-ssec" "^3.972.9" + "@aws-sdk/middleware-user-agent" "^3.972.29" + "@aws-sdk/region-config-resolver" "^3.972.11" + "@aws-sdk/signature-v4-multi-region" "^3.996.16" + "@aws-sdk/types" "^3.973.7" + "@aws-sdk/util-endpoints" "^3.996.6" + "@aws-sdk/util-user-agent-browser" "^3.972.9" + "@aws-sdk/util-user-agent-node" "^3.973.15" + "@smithy/config-resolver" "^4.4.14" + "@smithy/core" "^3.23.14" + "@smithy/eventstream-serde-browser" "^4.2.13" + "@smithy/eventstream-serde-config-resolver" "^4.3.13" + "@smithy/eventstream-serde-node" "^4.2.13" + "@smithy/fetch-http-handler" "^5.3.16" + "@smithy/hash-blob-browser" "^4.2.14" + "@smithy/hash-node" "^4.2.13" + "@smithy/hash-stream-node" "^4.2.13" + "@smithy/invalid-dependency" "^4.2.13" + "@smithy/md5-js" "^4.2.13" + "@smithy/middleware-content-length" "^4.2.13" + "@smithy/middleware-endpoint" "^4.4.29" + "@smithy/middleware-retry" "^4.5.0" + "@smithy/middleware-serde" "^4.2.17" + "@smithy/middleware-stack" "^4.2.13" + "@smithy/node-config-provider" "^4.3.13" + "@smithy/node-http-handler" "^4.5.2" + "@smithy/protocol-http" "^5.3.13" + "@smithy/smithy-client" "^4.12.9" + "@smithy/types" "^4.14.0" + "@smithy/url-parser" "^4.2.13" + "@smithy/util-base64" "^4.3.2" + "@smithy/util-body-length-browser" "^4.2.2" + "@smithy/util-body-length-node" "^4.2.3" + "@smithy/util-defaults-mode-browser" "^4.3.45" + "@smithy/util-defaults-mode-node" "^4.2.49" + "@smithy/util-endpoints" "^3.3.4" + "@smithy/util-middleware" "^4.2.13" + "@smithy/util-retry" "^4.3.0" + "@smithy/util-stream" "^4.5.22" + "@smithy/util-utf8" "^4.2.2" + "@smithy/util-waiter" "^4.2.15" + tslib "^2.6.2" + "@aws-sdk/client-secrets-manager@^3.1021.0": version "3.1021.0" resolved "https://registry.yarnpkg.com/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.1021.0.tgz#57c6348c63146642132ffa7e885a2abba08c6ff4" @@ -633,6 +715,14 @@ "@smithy/util-utf8" "^4.2.2" tslib "^2.6.2" +"@aws-sdk/crc64-nvme@^3.972.6": + version "3.972.6" + resolved "https://registry.yarnpkg.com/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.6.tgz#4e023b3e3b5f67d3129c97c5caa3e18699d3d550" + integrity sha512-NMbiqKdruhwwgI6nzBVe2jWMkXjaoQz2YOs3rFX+2F3gGyrJDkDPwMpV/RsTFeq2vAQ055wZNtOXFK4NYSkM8g== + dependencies: + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@aws-sdk/credential-provider-env@^3.972.24": version "3.972.24" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.24.tgz#bc33a34f15704d02552aa8b3994d17008b991f86" @@ -911,6 +1001,19 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@aws-sdk/middleware-bucket-endpoint@^3.972.9": + version "3.972.9" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.9.tgz#4dc1e7a155e612b447387c268740781c785d5810" + integrity sha512-COToYKgquDyligbcAep7ygs48RK+mwe/IYprq4+TSrVFzNOYmzWvHf6werpnKV5VYpRiwdn+Wa5ZXkPqLVwcTg== + dependencies: + "@aws-sdk/types" "^3.973.7" + "@aws-sdk/util-arn-parser" "^3.972.3" + "@smithy/node-config-provider" "^4.3.13" + "@smithy/protocol-http" "^5.3.13" + "@smithy/types" "^4.14.0" + "@smithy/util-config-provider" "^4.2.2" + tslib "^2.6.2" + "@aws-sdk/middleware-endpoint-discovery@^3.972.9": version "3.972.9" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.972.9.tgz#664f9074b0017255680c200bd9b8b23a864c0ad5" @@ -933,6 +1036,36 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@aws-sdk/middleware-expect-continue@^3.972.9": + version "3.972.9" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.9.tgz#ad62cbc4c5f310a5d104b7fc1150eca13a3c07a4" + integrity sha512-V/FNCjFxnh4VGu+HdSiW4Yg5GELihA1MIDSAdsEPvuayXBVmr0Jaa6jdLAZLH38KYXl/vVjri9DQJWnTAujHEA== + dependencies: + "@aws-sdk/types" "^3.973.7" + "@smithy/protocol-http" "^5.3.13" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-flexible-checksums@^3.974.7": + version "3.974.7" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.7.tgz#cc2c8efc5932e7bb55d58d717fe60c45fbf21a41" + integrity sha512-uU4/ch2CLHB8Phu1oTKnnQ4e8Ujqi49zEnQYBhWYT53zfFvtJCdGsaOoypBr8Fm/pmCBssRmGoIQ4sixgdLP9w== + dependencies: + "@aws-crypto/crc32" "5.2.0" + "@aws-crypto/crc32c" "5.2.0" + "@aws-crypto/util" "5.2.0" + "@aws-sdk/core" "^3.973.27" + "@aws-sdk/crc64-nvme" "^3.972.6" + "@aws-sdk/types" "^3.973.7" + "@smithy/is-array-buffer" "^4.2.2" + "@smithy/node-config-provider" "^4.3.13" + "@smithy/protocol-http" "^5.3.13" + "@smithy/types" "^4.14.0" + "@smithy/util-middleware" "^4.2.13" + "@smithy/util-stream" "^4.5.22" + "@smithy/util-utf8" "^4.2.2" + tslib "^2.6.2" + "@aws-sdk/middleware-host-header@^3.972.8": version "3.972.8" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz#72186e96500b49b38fb5482d6b7bf95e5b985281" @@ -953,6 +1086,15 @@ "@smithy/types" "^4.14.0" tslib "^2.6.2" +"@aws-sdk/middleware-location-constraint@^3.972.9": + version "3.972.9" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.9.tgz#35a7a35b678d931970b146024078c509631861ad" + integrity sha512-TyfOi2XNdOZpNKeTJwRUsVAGa+14nkyMb2VVGG+eDgcWG/ed6+NUo72N3hT6QJioxym80NSinErD+LBRF0Ir1w== + dependencies: + "@aws-sdk/types" "^3.973.7" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@aws-sdk/middleware-logger@^3.972.8": version "3.972.8" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz#7fee4223afcb6f7828dbdf4ea745ce15027cf384" @@ -993,6 +1135,35 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@aws-sdk/middleware-sdk-s3@^3.972.28": + version "3.972.28" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.28.tgz#cfdcaab69da8870e039dc58499ac323cd7667242" + integrity sha512-qJHcJQH9UNPUrnPlRtCozKjtqAaypQ5IgQxTNoPsVYIQeuwNIA8Rwt3NvGij1vCDYDfCmZaPLpnJEHlZXeFqmg== + dependencies: + "@aws-sdk/core" "^3.973.27" + "@aws-sdk/types" "^3.973.7" + "@aws-sdk/util-arn-parser" "^3.972.3" + "@smithy/core" "^3.23.14" + "@smithy/node-config-provider" "^4.3.13" + "@smithy/protocol-http" "^5.3.13" + "@smithy/signature-v4" "^5.3.13" + "@smithy/smithy-client" "^4.12.9" + "@smithy/types" "^4.14.0" + "@smithy/util-config-provider" "^4.2.2" + "@smithy/util-middleware" "^4.2.13" + "@smithy/util-stream" "^4.5.22" + "@smithy/util-utf8" "^4.2.2" + tslib "^2.6.2" + +"@aws-sdk/middleware-ssec@^3.972.9": + version "3.972.9" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.9.tgz#3658fd92752682316c48b736d6c013a75cfcd7aa" + integrity sha512-wSA2BR7L0CyBNDJeSrleIIzC+DzL93YNTdfU0KPGLiocK6YsRv1nPAzPF+BFSdcs0Qa5ku5Kcf4KvQcWwKGenQ== + dependencies: + "@aws-sdk/types" "^3.973.7" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@aws-sdk/middleware-user-agent@^3.972.28": version "3.972.28" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.28.tgz#7f81d96d2fed0334ff601af62d77e14f67fb9d22" @@ -1149,6 +1320,32 @@ "@smithy/types" "^4.14.0" tslib "^2.6.2" +"@aws-sdk/s3-request-presigner@^3.1021.0": + version "3.1030.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1030.0.tgz#196ae8be725272533390c696cda014de581e419b" + integrity sha512-rLM1DjBb9QlQwijKGtVSfWGi2gEz8yYj244RRWsPoGAhl57xKS0OGq6MygP/UYTPVc6r5qr4a8Gq1wos4QxnVw== + dependencies: + "@aws-sdk/signature-v4-multi-region" "^3.996.16" + "@aws-sdk/types" "^3.973.7" + "@aws-sdk/util-format-url" "^3.972.9" + "@smithy/middleware-endpoint" "^4.4.29" + "@smithy/protocol-http" "^5.3.13" + "@smithy/smithy-client" "^4.12.9" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + +"@aws-sdk/signature-v4-multi-region@^3.996.16": + version "3.996.16" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.16.tgz#a078e17caa4b94dad8add2e8b1be6f2362d4c83f" + integrity sha512-EMdXYB4r/k5RWq86fugjRhid5JA+Z6MpS7n4sij4u5/C+STrkvuf9aFu41rJA9MjUzxCLzv8U2XL8cH2GSRYpQ== + dependencies: + "@aws-sdk/middleware-sdk-s3" "^3.972.28" + "@aws-sdk/types" "^3.973.7" + "@smithy/protocol-http" "^5.3.13" + "@smithy/signature-v4" "^5.3.13" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@aws-sdk/token-providers@3.1021.0": version "3.1021.0" resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.1021.0.tgz#90905a8def49f90e54a73849e25ad4bcc4dbea2a" @@ -1191,6 +1388,13 @@ "@smithy/types" "^4.14.0" tslib "^2.6.2" +"@aws-sdk/util-arn-parser@^3.972.3": + version "3.972.3" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz#ed989862bbb172ce16d9e1cd5790e5fe367219c2" + integrity sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA== + dependencies: + tslib "^2.6.2" + "@aws-sdk/util-dynamodb@^3.996.2": version "3.996.2" resolved "https://registry.yarnpkg.com/@aws-sdk/util-dynamodb/-/util-dynamodb-3.996.2.tgz#9521dfe84c031809f8cf2e32f03c58fd8a4bb84f" @@ -1230,6 +1434,16 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@aws-sdk/util-format-url@^3.972.9": + version "3.972.9" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-format-url/-/util-format-url-3.972.9.tgz#a52e141dc7b8dcb954460e34fe4a0b9451734d7b" + integrity sha512-fNJXHrs0ZT7Wx0KGIqKv7zLxlDXt2vqjx9z6oKUQFmpE5o4xxnSryvVHfHpIifYHWKz94hFccIldJ0YSZjlCBw== + dependencies: + "@aws-sdk/types" "^3.973.7" + "@smithy/querystring-builder" "^4.2.13" + "@smithy/types" "^4.14.0" + tslib "^2.6.2" + "@aws-sdk/util-locate-window@^3.0.0": version "3.965.5" resolved "https://registry.yarnpkg.com/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz#e30e6ff2aff6436209ed42c765dec2d2a48df7c0" @@ -2913,6 +3127,21 @@ dependencies: "@sinonjs/commons" "^3.0.1" +"@smithy/chunked-blob-reader-native@^4.2.3": + version "4.2.3" + resolved "https://registry.yarnpkg.com/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.3.tgz#9e79a80d8d44798e7ce7a8f968cbbbaf5a40d950" + integrity sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw== + dependencies: + "@smithy/util-base64" "^4.3.2" + tslib "^2.6.2" + +"@smithy/chunked-blob-reader@^5.2.2": + version "5.2.2" + resolved "https://registry.yarnpkg.com/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz#3af48e37b10e5afed478bb31d2b7bc03c81d196c" + integrity sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw== + dependencies: + tslib "^2.6.2" + "@smithy/config-resolver@^4.4.13": version "4.4.13" resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-4.4.13.tgz#8bffd41de647ec349b4a74bf02bdd1b32452bacd" @@ -3001,6 +3230,16 @@ "@smithy/util-hex-encoding" "^4.2.2" tslib "^2.6.2" +"@smithy/eventstream-codec@^4.2.14": + version "4.2.14" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-codec/-/eventstream-codec-4.2.14.tgz#4963ca27242b80c5b1d11dcd3ea1bee2a3c5f96d" + integrity sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw== + dependencies: + "@aws-crypto/crc32" "5.2.0" + "@smithy/types" "^4.14.1" + "@smithy/util-hex-encoding" "^4.2.2" + tslib "^2.6.2" + "@smithy/eventstream-serde-browser@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.12.tgz#3ceb8743750edaf5d6e42cd1a2327e048f85ba4e" @@ -3010,6 +3249,15 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/eventstream-serde-browser@^4.2.13": + version "4.2.14" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.14.tgz#b483667ea358975afb2170cd2618b9aa53a0fb29" + integrity sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ== + dependencies: + "@smithy/eventstream-serde-universal" "^4.2.14" + "@smithy/types" "^4.14.1" + tslib "^2.6.2" + "@smithy/eventstream-serde-config-resolver@^4.3.12": version "4.3.12" resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.12.tgz#a29164bc5480d935ece9dbdca0f79924259e519a" @@ -3018,6 +3266,14 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/eventstream-serde-config-resolver@^4.3.13": + version "4.3.14" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.14.tgz#2eb23acad43414b9bc0b43f34ae9afbd5464e484" + integrity sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA== + dependencies: + "@smithy/types" "^4.14.1" + tslib "^2.6.2" + "@smithy/eventstream-serde-node@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.12.tgz#2cc06a1ea1108f679d376aab81e95a6f69877b4a" @@ -3027,6 +3283,15 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/eventstream-serde-node@^4.2.13": + version "4.2.14" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.14.tgz#402c2a3b0437b7ac9747090a38a60d3642813490" + integrity sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw== + dependencies: + "@smithy/eventstream-serde-universal" "^4.2.14" + "@smithy/types" "^4.14.1" + tslib "^2.6.2" + "@smithy/eventstream-serde-universal@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.12.tgz#a3640d1e7c3e348168360035661db8d21b51e078" @@ -3036,6 +3301,15 @@ "@smithy/types" "^4.13.1" tslib "^2.6.2" +"@smithy/eventstream-serde-universal@^4.2.14": + version "4.2.14" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.14.tgz#1e1d29c111e580a93f3c197139c5ca8c976ec205" + integrity sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg== + dependencies: + "@smithy/eventstream-codec" "^4.2.14" + "@smithy/types" "^4.14.1" + tslib "^2.6.2" + "@smithy/fetch-http-handler@^5.3.15": version "5.3.15" resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz#acf69a8b3bab0396d2782fc901bad0b957c8c6a2" @@ -3058,6 +3332,16 @@ "@smithy/util-base64" "^4.3.2" tslib "^2.6.2" +"@smithy/hash-blob-browser@^4.2.14": + version "4.2.15" + resolved "https://registry.yarnpkg.com/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.15.tgz#1323f9717cad352b3e18065b738387bb9684f993" + integrity sha512-0PJ4Al3fg2nM4qKrAIxyNcApgqHAXcBkN8FeizOz69z0rb26uZ6lMESYtxegaTlXB5Hj84JfwMPavMrwDMjucA== + dependencies: + "@smithy/chunked-blob-reader" "^5.2.2" + "@smithy/chunked-blob-reader-native" "^4.2.3" + "@smithy/types" "^4.14.1" + tslib "^2.6.2" + "@smithy/hash-node@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/hash-node/-/hash-node-4.2.12.tgz#0ee7f6a1d2958c313ee24b07159dcb9547792441" @@ -3078,6 +3362,15 @@ "@smithy/util-utf8" "^4.2.2" tslib "^2.6.2" +"@smithy/hash-stream-node@^4.2.13": + version "4.2.14" + resolved "https://registry.yarnpkg.com/@smithy/hash-stream-node/-/hash-stream-node-4.2.14.tgz#98bc14e79e2be852d04ff6cbfe4b0babe48fb10d" + integrity sha512-tw4GANWkZPb6+BdD4Fgucqzey2+r73Z/GRo9zklsCdwrnxxumUV83ZIaBDdudV4Ylazw3EPTiJZhpX42105ruQ== + dependencies: + "@smithy/types" "^4.14.1" + "@smithy/util-utf8" "^4.2.2" + tslib "^2.6.2" + "@smithy/invalid-dependency@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz#1a28c13fb33684b91848d4d6ec5104a1c1413e7f" @@ -3108,6 +3401,15 @@ dependencies: tslib "^2.6.2" +"@smithy/md5-js@^4.2.13": + version "4.2.14" + resolved "https://registry.yarnpkg.com/@smithy/md5-js/-/md5-js-4.2.14.tgz#c066572ec84def147af24e55a402c44d0d7dcd7b" + integrity sha512-V2v0vx+h0iUSNG1Alt+GNBMSLGCrl9iVsdd+Ap67HPM9PN479x12V8LkuMoKImNZxn3MXeuyUjls+/7ZACZghA== + dependencies: + "@smithy/types" "^4.14.1" + "@smithy/util-utf8" "^4.2.2" + tslib "^2.6.2" + "@smithy/middleware-content-length@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz#dec97ea1444b12e734156b764e9953b2b37c70fd" @@ -3425,6 +3727,13 @@ dependencies: tslib "^2.6.2" +"@smithy/types@^4.14.1": + version "4.14.1" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.14.1.tgz#aba92b4cdb406f2a2b062e82f1e3728d809a7c23" + integrity sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg== + dependencies: + tslib "^2.6.2" + "@smithy/url-parser@^4.2.12": version "4.2.12" resolved "https://registry.yarnpkg.com/@smithy/url-parser/-/url-parser-4.2.12.tgz#e940557bf0b8e9a25538a421970f64bd827f456f" @@ -3891,6 +4200,13 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== +"@types/ws@^8.18.0": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" + integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "21.0.3" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" @@ -10270,6 +10586,11 @@ write-file-atomic@^5.0.1: imurmurhash "^0.1.4" signal-exit "^4.0.1" +ws@^8.18.0: + version "8.20.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.20.0.tgz#4cd9532358eba60bc863aad1623dfb045a4d4af8" + integrity sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA== + xml@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"