diff --git a/.gitignore b/.gitignore index 63ee6c1ea..b93110bf0 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ lib/ !05-blueprints/**/lib/ !02-use-cases/visa-b2b-account-payable-agent/infrastructure/lib/ !04-infrastructure-as-code/cdk/typescript/knowledge-base-rag-agent/infrastructure/lib/ +!04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/lib/ !01-tutorials/01-AgentCore-runtime/01-hosting-agent/05-java-agents/01-springai-with-bedrock-model/infra/lib/ !01-tutorials/01-AgentCore-runtime/01-hosting-agent/05-java-agents/02-embabel-with-bedrock-model/infra/lib/ lib64/ @@ -255,4 +256,5 @@ Test-Downloads/ ### Docker ### Dockerfile +!04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/**/Dockerfile .dockerignore diff --git a/04-infrastructure-as-code/README.md b/04-infrastructure-as-code/README.md index c179ebf31..864c684db 100644 --- a/04-infrastructure-as-code/README.md +++ b/04-infrastructure-as-code/README.md @@ -117,7 +117,8 @@ For Terraform samples, also install: │ │ ├── multi-agent-runtime/ │ │ └── end-to-end-weather-agent/ │ └── typescript/ # TypeScript CDK samples -│ └── knowledge-base-rag-agent/ +│ ├── knowledge-base-rag-agent/ +│ └── digital-preservation-agent/ └── terraform/ # Terraform samples ├── README.md # Terraform-specific guide ├── basic-runtime/ diff --git a/04-infrastructure-as-code/cdk/README.md b/04-infrastructure-as-code/cdk/README.md index e1a8506e4..656de550a 100644 --- a/04-infrastructure-as-code/cdk/README.md +++ b/04-infrastructure-as-code/cdk/README.md @@ -7,7 +7,7 @@ Deploy Amazon Bedrock AgentCore resources using AWS CDK in Python or TypeScript. | Language | Description | Samples | |----------|-------------|---------| | **[Python](./python/)** | Familiar syntax for Python developers, quick prototyping | 4 samples | -| **[TypeScript](./typescript/)** | Strong typing, rich npm ecosystem, compile-time checks | 1 sample | +| **[TypeScript](./typescript/)** | Strong typing, rich npm ecosystem, compile-time checks | 2 samples | ## Prerequisites @@ -41,6 +41,7 @@ Deploy Amazon Bedrock AgentCore resources using AWS CDK in Python or TypeScript. | Sample | Description | |--------|-------------| | [knowledge-base-rag-agent](./typescript/knowledge-base-rag-agent/) | Full-stack RAG agent with Knowledge Base, OpenSearch Serverless, web interface, and Cognito authentication | +| [digital-preservation-agent](./typescript/digital-preservation-agent/) | Digital preservation agent with Apache Tika, Siegfried, DROID, and MediaInfo on ECS Fargate, AgentCore Gateway (MCP), and AgentCore Runtime | ## CDK Advantages Over CloudFormation diff --git a/04-infrastructure-as-code/cdk/typescript/README.md b/04-infrastructure-as-code/cdk/typescript/README.md index 62b8ca13e..0c46a2a34 100644 --- a/04-infrastructure-as-code/cdk/typescript/README.md +++ b/04-infrastructure-as-code/cdk/typescript/README.md @@ -26,6 +26,7 @@ cdk deploy --all ## Samples - **[knowledge-base-rag-agent/](./knowledge-base-rag-agent/)** - Full-stack RAG agent with Knowledge Base, OpenSearch Serverless, web interface, and Cognito authentication +- **[digital-preservation-agent/](./digital-preservation-agent/)** - Document processing agent with Apache Tika on ECS Fargate, AgentCore Gateway, and AgentCore Runtime ## TypeScript CDK Advantages diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/.gitignore b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/.gitignore new file mode 100644 index 000000000..dfe76b3e1 --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/.gitignore @@ -0,0 +1,11 @@ +node_modules/ +cdk.out/ +cdk.context.json +dist/ +*.js +*.d.ts +*.js.map +!jest.config.js +.env +__pycache__/ +*.pyc diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/README.md b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/README.md new file mode 100644 index 000000000..df7bb4eaa --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/README.md @@ -0,0 +1,149 @@ +# Digital Preservation Agent (CDK TypeScript) + +Deploy a digital preservation agent using Amazon Bedrock AgentCore with multiple file analysis tools running on ECS Fargate: [Apache Tika](https://tika.apache.org/), [Siegfried](https://www.itforarchivists.com/siegfried), [DROID](https://digital-preservation.github.io/droid/), and [MediaInfo](https://mediaarea.net/en/MediaInfo). An AgentCore Gateway exposes all tools via MCP, Lambda functions bridge tool calls to each service, and an AgentCore Runtime hosts a Strands agent that orchestrates analysis workflows. + +## Architecture + +``` +User → AgentCore Runtime (Strands Agent, Claude 3 Haiku) + ↓ MCP + AgentCore Gateway + ↓ + Lambda functions (tool bridges) + ↓ + Internal ALB (path-based routing) + ├── /tika*, /detect/*, /meta* → ECS Fargate (Apache Tika :9998) + ├── /identify/* → ECS Fargate (Siegfried :5138) + ├── /api/* → ECS Fargate (DROID :8080) + └── /mediainfo/* → ECS Fargate (MediaInfo :8081) + ↓ + S3 Bucket (document uploads + reports) +``` + +## Prerequisites + +- [Node.js 18+](https://nodejs.org/) and npm +- [Docker](https://docs.docker.com/get-docker/) (required for building container images) +- [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting-started.html) (`npm install -g aws-cdk`) +- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) configured with appropriate credentials +- [Bedrock Foundation model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) — ensure Claude 3 Haiku is enabled in your target region + +## Deployment + +```bash +cd infrastructure +npm install +npx cdk bootstrap # first time only +npx cdk deploy +``` + +> Use `npx cdk` to run the project-local CDK CLI. A globally installed CDK may be too old for the `aws-cdk-lib` version used here. + +### Stack Outputs + +| Output | Description | +|---|---| +| `RuntimeArn` | AgentCore Runtime ARN (use to invoke the agent) | +| `GatewayUrl` | AgentCore Gateway URL (MCP endpoint) | +| `GatewayId` | AgentCore Gateway ID | +| `AlbDns` | Internal ALB DNS name | +| `DocsBucketName` | S3 bucket for uploads and reports | + +## Usage + +1. Upload a file: + ```bash + aws s3 cp my-report.pdf s3:///my-report.pdf + ``` + +2. Invoke the agent: + ```python + import boto3, json + client = boto3.client("bedrock-agentcore") + response = client.invoke_agent_runtime( + agentRuntimeArn="", + request={"prompt": "Identify the format of my-report.pdf using Siegfried and extract its text with Tika"}, + ) + print(json.dumps(response, indent=2, default=str)) + ``` + +## Available Tools + +| Tool | Service | Description | +|---|---|---| +| `tika_process` | Tika | Fetch S3 file → extract text, metadata, or detect MIME type (handles archives directly) | +| `siegfried_identify` | Siegfried | Identify file format using PRONOM registry (requires extraction for archives) | +| `droid_profile` | DROID | Profile file format using DROID (requires extraction for archives) | +| `mediainfo_analyze` | MediaInfo | Analyze media file technical metadata (requires extraction for archives) | +| `extract_archive` | Lambda | Extract ZIP/TAR archives to S3 | +| `save_report_to_s3` | Lambda | Save analysis report as JSON to S3 | + +## ALB Routing + +| Path Pattern | Target | Port | +|---|---|---| +| `/tika*`, `/detect/*`, `/meta*` | Apache Tika | 9998 | +| `/identify/*` | Siegfried | 5138 | +| `/api/*` | DROID | 8080 | +| `/mediainfo/*` | MediaInfo | 8081 | + +## Sample Prompts + +- Identify the format of my-report.pdf using Siegfried +- Extract text from presentation.pptx +- Analyze the media metadata of video.mp4 +- Run a full preservation analysis on archive.zip — extract it, then identify all files +- Profile document.docx with DROID and save the results as a report + +## Project Structure + +``` +digital-preservation-agent/ +├── agent/ +│ ├── main.py # Strands agent for AgentCore Runtime +│ ├── requirements.txt +│ └── Dockerfile +├── backend/ +│ ├── tika_handler.py # Tika tool bridge +│ ├── siegfried_handler.py # Siegfried tool bridge +│ ├── droid_handler.py # DROID tool bridge +│ ├── mediainfo_handler.py # MediaInfo tool bridge +│ ├── extract_handler.py # Archive extraction (S3 only) +│ └── s3_report_handler.py # Report persistence (S3 only) +├── containers/ +│ ├── droid/ # DROID Docker image (eclipse-temurin:17-jre) +│ └── mediainfo/ # MediaInfo Docker image (alpine:3.20) +├── infrastructure/ +│ ├── bin/app.ts +│ ├── lib/stacks/ +│ │ └── digital-preservation-stack.ts +│ ├── config.json +│ ├── package.json +│ ├── tsconfig.json +│ └── cdk.json +├── .gitignore +└── README.md +``` + +## Configuration + +Edit `infrastructure/config.json`: + +| Key | Default | Description | +|---|---|---| +| `agentModelId` | `eu.anthropic.claude-3-5-haiku-20241022-v1:0` | Foundation model ([cross-region inference profile](https://docs.aws.amazon.com/bedrock/latest/userguide/cross-region-inference.html)) | +| `tikaImageTag` | `3.2.3.0-full` | Apache Tika Docker image tag | +| `fargateMemoryMiB` | `2048` | Fargate task memory (shared across all services) | +| `fargateCpu` | `1024` | Fargate task CPU | +| `desiredCount` | `1` | Number of Fargate tasks per service | + +> Siegfried uses the pre-built `ghcr.io/keeps/siegfried:v1.10.1` image directly (no Dockerfile). DROID and MediaInfo have custom Dockerfiles in `containers/` and are built with `--platform linux/amd64` for ECS Fargate compatibility. + +> The default `agentModelId` uses the `eu.` cross-region inference prefix, which routes requests to EU-based Bedrock endpoints. If deploying to a non-EU region, change this to a region-appropriate prefix (e.g., `us.anthropic.claude-3-5-haiku-20241022-v1:0`) or use the base model ID `anthropic.claude-3-5-haiku-20241022-v1:0`. See [cross-region inference](https://docs.aws.amazon.com/bedrock/latest/userguide/cross-region-inference.html) for available prefixes. + +## Clean Up + +```bash +cd infrastructure +npx cdk destroy +``` diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/agent/Dockerfile b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/agent/Dockerfile new file mode 100644 index 000000000..5ad45c22f --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/agent/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY main.py . + +EXPOSE 8080 +CMD ["opentelemetry-instrument", "python", "main.py"] diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/agent/main.py b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/agent/main.py new file mode 100644 index 000000000..d9511a3dc --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/agent/main.py @@ -0,0 +1,82 @@ +""" +Strands agent for digital preservation with Apache Tika, Siegfried, DROID, and MediaInfo. + +Deployed to AgentCore Runtime, connects to an AgentCore Gateway +that exposes tools via MCP. Uses SigV4 signing for IAM-authenticated +Gateway access. +""" + +import os +import logging + +from bedrock_agentcore.runtime import BedrockAgentCoreApp +from strands import Agent +from strands.models import BedrockModel +from strands.tools.mcp import MCPClient +from mcp_proxy_for_aws.client import aws_iam_streamablehttp_client + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +app = BedrockAgentCoreApp() + +# --- Module-level config (read once at container startup) --- +GATEWAY_URL = os.environ.get("GATEWAY_URL", "") +AWS_REGION = os.environ.get("AWS_REGION", "us-east-1") +MODEL_ID = os.environ.get("MODEL_ID", "eu.anthropic.claude-3-5-haiku-20241022-v1:0") +AGENT_INSTRUCTION = os.environ.get( + "AGENT_INSTRUCTION", + "You are a digital preservation assistant with access to Apache Tika, " + "Siegfried, DROID, and MediaInfo for file format identification, text " + "extraction, metadata retrieval, and media analysis. You can also " + "extract archives and save analysis reports to S3. Apache Tika can " + "process archives directly, but Siegfried, DROID, and MediaInfo require " + "files to be extracted first using extract_archive before analysis.", +) + +# Cache the model instance — it's stateless and safe to reuse across requests. +model = BedrockModel(inference_profile_id=MODEL_ID, region_name=AWS_REGION) + + +def _create_mcp_client(): + """Create an MCP client factory for the AgentCore Gateway.""" + return MCPClient( + lambda: aws_iam_streamablehttp_client( + endpoint=GATEWAY_URL, + aws_region=AWS_REGION, + aws_service="bedrock-agentcore", + ) + ) + + +@app.entrypoint +def handler(payload: dict) -> dict: + """Handle incoming agent requests from AgentCore Runtime.""" + prompt = payload.get("prompt", payload.get("message", "")) + if not prompt: + return {"error": "No prompt provided", "status": "error"} + + logger.info( + "Received prompt: %s | model=%s region=%s", prompt[:200], MODEL_ID, AWS_REGION + ) + + # MCPClient uses a context manager to manage the MCP session lifecycle. + # Each request gets its own session to avoid stale connection issues. + mcp_client = _create_mcp_client() + + with mcp_client: + tools = list(mcp_client.list_tools_sync()) + logger.info("Available tools: %s", [t.tool_name for t in tools]) + + agent = Agent( + model=model, + tools=tools, + system_prompt=AGENT_INSTRUCTION, + ) + + result = agent(prompt) + return {"response": str(result.message), "status": "success"} + + +if __name__ == "__main__": + app.run() diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/agent/requirements.txt b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/agent/requirements.txt new file mode 100644 index 000000000..afc72605b --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/agent/requirements.txt @@ -0,0 +1,7 @@ +bedrock-agentcore +strands-agents +strands-agents-tools +mcp +mcp-proxy-for-aws +boto3 +aws-opentelemetry-distro>=0.10.0 diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/backend/droid_handler.py b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/backend/droid_handler.py new file mode 100644 index 000000000..13321d495 --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/backend/droid_handler.py @@ -0,0 +1,60 @@ +"""Lambda bridging AgentCore Gateway MCP tool calls to DROID on Fargate.""" + +import json +import logging +import os +import urllib.request +import boto3 +from botocore.exceptions import ClientError + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +ALB_URL = os.environ["ALB_URL"] +DOCS_BUCKET = os.environ.get("DOCS_BUCKET", "") +s3 = boto3.client("s3") +MAX_FILE_SIZE = 100 * 1024 * 1024 + + +def handler(event, context): + logger.info("Event: %s", json.dumps(event, default=str)) + + s3_key = event.get("s3_key", "") + if not s3_key: + return _resp({"error": "s3_key is required"}) + + if s3_key.startswith("s3://"): + s3_key = s3_key[5:].split("/", 1)[-1] + + try: + head = s3.head_object(Bucket=DOCS_BUCKET, Key=s3_key) + if head.get("ContentLength", 0) > MAX_FILE_SIZE: + return _resp( + {"error": f"File exceeds {MAX_FILE_SIZE // (1024 * 1024)} MB limit"} + ) + obj = s3.get_object(Bucket=DOCS_BUCKET, Key=s3_key) + file_bytes = obj["Body"].read() + except ClientError as e: + code = e.response["Error"]["Code"] + if code in ("NoSuchKey", "NoSuchBucket"): + return _resp({"error": f"Not found: s3://{DOCS_BUCKET}/{s3_key}"}) + return _resp({"error": "Failed to retrieve file from S3"}) + + filename = s3_key.split("/")[-1] + try: + req = urllib.request.Request( + f"{ALB_URL}/api/identify/{filename}", + data=file_bytes, + headers={"Content-Type": "application/octet-stream"}, + method="POST", + ) + with urllib.request.urlopen(req, timeout=120) as resp: + result = json.loads(resp.read().decode("utf-8")) + return _resp({"s3_key": s3_key, "profile": result}) + except Exception: + logger.exception("DROID call failed") + return _resp({"error": "DROID profiling failed"}) + + +def _resp(body): + return {"output": json.dumps(body)} diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/backend/extract_handler.py b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/backend/extract_handler.py new file mode 100644 index 000000000..7a198dffb --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/backend/extract_handler.py @@ -0,0 +1,118 @@ +"""Lambda that extracts archive files (ZIP, TAR, etc.) from S3 back into S3.""" + +import json +import logging +import os +import io +import zipfile +import tarfile +import boto3 +from botocore.exceptions import ClientError + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +DOCS_BUCKET = os.environ.get("DOCS_BUCKET", "") +s3_client = boto3.client("s3") +MAX_FILE_SIZE_BYTES = 500 * 1024 * 1024 # 500 MB for archives + + +def handler(event, context): + logger.info("Received event: %s", json.dumps(event, default=str)) + tool_input = event + + s3_key = tool_input.get("s3_key") + if not s3_key: + return _response({"error": "s3_key is required"}) + + if s3_key.startswith("s3://"): + parts = s3_key[5:].split("/", 1) + s3_key = parts[1] if len(parts) > 1 else "" + if not s3_key: + return _response({"error": "Could not extract object key from S3 URI"}) + + # Destination prefix defaults to archive name without extension + dest_prefix = tool_input.get("destination_prefix", "") + if not dest_prefix: + base = s3_key.rsplit("/", 1)[-1] + dest_prefix = base.rsplit(".", 1)[0] + "_extracted/" + + try: + head = s3_client.head_object(Bucket=DOCS_BUCKET, Key=s3_key) + if head.get("ContentLength", 0) > MAX_FILE_SIZE_BYTES: + return _response( + { + "error": f"Archive exceeds {MAX_FILE_SIZE_BYTES // (1024 * 1024)} MB limit" + } + ) + obj = s3_client.get_object(Bucket=DOCS_BUCKET, Key=s3_key) + archive_bytes = obj["Body"].read() + except ClientError as e: + code = e.response["Error"]["Code"] + if code in ("NoSuchKey", "NoSuchBucket"): + return _response({"error": f"Not found: s3://{DOCS_BUCKET}/{s3_key}"}) + return _response({"error": "Failed to retrieve archive from S3"}) + + extracted_files = [] + try: + if zipfile.is_zipfile(io.BytesIO(archive_bytes)): + extracted_files = _extract_zip(archive_bytes, dest_prefix) + elif _is_tarfile(archive_bytes): + extracted_files = _extract_tar(archive_bytes, dest_prefix) + else: + return _response( + {"error": "Unsupported archive format. Supported: ZIP, TAR, TAR.GZ"} + ) + except Exception: + logger.exception("Archive extraction failed") + return _response({"error": "Archive extraction failed"}) + + return _response( + { + "s3_key": s3_key, + "destination_prefix": dest_prefix, + "extracted_count": len(extracted_files), + "extracted_files": extracted_files[:50], # cap listing + } + ) + + +def _extract_zip(archive_bytes, dest_prefix): + extracted = [] + with zipfile.ZipFile(io.BytesIO(archive_bytes)) as zf: + for info in zf.infolist(): + if info.is_dir(): + continue + dest_key = dest_prefix + info.filename + s3_client.put_object( + Bucket=DOCS_BUCKET, Key=dest_key, Body=zf.read(info.filename) + ) + extracted.append(dest_key) + return extracted + + +def _extract_tar(archive_bytes, dest_prefix): + extracted = [] + with tarfile.open(fileobj=io.BytesIO(archive_bytes)) as tf: + for member in tf.getmembers(): + if not member.isfile(): + continue + f = tf.extractfile(member) + if f is None: + continue + dest_key = dest_prefix + member.name + s3_client.put_object(Bucket=DOCS_BUCKET, Key=dest_key, Body=f.read()) + extracted.append(dest_key) + return extracted + + +def _is_tarfile(data): + try: + with tarfile.open(fileobj=io.BytesIO(data)) as _: + return True + except (tarfile.TarError, Exception): + return False + + +def _response(body): + return {"output": json.dumps(body)} diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/backend/mediainfo_handler.py b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/backend/mediainfo_handler.py new file mode 100644 index 000000000..a89db44e2 --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/backend/mediainfo_handler.py @@ -0,0 +1,60 @@ +"""Lambda bridging AgentCore Gateway MCP tool calls to MediaInfo on Fargate.""" + +import json +import logging +import os +import urllib.request +import boto3 +from botocore.exceptions import ClientError + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +ALB_URL = os.environ["ALB_URL"] +DOCS_BUCKET = os.environ.get("DOCS_BUCKET", "") +s3 = boto3.client("s3") +MAX_FILE_SIZE = 100 * 1024 * 1024 + + +def handler(event, context): + logger.info("Event: %s", json.dumps(event, default=str)) + + s3_key = event.get("s3_key", "") + if not s3_key: + return _resp({"error": "s3_key is required"}) + + if s3_key.startswith("s3://"): + s3_key = s3_key[5:].split("/", 1)[-1] + + try: + head = s3.head_object(Bucket=DOCS_BUCKET, Key=s3_key) + if head.get("ContentLength", 0) > MAX_FILE_SIZE: + return _resp( + {"error": f"File exceeds {MAX_FILE_SIZE // (1024 * 1024)} MB limit"} + ) + obj = s3.get_object(Bucket=DOCS_BUCKET, Key=s3_key) + file_bytes = obj["Body"].read() + except ClientError as e: + code = e.response["Error"]["Code"] + if code in ("NoSuchKey", "NoSuchBucket"): + return _resp({"error": f"Not found: s3://{DOCS_BUCKET}/{s3_key}"}) + return _resp({"error": "Failed to retrieve file from S3"}) + + filename = s3_key.split("/")[-1] + try: + req = urllib.request.Request( + f"{ALB_URL}/mediainfo/{filename}", + data=file_bytes, + headers={"Content-Type": "application/octet-stream"}, + method="POST", + ) + with urllib.request.urlopen(req, timeout=120) as resp: + result = json.loads(resp.read().decode("utf-8")) + return _resp({"s3_key": s3_key, "media_info": result}) + except Exception: + logger.exception("MediaInfo call failed") + return _resp({"error": "MediaInfo analysis failed"}) + + +def _resp(body): + return {"output": json.dumps(body)} diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/backend/s3_report_handler.py b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/backend/s3_report_handler.py new file mode 100644 index 000000000..2bc60cdf2 --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/backend/s3_report_handler.py @@ -0,0 +1,57 @@ +"""Lambda that saves analysis reports as JSON to S3.""" + +import json +import logging +import os +import datetime +import boto3 + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +DOCS_BUCKET = os.environ.get("DOCS_BUCKET", "") +s3_client = boto3.client("s3") + + +def handler(event, context): + logger.info("Received event: %s", json.dumps(event, default=str)) + tool_input = event + + report_data = tool_input.get("report_data") + if not report_data: + return _response({"error": "report_data is required"}) + + report_name = tool_input.get("report_name", "") + if not report_name: + ts = datetime.datetime.utcnow().strftime("%Y%m%d_%H%M%S") + report_name = f"analysis_report_{ts}" + + s3_key = f"reports/{report_name}.json" + + # report_data can be a string or dict + if isinstance(report_data, str): + body = report_data + else: + body = json.dumps(report_data, indent=2, default=str) + + try: + s3_client.put_object( + Bucket=DOCS_BUCKET, + Key=s3_key, + Body=body.encode("utf-8"), + ContentType="application/json", + ) + return _response( + { + "s3_key": s3_key, + "s3_uri": f"s3://{DOCS_BUCKET}/{s3_key}", + "status": "saved", + } + ) + except Exception: + logger.exception("Failed to save report to S3") + return _response({"error": "Failed to save report to S3"}) + + +def _response(body): + return {"output": json.dumps(body)} diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/backend/siegfried_handler.py b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/backend/siegfried_handler.py new file mode 100644 index 000000000..7f8f1df08 --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/backend/siegfried_handler.py @@ -0,0 +1,77 @@ +"""Lambda bridging AgentCore Gateway MCP tool calls to Siegfried on Fargate. + +Siegfried server accepts POST /identify with file as form-data. +Returns PRONOM format identification results (JSON). +""" + +import json +import logging +import os +import urllib.request +import urllib.error +import boto3 +from botocore.exceptions import ClientError + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +ALB_URL = os.environ["ALB_URL"] +DOCS_BUCKET = os.environ.get("DOCS_BUCKET", "") +s3 = boto3.client("s3") +MAX_FILE_SIZE = 100 * 1024 * 1024 + + +def handler(event, context): + logger.info("Event: %s", json.dumps(event, default=str)) + + s3_key = event.get("s3_key", "") + if not s3_key: + return _resp({"error": "s3_key is required"}) + + if s3_key.startswith("s3://"): + s3_key = s3_key[5:].split("/", 1)[-1] + + try: + head = s3.head_object(Bucket=DOCS_BUCKET, Key=s3_key) + if head.get("ContentLength", 0) > MAX_FILE_SIZE: + return _resp( + {"error": f"File exceeds {MAX_FILE_SIZE // (1024 * 1024)} MB limit"} + ) + obj = s3.get_object(Bucket=DOCS_BUCKET, Key=s3_key) + file_bytes = obj["Body"].read() + except ClientError as e: + code = e.response["Error"]["Code"] + if code in ("NoSuchKey", "NoSuchBucket"): + return _resp({"error": f"Not found: s3://{DOCS_BUCKET}/{s3_key}"}) + return _resp({"error": "Failed to retrieve file from S3"}) + + filename = s3_key.split("/")[-1] + try: + # Siegfried server expects POST /identify with multipart form-data + boundary = "----SiegfriedBoundary" + body = ( + ( + f"--{boundary}\r\n" + f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n' + f"Content-Type: application/octet-stream\r\n\r\n" + ).encode() + + file_bytes + + f"\r\n--{boundary}--\r\n".encode() + ) + + req = urllib.request.Request( + f"{ALB_URL}/identify?format=json", + data=body, + headers={"Content-Type": f"multipart/form-data; boundary={boundary}"}, + method="POST", + ) + with urllib.request.urlopen(req, timeout=120) as resp: + result = json.loads(resp.read().decode("utf-8")) + return _resp({"s3_key": s3_key, "identification": result}) + except Exception: + logger.exception("Siegfried call failed") + return _resp({"error": "Siegfried identification failed"}) + + +def _resp(body): + return {"output": json.dumps(body)} diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/backend/tika_handler.py b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/backend/tika_handler.py new file mode 100644 index 000000000..037fb56a8 --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/backend/tika_handler.py @@ -0,0 +1,98 @@ +"""Lambda bridging AgentCore Gateway MCP tool calls to Apache Tika on Fargate. + +Single tool (tika_process): fetches a file from S3 and runs text extraction, +metadata extraction, or MIME type detection. Tika handles archives natively. +""" + +import json +import logging +import os +import time +import urllib.request +import urllib.error +import boto3 +from botocore.exceptions import ClientError + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +ALB_URL = os.environ["ALB_URL"] +DOCS_BUCKET = os.environ.get("DOCS_BUCKET", "") +s3 = boto3.client("s3") +MAX_FILE_SIZE = 100 * 1024 * 1024 # 100 MB + +ACTIONS = { + "extract_text": ("/tika", "text/plain"), + "extract_metadata": ("/meta", "application/json"), + "detect_type": ("/detect/stream", "text/plain"), +} + + +def handler(event, context): + logger.info("Event: %s", json.dumps(event, default=str)) + + s3_key = event.get("s3_key", "") + if not s3_key: + return _resp({"error": "s3_key is required"}) + + # Strip s3:// prefix if present + if s3_key.startswith("s3://"): + s3_key = s3_key[5:].split("/", 1)[-1] + + action = event.get("action", "extract_text") + if action not in ACTIONS: + return _resp({"error": f"Invalid action '{action}'. Use: {', '.join(ACTIONS)}"}) + + # Fetch from S3 + try: + head = s3.head_object(Bucket=DOCS_BUCKET, Key=s3_key) + if head.get("ContentLength", 0) > MAX_FILE_SIZE: + return _resp( + {"error": f"File exceeds {MAX_FILE_SIZE // (1024 * 1024)} MB limit"} + ) + obj = s3.get_object(Bucket=DOCS_BUCKET, Key=s3_key) + file_bytes = obj["Body"].read() + content_type = obj.get("ContentType", "application/octet-stream") + except ClientError as e: + code = e.response["Error"]["Code"] + if code in ("NoSuchKey", "NoSuchBucket"): + return _resp({"error": f"Not found: s3://{DOCS_BUCKET}/{s3_key}"}) + return _resp({"error": "Failed to retrieve file from S3"}) + + # Call Tika + tika_path, accept = ACTIONS[action] + try: + result = _call_tika(tika_path, file_bytes, content_type, accept) + except Exception: + logger.exception("Tika call failed") + return _resp({"error": "Tika processing failed"}) + + if action == "extract_metadata": + return _resp({"s3_key": s3_key, "metadata": json.loads(result)}) + if action == "detect_type": + return _resp({"s3_key": s3_key, "mime_type": result.strip()}) + return _resp({"s3_key": s3_key, "extracted_text": result}) + + +def _call_tika(path, data, content_type, accept, timeout=90, retries=3): + last_err = None + for i in range(retries): + try: + req = urllib.request.Request( + f"{ALB_URL}{path}", + data=data, + headers={"Content-Type": content_type, "Accept": accept}, + method="PUT", + ) + with urllib.request.urlopen(req, timeout=timeout) as resp: + return resp.read().decode("utf-8") + except (urllib.error.URLError, ConnectionError) as e: + last_err = e + if i < retries - 1: + time.sleep(2**i) + logger.warning("Tika retry %d/%d: %s", i + 1, retries, e) + raise last_err + + +def _resp(body): + return {"output": json.dumps(body)} diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/containers/droid/Dockerfile b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/containers/droid/Dockerfile new file mode 100644 index 000000000..b8b830d6a --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/containers/droid/Dockerfile @@ -0,0 +1,25 @@ +FROM eclipse-temurin:17-jre + +# Download DROID binary +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl unzip python3 && \ + curl -L -o /tmp/droid.zip \ + "https://github.com/digital-preservation/droid/releases/download/droid-6.8.0/droid-binary-6.8.0-bin.zip" && \ + mkdir -p /opt/droid && \ + unzip /tmp/droid.zip -d /opt/droid && \ + rm /tmp/droid.zip && \ + chmod +x /opt/droid/droid.sh && \ + apt-get purge -y curl unzip && \ + apt-get autoremove -y && \ + rm -rf /var/lib/apt/lists/* + +ENV PATH="/opt/droid:${PATH}" + +# Pre-warm DROID: load signature database and JVM classes at build time +RUN echo "test" > /tmp/warmup.txt && \ + droid.sh -A /tmp/warmup.txt -p /tmp/warmup.droid || true && \ + rm -f /tmp/warmup.txt /tmp/warmup.droid + +COPY server.py /app/server.py +EXPOSE 8080 +CMD ["python3", "/app/server.py"] diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/containers/droid/server.py b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/containers/droid/server.py new file mode 100644 index 000000000..888c7313f --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/containers/droid/server.py @@ -0,0 +1,88 @@ +"""Lightweight HTTP wrapper around the DROID CLI for format profiling.""" + +import json +import subprocess +import tempfile +import os +import csv +from http.server import HTTPServer, BaseHTTPRequestHandler + + +class DroidHandler(BaseHTTPRequestHandler): + def do_GET(self): + if self.path == "/health": + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps({"status": "healthy"}).encode()) + else: + self.send_response(404) + self.end_headers() + + def do_POST(self): + if not self.path.startswith("/api/identify/"): + self.send_response(404) + self.end_headers() + return + + filename = os.path.basename(self.path.split("/api/identify/", 1)[1]) + if not filename: + self._respond(400, {"error": "filename is required"}) + return + content_length = int(self.headers.get("Content-Length", 0)) + file_bytes = self.rfile.read(content_length) + + with tempfile.TemporaryDirectory() as tmpdir: + file_path = os.path.join(tmpdir, filename) + profile_path = os.path.join(tmpdir, "profile.droid") + export_path = os.path.join(tmpdir, "export.csv") + + with open(file_path, "wb") as f: + f.write(file_bytes) + + try: + subprocess.run( + ["droid.sh", "-A", file_path, "-p", profile_path], + capture_output=True, + text=True, + timeout=90, + ) + subprocess.run( + ["droid.sh", "-p", profile_path, "-e", export_path], + capture_output=True, + text=True, + timeout=90, + ) + + results = [] + if os.path.exists(export_path): + with open(export_path, "r") as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + if row.get("TYPE") == "File": + results.append( + { + "puid": row.get("PUID", ""), + "format_name": row.get("FORMAT_NAME", ""), + "format_version": row.get("FORMAT_VERSION", ""), + "mime_type": row.get("MIME_TYPE", ""), + "method": row.get("METHOD", ""), + "file_size": row.get("SIZE", ""), + } + ) + + self._respond(200, {"results": results}) + except subprocess.TimeoutExpired: + self._respond(504, {"error": "DROID timed out"}) + + def _respond(self, status, body): + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps(body).encode()) + + +if __name__ == "__main__": + server = HTTPServer(("0.0.0.0", 8080), DroidHandler) + print("DROID server listening on port 8080") + server.serve_forever() diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/containers/mediainfo/Dockerfile b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/containers/mediainfo/Dockerfile new file mode 100644 index 000000000..ea5ee90f2 --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/containers/mediainfo/Dockerfile @@ -0,0 +1,7 @@ +FROM alpine:3.20 + +RUN apk add --no-cache python3 mediainfo + +COPY server.py /app/server.py +EXPOSE 8081 +CMD ["python3", "/app/server.py"] diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/containers/mediainfo/server.py b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/containers/mediainfo/server.py new file mode 100644 index 000000000..9497982e7 --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/containers/mediainfo/server.py @@ -0,0 +1,64 @@ +"""Lightweight HTTP wrapper around the MediaInfo CLI for audio/video analysis.""" + +import json +import subprocess +import tempfile +import os +from http.server import HTTPServer, BaseHTTPRequestHandler + + +class MediaInfoHandler(BaseHTTPRequestHandler): + def do_GET(self): + if self.path == "/health": + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps({"status": "healthy"}).encode()) + else: + self.send_response(404) + self.end_headers() + + def do_POST(self): + if not self.path.startswith("/mediainfo/"): + self.send_response(404) + self.end_headers() + return + + filename = os.path.basename(self.path.split("/mediainfo/", 1)[1]) + if not filename: + self.send_response(400) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps({"error": "filename is required"}).encode()) + return + content_length = int(self.headers.get("Content-Length", 0)) + file_bytes = self.rfile.read(content_length) + + with tempfile.NamedTemporaryFile(delete=False, suffix=f"_{filename}") as tmp: + tmp.write(file_bytes) + tmp_path = tmp.name + + try: + result = subprocess.run( + ["mediainfo", "--Output=JSON", tmp_path], + capture_output=True, + text=True, + timeout=60, + ) + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(result.stdout.encode()) + except subprocess.TimeoutExpired: + self.send_response(504) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps({"error": "MediaInfo timed out"}).encode()) + finally: + os.unlink(tmp_path) + + +if __name__ == "__main__": + server = HTTPServer(("0.0.0.0", 8081), MediaInfoHandler) + print("MediaInfo server listening on port 8081") + server.serve_forever() diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/bin/app.ts b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/bin/app.ts new file mode 100644 index 000000000..fd1532ebd --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/bin/app.ts @@ -0,0 +1,21 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { Aspects } from 'aws-cdk-lib'; +import { AwsSolutionsChecks } from 'cdk-nag'; +import { DigitalPreservationStack } from '../lib/stacks/digital-preservation-stack'; + +const app = new cdk.App(); + +Aspects.of(app).add(new AwsSolutionsChecks()); + +new DigitalPreservationStack(app, 'DigitalPreservationAgentStack', { + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + }, + description: + 'Digital preservation agent with Apache Tika, Siegfried, DROID, and MediaInfo on ECS Fargate, AgentCore Gateway, and AgentCore Runtime', +}); + +app.synth(); diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/cdk.json b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/cdk.json new file mode 100644 index 000000000..5a4b37831 --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/cdk.json @@ -0,0 +1,19 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/app.ts", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "node_modules", + "dist" + ] + }, + "context": { + "@aws-cdk/core:target-partitions": ["aws", "aws-cn"] + } +} diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/config.json b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/config.json new file mode 100644 index 000000000..7ce7f9266 --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/config.json @@ -0,0 +1,9 @@ +{ + "agentDescription": "Digital preservation agent with Apache Tika, Siegfried, DROID, and MediaInfo on ECS Fargate for file format identification, text extraction, metadata retrieval, and media analysis", + "agentInstruction": "You are a digital preservation assistant. You help users analyze, identify, and process digital files for long-term preservation. You have access to multiple tools: Apache Tika for text extraction, metadata retrieval, and MIME type detection; Siegfried for file format identification using the PRONOM registry; DROID for file format profiling; MediaInfo for audio/video/image technical metadata analysis. You can also extract archives (ZIP, TAR) and save analysis reports to S3. IMPORTANT: Apache Tika can process archive files (ZIP, TAR, etc.) directly without extraction — it will recursively parse all contained files and return combined text/metadata. However, Siegfried, DROID, and MediaInfo can only analyze individual files, so when a user wants to identify or profile files inside an archive, you must first use extract_archive to extract the contents to S3, then run the identification tools on each extracted file individually. Users upload files to the S3 bucket and ask you to process them by providing the S3 object key or full S3 URI. When a user provides a full S3 URI like s3://bucket/path/file.pdf, extract just the object key (path/file.pdf) and pass it to the appropriate tool. Always call the tool — do not guess or refuse without trying. For comprehensive analysis, consider running multiple tools on the same file to cross-reference results.", + "agentModelId": "eu.anthropic.claude-3-5-haiku-20241022-v1:0", + "tikaImageTag": "3.2.3.0-full", + "fargateMemoryMiB": 2048, + "fargateCpu": 1024, + "desiredCount": 1 +} diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/lib/stacks/digital-preservation-stack.ts b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/lib/stacks/digital-preservation-stack.ts new file mode 100644 index 000000000..0ca994397 --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/lib/stacks/digital-preservation-stack.ts @@ -0,0 +1,586 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import * as ecs from 'aws-cdk-lib/aws-ecs'; +import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as logs from 'aws-cdk-lib/aws-logs'; +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as ecr_assets from 'aws-cdk-lib/aws-ecr-assets'; +import * as agentcore from '@aws-cdk/aws-bedrock-agentcore-alpha'; +import { NagSuppressions } from 'cdk-nag'; + +interface Config { + agentDescription: string; + agentInstruction: string; + agentModelId: string; + tikaImageTag: string; + fargateMemoryMiB: number; + fargateCpu: number; + desiredCount: number; +} + +export class DigitalPreservationStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const configPath = path.join(__dirname, '../../config.json'); + const config: Config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + const projectRoot = path.join(__dirname, '../../..'); + + // --- VPC --- + const vpc = new ec2.Vpc(this, 'Vpc', { maxAzs: 2, natGateways: 1 }); + + const vpcFlowLogGroup = new logs.LogGroup(this, 'VpcFlowLogGroup', { + retention: logs.RetentionDays.ONE_WEEK, + }); + vpc.addFlowLog('FlowLog', { + destination: ec2.FlowLogDestination.toCloudWatchLogs(vpcFlowLogGroup), + }); + vpc.addGatewayEndpoint('S3Endpoint', { + service: ec2.GatewayVpcEndpointAwsService.S3, + }); + + // --- ECS Cluster --- + const cluster = new ecs.Cluster(this, 'Cluster', { + vpc, + containerInsightsV2: ecs.ContainerInsights.ENABLED, + }); + + // --- Tika Fargate Service --- + const tikaTaskDef = new ecs.FargateTaskDefinition(this, 'TikaTaskDef', { + memoryLimitMiB: config.fargateMemoryMiB, + cpu: config.fargateCpu, + }); + tikaTaskDef.addContainer('tika', { + image: ecs.ContainerImage.fromRegistry(`apache/tika:${config.tikaImageTag}`), + portMappings: [{ containerPort: 9998 }], + logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'tika', logRetention: logs.RetentionDays.ONE_WEEK }), + }); + const tikaService = new ecs.FargateService(this, 'TikaService', { + cluster, taskDefinition: tikaTaskDef, desiredCount: config.desiredCount, + assignPublicIp: false, vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, + }); + + // --- Siegfried Fargate Service --- + const siegfriedTaskDef = new ecs.FargateTaskDefinition(this, 'SiegfriedTaskDef', { + memoryLimitMiB: config.fargateMemoryMiB, + cpu: config.fargateCpu, + }); + siegfriedTaskDef.addContainer('siegfried', { + image: ecs.ContainerImage.fromRegistry('ghcr.io/keeps/siegfried:v1.10.1'), + portMappings: [{ containerPort: 5138 }], + logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'siegfried', logRetention: logs.RetentionDays.ONE_WEEK }), + }); + const siegfriedService = new ecs.FargateService(this, 'SiegfriedService', { + cluster, taskDefinition: siegfriedTaskDef, desiredCount: config.desiredCount, + assignPublicIp: false, vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, + }); + + // --- DROID Fargate Service --- + const droidTaskDef = new ecs.FargateTaskDefinition(this, 'DroidTaskDef', { + memoryLimitMiB: config.fargateMemoryMiB, + cpu: config.fargateCpu, + }); + droidTaskDef.addContainer('droid', { + image: ecs.ContainerImage.fromAsset(path.join(projectRoot, 'containers/droid'), { + platform: ecr_assets.Platform.LINUX_AMD64, + }), + portMappings: [{ containerPort: 8080 }], + logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'droid', logRetention: logs.RetentionDays.ONE_WEEK }), + }); + const droidService = new ecs.FargateService(this, 'DroidService', { + cluster, taskDefinition: droidTaskDef, desiredCount: config.desiredCount, + assignPublicIp: false, vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, + }); + + // --- MediaInfo Fargate Service --- + const mediainfoTaskDef = new ecs.FargateTaskDefinition(this, 'MediaInfoTaskDef', { + memoryLimitMiB: config.fargateMemoryMiB, + cpu: config.fargateCpu, + }); + mediainfoTaskDef.addContainer('mediainfo', { + image: ecs.ContainerImage.fromAsset(path.join(projectRoot, 'containers/mediainfo'), { + platform: ecr_assets.Platform.LINUX_AMD64, + }), + portMappings: [{ containerPort: 8081 }], + logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'mediainfo', logRetention: logs.RetentionDays.ONE_WEEK }), + }); + const mediainfoService = new ecs.FargateService(this, 'MediaInfoService', { + cluster, taskDefinition: mediainfoTaskDef, desiredCount: config.desiredCount, + assignPublicIp: false, vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, + }); + + // --- S3 Buckets --- + const accessLogsBucket = new s3.Bucket(this, 'AccessLogsBucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteObjects: true, + blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, + enforceSSL: true, + }); + + const docsBucket = new s3.Bucket(this, 'DocsBucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteObjects: true, + blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, + enforceSSL: true, + serverAccessLogsBucket: accessLogsBucket, + serverAccessLogsPrefix: 's3-access-logs/', + }); + + // --- Internal ALB with path-based routing --- + const alb = new elbv2.ApplicationLoadBalancer(this, 'Alb', { + vpc, internetFacing: false, + }); + alb.logAccessLogs(accessLogsBucket, 'alb-logs'); + + // Restrict ALB SG to VPC CIDR + const albSg = alb.connections.securityGroups[0]; + const cfnSg = albSg.node.defaultChild as ec2.CfnSecurityGroup; + cfnSg.addPropertyOverride('SecurityGroupIngress', [ + { IpProtocol: 'tcp', FromPort: 80, ToPort: 80, CidrIp: vpc.vpcCidrBlock, Description: 'Allow HTTP from VPC' }, + ]); + + const listener = alb.addListener('Listener', { port: 80, open: false }); + + // Default action: 404 + listener.addAction('Default', { + action: elbv2.ListenerAction.fixedResponse(404, { + contentType: 'text/plain', + messageBody: 'Not Found', + }), + }); + + // Tika target group: /tika*, /detect/*, /meta* + const tikaTargetGroup = new elbv2.ApplicationTargetGroup(this, 'TikaTargetGroup', { + vpc, port: 9998, protocol: elbv2.ApplicationProtocol.HTTP, + targets: [tikaService], + healthCheck: { path: '/tika', interval: cdk.Duration.seconds(30) }, + }); + listener.addAction('TikaRoute', { + priority: 10, + conditions: [elbv2.ListenerCondition.pathPatterns(['/tika*', '/detect/*', '/meta*'])], + action: elbv2.ListenerAction.forward([tikaTargetGroup]), + }); + + // Siegfried target group: /identify/* + const siegfriedTargetGroup = new elbv2.ApplicationTargetGroup(this, 'SiegfriedTargetGroup', { + vpc, port: 5138, protocol: elbv2.ApplicationProtocol.HTTP, + targets: [siegfriedService], + healthCheck: { + protocol: elbv2.Protocol.HTTP, + path: '/identify/', + interval: cdk.Duration.seconds(30), + healthyHttpCodes: '200-405', + }, + }); + listener.addAction('SiegfriedRoute', { + priority: 20, + conditions: [elbv2.ListenerCondition.pathPatterns(['/identify/*'])], + action: elbv2.ListenerAction.forward([siegfriedTargetGroup]), + }); + + // DROID target group: /api/* + const droidTargetGroup = new elbv2.ApplicationTargetGroup(this, 'DroidTargetGroup', { + vpc, port: 8080, protocol: elbv2.ApplicationProtocol.HTTP, + targets: [droidService], + healthCheck: { path: '/health', interval: cdk.Duration.seconds(30) }, + }); + listener.addAction('DroidRoute', { + priority: 30, + conditions: [elbv2.ListenerCondition.pathPatterns(['/api/*'])], + action: elbv2.ListenerAction.forward([droidTargetGroup]), + }); + + // MediaInfo target group: /mediainfo/* + const mediainfoTargetGroup = new elbv2.ApplicationTargetGroup(this, 'MediaInfoTargetGroup', { + vpc, port: 8081, protocol: elbv2.ApplicationProtocol.HTTP, + targets: [mediainfoService], + healthCheck: { path: '/health', interval: cdk.Duration.seconds(30) }, + }); + listener.addAction('MediaInfoRoute', { + priority: 40, + conditions: [elbv2.ListenerCondition.pathPatterns(['/mediainfo/*'])], + action: elbv2.ListenerAction.forward([mediainfoTargetGroup]), + }); + + // --- Lambda Functions --- + const lambdaLogGroup = new logs.LogGroup(this, 'LambdaLogGroup', { + retention: logs.RetentionDays.ONE_WEEK, + }); + + const baseLambdaPolicy = new iam.ManagedPolicy(this, 'LambdaBasicPolicy', { + description: 'Allows Lambda functions to write to CloudWatch Logs', + statements: [ + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], + resources: [lambdaLogGroup.logGroupArn], + }), + ], + }); + + const vpcLambdaRole = new iam.Role(this, 'VpcLambdaRole', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + managedPolicies: [ + baseLambdaPolicy, + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaVPCAccessExecutionRole'), + ], + }); + + const nonVpcLambdaRole = new iam.Role(this, 'NonVpcLambdaRole', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + managedPolicies: [baseLambdaPolicy], + }); + + const backendCode = lambda.Code.fromAsset(path.join(projectRoot, 'backend')); + const albUrl = `http://${alb.loadBalancerDnsName}`; + + // Shared VPC Lambda props + const vpcLambdaProps = { + runtime: lambda.Runtime.PYTHON_3_12, + code: backendCode, + timeout: cdk.Duration.seconds(120), + memorySize: 256, + role: vpcLambdaRole, + logGroup: lambdaLogGroup, + loggingFormat: lambda.LoggingFormat.JSON, + vpc, + vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, + }; + + // Tika Lambda + const tikaLambda = new lambda.Function(this, 'TikaLambda', { + ...vpcLambdaProps, + handler: 'tika_handler.handler', + environment: { ALB_URL: albUrl, DOCS_BUCKET: docsBucket.bucketName }, + }); + + // Siegfried Lambda + const siegfriedLambda = new lambda.Function(this, 'SiegfriedLambda', { + ...vpcLambdaProps, + handler: 'siegfried_handler.handler', + environment: { ALB_URL: albUrl, DOCS_BUCKET: docsBucket.bucketName }, + }); + + // DROID Lambda + const droidLambda = new lambda.Function(this, 'DroidLambda', { + ...vpcLambdaProps, + handler: 'droid_handler.handler', + environment: { ALB_URL: albUrl, DOCS_BUCKET: docsBucket.bucketName }, + }); + + // MediaInfo Lambda + const mediainfoLambda = new lambda.Function(this, 'MediaInfoLambda', { + ...vpcLambdaProps, + handler: 'mediainfo_handler.handler', + environment: { ALB_URL: albUrl, DOCS_BUCKET: docsBucket.bucketName }, + }); + + // Extract Lambda (no VPC needed — S3 only) + const extractLambda = new lambda.Function(this, 'ExtractLambda', { + runtime: lambda.Runtime.PYTHON_3_12, + code: backendCode, + handler: 'extract_handler.handler', + timeout: cdk.Duration.seconds(300), + memorySize: 512, + role: nonVpcLambdaRole, + logGroup: lambdaLogGroup, + loggingFormat: lambda.LoggingFormat.JSON, + environment: { DOCS_BUCKET: docsBucket.bucketName }, + }); + + // S3 Report Lambda (no VPC needed — S3 only) + const reportLambda = new lambda.Function(this, 'ReportLambda', { + runtime: lambda.Runtime.PYTHON_3_12, + code: backendCode, + handler: 's3_report_handler.handler', + timeout: cdk.Duration.seconds(60), + memorySize: 256, + role: nonVpcLambdaRole, + logGroup: lambdaLogGroup, + loggingFormat: lambda.LoggingFormat.JSON, + environment: { DOCS_BUCKET: docsBucket.bucketName }, + }); + + // S3 permissions + docsBucket.grantRead(tikaLambda); + docsBucket.grantRead(siegfriedLambda); + docsBucket.grantRead(droidLambda); + docsBucket.grantRead(mediainfoLambda); + docsBucket.grantReadWrite(extractLambda); + docsBucket.grantReadWrite(reportLambda); + + // Allow VPC Lambdas to reach ALB + const vpcLambdas = [tikaLambda, siegfriedLambda, droidLambda, mediainfoLambda]; + for (const fn of vpcLambdas) { + alb.connections.allowFrom(fn, ec2.Port.tcp(80)); + } + + // --- AgentCore Gateway --- + const gateway = new agentcore.Gateway(this, 'Gateway', { + gatewayName: 'digital-preservation-gw', + description: 'Gateway exposing digital preservation tools (Tika, Siegfried, DROID, MediaInfo) via MCP', + authorizerConfiguration: agentcore.GatewayAuthorizer.usingAwsIam(), + exceptionLevel: agentcore.GatewayExceptionLevel.DEBUG, + }); + + const SDT = agentcore.SchemaDefinitionType; + + // Grant the Gateway's service role permission to invoke all target Lambdas + // This MUST happen before addLambdaTarget calls — the GatewayTarget custom resource + // handler validates invoke permissions at creation time. + const allLambdas = [tikaLambda, siegfriedLambda, droidLambda, mediainfoLambda, extractLambda, reportLambda]; + const invokeGrants = allLambdas.map(fn => fn.grantInvoke(gateway.role)); + + // --- Tika tools target --- + gateway.addLambdaTarget('TikaTarget', { + gatewayTargetName: 'tika-tools', + description: 'Apache Tika document processing tools', + lambdaFunction: tikaLambda, + toolSchema: agentcore.ToolSchema.fromInline([ + { + name: 'tika_process', + description: + 'Process an S3 file with Apache Tika. Supports text extraction, metadata extraction, and MIME type detection. ' + + 'Tika can handle archives (ZIP, TAR) directly — it recursively parses all contained files.', + inputSchema: { + type: SDT.OBJECT, + properties: { + s3_key: { type: SDT.STRING, description: 'S3 object key of the file to process' }, + action: { type: SDT.STRING, description: 'Processing action: extract_text, extract_metadata, or detect_type. Defaults to extract_text.' }, + }, + required: ['s3_key'], + }, + }, + ]), + }); + + // --- Siegfried tools target --- + gateway.addLambdaTarget('SiegfriedTarget', { + gatewayTargetName: 'siegfried-tools', + description: 'Siegfried file format identification tools', + lambdaFunction: siegfriedLambda, + toolSchema: agentcore.ToolSchema.fromInline([ + { + name: 'siegfried_identify', + description: 'Identify the file format of an S3 object using Siegfried (PRONOM registry). Returns format name, PUID, MIME type, and match basis.', + inputSchema: { + type: SDT.OBJECT, + properties: { + s3_key: { type: SDT.STRING, description: 'S3 object key of the file to identify' }, + }, + required: ['s3_key'], + }, + }, + ]), + }); + + // --- DROID tools target --- + gateway.addLambdaTarget('DroidTarget', { + gatewayTargetName: 'droid-tools', + description: 'DROID file format profiling tools', + lambdaFunction: droidLambda, + toolSchema: agentcore.ToolSchema.fromInline([ + { + name: 'droid_profile', + description: 'Profile a file using DROID (Digital Record Object Identification). Returns PRONOM format identification results.', + inputSchema: { + type: SDT.OBJECT, + properties: { + s3_key: { type: SDT.STRING, description: 'S3 object key of the file to profile' }, + }, + required: ['s3_key'], + }, + }, + ]), + }); + + // --- MediaInfo tools target --- + gateway.addLambdaTarget('MediaInfoTarget', { + gatewayTargetName: 'mediainfo-tools', + description: 'MediaInfo media file analysis tools', + lambdaFunction: mediainfoLambda, + toolSchema: agentcore.ToolSchema.fromInline([ + { + name: 'mediainfo_analyze', + description: 'Analyze a media file (audio, video, image) from S3 using MediaInfo. Returns codec, bitrate, resolution, duration, and other technical metadata.', + inputSchema: { + type: SDT.OBJECT, + properties: { + s3_key: { type: SDT.STRING, description: 'S3 object key of the media file to analyze' }, + }, + required: ['s3_key'], + }, + }, + ]), + }); + + // --- Extract archive tools target --- + gateway.addLambdaTarget('ExtractTarget', { + gatewayTargetName: 'extract-tools', + description: 'Archive extraction tools', + lambdaFunction: extractLambda, + toolSchema: agentcore.ToolSchema.fromInline([ + { + name: 'extract_archive', + description: 'Extract a ZIP or TAR archive from S3 and write extracted files back to S3. Useful for batch processing archived collections.', + inputSchema: { + type: SDT.OBJECT, + properties: { + s3_key: { type: SDT.STRING, description: 'S3 object key of the archive file' }, + destination_prefix: { type: SDT.STRING, description: 'S3 key prefix for extracted files. Defaults to _extracted/.' }, + }, + required: ['s3_key'], + }, + }, + ]), + }); + + // --- S3 report tools target --- + gateway.addLambdaTarget('ReportTarget', { + gatewayTargetName: 'report-tools', + description: 'Analysis report persistence tools', + lambdaFunction: reportLambda, + toolSchema: agentcore.ToolSchema.fromInline([ + { + name: 'save_report_to_s3', + description: 'Save an analysis report as JSON to S3. Use after running identification/analysis tools to persist results.', + inputSchema: { + type: SDT.OBJECT, + properties: { + report_data: { type: SDT.STRING, description: 'Report content (JSON string or object) to save' }, + report_name: { type: SDT.STRING, description: 'Report filename (without extension). Defaults to analysis_report_.' }, + }, + required: ['report_data'], + }, + }, + ]), + }); + + // Add explicit dependencies so each GatewayTarget waits for IAM permissions. + // The grantInvoke calls attach statements to the gateway role's default policy. + // We need each GatewayTarget CFN resource to depend on that policy. + const gatewayRolePolicy = gateway.role.node.tryFindChild('DefaultPolicy'); + const targetIds = ['TikaTarget', 'SiegfriedTarget', 'DroidTarget', 'MediaInfoTarget', 'ExtractTarget', 'ReportTarget']; + for (const targetId of targetIds) { + const targetConstruct = gateway.node.tryFindChild(targetId); + if (targetConstruct && gatewayRolePolicy) { + targetConstruct.node.addDependency(gatewayRolePolicy); + } + } + + // --- AgentCore Runtime --- + const runtime = new agentcore.Runtime(this, 'AgentRuntime', { + runtimeName: 'digital_preservation_agent', + agentRuntimeArtifact: agentcore.AgentRuntimeArtifact.fromAsset( + path.join(projectRoot, 'agent'), + ), + description: config.agentDescription, + networkConfiguration: agentcore.RuntimeNetworkConfiguration.usingPublicNetwork(), + environmentVariables: { + GATEWAY_URL: gateway.gatewayUrl ?? '', + AWS_REGION: cdk.Stack.of(this).region, + MODEL_ID: config.agentModelId, + AGENT_INSTRUCTION: config.agentInstruction, + }, + }); + + gateway.grantInvoke(runtime); + runtime.addToRolePolicy( + new iam.PolicyStatement({ + actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'], + resources: ['arn:aws:bedrock:*::foundation-model/*'], + }), + ); + runtime.addToRolePolicy( + new iam.PolicyStatement({ + actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'], + resources: [`arn:aws:bedrock:*:${cdk.Stack.of(this).account}:inference-profile/*`], + }), + ); + + // --- Outputs --- + new cdk.CfnOutput(this, 'RuntimeArn', { value: runtime.agentRuntimeArn }); + new cdk.CfnOutput(this, 'GatewayUrl', { value: gateway.gatewayUrl ?? '' }); + new cdk.CfnOutput(this, 'GatewayId', { value: gateway.gatewayId }); + new cdk.CfnOutput(this, 'AlbDns', { value: alb.loadBalancerDnsName }); + new cdk.CfnOutput(this, 'DocsBucketName', { value: docsBucket.bucketName }); + + // --- cdk-nag suppressions --- + NagSuppressions.addResourceSuppressions( + vpcLambdaRole, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Wildcard S3 actions from CDK grantRead() scoped to the docs bucket only.', + }, + { + id: 'AwsSolutions-IAM4', + reason: 'AWSLambdaVPCAccessExecutionRole is required for VPC-attached Lambdas.', + }, + ], + true, + ); + + NagSuppressions.addResourceSuppressions( + nonVpcLambdaRole, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Wildcard S3 actions from CDK grantReadWrite() scoped to the docs bucket only.', + }, + ], + true, + ); + + NagSuppressions.addResourceSuppressions( + gateway, + [ + { + id: 'AwsSolutions-IAM5', + reason: + 'Gateway service role needs lambda:InvokeFunction with :* suffix for versioned invocations. ' + + 'Generated by CDK grantInvoke() and scoped to target Lambda functions only.', + }, + ], + true, + ); + + NagSuppressions.addResourceSuppressions( + runtime, + [ + { + id: 'AwsSolutions-IAM5', + reason: + 'AgentCore Runtime execution role requires wildcard permissions for: ' + + 'CloudWatch Logs, workload identity tokens, Bedrock model and inference profile invocation.', + }, + ], + true, + ); + + NagSuppressions.addStackSuppressions(this, [ + { + id: 'AwsSolutions-IAM4', + reason: 'CDK-managed custom resource, VPC flow log, and AgentCore roles use AWS managed policies by design.', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'CDK-managed custom resource Lambda and AgentCore constructs require broad permissions.', + appliesTo: ['Resource::*'], + }, + { + id: 'AwsSolutions-L1', + reason: 'CDK-managed custom resource Lambda runtime is controlled by the CDK framework.', + }, + { + id: 'AwsSolutions-S1', + reason: 'The access logs bucket itself does not need server access logs to avoid infinite recursion.', + }, + ]); + } +} diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/package-lock.json b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/package-lock.json new file mode 100644 index 000000000..3c9b98668 --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/package-lock.json @@ -0,0 +1,767 @@ +{ + "name": "digital-preservation-agent", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "digital-preservation-agent", + "version": "1.0.0", + "dependencies": { + "@aws-cdk/aws-bedrock-agentcore-alpha": "2.244.0-alpha.0", + "aws-cdk-lib": "^2.244.0", + "constructs": "^10.4.2", + "source-map-support": "^0.5.21" + }, + "devDependencies": { + "@types/node": "^22.7.0", + "aws-cdk": "^2.244.0", + "cdk-nag": "^2.28.196", + "ts-node": "^10.9.2", + "typescript": "^5.6.0" + } + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.263", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.263.tgz", + "integrity": "sha512-X9JvcJhYcb7PHs8R7m4zMablO5C9PGb/hYfLnxds9h/rKJu6l7MiXE/SabCibuehxPnuO/vk+sVVJiUWrccarQ==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.1.tgz", + "integrity": "sha512-We4bmHaowOPHr+IQR4/FyTGjRfjgBj4ICMjtqmJeBDWad3Q/6St12NT07leNtyuukv2qMhtSZJQorD8KpKTwRA==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/aws-bedrock-agentcore-alpha": { + "version": "2.244.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-bedrock-agentcore-alpha/-/aws-bedrock-agentcore-alpha-2.244.0-alpha.0.tgz", + "integrity": "sha512-juotD1baBGzuk9Nn7sZP15eZEeC0PFIg08H4zpulojpe+B6ObX7R7wO6BQQtKeWi01ogvc/stq5lZINOVbJ+CQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "@aws-cdk/aws-bedrock-alpha": "2.244.0-alpha.0", + "aws-cdk-lib": "^2.244.0", + "constructs": "^10.5.0" + } + }, + "node_modules/@aws-cdk/aws-bedrock-alpha": { + "version": "2.244.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-bedrock-alpha/-/aws-bedrock-alpha-2.244.0-alpha.0.tgz", + "integrity": "sha512-V2xW8VEPaxpyfbKVWlUdzTGGSfEstLyUCoAsP375T9oZ1oquiGpng9YvcrZ6n0EAujoYXrTUOpWul68imyABMQ==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.244.0", + "constructs": "^10.5.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "52.2.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-52.2.0.tgz", + "integrity": "sha512-ourZjixQ/UfsZc7gdk3vt1eHBODMUjQTYYYCY3ZX8fiXyHtWNDAYZPrXUK96jpCC2fLP+tfHTJrBjZ563pmcEw==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.3" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.3", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/aws-cdk": { + "version": "2.1113.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1113.0.tgz", + "integrity": "sha512-7D2cVJ66tRZ7KbdTfQLgwPu/XvAeb3r42MMVG605kaxWdIUczdJcuk9x5JVahDCFKRcjepLLYu5N+ovnHFYxBQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.244.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.244.0.tgz", + "integrity": "sha512-j5FVeZv5W+v6j6OnW8RjoN04T+8pYvDJJV7yXhhj4IiGDKPgMH3fflQLQXJousd2QQk+nSAjghDVJcrZ4GFyGA==", + "bundleDependencies": [ + "@balena/dockerignore", + "@aws-cdk/cloud-assembly-api", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "license": "Apache-2.0", + "dependencies": { + "@aws-cdk/asset-awscli-v1": "2.2.263", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.1", + "@aws-cdk/cloud-assembly-api": "^2.1.1", + "@aws-cdk/cloud-assembly-schema": "^52.1.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.3", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^10.2.3", + "punycode": "^2.3.1", + "semver": "^7.7.4", + "table": "^6.9.0", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "constructs": "^10.5.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": { + "version": "2.1.1", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.3" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "@aws-cdk/cloud-assembly-schema": ">=52.1.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api/node_modules/semver": { + "version": "7.7.3", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.18.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "4.0.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "5.0.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.1.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.2.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "10.2.4", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.7.4", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.9.0", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/cdk-nag": { + "version": "2.37.55", + "resolved": "https://registry.npmjs.org/cdk-nag/-/cdk-nag-2.37.55.tgz", + "integrity": "sha512-xcAkygwbph3pp7N0UEzJBmXUH/MIsluV7DYJSeZ/V3yCr0Y0QaRGO298WyD6mi4K+Rmnpl+EJoWUxcOblOqLKA==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "aws-cdk-lib": "^2.176.0", + "constructs": "^10.0.5" + } + }, + "node_modules/constructs": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.6.0.tgz", + "integrity": "sha512-TxHOnBO5zMo/G76ykzGF/wMpEHu257TbWiIxP9K0Yv/+t70UzgBQiTqjkAsWOPC6jW91DzJI0+ehQV6xDRNBuQ==", + "license": "Apache-2.0" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/package.json b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/package.json new file mode 100644 index 000000000..8ccd866ff --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/package.json @@ -0,0 +1,28 @@ +{ + "name": "digital-preservation-agent", + "version": "1.0.0", + "description": "CDK TypeScript stack deploying a document processing agent with Apache Tika on ECS Fargate, AgentCore Gateway, and AgentCore Runtime", + "main": "bin/app.js", + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "cdk": "cdk", + "synth": "cdk synth", + "deploy": "cdk deploy", + "destroy": "cdk destroy", + "diff": "cdk diff" + }, + "devDependencies": { + "@types/node": "^22.7.0", + "aws-cdk": "^2.244.0", + "cdk-nag": "^2.28.196", + "ts-node": "^10.9.2", + "typescript": "^5.6.0" + }, + "dependencies": { + "@aws-cdk/aws-bedrock-agentcore-alpha": "2.244.0-alpha.0", + "aws-cdk-lib": "^2.244.0", + "constructs": "^10.4.2", + "source-map-support": "^0.5.21" + } +} diff --git a/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/tsconfig.json b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/tsconfig.json new file mode 100644 index 000000000..63ea86994 --- /dev/null +++ b/04-infrastructure-as-code/cdk/typescript/digital-preservation-agent/infrastructure/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "commonjs", + "lib": ["ES2023"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "node", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "." + }, + "include": ["bin/**/*.ts", "lib/**/*.ts"], + "exclude": ["node_modules", "cdk.out", "dist"] +} diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 67da69f89..0a10a6e84 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -100,4 +100,4 @@ - David Kaleko - Diego Brasil - Dumitru Pascu (dumip) - +- Daren Wong (darenwkt)